Add new activity events and enhance ActivityBloc for better state management

- Introduced LoadTripActivitiesPreservingSearch event to load trip activities while preserving search results.
- Added RemoveFromSearchResults and AddActivityAndRemoveFromSearch events for improved activity handling.
- Updated ActivitiesPage to show loading dialog during activity addition and provide user feedback.
- Increased maxResults for activity search to load more activities.
This commit is contained in:
Dayron
2025-11-13 10:46:36 +01:00
parent 236327f6fa
commit dd8de46e71
4 changed files with 209 additions and 41 deletions

View File

@@ -23,6 +23,7 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
super(const ActivityInitial()) { super(const ActivityInitial()) {
on<LoadActivities>(_onLoadActivities); on<LoadActivities>(_onLoadActivities);
on<LoadTripActivitiesPreservingSearch>(_onLoadTripActivitiesPreservingSearch);
on<SearchActivities>(_onSearchActivities); on<SearchActivities>(_onSearchActivities);
on<SearchActivitiesWithCoordinates>(_onSearchActivitiesWithCoordinates); on<SearchActivitiesWithCoordinates>(_onSearchActivitiesWithCoordinates);
on<SearchActivitiesByText>(_onSearchActivitiesByText); on<SearchActivitiesByText>(_onSearchActivitiesByText);
@@ -36,6 +37,8 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
on<UpdateActivity>(_onUpdateActivity); on<UpdateActivity>(_onUpdateActivity);
on<ToggleActivityFavorite>(_onToggleActivityFavorite); on<ToggleActivityFavorite>(_onToggleActivityFavorite);
on<RestoreCachedSearchResults>(_onRestoreCachedSearchResults); on<RestoreCachedSearchResults>(_onRestoreCachedSearchResults);
on<RemoveFromSearchResults>(_onRemoveFromSearchResults);
on<AddActivityAndRemoveFromSearch>(_onAddActivityAndRemoveFromSearch);
} }
/// Handles loading activities for a trip /// Handles loading activities for a trip
@@ -58,6 +61,31 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
} }
} }
/// Handles loading trip activities while preserving search results state
Future<void> _onLoadTripActivitiesPreservingSearch(
LoadTripActivitiesPreservingSearch event,
Emitter<ActivityState> emit,
) async {
try {
final activities = await _repository.getActivitiesByTrip(event.tripId);
// Si on a un état de recherche actif, on le préserve
if (state is ActivitySearchResults) {
// On garde l'état de recherche inchangé, pas besoin d'émettre
return;
}
// Sinon, on charge normalement
emit(ActivityLoaded(
activities: activities,
filteredActivities: activities,
));
} catch (e) {
_errorService.logError('activity_bloc', 'Erreur chargement activités: $e');
emit(const ActivityError('Impossible de charger les activités'));
}
}
/// Handles searching activities using Google Places API /// Handles searching activities using Google Places API
Future<void> _onSearchActivities( Future<void> _onSearchActivities(
SearchActivities event, SearchActivities event,
@@ -200,12 +228,74 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
final activityId = await _repository.addActivity(event.activity); final activityId = await _repository.addActivity(event.activity);
if (activityId != null) { if (activityId != null) {
// Si on est en état de recherche (suggestions Google), préserver cet état
if (state is ActivitySearchResults) {
// On ne change rien à l'état de recherche, on le garde tel quel
// La suppression de l'activité des résultats se fait dans _onRemoveFromSearchResults
return;
}
// Sinon, émettre l'état d'ajout réussi
emit(ActivityAdded( emit(ActivityAdded(
activity: event.activity.copyWith(id: activityId), activity: event.activity.copyWith(id: activityId),
message: 'Activité ajoutée avec succès', message: 'Activité ajoutée avec succès',
)); ));
// Reload activities // Reload activities while preserving search results
add(LoadActivities(event.activity.tripId)); add(LoadTripActivitiesPreservingSearch(event.activity.tripId));
} else {
emit(const ActivityError('Impossible d\'ajouter l\'activité'));
}
} catch (e) {
_errorService.logError('activity_bloc', 'Erreur ajout activité: $e');
emit(const ActivityError('Impossible d\'ajouter l\'activité'));
}
}
/// Handles adding an activity and removing it from search results in one action
Future<void> _onAddActivityAndRemoveFromSearch(
AddActivityAndRemoveFromSearch event,
Emitter<ActivityState> emit,
) async {
try {
// Check if activity already exists
if (event.activity.placeId != null) {
final existing = await _repository.findExistingActivity(
event.activity.tripId,
event.activity.placeId!,
);
if (existing != null) {
emit(const ActivityError('Cette activité a déjà été ajoutée'));
return;
}
}
final activityId = await _repository.addActivity(event.activity);
if (activityId != null) {
// Si on est en état de recherche (suggestions Google), préserver cet état
// en supprimant l'activité des résultats
if (state is ActivitySearchResults) {
final currentState = state as ActivitySearchResults;
final updatedResults = currentState.searchResults
.where((activity) => activity.id != event.googleActivityId)
.toList();
emit(ActivitySearchResults(
searchResults: updatedResults,
query: currentState.query,
isLoading: false,
));
return;
}
// Sinon, émettre l'état d'ajout réussi
emit(ActivityAdded(
activity: event.activity.copyWith(id: activityId),
message: 'Activité ajoutée avec succès',
));
// Reload activities while preserving search results
add(LoadTripActivitiesPreservingSearch(event.activity.tripId));
} else { } else {
emit(const ActivityError('Impossible d\'ajouter l\'activité')); emit(const ActivityError('Impossible d\'ajouter l\'activité'));
} }
@@ -482,6 +572,29 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
return filtered; return filtered;
} }
/// Removes an activity from search results
Future<void> _onRemoveFromSearchResults(
RemoveFromSearchResults event,
Emitter<ActivityState> emit,
) async {
// Si on est actuellement dans un état de résultats de recherche
if (state is ActivitySearchResults) {
final currentState = state as ActivitySearchResults;
// Filtrer l'activité à retirer
final updatedResults = currentState.searchResults
.where((activity) => activity.id != event.activityId)
.toList();
// Émettre le nouvel état avec l'activité retirée
emit(ActivitySearchResults(
searchResults: updatedResults,
query: currentState.query,
isLoading: false,
));
}
}
/// Restores cached search results /// Restores cached search results
Future<void> _onRestoreCachedSearchResults( Future<void> _onRestoreCachedSearchResults(
RestoreCachedSearchResults event, RestoreCachedSearchResults event,

View File

@@ -19,6 +19,16 @@ class LoadActivities extends ActivityEvent {
List<Object> get props => [tripId]; List<Object> get props => [tripId];
} }
/// Event to load trip activities without resetting search results
class LoadTripActivitiesPreservingSearch extends ActivityEvent {
final String tripId;
const LoadTripActivitiesPreservingSearch(this.tripId);
@override
List<Object> get props => [tripId];
}
/// Event to search activities using Google Places API /// Event to search activities using Google Places API
class SearchActivities extends ActivityEvent { class SearchActivities extends ActivityEvent {
final String tripId; final String tripId;
@@ -194,4 +204,28 @@ class RestoreCachedSearchResults extends ActivityEvent {
@override @override
List<Object?> get props => [searchResults]; List<Object?> get props => [searchResults];
}
/// Event to remove an activity from search results
class RemoveFromSearchResults extends ActivityEvent {
final String activityId;
const RemoveFromSearchResults({required this.activityId});
@override
List<Object> get props => [activityId];
}
/// Event to add an activity and remove it from search results in one action
class AddActivityAndRemoveFromSearch extends ActivityEvent {
final Activity activity;
final String googleActivityId;
const AddActivityAndRemoveFromSearch({
required this.activity,
required this.googleActivityId,
});
@override
List<Object> get props => [activity, googleActivityId];
} }

View File

@@ -7,6 +7,7 @@ import '../../models/trip.dart';
import '../../models/activity.dart'; import '../../models/activity.dart';
import '../../services/activity_cache_service.dart'; import '../../services/activity_cache_service.dart';
import '../activities/add_activity_bottom_sheet.dart'; import '../activities/add_activity_bottom_sheet.dart';
import '../loading/laoding_content.dart';
class ActivitiesPage extends StatefulWidget { class ActivitiesPage extends StatefulWidget {
final Trip trip; final Trip trip;
@@ -138,6 +139,37 @@ class _ActivitiesPageState extends State<ActivitiesPage>
); );
} }
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
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${state.activity.name} ajoutée au voyage !'),
duration: const Duration(seconds: 2),
backgroundColor: Colors.green,
action: SnackBarAction(
label: 'Voir',
textColor: Colors.white,
onPressed: () {
// Revenir à l'onglet des activités du voyage
_tabController.animateTo(0);
},
),
),
);
});
}
if (state is ActivityLoaded) { if (state is ActivityLoaded) {
// Stocker les activités localement // Stocker les activités localement
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -172,11 +204,6 @@ class _ActivitiesPageState extends State<ActivitiesPage>
// Recharger les activités du voyage pour mettre à jour les votes // Recharger les activités du voyage pour mettre à jour les votes
_loadActivities(); _loadActivities();
} }
if (state is ActivityAdded) {
// Recharger automatiquement les activités du voyage
_loadActivities();
}
}, },
child: Scaffold( child: Scaffold(
backgroundColor: theme.colorScheme.surface, backgroundColor: theme.colorScheme.surface,
@@ -754,21 +781,6 @@ class _ActivitiesPageState extends State<ActivitiesPage>
), ),
), ),
), ),
// Bouton de debug temporaire
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: OutlinedButton.icon(
onPressed: () {
_checkAndLoadMoreActivitiesIfNeeded();
},
icon: const Icon(Icons.bug_report, size: 16),
label: const Text('🧪 Test Auto-Reload'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 8),
),
),
),
], ],
), ),
], ],
@@ -1264,23 +1276,32 @@ class _ActivitiesPageState extends State<ActivitiesPage>
id: DateTime.now().millisecondsSinceEpoch.toString(), id: DateTime.now().millisecondsSinceEpoch.toString(),
); );
context.read<ActivityBloc>().add(AddActivity(newActivity)); // Afficher le LoadingContent avec la tâche d'ajout
showDialog(
// Afficher un feedback à l'utilisateur context: context,
ScaffoldMessenger.of(context).showSnackBar( barrierDismissible: false,
SnackBar( builder: (BuildContext dialogContext) {
content: Text('${activity.name} ajoutée au voyage !'), return LoadingContent(
duration: const Duration(seconds: 2), loadingText: 'Ajout de ${activity.name}...',
backgroundColor: Colors.green, onBackgroundTask: () async {
action: SnackBarAction( // Ajouter l'activité au voyage
label: 'Voir', context.read<ActivityBloc>().add(
textColor: Colors.white, AddActivityAndRemoveFromSearch(
onPressed: () { activity: newActivity,
// Revenir à l'onglet des activités du voyage googleActivityId: activity.id,
_tabController.animateTo(0); ),
);
// 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();
}
},
);
},
); );
} }

View File

@@ -42,7 +42,7 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
return; // Utiliser le cache return; // Utiliser le cache
} }
// Sinon, lancer la recherche // Sinon, lancer la recherche avec le maximum d'activités
context.read<ActivityBloc>().add( context.read<ActivityBloc>().add(
widget.trip.hasCoordinates widget.trip.hasCoordinates
? SearchActivitiesWithCoordinates( ? SearchActivitiesWithCoordinates(
@@ -50,14 +50,14 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
latitude: widget.trip.latitude!, latitude: widget.trip.latitude!,
longitude: widget.trip.longitude!, longitude: widget.trip.longitude!,
category: null, category: null,
maxResults: 6, maxResults: 100, // Charger le maximum d'activités possible
reset: true, reset: true,
) )
: SearchActivities( : SearchActivities(
tripId: widget.trip.id!, tripId: widget.trip.id!,
destination: widget.trip.location, destination: widget.trip.location,
category: null, category: null,
maxResults: 6, maxResults: 100, // Charger le maximum d'activités possible
reset: true, reset: true,
), ),
); );