import 'dart:io'; import 'package:dio_cache_interceptor/dio_cache_interceptor.dart'; import 'package:dio_cache_interceptor_file_store/dio_cache_interceptor_file_store.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_animations/flutter_map_animations.dart'; import 'package:flutter_map_cache/flutter_map_cache.dart'; import 'package:gap/gap.dart'; import 'package:get/get.dart'; import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:latlong2/latlong.dart'; import 'package:path_provider/path_provider.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/data/weather.dart'; import 'package:rain/app/modules/cards/view/info_weather_card.dart'; import 'package:rain/app/modules/cards/widgets/create_card_weather.dart'; import 'package:rain/app/modules/cards/widgets/weather_card_container.dart'; import 'package:rain/app/widgets/status/status_data.dart'; import 'package:rain/app/widgets/status/status_weather.dart'; import 'package:rain/app/widgets/text_form.dart'; import 'package:rain/main.dart'; class MapWeather extends StatefulWidget { const MapWeather({super.key}); @override State createState() => _MapWeatherState(); } class _MapWeatherState extends State with TickerProviderStateMixin { late final AnimatedMapController _animatedMapController = AnimatedMapController(vsync: this); final weatherController = Get.put(WeatherController()); final statusWeather = StatusWeather(); final statusData = StatusData(); final Future _cacheStoreFuture = _getCacheStore(); final bool _isDarkMode = Get.theme.brightness == Brightness.dark; WeatherCard? _selectedWeatherCard; bool _isCardVisible = false; double _cardBottomPosition = -200; final _focusNode = FocusNode(); late final TextEditingController _controllerSearch = TextEditingController(); static Future _getCacheStore() async { final dir = await getTemporaryDirectory(); return FileCacheStore('${dir.path}${Platform.pathSeparator}MapTiles'); } @override void dispose() { _animatedMapController.dispose(); _controllerSearch.dispose(); super.dispose(); } void _onMarkerTap(WeatherCard weatherCard) { setState(() { _selectedWeatherCard = weatherCard; _cardBottomPosition = 0; _isCardVisible = true; }); } void _hideCard() { setState(() { _cardBottomPosition = -200; }); Future.delayed(const Duration(milliseconds: 300), () { setState(() { _isCardVisible = false; }); }); _focusNode.unfocus(); } Widget _buidStyleMarkers(int weathercode, String time, String sunrise, String sunset, double temperature2M) { return Card( child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Image.asset( statusWeather.getImageNow( weathercode, time, sunrise, sunset, ), scale: 18, ), const MaxGap(5), Text( statusData .getDegree(roundDegree ? temperature2M.round() : temperature2M), style: context.textTheme.labelLarge?.copyWith( fontWeight: FontWeight.bold, fontSize: 16, ), ), ], ), ); } Marker _buildMainLocationMarker( WeatherCard weatherCard, int hourOfDay, int dayOfNow) { return Marker( height: 50, width: 100, point: LatLng(weatherCard.lat!, weatherCard.lon!), child: GestureDetector( onTap: () => _onMarkerTap(weatherCard), child: _buidStyleMarkers( weatherCard.weathercode![hourOfDay], weatherCard.time![hourOfDay], weatherCard.sunrise![dayOfNow], weatherCard.sunset![dayOfNow], weatherCard.temperature2M![hourOfDay], ), ), ); } Marker _buildCardMarker(WeatherCard weatherCardList) { return Marker( height: 50, width: 100, point: LatLng(weatherCardList.lat!, weatherCardList.lon!), child: GestureDetector( onTap: () => _onMarkerTap(weatherCardList), child: _buidStyleMarkers( weatherCardList.weathercode![weatherController.getTime( weatherCardList.time!, weatherCardList.timezone!)], weatherCardList.time![weatherController.getTime( weatherCardList.time!, weatherCardList.timezone!)], weatherCardList.sunrise![weatherController.getDay( weatherCardList.timeDaily!, weatherCardList.timezone!)], weatherCardList.sunset![weatherController.getDay( weatherCardList.timeDaily!, weatherCardList.timezone!)], weatherCardList.temperature2M![weatherController.getTime( weatherCardList.time!, weatherCardList.timezone!)], ), ), ); } Widget _buildMapTileLayer(CacheStore cacheStore) { return TileLayer( urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', userAgentPackageName: 'com.darkmoonight.rain', tileProvider: CachedTileProvider( store: cacheStore, maxStale: const Duration(days: 30), ), ); } Widget _buildWeatherCard() { return AnimatedPositioned( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, left: 0, right: 0, bottom: _cardBottomPosition, child: AnimatedOpacity( opacity: _isCardVisible ? 1.0 : 0.0, duration: const Duration(milliseconds: 300), child: _isCardVisible ? GestureDetector( onTap: () => Get.to( () => InfoWeatherCard(weatherCard: _selectedWeatherCard!), transition: Transition.downToUp, ), child: WeatherCardContainer( time: _selectedWeatherCard!.time!, timeDaily: _selectedWeatherCard!.timeDaily!, timeDay: _selectedWeatherCard!.sunrise!, timeNight: _selectedWeatherCard!.sunset!, weather: _selectedWeatherCard!.weathercode!, degree: _selectedWeatherCard!.temperature2M!, district: _selectedWeatherCard!.district!, city: _selectedWeatherCard!.city!, timezone: _selectedWeatherCard!.timezone!, ), ) : const SizedBox.shrink(), ), ); } @override Widget build(BuildContext context) { final mainLocation = weatherController.location; final mainWeather = weatherController.mainWeather; final hourOfDay = weatherController.hourOfDay.value; final dayOfNow = weatherController.dayOfNow.value; return FutureBuilder( future: _cacheStoreFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } if (snapshot.hasError) { return Center(child: Text(snapshot.error.toString())); } final cacheStore = snapshot.data!; return Stack( children: [ FlutterMap( mapController: _animatedMapController.mapController, options: MapOptions( backgroundColor: context.theme.colorScheme.surface, initialCenter: LatLng(mainLocation.lat!, mainLocation.lon!), initialZoom: 12, cameraConstraint: CameraConstraint.contain( bounds: LatLngBounds( const LatLng(-90, -180), const LatLng(90, 180), ), ), onTap: (_, __) => _hideCard(), onLongPress: (tapPosition, point) => showModalBottomSheet( context: context, isScrollControlled: true, enableDrag: false, builder: (BuildContext context) => CreateWeatherCard( latitude: '${point.latitude}', longitude: '${point.longitude}', ), ), ), children: [ if (_isDarkMode) ColorFiltered( colorFilter: const 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 ]), child: _buildMapTileLayer(cacheStore), ) else _buildMapTileLayer(cacheStore), RichAttributionWidget( animationConfig: const ScaleRAWA(), attributions: [ TextSourceAttribution( 'OpenStreetMap contributors', onTap: () => weatherController .urlLauncher('https://openstreetmap.org/copyright'), ), ], ), Obx(() { final mainMarker = _buildMainLocationMarker( WeatherCard.fromJson({ ...mainWeather.toJson(), ...mainLocation.toJson(), }), hourOfDay, dayOfNow, ); final cardMarkers = weatherController.weatherCards .map((weatherCardList) => _buildCardMarker(weatherCardList)) .toList(); return MarkerLayer( markers: [mainMarker, ...cardMarkers], ); }), _buildWeatherCard(), ], ), RawAutocomplete( focusNode: _focusNode, textEditingController: _controllerSearch, fieldViewBuilder: (BuildContext context, TextEditingController fieldTextEditingController, FocusNode fieldFocusNode, VoidCallback onFieldSubmitted) { return MyTextForm( labelText: 'search'.tr, type: TextInputType.text, icon: const Icon(IconsaxPlusLinear.global_search), controller: _controllerSearch, margin: const EdgeInsets.only(left: 10, right: 10, top: 10), focusNode: _focusNode, onChanged: (value) => setState(() {}), iconButton: _controllerSearch.text.isNotEmpty ? IconButton( onPressed: () { _controllerSearch.clear(); }, icon: const Icon( IconsaxPlusLinear.close_circle, color: Colors.grey, size: 20, ), ) : null, ); }, optionsBuilder: (TextEditingValue textEditingValue) { if (textEditingValue.text.isEmpty) { return const Iterable.empty(); } return WeatherAPI().getCity(textEditingValue.text, locale); }, onSelected: (Result selection) { _animatedMapController.mapController .move(LatLng(selection.latitude, selection.longitude), 14); _controllerSearch.clear(); _focusNode.unfocus(); }, displayStringForOption: (Result option) => '${option.name}, ${option.admin1}', optionsViewBuilder: (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, ), ), ); }, ), ), ), ); }, ), ], ); }, ); } }