diff --git a/lib/components/home/create_trip_content.dart b/lib/components/home/create_trip_content.dart index d540247..9b61b28 100644 --- a/lib/components/home/create_trip_content.dart +++ b/lib/components/home/create_trip_content.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:travel_mate/models/trip.dart'; @@ -16,6 +17,8 @@ import '../../models/group.dart'; import '../../models/group_member.dart'; import '../../services/user_service.dart'; import '../../repositories/group_repository.dart'; +import 'package:http/http.dart' as http; +import 'package:flutter_dotenv/flutter_dotenv.dart'; class CreateTripContent extends StatefulWidget { final Trip? tripToEdit; @@ -43,15 +46,23 @@ class _CreateTripContentState extends State { bool _isLoading = false; String? _createdTripId; + static final String _apiKey = dotenv.env['GOOGLE_MAPS_API_KEY'] ?? ''; + final List _participants = []; final _participantController = TextEditingController(); + List _placeSuggestions = []; + bool _isLoadingSuggestions = false; + OverlayEntry? _suggestionsOverlay; + final LayerLink _layerLink = LayerLink(); + bool get isEditing => widget.tripToEdit != null; @override void initState() { super.initState(); _initializeFormWithTrip(); + _locationController.addListener(_onLocationChanged); } Future _initializeFormWithTrip() async { @@ -71,6 +82,141 @@ class _CreateTripContentState extends State { } } + void _onLocationChanged() { + final query = _locationController.text.trim(); + + if (query.length < 2) { + _hideSuggestions(); + return; + } + + _fetchPlaceSuggestions(query); + } + + Future _fetchPlaceSuggestions(String query) async { + if (_apiKey.isEmpty) { + return; + } + + setState(() { + _isLoadingSuggestions = true; + }); + + try { + final url = Uri.parse( + 'https://maps.googleapis.com/maps/api/place/autocomplete/json' + '?input=${Uri.encodeComponent(query)}' + '&types=(cities)' + '&language=fr' + '&key=$_apiKey' + ); + + final response = await http.get(url); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + + if (data['status'] == 'OK') { + final predictions = data['predictions'] as List; + setState(() { + _placeSuggestions = predictions.map((prediction) { + return PlaceSuggestion( + placeId: prediction['place_id'], + description: prediction['description'], + ); + }).toList(); + _isLoadingSuggestions = false; + }); + + if (_placeSuggestions.isNotEmpty) { + _showSuggestions(); + } else { + _hideSuggestions(); + } + } else { + setState(() { + _placeSuggestions = []; + _isLoadingSuggestions = false; + }); + _hideSuggestions(); + } + } else { + setState(() { + _placeSuggestions = []; + _isLoadingSuggestions = false; + }); + _hideSuggestions(); + } + } catch (e) { + setState(() { + _placeSuggestions = []; + _isLoadingSuggestions = false; + }); + _hideSuggestions(); + } + } + + // Nouvelle méthode pour afficher les suggestions + void _showSuggestions() { + _hideSuggestions(); // Masquer d'abord les suggestions existantes + + if (_placeSuggestions.isEmpty) return; + + _suggestionsOverlay = OverlayEntry( + builder: (context) => Positioned( + width: MediaQuery.of(context).size.width - 32, // Largeur du champ avec padding + child: CompositedTransformFollower( + link: _layerLink, + showWhenUnlinked: false, + offset: const Offset(0, 60), // Position sous le champ + child: Material( + elevation: 4, + borderRadius: BorderRadius.circular(8), + child: Container( + constraints: const BoxConstraints(maxHeight: 200), + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey[300]!), + ), + child: ListView.builder( + shrinkWrap: true, + itemCount: _placeSuggestions.length, + itemBuilder: (context, index) { + final suggestion = _placeSuggestions[index]; + return ListTile( + leading: const Icon(Icons.location_on, color: Colors.grey), + title: Text( + suggestion.description, + style: const TextStyle(fontSize: 14), + ), + dense: true, + onTap: () => _selectSuggestion(suggestion), + ); + }, + ), + ), + ), + ), + ), + ); + + Overlay.of(context).insert(_suggestionsOverlay!); + } + + void _hideSuggestions() { + _suggestionsOverlay?.remove(); + _suggestionsOverlay = null; + } + + void _selectSuggestion(PlaceSuggestion suggestion) { + _locationController.text = suggestion.description; + _hideSuggestions(); + setState(() { + _placeSuggestions = []; + }); + } + Future _loadParticipantEmails(List participantIds) async { final userState = context.read().state; String? currentUserId; @@ -108,45 +254,45 @@ class _CreateTripContentState extends State { super.dispose(); } - @override + @override Widget build(BuildContext context) { return BlocListener( - listener: (context, tripState) { - if (tripState is TripCreated) { - // Stocker l'ID du trip et créer le groupe - _createdTripId = tripState.tripId; - _createGroupAndAccountForTrip(_createdTripId!); - } else if (tripState is TripOperationSuccess) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(tripState.message), - backgroundColor: Colors.green, - ), - ); - setState(() { - _isLoading = false; - }); - Navigator.pop(context); - if (isEditing) { - Navigator.pop(context); - } - } - } else if (tripState is TripError) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(tripState.message), - backgroundColor: Colors.red, - ), - ); - setState(() { - _isLoading = false; - }); - } + listener: (context, tripState) { + if (tripState is TripCreated) { + // Stocker l'ID du trip et créer le groupe + _createdTripId = tripState.tripId; + _createGroupAndAccountForTrip(_createdTripId!); + } else if (tripState is TripOperationSuccess) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(tripState.message), + backgroundColor: Colors.green, + ), + ); + setState(() { + _isLoading = false; + }); + Navigator.pop(context); + if (isEditing) { + Navigator.pop(context); } - }, - child: BlocBuilder( + } + } else if (tripState is TripError) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(tripState.message), + backgroundColor: Colors.red, + ), + ); + setState(() { + _isLoading = false; + }); + } + } + }, + child: BlocBuilder( builder: (context, userState) { if (userState is! user_state.UserLoaded) { return Scaffold( @@ -163,216 +309,230 @@ class _CreateTripContentState extends State { backgroundColor: Theme.of(context).colorScheme.primary, foregroundColor: Colors.white, ), - body: SingleChildScrollView( - padding: EdgeInsets.all(16), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildSectionTitle('Informations générales'), - SizedBox(height: 16), + body: GestureDetector( + onTap: _hideSuggestions, // Masquer les suggestions en tapant ailleurs + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSectionTitle('Informations générales'), + const SizedBox(height: 16), - TextFormField( - controller: _titleController, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'Titre requis'; - } - return null; - }, - decoration: InputDecoration( - labelText: 'Titre du voyage *', - hintText: 'ex: Voyage à Paris', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), + TextFormField( + controller: _titleController, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Titre requis'; + } + return null; + }, + decoration: InputDecoration( + labelText: 'Titre du voyage *', + hintText: 'ex: Voyage à Paris', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + prefixIcon: const Icon(Icons.travel_explore), ), - prefixIcon: Icon(Icons.travel_explore), ), - ), - SizedBox(height: 16), + const SizedBox(height: 16), - TextFormField( - controller: _descriptionController, - maxLines: 3, - decoration: InputDecoration( - labelText: 'Description', - hintText: 'Décrivez votre voyage...', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), + TextFormField( + controller: _descriptionController, + maxLines: 3, + decoration: InputDecoration( + labelText: 'Description', + hintText: 'Décrivez votre voyage...', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + prefixIcon: const Icon(Icons.description), ), - prefixIcon: Icon(Icons.description), ), - ), - SizedBox(height: 16), + const SizedBox(height: 16), - TextFormField( - controller: _locationController, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'Destination requise'; - } - return null; - }, - decoration: InputDecoration( - labelText: 'Destination *', - hintText: 'ex: Paris, France', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - ), - prefixIcon: Icon(Icons.location_on), - ), - ), - - SizedBox(height: 24), - - _buildSectionTitle('Dates du voyage'), - SizedBox(height: 16), - - Row( - children: [ - Expanded( - child: _buildDateField( - label: 'Date de début *', - date: _startDate, - onTap: () => _selectStartDate(context), + // Champ de localisation avec suggestions + CompositedTransformTarget( + link: _layerLink, + child: TextFormField( + controller: _locationController, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Destination requise'; + } + return null; + }, + decoration: InputDecoration( + labelText: 'Destination *', + hintText: 'ex: Paris, France', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + prefixIcon: const Icon(Icons.location_on), + suffixIcon: _isLoadingSuggestions + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : null, ), ), - SizedBox(width: 16), - Expanded( - child: _buildDateField( - label: 'Date de fin *', - date: _endDate, - onTap: () => _selectEndDate(context), + ), + + const SizedBox(height: 24), + + _buildSectionTitle('Dates du voyage'), + const SizedBox(height: 16), + + Row( + children: [ + Expanded( + child: _buildDateField( + label: 'Date de début *', + date: _startDate, + onTap: () => _selectStartDate(context), + ), + ), + const SizedBox(width: 16), + Expanded( + child: _buildDateField( + label: 'Date de fin *', + date: _endDate, + onTap: () => _selectEndDate(context), + ), + ), + ], + ), + + const SizedBox(height: 24), + + _buildSectionTitle('Budget'), + const SizedBox(height: 16), + + TextFormField( + controller: _budgetController, + keyboardType: TextInputType.numberWithOptions(decimal: true), + decoration: InputDecoration( + labelText: 'Budget estimé', + hintText: 'ex: 1200.50', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + prefixIcon: const Icon(Icons.euro), + suffixText: '€', + ), + ), + + const SizedBox(height: 24), + + _buildSectionTitle('Participants'), + const SizedBox(height: 8), + Text( + 'Ajoutez les emails des personnes que vous souhaitez inviter', + style: TextStyle(color: Colors.grey[600], fontSize: 14), + ), + const SizedBox(height: 16), + + Row( + children: [ + Expanded( + child: TextFormField( + controller: _participantController, + keyboardType: TextInputType.emailAddress, + decoration: InputDecoration( + labelText: 'Email du participant', + hintText: 'ex: ami@email.com', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + prefixIcon: const Icon(Icons.person_add), + ), + ), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: _addParticipant, + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.all(16), + ), + child: const Icon(Icons.add), + ), + ], + ), + + const SizedBox(height: 16), + + if (_participants.isNotEmpty) ...[ + Text( + 'Participants ajoutés (${_participants.length})', + style: const TextStyle(fontWeight: FontWeight.w500), + ), + const SizedBox(height: 8), + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey[300]!), + borderRadius: BorderRadius.circular(12), + ), + child: Wrap( + spacing: 8, + runSpacing: 8, + children: _participants + .map( + (email) => Chip( + label: Text(email, style: const TextStyle(fontSize: 12)), + deleteIcon: const Icon(Icons.close, size: 18), + onDeleted: () => _removeParticipant(email), + backgroundColor: Theme.of(context) + .colorScheme + .primary + .withValues(alpha: 0.1), + ), + ) + .toList(), ), ), ], - ), - SizedBox(height: 24), + const SizedBox(height: 32), - _buildSectionTitle('Budget'), - SizedBox(height: 16), - - TextFormField( - controller: _budgetController, - keyboardType: TextInputType.numberWithOptions(decimal: true), - decoration: InputDecoration( - labelText: 'Budget estimé', - hintText: 'ex: 1200.50', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - ), - prefixIcon: Icon(Icons.euro), - suffixText: '€', - ), - ), - - SizedBox(height: 24), - - _buildSectionTitle('Participants'), - SizedBox(height: 8), - Text( - 'Ajoutez les emails des personnes que vous souhaitez inviter', - style: TextStyle(color: Colors.grey[600], fontSize: 14), - ), - SizedBox(height: 16), - - Row( - children: [ - Expanded( - child: TextFormField( - controller: _participantController, - keyboardType: TextInputType.emailAddress, - decoration: InputDecoration( - labelText: 'Email du participant', - hintText: 'ex: ami@email.com', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - ), - prefixIcon: Icon(Icons.person_add), - ), - ), - ), - SizedBox(width: 8), - ElevatedButton( - onPressed: _addParticipant, + SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + onPressed: _isLoading ? null : () => _saveTrip(userState.user), style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), - padding: EdgeInsets.all(16), ), - child: Icon(Icons.add), - ), - ], - ), - - SizedBox(height: 16), - - if (_participants.isNotEmpty) ...[ - Text( - 'Participants ajoutés (${_participants.length})', - style: TextStyle(fontWeight: FontWeight.w500), - ), - SizedBox(height: 8), - Container( - width: double.infinity, - padding: EdgeInsets.all(12), - decoration: BoxDecoration( - border: Border.all(color: Colors.grey[300]!), - borderRadius: BorderRadius.circular(12), - ), - child: Wrap( - spacing: 8, - runSpacing: 8, - children: _participants - .map( - (email) => Chip( - label: Text(email, style: TextStyle(fontSize: 12)), - deleteIcon: Icon(Icons.close, size: 18), - onDeleted: () => _removeParticipant(email), - backgroundColor: Theme.of(context) - .colorScheme - .primary - .withValues(alpha: 0.1), + child: _isLoading + ? const CircularProgressIndicator(color: Colors.white) + : Text( + isEditing ? 'Mettre à jour le voyage' : 'Créer le voyage', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), ), - ) - .toList(), ), ), + + const SizedBox(height: 20), ], - - SizedBox(height: 32), - - SizedBox( - width: double.infinity, - height: 50, - child: ElevatedButton( - onPressed: _isLoading ? null : () => _saveTrip(userState.user), - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.primary, - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: _isLoading - ? CircularProgressIndicator(color: Colors.white) - : Text( - isEditing ? 'Mettre à jour le voyage' : 'Créer le voyage', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - - SizedBox(height: 20), - ], + ), ), ), ), @@ -762,4 +922,14 @@ class _CreateTripContentState extends State { return participantsData; } +} + +class PlaceSuggestion { + final String placeId; + final String description; + + PlaceSuggestion({ + required this.placeId, + required this.description, + }); } \ No newline at end of file diff --git a/lib/components/home/show_trip_details_content.dart b/lib/components/home/show_trip_details_content.dart index 5cb1811..c746d9f 100644 --- a/lib/components/home/show_trip_details_content.dart +++ b/lib/components/home/show_trip_details_content.dart @@ -4,6 +4,8 @@ import 'package:travel_mate/blocs/trip/trip_bloc.dart'; import 'package:travel_mate/blocs/trip/trip_event.dart'; import 'package:travel_mate/components/home/create_trip_content.dart'; import 'package:travel_mate/models/trip.dart'; +import 'package:url_launcher/url_launcher.dart'; // Ajouter cet import +import 'package:travel_mate/components/map/map_content.dart'; // Ajouter cet import si la page carte existe class ShowTripDetailsContent extends StatefulWidget { final Trip trip; @@ -14,6 +16,57 @@ class ShowTripDetailsContent extends StatefulWidget { } class _ShowTripDetailsContentState extends State { + // Méthode pour ouvrir la carte interne + void _openInternalMap() { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MapContent( + initialSearchQuery: widget.trip.location, + ), + ), + ); + } + + // Méthode pour ouvrir Google Maps + Future _openGoogleMaps() async { + final location = Uri.encodeComponent(widget.trip.location); + final url = 'https://www.google.com/maps/search/?api=1&query=$location'; + + try { + final uri = Uri.parse(url); + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } else { + // Fallback: essayer l'URL scheme pour l'app mobile + final appUrl = 'comgooglemaps://?q=$location'; + final appUri = Uri.parse(appUrl); + if (await canLaunchUrl(appUri)) { + await launchUrl(appUri); + } else { + // Si rien ne marche, afficher un message d'erreur + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Impossible d\'ouvrir Google Maps. Vérifiez que l\'application est installée.'), + backgroundColor: Colors.red, + ), + ); + } + } + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Erreur lors de l\'ouverture de Google Maps: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + } + @override Widget build(BuildContext context) { @@ -84,7 +137,72 @@ class _ShowTripDetailsContentState extends State { 'Budget: ${widget.trip.budget ?? 'N/A'}€', style: TextStyle(color: textColor), ), + SizedBox(height: 24), + + // Section des boutons de carte + Text( + 'Explorer la destination', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: textColor, + ), + ), SizedBox(height: 16), + + // Boutons en ligne + Row( + children: [ + // Bouton carte interne + Expanded( + child: ElevatedButton.icon( + onPressed: _openInternalMap, + icon: Icon(Icons.map, color: Colors.white), + label: Text( + 'Voir sur la carte', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primary, + padding: EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ), + SizedBox(width: 12), + + // Bouton Google Maps + Expanded( + child: ElevatedButton.icon( + onPressed: _openGoogleMaps, + icon: Icon(Icons.directions, color: Colors.white), + label: Text( + 'Google Maps', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green[700], + padding: EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ), + ], + ), + + SizedBox(height: 24), + + // Boutons existants SizedBox( width: double.infinity, height: 50, diff --git a/lib/components/map/map_content.dart b/lib/components/map/map_content.dart index ca388ca..10aaf92 100644 --- a/lib/components/map/map_content.dart +++ b/lib/components/map/map_content.dart @@ -7,7 +7,8 @@ import 'dart:ui' as ui; import 'package:flutter_dotenv/flutter_dotenv.dart'; class MapContent extends StatefulWidget { - const MapContent({super.key}); + final String? initialSearchQuery; + const MapContent({super.key, this.initialSearchQuery}); @override State createState() => _MapContentState(); @@ -32,6 +33,15 @@ class _MapContentState extends State { void initState() { super.initState(); _getCurrentLocation(); + + // Si une recherche initiale est fournie, la pré-remplir et lancer la recherche + if (widget.initialSearchQuery != null && widget.initialSearchQuery!.isNotEmpty) { + _searchController.text = widget.initialSearchQuery!; + // Lancer la recherche automatiquement après un court délai pour laisser l'interface se charger + Future.delayed(const Duration(milliseconds: 500), () { + _performInitialSearch(widget.initialSearchQuery!); + }); + } } @override @@ -41,6 +51,112 @@ class _MapContentState extends State { super.dispose(); } + // Nouvelle méthode pour effectuer la recherche initiale + Future _performInitialSearch(String query) async { + if (query.isEmpty) return; + + setState(() { + _isSearching = true; + }); + + try { + final url = Uri.parse( + 'https://maps.googleapis.com/maps/api/place/autocomplete/json' + '?input=${Uri.encodeComponent(query)}' + '&key=$_apiKey' + '&language=fr' + ); + + final response = await http.get(url); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + + if (data['status'] == 'OK') { + final predictions = data['predictions'] as List; + + if (predictions.isNotEmpty) { + // Prendre automatiquement la première suggestion + final firstPrediction = predictions.first; + final suggestion = PlaceSuggestion( + placeId: firstPrediction['place_id'], + description: firstPrediction['description'], + ); + + // Effectuer la sélection automatique + await _selectPlaceForInitialSearch(suggestion); + } else { + setState(() { + _isSearching = false; + }); + } + } else { + setState(() { + _isSearching = false; + }); + } + } + } catch (e) { + _showError('Erreur lors de la recherche initiale: $e'); + setState(() { + _isSearching = false; + }); + } + } + + // Version modifiée de _selectPlace pour la recherche initiale + Future _selectPlaceForInitialSearch(PlaceSuggestion suggestion) async { + try { + final url = Uri.parse( + 'https://maps.googleapis.com/maps/api/place/details/json' + '?place_id=${suggestion.placeId}' + '&key=$_apiKey' + '&language=fr', + ); + + final response = await http.get(url); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + + if (data['status'] == 'OK') { + final location = data['result']['geometry']['location']; + final lat = location['lat']; + final lng = location['lng']; + final name = data['result']['name']; + + final newPosition = LatLng(lat, lng); + + // Ajouter un marqueur pour le lieu recherché + setState(() { + // Garder le marqueur de position utilisateur s'il existe + _markers.removeWhere((m) => m.markerId.value != 'user_location'); + + // Ajouter le nouveau marqueur de lieu + _markers.add( + Marker( + markerId: MarkerId(suggestion.placeId), + position: newPosition, + infoWindow: InfoWindow(title: name), + ), + ); + _isSearching = false; + }); + + // Animer la caméra vers le lieu trouvé + _mapController?.animateCamera( + CameraUpdate.newLatLngZoom(newPosition, 15), + ); + } + } + } catch (e) { + _showError('Erreur lors de la sélection du lieu initial: $e'); + setState(() { + _isSearching = false; + }); + } + } + Future _getCurrentLocation() async { setState(() { _isLoadingLocation = true; @@ -449,6 +565,10 @@ class _MapContentState extends State { ), ), onChanged: (value) { + // Ne pas rechercher si c'est juste le remplissage initial + if (widget.initialSearchQuery != null && value == widget.initialSearchQuery) { + return; + } _searchPlaces(value); }, ), diff --git a/pubspec.lock b/pubspec.lock index 5dc8c73..1516e49 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1021,6 +1021,70 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "5c8b6c2d89a78f5a1cca70a73d9d5f86c701b36b42f9c9dac7bad592113c28e9" + url: "https://pub.dev" + source: hosted + version: "6.3.24" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "6b63f1441e4f653ae799166a72b50b1767321ecc263a57aadf825a7a2a5477d9" + url: "https://pub.dev" + source: hosted + version: "6.3.5" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "8262208506252a3ed4ff5c0dc1e973d2c0e0ef337d0a074d35634da5d44397c9" + url: "https://pub.dev" + source: hosted + version: "3.2.4" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" uuid: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index fd27da1..33c77d6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -54,6 +54,7 @@ dependencies: image_picker: ^1.2.0 intl: ^0.20.2 firebase_storage: ^13.0.3 + url_launcher: ^6.3.2 dev_dependencies: flutter_launcher_icons: ^0.13.1