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

@@ -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

@@ -0,0 +1,162 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:rasadyar_core/core.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<LatLng> allMarkers = <LatLng>[].obs;
Rx<MapController> mapController = MapController().obs;
RxList<ErrorLocationType> errorLocationType = RxList();
late final AnimatedMapController animatedMapController;
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();
animatedMapController = AnimatedMapController(
vsync: this,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
cancelPreviousAnimations: true,
);
locationServiceEnabled().then((value) {
if (!value) {
errorLocationType.add(ErrorLocationType.serviceDisabled);
}
});
checkPermission().then((value) {
if (!value) {
errorLocationType.add(ErrorLocationType.permissionDenied);
}
});
listenToLocationServiceStatus().listen((event) {
if (!event) {
errorLocationType.add(ErrorLocationType.serviceDisabled);
} else {
errorLocationType.remove(ErrorLocationType.serviceDisabled);
}
});
}
@override
void onReady() {
super.onReady();
determineCurrentPosition();
}
@override
void onClose() {
super.onClose();
_debounceTimer?.cancel();
animatedMapController.dispose();
mapController.close();
}
Stream<bool> listenToLocationServiceStatus() {
return Geolocator.getServiceStatusStream().map((status) {
return status == ServiceStatus.enabled;
});
}
Future<bool> locationServiceEnabled() async {
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
return false;
}
return true;
}
Future<bool> checkPermission({bool request = false}) async {
try {
final LocationPermission permission = await Geolocator.checkPermission();
switch (permission) {
case LocationPermission.denied:
final LocationPermission requestResult = await Geolocator.requestPermission();
return requestResult != LocationPermission.denied &&
requestResult != LocationPermission.deniedForever;
case LocationPermission.deniedForever:
return request ? await Geolocator.openAppSettings() : false;
case LocationPermission.always:
case LocationPermission.whileInUse:
return true;
default:
return false;
}
} catch (e) {
eLog(e);
return await Geolocator.openLocationSettings();
}
}
Future<void> determineCurrentPosition() async {
final position = await Geolocator.getCurrentPosition(
locationSettings: AndroidSettings(accuracy: LocationAccuracy.best),
);
final latLng = LatLng(position.latitude, position.longitude);
currentLocation.value = latLng;
animatedMapController.animateTo(
dest: latLng,
zoom: 18,
curve: Curves.easeInOut,
duration: const Duration(milliseconds: 1500),
);
}
void debouncedUpdateVisibleMarkers({required LatLng center}) {
_debounceTimer?.cancel();
_debounceTimer = Timer(const Duration(milliseconds: 300), () {
final filtered = filterNearbyMarkers({
'markers': allMarkers,
'centerLat': center.latitude,
'centerLng': center.longitude,
'radius': 1000.0,
});
// markers.addAll(filtered);
});
}
List<LatLng> filterNearbyMarkers(Map<String, dynamic> args) {
final List<LatLng> rawMarkers = args['markers'];
final double centerLat = args['centerLat'];
final double centerLng = args['centerLng'];
final double radiusInMeters = args['radius'];
final center = LatLng(centerLat, centerLng);
final distance = Distance();
return rawMarkers.where((marker) => distance(center, marker) <= radiusInMeters).toList();
}
}

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() {