diff --git a/lib/app/api/api.dart b/lib/app/api/api.dart index 9366b1d..95ced66 100644 --- a/lib/app/api/api.dart +++ b/lib/app/api/api.dart @@ -2,6 +2,7 @@ import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:rain/app/api/card_api.dart'; +import 'package:rain/app/api/city.dart'; import 'package:rain/app/api/daily.dart'; import 'package:rain/app/api/hourly.dart'; import 'package:rain/app/data/weather.dart'; @@ -77,16 +78,23 @@ class WeatherAPI { } } - Future> getSuggestions( + Future> getSuggestions( String query, Locale? locale, String apiKey) async { final url = 'https://api.geoapify.com/v1/geocode/search?city=$query&apiKey=$apiKey&lang=${locale?.languageCode}&format=json'; try { Response response = await dioLocation.get(url); if (response.statusCode == 200) { - final data = response.data; - final results = data['results']; - return results; + CityApi cityData = CityApi.fromJson(response.data); + return cityData.results.map( + (e) => Result( + country: e.country, + state: e.state, + city: e.city, + lon: e.lon, + lat: e.lat, + ), + ); } else { throw Exception('Failed to load suggestions'); } diff --git a/lib/app/api/city.dart b/lib/app/api/city.dart new file mode 100644 index 0000000..1da57dc --- /dev/null +++ b/lib/app/api/city.dart @@ -0,0 +1,36 @@ +class CityApi { + CityApi({ + required this.results, + }); + + List results; + + factory CityApi.fromJson(Map json) => CityApi( + results: + List.from(json["results"].map((x) => Result.fromJson(x))), + ); +} + +class Result { + Result({ + this.country, + this.state, + this.city, + required this.lon, + required this.lat, + }); + + String? country; + String? state; + String? city; + double lon; + double lat; + + factory Result.fromJson(Map json) => Result( + country: json["country"], + state: json["state"], + city: json["city"], + lon: json["lon"], + lat: json["lat"], + ); +} diff --git a/lib/app/modules/home.dart b/lib/app/modules/home.dart index d565a79..59dce49 100644 --- a/lib/app/modules/home.dart +++ b/lib/app/modules/home.dart @@ -1,4 +1,3 @@ -import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:custom_navigation_bar/custom_navigation_bar.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -6,6 +5,7 @@ import 'package:get/get.dart'; import 'package:iconsax/iconsax.dart'; import 'package:rain/api_key.dart'; import 'package:rain/app/api/api.dart'; +import 'package:rain/app/api/city.dart'; import 'package:rain/app/controller/controller.dart'; import 'package:rain/app/modules/card_weather.dart'; import 'package:rain/app/modules/settings.dart'; @@ -25,6 +25,7 @@ class _HomePageState extends State { final locale = Get.locale; final locationController = Get.put(LocationController()); final TextEditingController _controller = TextEditingController(); + final FocusNode _focusNode = FocusNode(); @override void initState() { @@ -66,68 +67,83 @@ class _HomePageState extends State { scale: 20, ), title: visible - ? TypeAheadField( - suggestionsBoxDecoration: SuggestionsBoxDecoration( - color: context.theme.scaffoldBackgroundColor, - borderRadius: BorderRadius.circular(20), - ), - textFieldConfiguration: TextFieldConfiguration( - controller: _controller, - decoration: InputDecoration( - hintText: 'search'.tr, - border: InputBorder.none, - ), - ), - errorBuilder: (context, error) { - return Container( - margin: - const EdgeInsets.symmetric(horizontal: 10, vertical: 5), - height: 45, - child: Center( - child: Text( - 'enter_name'.tr, - style: context.theme.textTheme.bodyLarge, - ), + ? RawAutocomplete( + focusNode: _focusNode, + textEditingController: _controller, + fieldViewBuilder: (BuildContext context, + TextEditingController fieldTextEditingController, + FocusNode fieldFocusNode, + VoidCallback onFieldSubmitted) { + return TextField( + controller: _controller, + focusNode: _focusNode, + decoration: InputDecoration( + hintText: 'search'.tr, + border: InputBorder.none, ), ); }, - noItemsFoundBuilder: (context) { - return Container( - margin: - const EdgeInsets.symmetric(horizontal: 10, vertical: 5), - height: 45, - child: Center( - child: Text( - 'notFound'.tr, - style: context.theme.textTheme.bodyLarge, - ), - ), - ); + optionsBuilder: (TextEditingValue textEditingValue) { + if (textEditingValue.text.isEmpty) { + return const Iterable.empty(); + } + return WeatherAPI() + .getSuggestions(textEditingValue.text, locale, apiKey); }, - suggestionsCallback: (query) => - WeatherAPI().getSuggestions(query, locale, apiKey), - itemBuilder: (context, suggestion) => ListTile( - title: Text( - suggestion['state'] == null - ? '${suggestion['city']}, ${suggestion['country']}' - : suggestion['city'] == null - ? '${suggestion['state']}, ${suggestion['country']}' - : '${suggestion['city']}, ${suggestion['state']}', - style: context.theme.textTheme.bodyLarge, - ), - ), - onSuggestionSelected: (suggestion) async { + onSelected: (Result selection) async { await locationController.deleteAll(true); await locationController.getLocation( - double.parse('${suggestion['lat'].toStringAsFixed(4)}'), - double.parse('${suggestion['lon'].toStringAsFixed(4)}'), - suggestion['state'] ?? suggestion['country'], - suggestion['city'] ?? suggestion['state'], + double.parse(selection.lat.toStringAsFixed(4)), + double.parse(selection.lon.toStringAsFixed(4)), + selection.state ?? selection.country!, + selection.city ?? selection.state!, ); visible = false; _controller.clear(); + _focusNode.unfocus(); setState(() {}); }, + displayStringForOption: (Result option) => option.state == null + ? '${option.city}, ${option.country}' + : option.city == null + ? '${option.state}, ${option.country}' + : '${option.city}, ${option.state}', + optionsViewBuilder: (BuildContext context, + AutocompleteOnSelected onSelected, + Iterable options) { + return Align( + alignment: Alignment.topLeft, + child: Material( + color: context.theme.scaffoldBackgroundColor, + borderRadius: BorderRadius.circular(20), + elevation: 4.0, + child: SizedBox( + width: 250, + child: ListView.builder( + padding: EdgeInsets.zero, + shrinkWrap: true, + itemCount: options.length, + itemBuilder: (BuildContext context, int index) { + final Result option = options.elementAt(index); + return InkWell( + onTap: () => onSelected(option), + child: ListTile( + title: Text( + option.state == null + ? '${option.city}, ${option.country}' + : option.city == null + ? '${option.state}, ${option.country}' + : '${option.city}, ${option.state}', + style: context.theme.textTheme.bodyLarge, + ), + ), + ); + }, + ), + ), + ), + ); + }, ) : Obx(() => Text( locationController.isLoading.isFalse @@ -154,6 +170,8 @@ class _HomePageState extends State { IconButton( onPressed: () { if (visible) { + _controller.clear(); + _focusNode.unfocus(); visible = false; } else { visible = true; diff --git a/lib/app/widgets/create_card_weather.dart b/lib/app/widgets/create_card_weather.dart index 0c4daa9..856847d 100644 --- a/lib/app/widgets/create_card_weather.dart +++ b/lib/app/widgets/create_card_weather.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:get/get.dart'; import 'package:iconsax/iconsax.dart'; import 'package:rain/api_key.dart'; import 'package:rain/app/api/api.dart'; +import 'package:rain/app/api/city.dart'; import 'package:rain/app/controller/controller.dart'; import 'package:rain/app/widgets/text_form.dart'; @@ -20,6 +20,7 @@ class _CreateWeatherCardState extends State { final formKey = GlobalKey(); final locationController = Get.put(LocationController()); final TextEditingController _controller = TextEditingController(); + final FocusNode _focusNode = FocusNode(); final TextEditingController _controllerLat = TextEditingController(); final TextEditingController _controllerLon = TextEditingController(); final TextEditingController _controllerCity = TextEditingController(); @@ -33,11 +34,12 @@ class _CreateWeatherCardState extends State { } void fillController(suggestion) { - _controllerLat.text = '${suggestion['lat'].toStringAsFixed(4)}'; - _controllerLon.text = '${suggestion['lon'].toStringAsFixed(4)}'; - _controllerCity.text = suggestion['city'] ?? suggestion['state']; - _controllerDistrict.text = suggestion['state'] ?? suggestion['country']; + _controllerLat.text = '${suggestion.lat.toStringAsFixed(4)}'; + _controllerLon.text = '${suggestion.lon.toStringAsFixed(4)}'; + _controllerCity.text = suggestion.city ?? suggestion.state; + _controllerDistrict.text = suggestion.state ?? suggestion.country; _controller.clear(); + _focusNode.unfocus(); setState(() {}); } @@ -110,70 +112,82 @@ class _CreateWeatherCardState extends State { Padding( padding: const EdgeInsets.only(left: 10, right: 10, top: 10), - child: TypeAheadField( - suggestionsBoxDecoration: SuggestionsBoxDecoration( - color: context.theme.scaffoldBackgroundColor, - borderRadius: BorderRadius.circular(20), - ), - textFieldConfiguration: TextFieldConfiguration( - controller: _controller, - decoration: InputDecoration( - prefixIcon: const Icon(Iconsax.global_search), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(50), - borderSide: BorderSide( - color: context.theme.disabledColor, - ), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(50), - borderSide: BorderSide( - color: context.theme.disabledColor, + child: RawAutocomplete( + focusNode: _focusNode, + textEditingController: _controller, + fieldViewBuilder: (BuildContext context, + TextEditingController fieldTextEditingController, + FocusNode fieldFocusNode, + VoidCallback onFieldSubmitted) { + return TextField( + controller: _controller, + focusNode: _focusNode, + decoration: InputDecoration( + prefixIcon: const Icon(Iconsax.global_search), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(50), + borderSide: BorderSide( + color: context.theme.disabledColor, + ), ), - ), - labelText: 'search'.tr, - ), - ), - errorBuilder: (context, error) { - return Container( - margin: const EdgeInsets.symmetric( - horizontal: 10, vertical: 5), - height: 45, - child: Center( - child: Text( - 'enter_name'.tr, - style: context.theme.textTheme.bodyLarge, + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(50), + borderSide: BorderSide( + color: context.theme.disabledColor, + ), ), + labelText: 'search'.tr, ), ); }, - noItemsFoundBuilder: (context) { - return Container( - margin: const EdgeInsets.symmetric( - horizontal: 10, vertical: 5), - height: 45, - child: Center( - child: Text( - 'notFound'.tr, - style: context.theme.textTheme.bodyLarge, + optionsBuilder: (TextEditingValue textEditingValue) { + if (textEditingValue.text.isEmpty) { + return const Iterable.empty(); + } + return WeatherAPI().getSuggestions( + textEditingValue.text, locale, apiKey); + }, + onSelected: (Result selection) => + fillController(selection), + displayStringForOption: (Result option) => + option.state == null + ? '${option.city}, ${option.country}' + : option.city == null + ? '${option.state}, ${option.country}' + : '${option.city}, ${option.state}', + optionsViewBuilder: (BuildContext context, + AutocompleteOnSelected onSelected, + Iterable options) { + return Align( + alignment: Alignment.topCenter, + child: Material( + color: context.theme.scaffoldBackgroundColor, + borderRadius: BorderRadius.circular(20), + elevation: 4.0, + child: ListView.builder( + padding: EdgeInsets.zero, + shrinkWrap: true, + itemCount: options.length, + itemBuilder: (BuildContext context, int index) { + final Result option = options.elementAt(index); + return InkWell( + onTap: () => onSelected(option), + child: ListTile( + title: Text( + option.state == null + ? '${option.city}, ${option.country}' + : option.city == null + ? '${option.state}, ${option.country}' + : '${option.city}, ${option.state}', + style: context.theme.textTheme.bodyLarge, + ), + ), + ); + }, ), ), ); }, - suggestionsCallback: (query) => - WeatherAPI().getSuggestions(query, locale, apiKey), - itemBuilder: (context, suggestion) => ListTile( - title: Text( - suggestion['state'] == null - ? '${suggestion['city']}, ${suggestion['country']}' - : suggestion['city'] == null - ? '${suggestion['state']}, ${suggestion['country']}' - : '${suggestion['city']}, ${suggestion['state']}', - style: context.theme.textTheme.bodyLarge, - ), - ), - onSuggestionSelected: (suggestion) => - fillController(suggestion), ), ), MyTextForm( diff --git a/pubspec.lock b/pubspec.lock index 5ca4973..f5434d0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -286,54 +286,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.0" - flutter_keyboard_visibility: - dependency: transitive - description: - name: flutter_keyboard_visibility - sha256: "86b71bbaffa38e885f5c21b1182408b9be6951fd125432cf6652c636254cef2d" - url: "https://pub.dev" - source: hosted - version: "5.4.0" - flutter_keyboard_visibility_linux: - dependency: transitive - description: - name: flutter_keyboard_visibility_linux - sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - flutter_keyboard_visibility_macos: - dependency: transitive - description: - name: flutter_keyboard_visibility_macos - sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086 - url: "https://pub.dev" - source: hosted - version: "1.0.0" - flutter_keyboard_visibility_platform_interface: - dependency: transitive - description: - name: flutter_keyboard_visibility_platform_interface - sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4 - url: "https://pub.dev" - source: hosted - version: "2.0.0" - flutter_keyboard_visibility_web: - dependency: transitive - description: - name: flutter_keyboard_visibility_web - sha256: d3771a2e752880c79203f8d80658401d0c998e4183edca05a149f5098ce6e3d1 - url: "https://pub.dev" - source: hosted - version: "2.0.0" - flutter_keyboard_visibility_windows: - dependency: transitive - description: - name: flutter_keyboard_visibility_windows - sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73 - url: "https://pub.dev" - source: hosted - version: "1.0.0" flutter_launcher_icons: dependency: "direct dev" description: @@ -368,14 +320,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_typeahead: - dependency: "direct main" - description: - name: flutter_typeahead - sha256: edfc51579ca3756adaa32b6849cf44af03276b93e8a8a68d8247ee243598f1b2 - url: "https://pub.dev" - source: hosted - version: "4.3.7" flutter_web_plugins: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index d42624a..318ab8d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A new Flutter project. publish_to: "none" -version: 1.1.1+12 +version: 1.1.2+13 environment: sdk: ">=2.19.4 <3.0.0" @@ -25,7 +25,6 @@ dependencies: flutter_glow: ^0.2.0 url_launcher: ^6.1.10 path_provider: ^2.0.14 - flutter_typeahead: ^4.3.7 package_info_plus: ^3.0.3 connectivity_plus: ^3.0.3 isar_flutter_libs: ^3.0.5