feat: Integrate Google Maps functionality with place suggestions and navigation options

This commit is contained in:
Dayron
2025-10-23 12:32:34 +02:00
parent 5b8d3ec45f
commit 37f5bb1710
5 changed files with 695 additions and 222 deletions

View File

@@ -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<CreateTripContent> {
bool _isLoading = false;
String? _createdTripId;
static final String _apiKey = dotenv.env['GOOGLE_MAPS_API_KEY'] ?? '';
final List<String> _participants = [];
final _participantController = TextEditingController();
List<PlaceSuggestion> _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<void> _initializeFormWithTrip() async {
@@ -71,6 +82,141 @@ class _CreateTripContentState extends State<CreateTripContent> {
}
}
void _onLocationChanged() {
final query = _locationController.text.trim();
if (query.length < 2) {
_hideSuggestions();
return;
}
_fetchPlaceSuggestions(query);
}
Future<void> _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<void> _loadParticipantEmails(List<String> participantIds) async {
final userState = context.read<UserBloc>().state;
String? currentUserId;
@@ -108,45 +254,45 @@ class _CreateTripContentState extends State<CreateTripContent> {
super.dispose();
}
@override
@override
Widget build(BuildContext context) {
return BlocListener<TripBloc, TripState>(
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<UserBloc, user_state.UserState>(
}
} else if (tripState is TripError) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(tripState.message),
backgroundColor: Colors.red,
),
);
setState(() {
_isLoading = false;
});
}
}
},
child: BlocBuilder<UserBloc, user_state.UserState>(
builder: (context, userState) {
if (userState is! user_state.UserLoaded) {
return Scaffold(
@@ -163,216 +309,230 @@ class _CreateTripContentState extends State<CreateTripContent> {
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),
],
),
),
),
),
@@ -763,3 +923,13 @@ class _CreateTripContentState extends State<CreateTripContent> {
return participantsData;
}
}
class PlaceSuggestion {
final String placeId;
final String description;
PlaceSuggestion({
required this.placeId,
required this.description,
});
}

View File

@@ -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<ShowTripDetailsContent> {
// 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<void> _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<ShowTripDetailsContent> {
'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,

View File

@@ -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<MapContent> createState() => _MapContentState();
@@ -32,6 +33,15 @@ class _MapContentState extends State<MapContent> {
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<MapContent> {
super.dispose();
}
// Nouvelle méthode pour effectuer la recherche initiale
Future<void> _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<void> _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<void> _getCurrentLocation() async {
setState(() {
_isLoadingLocation = true;
@@ -449,6 +565,10 @@ class _MapContentState extends State<MapContent> {
),
),
onChanged: (value) {
// Ne pas rechercher si c'est juste le remplissage initial
if (widget.initialSearchQuery != null && value == widget.initialSearchQuery) {
return;
}
_searchPlaces(value);
},
),

View File

@@ -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:

View File

@@ -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