384 lines
12 KiB
Dart
384 lines
12 KiB
Dart
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<MapAddressScreen> createState() => _MapAddressScreenState();
|
|
}
|
|
|
|
class _MapAddressScreenState extends ConsumerState<MapAddressScreen> {
|
|
final MapController _mapController = MapController();
|
|
|
|
LatLng _selectedLocation = const LatLng(-6.2000, 106.8166);
|
|
|
|
String _fullAddress = "";
|
|
|
|
bool _isSearching = false;
|
|
|
|
bool _isLoadingMap = false;
|
|
|
|
List<dynamic> _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<void> _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<void> _updateLocation(LatLng point) async {
|
|
setState(() {
|
|
_selectedLocation = point;
|
|
_isLoadingMap = true;
|
|
});
|
|
|
|
_mapController.move(point, 16.0);
|
|
|
|
try {
|
|
List<Placemark> 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,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|