import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:gap/gap.dart'; import 'package:geolocator/geolocator.dart'; import 'package:get/get.dart'; import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:latlong2/latlong.dart'; import 'package:rain/app/api/api.dart'; import 'package:rain/app/api/city_api.dart'; import 'package:rain/app/controller/controller.dart'; import 'package:rain/app/ui/home.dart'; import 'package:rain/app/ui/widgets/button.dart'; import 'package:rain/app/ui/widgets/text_form.dart'; import 'package:rain/main.dart'; class SelectGeolocation extends StatefulWidget { const SelectGeolocation({super.key, required this.isStart}); final bool isStart; @override State createState() => _SelectGeolocationState(); } class _SelectGeolocationState extends State { bool isLoading = false; final formKeySearch = GlobalKey(); final _focusNode = FocusNode(); final weatherController = Get.put(WeatherController()); final _controller = TextEditingController(); final _controllerLat = TextEditingController(); final _controllerLon = TextEditingController(); final _controllerCity = TextEditingController(); final _controllerDistrict = TextEditingController(); static const kTextFieldElevation = 4.0; static const colorFilter = ColorFilter.matrix([ -0.2, -0.7, -0.08, 0, 255, // Red channel -0.2, -0.7, -0.08, 0, 255, // Green channel -0.2, -0.7, -0.08, 0, 255, // Blue channel 0, 0, 0, 1, 0, // Alpha channel ]); final bool _isDarkMode = Get.theme.brightness == Brightness.dark; final mapController = MapController(); void textTrim(TextEditingController value) { value.text = value.text.trim(); while (value.text.contains(' ')) { value.text = value.text.replaceAll(' ', ' '); } } void fillController(Result selection) { _controllerLat.text = '${selection.latitude}'; _controllerLon.text = '${selection.longitude}'; _controllerCity.text = selection.name; _controllerDistrict.text = selection.admin1; _controller.clear(); _focusNode.unfocus(); setState(() {}); } void fillControllerGeo(Map location) { _controllerLat.text = '${location['lat']}'; _controllerLon.text = '${location['lon']}'; _controllerCity.text = location['district']; _controllerDistrict.text = location['city']; setState(() {}); } void fillMap(double latitude, double longitude) { _controllerLat.text = '$latitude'; _controllerLon.text = '$longitude'; setState(() {}); } Widget _buildMapTileLayer() { return TileLayer( urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', userAgentPackageName: 'com.darkmoonight.rain', ); } Widget _buildMap() { return ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(20)), child: SizedBox( height: Get.size.height * 0.3, child: FlutterMap( mapController: mapController, options: MapOptions( backgroundColor: context.theme.colorScheme.surface, initialCenter: const LatLng(55.7522, 37.6156), initialZoom: 3, interactionOptions: const InteractionOptions( flags: InteractiveFlag.all & ~InteractiveFlag.rotate, ), cameraConstraint: CameraConstraint.contain( bounds: LatLngBounds( const LatLng(-90, -180), const LatLng(90, 180), ), ), onLongPress: (tapPosition, point) => fillMap(point.latitude, point.longitude), ), children: [ if (_isDarkMode) ColorFiltered( colorFilter: colorFilter, child: _buildMapTileLayer(), ) else _buildMapTileLayer(), RichAttributionWidget( animationConfig: const ScaleRAWA(), attributions: [ TextSourceAttribution( 'OpenStreetMap contributors', onTap: () => weatherController.urlLauncher( 'https://openstreetmap.org/copyright', ), ), ], ), ], ), ), ); } Widget _buildSearchField() { return RawAutocomplete( focusNode: _focusNode, textEditingController: _controller, fieldViewBuilder: ( BuildContext context, TextEditingController fieldTextEditingController, FocusNode fieldFocusNode, VoidCallback onFieldSubmitted, ) { return MyTextForm( elevation: kTextFieldElevation, labelText: 'search'.tr, type: TextInputType.text, icon: const Icon(IconsaxPlusLinear.global_search), controller: _controller, margin: const EdgeInsets.only(left: 10, right: 10, top: 10), focusNode: _focusNode, ); }, optionsBuilder: (TextEditingValue textEditingValue) { if (textEditingValue.text.isEmpty) { return const Iterable.empty(); } return WeatherAPI().getCity(textEditingValue.text, locale); }, onSelected: (Result selection) => fillController(selection), displayStringForOption: (Result option) => '${option.name}, ${option.admin1}', optionsViewBuilder: ( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { return _buildOptionsView(context, onSelected, options); }, ); } Widget _buildOptionsView( BuildContext context, AutocompleteOnSelected onSelected, Iterable options, ) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), child: Align( alignment: Alignment.topCenter, child: Material( 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.name}, ${option.admin1}', style: context.textTheme.labelLarge, ), ), ); }, ), ), ), ); } Widget _buildLocationButton() { return Card( elevation: kTextFieldElevation, margin: const EdgeInsets.only(top: 10, right: 10), child: Container( margin: const EdgeInsets.all(2.5), child: IconButton( onPressed: _handleLocationButtonPress, icon: const Icon(IconsaxPlusLinear.location), ), ), ); } Future _handleLocationButtonPress() async { bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { if (!context.mounted) return; await _showLocationDialog(); return; } setState(() => isLoading = true); final location = await weatherController.getCurrentLocationSearch(); fillControllerGeo(location); setState(() => isLoading = false); } Future _showLocationDialog() async { await showAdaptiveDialog( context: context, builder: (BuildContext context) { return AlertDialog.adaptive( title: Text('location'.tr, style: context.textTheme.titleLarge), content: Text('no_location'.tr, style: context.textTheme.titleMedium), actions: [ TextButton( onPressed: () => Get.back(result: false), child: Text( 'cancel'.tr, style: context.textTheme.titleMedium?.copyWith( color: Colors.blueAccent, ), ), ), TextButton( onPressed: () { Geolocator.openLocationSettings(); Get.back(result: true); }, child: Text( 'settings'.tr, style: context.textTheme.titleMedium?.copyWith( color: Colors.green, ), ), ), ], ); }, ); } Widget _buildLatitudeField() { return MyTextForm( elevation: kTextFieldElevation, controller: _controllerLat, labelText: 'lat'.tr, type: TextInputType.number, icon: const Icon(IconsaxPlusLinear.location), margin: const EdgeInsets.only(left: 10, right: 10, top: 10), validator: (value) => _validateLatitude(value), ); } Widget _buildLongitudeField() { return MyTextForm( elevation: kTextFieldElevation, controller: _controllerLon, labelText: 'lon'.tr, type: TextInputType.number, icon: const Icon(IconsaxPlusLinear.location), margin: const EdgeInsets.only(left: 10, right: 10, top: 10), validator: (value) => _validateLongitude(value), ); } Widget _buildCityField() { return MyTextForm( elevation: kTextFieldElevation, controller: _controllerCity, labelText: 'city'.tr, type: TextInputType.name, icon: const Icon(IconsaxPlusLinear.building_3), margin: const EdgeInsets.only(left: 10, right: 10, top: 10), validator: (value) => _validateCity(value), ); } Widget _buildDistrictField() { return MyTextForm( elevation: kTextFieldElevation, controller: _controllerDistrict, labelText: 'district'.tr, type: TextInputType.streetAddress, icon: const Icon(IconsaxPlusLinear.global), margin: const EdgeInsets.only(left: 10, right: 10, top: 10), ); } Widget _buildSubmitButton() { return Padding( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), child: MyTextButton(buttonName: 'done'.tr, onPressed: _handleSubmit), ); } String? _validateLatitude(String? value) { if (value == null || value.isEmpty) { return 'validateValue'.tr; } double? numericValue = double.tryParse(value); if (numericValue == null) { return 'validateNumber'.tr; } if (numericValue < -90 || numericValue > 90) { return 'validate90'.tr; } return null; } String? _validateLongitude(String? value) { if (value == null || value.isEmpty) { return 'validateValue'.tr; } double? numericValue = double.tryParse(value); if (numericValue == null) { return 'validateNumber'.tr; } if (numericValue < -180 || numericValue > 180) { return 'validate180'.tr; } return null; } String? _validateCity(String? value) { if (value == null || value.isEmpty) { return 'validateName'.tr; } return null; } Future _handleSubmit() async { if (formKeySearch.currentState!.validate()) { textTrim(_controllerLat); textTrim(_controllerLon); textTrim(_controllerCity); textTrim(_controllerDistrict); try { await weatherController.deleteAll(true); await weatherController.getLocation( double.parse(_controllerLat.text), double.parse(_controllerLon.text), _controllerDistrict.text, _controllerCity.text, ); widget.isStart ? Get.off(() => const HomePage(), transition: Transition.downToUp) : Get.back(); } catch (error) { Future.error(error); } } } @override Widget build(BuildContext context) { return Form( key: formKeySearch, child: Scaffold( resizeToAvoidBottomInset: true, appBar: _buildAppBar(), body: SafeArea( child: Stack( children: [ Column( children: [ Flexible( child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ Flexible( child: SingleChildScrollView( child: Column( children: [ Padding( padding: const EdgeInsets.symmetric( horizontal: 10, ), child: _buildMap(), ), Padding( padding: const EdgeInsets.fromLTRB( 10, 15, 10, 5, ), child: Text( 'searchMethod'.tr, style: context.theme.textTheme.bodyLarge ?.copyWith(fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), ), Row( children: [ Flexible(child: _buildSearchField()), _buildLocationButton(), ], ), _buildLatitudeField(), _buildLongitudeField(), _buildCityField(), _buildDistrictField(), const Gap(20), ], ), ), ), ], ), ), _buildSubmitButton(), ], ), if (isLoading) BackdropFilter( filter: ImageFilter.blur(sigmaY: 3, sigmaX: 3), child: const Center(child: CircularProgressIndicator()), ), ], ), ), ), ); } AppBar _buildAppBar() { return AppBar( centerTitle: true, leading: widget.isStart ? null : IconButton( onPressed: () { Get.back(); }, icon: const Icon(IconsaxPlusLinear.arrow_left_3, size: 20), splashColor: Colors.transparent, highlightColor: Colors.transparent, ), automaticallyImplyLeading: false, title: Text( 'searchCity'.tr, style: context.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, fontSize: 18, ), ), ); } }