import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:geocoding/geocoding.dart'; import 'package:geolocator/geolocator.dart'; import 'package:go_router/go_router.dart'; import 'package:latlong2/latlong.dart'; import 'package:http/http.dart' as http; import 'package:niogu_ecommerce_v1/core/constant/app_color.dart'; import 'package:niogu_ecommerce_v1/core/constant/app_font_size.dart'; import 'package:niogu_ecommerce_v1/core/router/app_route.dart'; import 'package:niogu_ecommerce_v1/core/utils/log_message.dart'; import 'package:niogu_ecommerce_v1/core/widgets/triangle_painter.dart'; import 'package:niogu_ecommerce_v1/features/account/domain/entities/account.dart'; import 'package:niogu_ecommerce_v1/features/account/presentation/providers/account_provider.dart'; import 'package:sizer/sizer.dart'; class MapAddressScreen extends ConsumerStatefulWidget { const MapAddressScreen({super.key}); @override ConsumerState createState() => _MapAddressScreenState(); } class _MapAddressScreenState extends ConsumerState { final MapController _mapController = MapController(); LatLng _selectedLocation = const LatLng(-6.2000, 106.8166); String _fullAddress = ""; bool _isSearching = false; bool _isLoadingMap = false; List _suggestions = []; Timer? _debounce; @override void initState() { // TODO: implement initState super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { _fetchSelectedAddress(); }); } void _fetchSelectedAddress() async { final selectedAddress = ref.read(selectedAddressProvider); if (selectedAddress != null) { await _updateLocation( LatLng(selectedAddress.latitude, selectedAddress.longitude), ); } } void _onSearchChanged(String query) { if (_debounce?.isActive ?? false) _debounce?.cancel(); if (query.isEmpty) { setState(() => _suggestions = []); return; } _debounce = Timer(const Duration(milliseconds: 800), () async { setState(() => _isSearching = true); try { final url = Uri.parse( 'https://nominatim.openstreetmap.org/search?q=$query&format=json&limit=5&countrycodes=id', ); final response = await http.get( url, headers: { 'User-Agent': 'NioguEcommerceApp/1.0 (niaganusantara@gmail.com)', 'Accept-Language': 'id', }, ); if (response.statusCode == 200) { setState(() { _suggestions = json.decode(response.body); _isSearching = false; }); } } catch (e, st) { LogMessage.log.e(e.toString(), error: e, stackTrace: st); setState(() => _isSearching = false); } }); } Future _getCurrentPosition() async { setState(() => _isLoadingMap = true); LocationPermission permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); } Position position = await Geolocator.getCurrentPosition(); final newLatLng = LatLng(position.latitude, position.longitude); await _updateLocation(newLatLng); } Future _updateLocation(LatLng point) async { setState(() { _selectedLocation = point; _isLoadingMap = true; }); _mapController.move(point, 16.0); try { List placemarks = await placemarkFromCoordinates( point.latitude, point.longitude, ); if (placemarks.isNotEmpty) { final place = placemarks[0]; setState(() { _fullAddress = "${place.street}, ${place.subLocality}, ${place.locality}, ${place.subAdministrativeArea}"; _isLoadingMap = false; _suggestions = []; }); } } catch (e) { setState(() => _isLoadingMap = false); } } void _selectedAddress() { ref.read(selectedAddressProvider.notifier).state = SelectedAddress( fullAddress: _fullAddress, latitude: _selectedLocation.latitude, longitude: _selectedLocation.longitude, ); } @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { return SafeArea( top: false, bottom: true, right: false, left: false, child: Scaffold( body: Stack( children: [ FlutterMap( mapController: _mapController, options: MapOptions( initialCenter: _selectedLocation, initialZoom: 16.0, onTap: (_, point) => _updateLocation(point), ), children: [ TileLayer( urlTemplate: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', subdomains: const ['a', 'b', 'c', 'd'], ), MarkerLayer( markers: [ Marker( point: _selectedLocation, width: 80.w, height: 15.h, alignment: Alignment.topCenter, child: Column( mainAxisSize: MainAxisSize.min, children: [ if (_fullAddress.isNotEmpty) ...[ _buildAddressBubble(), CustomPaint( size: Size(5.w, 2.5.w), painter: TrianglePainter(Colors.white), ), Icon( Icons.location_on, color: AppColor.primaryColor, size: 10.w, ), ], ], ), ), ], ), ], ), Positioned( top: 6.h, left: 5.w, right: 5.w, child: Column( children: [ _buildSearchBar(), if (_suggestions.isNotEmpty || _isSearching) _buildSuggestionList(), ], ), ), Positioned( bottom: 4.h, left: 5.w, right: 5.w, child: Column( children: [ Align( alignment: Alignment.centerRight, child: FloatingActionButton( mini: true, backgroundColor: Colors.white, onPressed: _getCurrentPosition, child: Icon( Icons.my_location, color: AppColor.primaryColor, ), ), ), SizedBox(height: 2.h), ElevatedButton( onPressed: _fullAddress.isEmpty ? null : () { _selectedAddress(); context.pushReplacementNamed( AppRoute.saveAddressScreen, ); }, style: ElevatedButton.styleFrom( minimumSize: Size(double.infinity, 6.h), backgroundColor: AppColor.primaryColor, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(2.5.w), ), ), child: Text( "Pilih Lokasi Ini", style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: AppFontSize.medium.sp, ), ), ), ], ), ), if (_isLoadingMap) Center( child: CircularProgressIndicator( color: AppColor.primaryColor, ), ), ], ), ), ); }, ); } Widget _buildSearchBar() { return Row( children: [ GestureDetector( onTap: () => context.pop(), child: Padding( padding: EdgeInsets.all(2.w), child: CircleAvatar( maxRadius: 5.w, minRadius: 5.w, backgroundColor: Colors.white.withOpacity(0.9), child: Center( child: Icon(Icons.arrow_back, color: Colors.black, size: 6.w), ), ), ), ), Expanded( child: Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(2.5.w), boxShadow: [ BoxShadow( color: Colors.black12, blurRadius: 10, offset: const Offset(0, 5), ), ], ), child: TextField( onChanged: _onSearchChanged, style: TextStyle(fontSize: AppFontSize.small.sp), decoration: InputDecoration( hintText: "Cari Lokasi...", hintStyle: TextStyle(fontSize: AppFontSize.small.sp), prefixIcon: Icon(Icons.search, size: 5.w), suffixIcon: _isSearching ? Transform.scale( scale: 0.5, child: CircularProgressIndicator( color: AppColor.primaryColor, ), ) : null, border: InputBorder.none, contentPadding: EdgeInsets.symmetric(vertical: 2.h), ), ), ), ), ], ); } Widget _buildSuggestionList() { return Container( margin: EdgeInsets.only(top: 1.h), constraints: BoxConstraints(maxHeight: 30.h), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(2.5.w), ), child: ListView.builder( shrinkWrap: true, padding: EdgeInsets.zero, itemCount: _suggestions.length, itemBuilder: (context, index) { final item = _suggestions[index]; return ListTile( leading: Icon(Icons.location_on_outlined, size: 5.w), title: Text( item['display_name'], style: TextStyle(fontSize: (AppFontSize.small - 1.25).sp), ), onTap: () { final lat = double.parse(item['lat']); final lon = double.parse(item['lon']); _updateLocation(LatLng(lat, lon)); }, ); }, ), ); } Widget _buildAddressBubble() { return Container( padding: EdgeInsets.all(2.w), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(2.w), boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 4)], ), child: Text( _fullAddress, maxLines: 2, textAlign: TextAlign.center, style: TextStyle( fontSize: (AppFontSize.small - 1.25).sp, fontWeight: FontWeight.w500, ), ), ); } }