diff --git a/lib/activeFires.dart b/lib/activeFires.dart index b28eaa4..e1b0822 100644 --- a/lib/activeFires.dart +++ b/lib/activeFires.dart @@ -72,7 +72,8 @@ class _ActiveFiresPageState extends State { child: Container( color: Colors.white, child: Text(S.of(context).addYourCurrentPosition, - style: const TextStyle(color: Colors.black38)), + style: const TextStyle( + color: Colors.black87, fontWeight: FontWeight.w500)), ), ), onTap: () { @@ -88,7 +89,8 @@ class _ActiveFiresPageState extends State { child: Container( color: Colors.white, child: Text(S.of(context).addSomePlace, - style: const TextStyle(color: Colors.black38)), + style: const TextStyle( + color: Colors.black87, fontWeight: FontWeight.w500)), ), ), onTap: () { diff --git a/lib/placesAutocompleteUtils.dart b/lib/placesAutocompleteUtils.dart index 3368c34..8ba76cd 100644 --- a/lib/placesAutocompleteUtils.dart +++ b/lib/placesAutocompleteUtils.dart @@ -1,24 +1,205 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:geocoding/geocoding.dart'; +import 'package:objectid/objectid.dart'; import 'models/yourLocation.dart'; - -/// Open a places dialog for selecting a location. -/// Currently returns a default location as the google_places_autocomplete package -/// is not maintained and doesn't support null safety. -/// TODO: Implement with a modern null-safe places API integration +/// Open a places dialog for selecting a location using geocoding. +/// Allows users to search for places by name and get coordinates. Future openPlacesDialog(GlobalKey sc) async { - // Show a snackbar informing the user that this feature is not yet available - final ScaffoldMessengerState messenger = ScaffoldMessenger.of(sc.currentContext!); - messenger.showSnackBar( - const SnackBar( - content: Text( - 'Place selection is currently unavailable. Please use manual location entry.'), - ), + final BuildContext? context = sc.currentContext; + if (context == null) { + return YourLocation.noLocation; + } + + final YourLocation? selectedLocation = await showDialog( + context: context, + builder: (BuildContext context) => const _PlaceSelectionDialog(), ); - // Return a default location - return YourLocation.noLocation; + return selectedLocation ?? YourLocation.noLocation; +} + +class _PlaceSelectionDialog extends StatefulWidget { + const _PlaceSelectionDialog(); + + @override + State<_PlaceSelectionDialog> createState() => _PlaceSelectionDialogState(); +} + +class _PlaceSelectionDialogState extends State<_PlaceSelectionDialog> { + final TextEditingController _searchController = TextEditingController(); + List _searchResults = []; + bool _isSearching = false; + String? _errorMessage; + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + Future _searchPlaces(String query) async { + if (query.isEmpty) { + setState(() { + _searchResults = []; + _errorMessage = null; + }); + return; + } + + setState(() { + _isSearching = true; + _errorMessage = null; + }); + + try { + final List locations = await locationFromAddress(query); + setState(() { + _searchResults = locations; + if (_searchResults.isEmpty) { + _errorMessage = 'No places found. Try a different search.'; + } + }); + } catch (e) { + setState(() { + _errorMessage = 'Search error: ${e.toString()}'; + _searchResults = []; + }); + } finally { + setState(() { + _isSearching = false; + }); + } + } + + Future _getPlaceName(Location location) async { + try { + final List placemarks = + await placemarkFromCoordinates(location.latitude, location.longitude); + if (placemarks.isNotEmpty) { + final Placemark place = placemarks.first; + final List parts = []; + if (place.name != null && place.name!.isNotEmpty) + parts.add(place.name!); + if (place.locality != null && place.locality!.isNotEmpty) + parts.add(place.locality!); + if (place.country != null && place.country!.isNotEmpty) + parts.add(place.country!); + if (parts.isNotEmpty) { + return parts.join(', '); + } + } + } catch (e) { + // Fall back to coordinates + } + return '${location.latitude.toStringAsFixed(4)}, ${location.longitude.toStringAsFixed(4)}'; + } + + void _selectLocation(Location location) async { + final String description = await _getPlaceName(location); + if (!mounted) return; + + final YourLocation yourLocation = YourLocation( + id: ObjectId(), + lat: location.latitude, + lon: location.longitude, + description: description, + distance: 10, + ); + + Navigator.of(context).pop(yourLocation); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Search for a place'), + content: SizedBox( + width: double.maxFinite, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: _searchController, + decoration: InputDecoration( + hintText: 'Enter city, address, or landmark', + prefixIcon: const Icon(Icons.search), + suffixIcon: _isSearching + ? const SizedBox( + width: 20, + height: 20, + child: Padding( + padding: EdgeInsets.all(8.0), + child: CircularProgressIndicator(strokeWidth: 2), + ), + ) + : null, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onChanged: (String value) { + if (value.length > 2) { + _searchPlaces(value); + } else { + setState(() { + _searchResults = []; + _errorMessage = null; + }); + } + }, + ), + const SizedBox(height: 16), + if (_errorMessage != null) + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + _errorMessage!, + style: const TextStyle(color: Colors.red, fontSize: 12), + ), + ), + if (_searchResults.isNotEmpty) + SizedBox( + height: 300, + child: ListView.builder( + shrinkWrap: true, + itemCount: _searchResults.length, + itemBuilder: (BuildContext context, int index) { + final Location location = _searchResults[index]; + return FutureBuilder( + future: _getPlaceName(location), + builder: (BuildContext context, + AsyncSnapshot snapshot) { + final String displayName = snapshot.data ?? + '${location.latitude.toStringAsFixed(4)}, ${location.longitude.toStringAsFixed(4)}'; + return ListTile( + leading: const Icon(Icons.location_on), + title: Text(displayName), + subtitle: Text( + '${location.latitude.toStringAsFixed(4)}, ${location.longitude.toStringAsFixed(4)}', + style: const TextStyle(fontSize: 12), + ), + onTap: () => _selectLocation(location), + ); + }, + ); + }, + ), + ), + ], + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + ], + ); + } }