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