diff --git a/lib/components/locations/add_location_by_search_form.dart b/lib/components/locations/add_location_by_search_form.dart new file mode 100644 index 0000000..e501646 --- /dev/null +++ b/lib/components/locations/add_location_by_search_form.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:wetter/models/location_model.dart'; +import 'package:wetter/services/openstreetmap_api_service.dart'; +import 'package:wetter/services/utils.dart'; + +class AddLocationBySearchForm extends StatefulWidget { + final Function saveNewLocation; + + const AddLocationBySearchForm({Key? key, required this.saveNewLocation}) + : super(key: key); + + @override + State createState() => + _AddLocationBySearchFormState(); +} + +class _AddLocationBySearchFormState extends State { + final _formKey = GlobalKey(); + final searchLocationFormController = TextEditingController(); + final _debouncer = Debouncer(milliseconds: 500); + bool _showResults = false; + bool _locationSearchHasFocus = false; + List? _locations; + + @override + Widget build(BuildContext context) { + return Form( + key: _formKey, + child: Column( + children: [ + FocusScope( + child: Focus( + onFocusChange: (focus) { + setState(() { + _locationSearchHasFocus = !_locationSearchHasFocus; + }); + }, + child: TextFormField( + onChanged: (value) { + _debouncer.run(() { + OpenStreetMapAPI.getLocationsFromSearch(value) + .then((l) { + setState(() { + _locations = l; + _showResults = true; + }); + }); + }); + }, + decoration: const InputDecoration( + border: UnderlineInputBorder(), + labelText: "Location Name"), + ))), + if (_showResults && _locationSearchHasFocus) _showResultList(), + ], + )); + } + + Column _showResultList() { + if (_locations == null) { + return Column( + children: const [], + ); + } + if (_locations!.isEmpty) { + return Column( + children: [_singleResult(name: "No result")], + ); + } + return Column(children: [ + for (LocationModel location in _locations!.getRange(0, 3)) + _singleResult(name: location.name, location: location) + ]); + } + + Widget _singleResult({required String name, LocationModel? location}) { + return GestureDetector( + onTap: () { + if (location != null) { + widget.saveNewLocation( + location.name, location.latitude, location.longitude); + } + }, + child: Card( + child: Padding( + padding: const EdgeInsets.all(5), + child: Row(children: [ + Flexible( + child: Text(name, + style: Theme.of(context).textTheme.titleMedium)) + ])))); + } +} diff --git a/lib/models/location_model.dart b/lib/models/location_model.dart new file mode 100644 index 0000000..ef642d3 --- /dev/null +++ b/lib/models/location_model.dart @@ -0,0 +1,13 @@ +class LocationModel { + late double latitude; + late double longitude; + late String name; + + LocationModel(); + + LocationModel.fromJson(Map json) { + name = json["display_name"]; + latitude = double.parse(json["lat"]); + longitude = double.parse(json["lon"]); + } +} diff --git a/lib/screens/locations.dart b/lib/screens/locations.dart index 1571398..3a0c607 100644 --- a/lib/screens/locations.dart +++ b/lib/screens/locations.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:wetter/components/locations/add_location_by_search_form.dart'; import 'package:wetter/components/locations/add_location_form.dart'; import 'package:wetter/components/locations/manage_single_location.dart'; import 'package:wetter/services/user_preferences.dart'; @@ -45,11 +46,16 @@ class _LocationsState extends State { for (String name in locations) ManageSingleLocation(name: name, delete: deleteLocation), ...[ - Expanded( - child: AddLocationsForm( - saveNewLocation: saveNewLocation, - savedLocations: locations), - ) + const SizedBox(height: 20), + const Text("Add location by search"), + AddLocationBySearchForm(saveNewLocation: saveNewLocation), + const SizedBox(height: 20), + const Text("Add location by coordinates"), + Flexible( + fit: FlexFit.loose, + child: AddLocationsForm( + saveNewLocation: saveNewLocation, + savedLocations: locations)), ], ])))); } diff --git a/lib/services/openstreetmap_api_service.dart b/lib/services/openstreetmap_api_service.dart new file mode 100644 index 0000000..65eab59 --- /dev/null +++ b/lib/services/openstreetmap_api_service.dart @@ -0,0 +1,24 @@ +import 'dart:convert'; + +import 'package:wetter/models/location_model.dart'; +import 'package:http/http.dart' as http; + +class OpenStreetMapAPI { + static Future> getLocationsFromSearch( + String search) async { + final response = await http.get( + Uri.parse( + 'https://nominatim.openstreetmap.org/search.php?q=$search&format=jsonv2'), + headers: {"Accept": "application/json"}); + if (response.statusCode == 200) { + List locations = []; + List results = jsonDecode(response.body); + for (Map result in results) { + locations.add(LocationModel.fromJson(result)); + } + return locations; + } else { + throw Exception('Failed to load weather data'); + } + } +} diff --git a/lib/services/utils.dart b/lib/services/utils.dart index 333fb9d..e846830 100644 --- a/lib/services/utils.dart +++ b/lib/services/utils.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:weather_icons/weather_icons.dart'; @@ -31,3 +33,18 @@ IconData weatherIcon(String icon) { return WeatherIcons.alien; } } + +class Debouncer { + final int milliseconds; + VoidCallback? action; + Timer? _timer; + + Debouncer({required this.milliseconds}); + + run(VoidCallback action) { + if (_timer != null) { + _timer!.cancel(); + } + _timer = Timer(Duration(milliseconds: milliseconds), action); + } +}