1298 lines
45 KiB
Dart
1298 lines
45 KiB
Dart
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<ActivitiesPage> createState() => _ActivitiesPageState();
|
|
}
|
|
|
|
class _ActivitiesPageState extends State<ActivitiesPage>
|
|
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<Activity> _tripActivities = [];
|
|
List<Activity> _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<ActivityBloc>().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<ActivityBloc>().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<ActivityBloc>().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<ActivityBloc, ActivityState>(
|
|
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<ActivityBloc, ActivityState>(
|
|
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<Activity> 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<UserBloc>().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<ActivityBloc>().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<ActivityBloc>().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<ActivityBloc>().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<ActivityBloc>().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<ActivityBloc>().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<ActivityBloc>().add(
|
|
SearchActivitiesWithCoordinates(
|
|
tripId: widget.trip.id!,
|
|
latitude: widget.trip.latitude!,
|
|
longitude: widget.trip.longitude!,
|
|
category: null,
|
|
maxResults: 6,
|
|
reset: true,
|
|
),
|
|
);
|
|
} else {
|
|
context.read<ActivityBloc>().add(
|
|
SearchActivities(
|
|
tripId: widget.trip.id!,
|
|
destination: widget.trip.location,
|
|
category: null,
|
|
maxResults: 6,
|
|
reset: true,
|
|
),
|
|
);
|
|
}
|
|
_googleSearchPerformed = true;
|
|
}
|
|
|
|
void _loadMoreGoogleActivities() {
|
|
final currentState = context.read<ActivityBloc>().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<ActivityBloc>().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<ActivityBloc>().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<ActivityBloc>().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<ActivityBloc>().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<ActivityBloc>().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<ActivityBloc>().add(
|
|
SearchActivitiesWithCoordinates(
|
|
tripId: widget.trip.id!,
|
|
latitude: widget.trip.latitude!,
|
|
longitude: widget.trip.longitude!,
|
|
category: null,
|
|
maxResults: totalToRequest,
|
|
reset: true,
|
|
),
|
|
);
|
|
} else {
|
|
context.read<ActivityBloc>().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<ActivityBloc>().add(
|
|
SearchActivitiesByText(
|
|
tripId: widget.trip.id!,
|
|
destination: widget.trip.location,
|
|
query: query,
|
|
),
|
|
);
|
|
_googleSearchPerformed = true;
|
|
}
|
|
}
|