feat : Map Widget

This commit is contained in:
2025-08-03 14:17:08 +03:30
parent 7d3ab64705
commit 693d8cbfab
9 changed files with 232 additions and 289 deletions

View File

@@ -1,10 +0,0 @@
import 'package:flutter/material.dart';
import 'package:latlong2/latlong.dart';
class CustomMarker {
final LatLng point;
final VoidCallback? onTap;
final int? id;
CustomMarker({ this.id, required this.point, this.onTap});
}

View File

@@ -1,207 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:rasadyar_core/presentation/common/app_color.dart';
import 'package:rasadyar_core/presentation/common/app_fonts.dart';
import 'package:rasadyar_core/presentation/common/assets.gen.dart';
import 'package:rasadyar_core/presentation/widget/buttons/elevated.dart';
import 'package:rasadyar_core/presentation/widget/buttons/fab.dart';
import 'package:rasadyar_core/presentation/widget/buttons/outline_elevated.dart';
import 'logic.dart';
class MapWidget extends GetView<MapWidgetLogic> {
final VoidCallback? initOnTap;
final Widget? initMarkerWidget;
final Widget markerWidget;
const MapWidget({
this.initOnTap,
this.initMarkerWidget,
required this.markerWidget,
super.key,
});
@override
Widget build(BuildContext context) {
return Stack(
children: [
ObxValue((errorType) {
if (errorType.isNotEmpty) {
if (errorType.contains(ErrorLocationType.serviceDisabled)) {
Future.microtask(() {
Get.defaultDialog(
title: 'خطا',
content: const Text('سرویس مکان‌یابی غیرفعال است'),
cancel: ROutlinedElevated(
text: 'بررسی مجدد',
width: 120,
textStyle: AppFonts.yekan16,
onPressed: () async {
var service = await controller.locationServiceEnabled();
if (service) {
controller.errorLocationType.remove(
ErrorLocationType.serviceDisabled,
);
Get.back();
}
// Don't call Get.back() if service is still disabled
},
),
confirm: RElevated(
text: 'روشن کردن',
textStyle: AppFonts.yekan16,
width: 120,
onPressed: () async {
var res = await Geolocator.openLocationSettings();
if (res) {
var service = await controller.locationServiceEnabled();
if (service) {
controller.errorLocationType.remove(
ErrorLocationType.serviceDisabled,
);
Get.back();
}
}
},
),
contentPadding: EdgeInsets.all(8),
onWillPop: () async {
return controller.errorLocationType.isEmpty;
},
barrierDismissible: false,
);
});
} else {
Future.microtask(() {
Get.defaultDialog(
title: 'خطا',
content: const Text(' دسترسی به سرویس مکان‌یابی غیرفعال است'),
cancel: ROutlinedElevated(
text: 'بررسی مجدد',
width: 120,
textStyle: AppFonts.yekan16,
onPressed: () async {
await controller.checkPermission();
},
),
confirm: RElevated(
text: 'اجازه دادن',
textStyle: AppFonts.yekan16,
width: 120,
onPressed: () async {
var res = await controller.checkPermission(request: true);
if (res) {
controller.errorLocationType.remove(
ErrorLocationType.permissionDenied,
);
Get.back();
}
},
),
contentPadding: EdgeInsets.all(8),
onWillPop: () async {
return controller.errorLocationType.isEmpty;
},
barrierDismissible: false,
);
});
}
}
return const SizedBox.shrink();
}, controller.errorLocationType),
_buildMap(),
_buildGpsButton(),
_buildFilterButton(),
],
);
}
Widget _buildMap() {
return ObxValue((currentLocation) {
return FlutterMap(
mapController: controller.animatedMapController.mapController,
options: MapOptions(
initialCenter: currentLocation.value,
initialZoom: 18,
onPositionChanged: (camera, hasGesture) {
if (hasGesture) {
controller.debouncedUpdateVisibleMarkers(center: camera.center);
}
//controller.debouncedUpdateVisibleMarkers(center: camera.center);
},
),
children: [
TileLayer(urlTemplate: controller.tileType),
ObxValue((markers) {
return MarkerLayer(
markers:
markers
.map(
(e) => Marker(
point: e.point,
child: GestureDetector(
onTap: e.id != -1 ? e.onTap : initOnTap,
child:
e.id != -1
? markerWidget
: initMarkerWidget ?? SizedBox.shrink(),
),
),
)
.toList(),
);
}, controller.markers),
],
);
}, controller.currentLocation);
}
Widget _buildGpsButton() {
return Positioned(
right: 10,
bottom: 83,
child: ObxValue((data) {
return RFab.small(
backgroundColor: AppColor.greenNormal,
isLoading: data.value,
icon: Assets.vec.gpsSvg.svg(),
onPressed: () async {
controller.isLoading.value = true;
await controller.determineCurrentPosition();
controller.isLoading.value = false;
},
);
}, controller.isLoading),
);
}
Widget _buildFilterButton() {
return Positioned(
right: 10,
bottom: 30,
child: RFab.small(
backgroundColor: AppColor.blueNormal,
icon: Assets.vec.filterSvg.svg(width: 24, height: 24),
onPressed: () {},
),
);
}
/*Marker markerWidget({required LatLng marker, required VoidCallback onTap}) {
return Marker(
point: marker,
child: GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: SizedBox(
width: 36,
height: 36,
child: Assets.vec.mapMarkerSvg.svg(width: 30, height: 30),
),
),
);
}*/
}

View File

@@ -2,24 +2,40 @@ import 'package:rasadyar_core/core.dart';
import 'package:rasadyar_livestock/data/common/dio_exception_handeler.dart';
import 'package:rasadyar_livestock/data/data_source/remote/auth/auth_remote.dart';
import 'package:rasadyar_livestock/data/data_source/remote/auth/auth_remote_imp.dart';
import 'package:rasadyar_livestock/data/repository/auth/auth_repository.dart';
import 'package:rasadyar_livestock/data/repository/auth/auth_repository_imp.dart';
import 'package:rasadyar_livestock/presentation/routes/app_pages.dart';
GetIt get diLiveStock => GetIt.instance;
Future setupLiveStockDI() async {
Future<void> setupLiveStockDI() async {
diLiveStock.registerSingleton(DioErrorHandler());
var tokenService = Get.find<TokenStorageService>();
final tokenService = Get.find<TokenStorageService>();
if (tokenService.baseurl.value == null) {
await tokenService.saveBaseUrl('https://api.dam.rasadyar.net/');
}
diLiveStock.registerLazySingleton<AuthRemoteDataSource>(
() => AuthRemoteDataSourceImp(diLiveStock.get<DioRemote>()),
);
diLiveStock.registerLazySingleton<AuthRepository>(
() => AuthRepositoryImp(diLiveStock.get<AuthRemoteDataSource>()),
);
diLiveStock.registerLazySingleton<AppInterceptor>(
() => AppInterceptor(
() => AppInterceptor(
refreshTokenCallback: () async {
var authRepository = diLiveStock.get<AuthRepositoryImp>();
var hasAuthenticated = await authRepository.hasAuthenticated();
final authRepository = diLiveStock.get<AuthRepository>();
final hasAuthenticated = await authRepository.hasAuthenticated();
if (hasAuthenticated) {
var newToken = await authRepository.loginWithRefreshToken(
final newToken = await authRepository.loginWithRefreshToken(
authRequest: {'refresh': tokenService.refreshToken.value},
);
return newToken?.access;
@@ -37,23 +53,13 @@ Future setupLiveStockDI() async {
),
);
// Register the DioRemote client
diLiveStock.registerLazySingleton<DioRemote>(
() => DioRemote(
() => DioRemote(
baseUrl: tokenService.baseurl.value,
interceptors: diLiveStock.get<AppInterceptor>(),
),
);
var dioRemoteClient = diLiveStock.get<DioRemote>();
await dioRemoteClient.init();
// Register the AuthRemote data source implementation
diLiveStock.registerLazySingleton<AuthRemoteDataSource>(
() => AuthRemoteDataSourceImp(diLiveStock.get<DioRemote>()),
);
// Register the AuthRepository implementation
diLiveStock.registerLazySingleton<AuthRepositoryImp>(
() => AuthRepositoryImp(diLiveStock.get<AuthRemoteDataSource>()),
);
await diLiveStock.get<DioRemote>().init();
}

View File

@@ -5,7 +5,7 @@ import 'package:rasadyar_core/core.dart';
import 'package:rasadyar_livestock/data/common/dio_exception_handeler.dart';
import 'package:rasadyar_livestock/data/model/request/login_request/login_request_model.dart';
import 'package:rasadyar_livestock/data/model/response/auth/auth_response_model.dart';
import 'package:rasadyar_livestock/data/repository/auth/auth_repository_imp.dart';
import 'package:rasadyar_livestock/data/repository/auth/auth_repository.dart' show AuthRepository;
import 'package:rasadyar_livestock/injection/live_stock_di.dart';
import 'package:rasadyar_livestock/presentation/routes/app_pages.dart';
import 'package:rasadyar_livestock/presentation/widgets/captcha/logic.dart';
@@ -44,7 +44,7 @@ class AuthLogic extends GetxController with GetTickerProviderStateMixin {
RxInt secondsRemaining = 120.obs;
Timer? _timer;
AuthRepositoryImp authRepository = diLiveStock.get<AuthRepositoryImp>();
AuthRepository authRepository = diLiveStock.get<AuthRepository>();
final Module _module = Get.arguments;

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:rasadyar_core/core.dart';
import 'package:rasadyar_core/presentation/widget/map/view.dart';
import 'package:rasadyar_livestock/presentation/page/map/widget/map_widget/view.dart';
import 'logic.dart';
class MapPage extends GetView<MapLogic> {
@@ -8,23 +9,6 @@ class MapPage extends GetView<MapLogic> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
MapWidget(
markerWidget: Icon(Icons.pin_drop_rounded),
initOnTap: () {
},
initMarkerWidget: Assets.vec.mapMarkerSvg.svg(
width: 30,
height: 30,
),
),
],
),
);
return Scaffold(body: Stack(children: [MapWidget()]));
}
}

View File

@@ -1,23 +1,17 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_animations/flutter_map_animations.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:latlong2/latlong.dart';
import 'package:rasadyar_core/utils/logger_utils.dart';
import 'package:rasadyar_core/core.dart';
import 'custom_marker.dart';
enum ErrorLocationType { serviceDisabled, permissionDenied, none }
class MapWidgetLogic extends GetxController with GetTickerProviderStateMixin {
Rx<LatLng> currentLocation = LatLng(35.824891, 50.948025).obs;
String tileType = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png';
RxDouble currentZoom = 15.0.obs;
RxList<CustomMarker> markers = <CustomMarker>[].obs;
RxList<LatLng> allMarkers = <LatLng>[].obs;
Rx<MapController> mapController = MapController().obs;
RxList<ErrorLocationType> errorLocationType = RxList();
@@ -25,6 +19,20 @@ class MapWidgetLogic extends GetxController with GetTickerProviderStateMixin {
Timer? _debounceTimer;
RxBool isLoading = false.obs;
RxList<LatLng> markerLocations = <LatLng>[
LatLng(35.824891, 50.948025),
LatLng(35.825000, 50.949000),
LatLng(35.823000, 50.947000),
LatLng(35.826000, 50.950000),
LatLng(35.827000, 50.951000),
LatLng(35.828000, 50.952000),
LatLng(35.829000, 50.953000),
LatLng(35.830000, 50.954000),
LatLng(35.831000, 50.955000),
LatLng(35.832000, 50.956000),
LatLng(35.832000, 50.956055),
].obs;
@override
void onInit() {
super.onInit();
@@ -89,8 +97,7 @@ class MapWidgetLogic extends GetxController with GetTickerProviderStateMixin {
switch (permission) {
case LocationPermission.denied:
final LocationPermission requestResult =
await Geolocator.requestPermission();
final LocationPermission requestResult = await Geolocator.requestPermission();
return requestResult != LocationPermission.denied &&
requestResult != LocationPermission.deniedForever;
@@ -117,9 +124,7 @@ class MapWidgetLogic extends GetxController with GetTickerProviderStateMixin {
final latLng = LatLng(position.latitude, position.longitude);
currentLocation.value = latLng;
markers.add(
CustomMarker(id: -1, point: latLng, ),
);
animatedMapController.animateTo(
dest: latLng,
zoom: 18,
@@ -138,7 +143,7 @@ class MapWidgetLogic extends GetxController with GetTickerProviderStateMixin {
'radius': 1000.0,
});
// markers.addAll(filtered);
// markers.addAll(filtered);
});
}
@@ -150,21 +155,8 @@ class MapWidgetLogic extends GetxController with GetTickerProviderStateMixin {
final center = LatLng(centerLat, centerLng);
final distance = Distance();
return rawMarkers
.where((marker) => distance(center, marker) <= radiusInMeters)
.toList();
return rawMarkers.where((marker) => distance(center, marker) <= radiusInMeters).toList();
}
void addMarker(CustomMarker marker) {
markers.add(marker);
}
void setMarkers(List<CustomMarker> newMarkers) {
markers.value = newMarkers;
}
void clearMarkers() {
markers.clear();
}
}

View File

@@ -0,0 +1,179 @@
import 'package:flutter/material.dart';
import 'package:rasadyar_core/core.dart';
import 'logic.dart';
class MapWidget extends GetView<MapWidgetLogic> {
const MapWidget({super.key});
@override
Widget build(BuildContext context) {
return Expanded(
child: Stack(
fit: StackFit.expand,
children: [
ObxValue((errorType) {
if (errorType.isNotEmpty) {
if (errorType.contains(ErrorLocationType.serviceDisabled)) {
Future.microtask(() {
Get.defaultDialog(
title: 'خطا',
content: const Text('سرویس مکان‌یابی غیرفعال است'),
cancel: ROutlinedElevated(
text: 'بررسی مجدد',
width: 120,
textStyle: AppFonts.yekan16,
onPressed: () async {
var service = await controller.locationServiceEnabled();
if (service) {
controller.errorLocationType.remove(ErrorLocationType.serviceDisabled);
Get.back();
}
// Don't call Get.back() if service is still disabled
},
),
confirm: RElevated(
text: 'روشن کردن',
textStyle: AppFonts.yekan16,
width: 120,
onPressed: () async {
var res = await Geolocator.openLocationSettings();
if (res) {
var service = await controller.locationServiceEnabled();
if (service) {
controller.errorLocationType.remove(ErrorLocationType.serviceDisabled);
Get.back();
}
}
},
),
contentPadding: EdgeInsets.all(8),
onWillPop: () async {
return controller.errorLocationType.isEmpty;
},
barrierDismissible: false,
);
});
} else {
Future.microtask(() {
Get.defaultDialog(
title: 'خطا',
content: const Text(' دسترسی به سرویس مکان‌یابی غیرفعال است'),
cancel: ROutlinedElevated(
text: 'بررسی مجدد',
width: 120,
textStyle: AppFonts.yekan16,
onPressed: () async {
await controller.checkPermission();
},
),
confirm: RElevated(
text: 'اجازه دادن',
textStyle: AppFonts.yekan16,
width: 120,
onPressed: () async {
var res = await controller.checkPermission(request: true);
if (res) {
controller.errorLocationType.remove(ErrorLocationType.permissionDenied);
Get.back();
}
},
),
contentPadding: EdgeInsets.all(8),
onWillPop: () async {
return controller.errorLocationType.isEmpty;
},
barrierDismissible: false,
);
});
}
}
return const SizedBox.shrink();
}, controller.errorLocationType),
ObxValue((currentLocation) {
return FlutterMap(
mapController: controller.animatedMapController.mapController,
options: MapOptions(
initialCenter: currentLocation.value,
interactionOptions: const InteractionOptions(
flags: InteractiveFlag.all & ~InteractiveFlag.rotate,
),
initialZoom: 15,
onPositionChanged: (camera, hasGesture) {
controller.currentZoom.value = camera.zoom;
/* controller.debouncedUpdateVisibleMarkers(
center: camera.center,
zoom: camera.zoom,
);*/
},
),
children: [
TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'ir.mnpc.rasadyar',
),
ObxValue((markers) {
return MarkerClusterLayerWidget(
options: MarkerClusterLayerOptions(
maxClusterRadius: 80,
size: const Size(40, 40),
alignment: Alignment.center,
padding: const EdgeInsets.all(50),
maxZoom: 18,
markers: buildMarkers(markers),
builder: (context, clusterMarkers) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: Colors.blue,
),
child: Center(
child: Text(
clusterMarkers.length.toString(),
style: const TextStyle(color: Colors.white),
),
),
);
},
),
);
}, controller.markerLocations),
],
);
}, controller.currentLocation),
// Uncomment the following lines to enable the search widget
/* Positioned(
top: 10,
left: 20,
right: 20,
child: ObxValue((data) {
if (data.value) {
return SearchWidget(
onSearchChanged: (data) {
controller.baseLogic.searchValue.value = data;
},
);
} else {
return SizedBox.shrink();
}
}, controller.baseLogic.isSearchSelected),
),*/
],
),
);
}
List<Marker> buildMarkers(RxList<LatLng> latLng) => latLng
.map(
(element) => Marker(
point: element,
child: FaIcon(FontAwesomeIcons.locationPin, color: AppColor.error),
),
)
.toList();
}

View File

@@ -1,8 +1,8 @@
import 'package:rasadyar_core/core.dart';
import 'package:rasadyar_core/presentation/widget/map/logic.dart';
import 'package:rasadyar_livestock/presentation/page/auth/logic.dart';
import 'package:rasadyar_livestock/presentation/page/auth/view.dart';
import 'package:rasadyar_livestock/presentation/page/map/logic.dart';
import 'package:rasadyar_livestock/presentation/page/map/widget/map_widget/logic.dart';
import 'package:rasadyar_livestock/presentation/page/profile/logic.dart';
import 'package:rasadyar_livestock/presentation/page/request_tagging/logic.dart';
import 'package:rasadyar_livestock/presentation/page/request_tagging/view.dart';
@@ -38,7 +38,6 @@ sealed class LiveStockPages {
Get.lazyPut(() => ProfileLogic());
Get.lazyPut(() => ProfileLogic());
Get.lazyPut(() => MapWidgetLogic());
Get.lazyPut(() => DraggableBottomSheetController());
}),
children: [
/*GetPage(

View File

@@ -1,14 +1,14 @@
import 'package:flutter/material.dart';
import 'package:rasadyar_core/core.dart';
import 'package:rasadyar_livestock/data/model/response/captcha/captcha_response_model.dart';
import 'package:rasadyar_livestock/data/repository/auth/auth_repository_imp.dart';
import 'package:rasadyar_livestock/data/repository/auth/auth_repository.dart';
import 'package:rasadyar_livestock/injection/live_stock_di.dart';
class CaptchaWidgetLogic extends GetxController with StateMixin<CaptchaResponseModel> {
TextEditingController textController = TextEditingController();
RxnString captchaKey = RxnString();
GlobalKey<FormState> formKey = GlobalKey<FormState>();
AuthRepositoryImp authRepository = diLiveStock.get<AuthRepositoryImp>();
AuthRepository authRepository = diLiveStock.get<AuthRepository>();
@override
void onInit() {