import 'dart:async'; import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:geolocator/geolocator.dart'; import '../services/location_platform_service.dart'; enum UserLocationIssue { servicesDisabled, permissionDenied, permissionDeniedForever, unavailable, } /// Состояние геолокации: позиция или причина отсутствия. /// Запрашивается один раз, кешируется до перезапуска провайдера. class UserLocation { final Position? position; final String? error; // null -- всё ок, иначе причина final UserLocationIssue? issue; final DateTime? updatedAt; const UserLocation({this.position, this.error, this.issue, this.updatedAt}); bool get hasPosition => position != null; bool get needsAppSettings => issue == UserLocationIssue.permissionDeniedForever; bool get needsLocationSettings => issue == UserLocationIssue.servicesDisabled; bool get canRequestPermission => issue == UserLocationIssue.permissionDenied; /// Расстояние в км до точки. Возвращает null если нет позиции /// или у цели нет координат. double? distanceToKm(double? lat, double? lon) { if (position == null || lat == null || lon == null) return null; return calculateDistanceKm( position!.latitude, position!.longitude, lat, lon, ); } } final userLocationProvider = NotifierProvider( () => UserLocationNotifier(), ); final locationPlatformServiceProvider = Provider( (ref) => DeviceLocationPlatformService(), ); class UserLocationNotifier extends Notifier { StreamSubscription? _sub; int _watchers = 0; @override UserLocation build() { ref.onDispose(() { _sub?.cancel(); _sub = null; }); return const UserLocation(); } /// Запросить текущую позицию. Первый вызов проверяет пермишены /// и отдаёт lastKnown мгновенно (если есть). Future fetch() async { if (state.hasPosition) return; await refresh(); } /// Начать непрерывное отслеживание. Вызывать из initState экрана. /// Ref-counted: несколько экранов могут вызвать startWatching, /// стрим остановится только когда все вызовут stopWatching. Future startWatching() async { _watchers++; await _startWatchingIfPossible(); } Future ensureWatchingStarted() async { if (_watchers == 0 || _sub != null) { return; } await _startWatchingIfPossible(); } Future _startWatchingIfPossible() async { final locationService = ref.read(locationPlatformServiceProvider); if (_sub != null) return; final permissionState = await _ensurePermission(requestIfDenied: false); if (!permissionState.isGranted) { state = permissionState.toLocation(); return; } if (!state.hasPosition) { try { final last = await locationService.getLastKnownPosition(); if (last != null) { state = _fromPosition(last); } } catch (_) {} } const settings = LocationSettings( accuracy: LocationAccuracy.low, distanceFilter: 20, ); _sub = locationService .getPositionStream(locationSettings: settings) .listen( (pos) => state = _fromPosition(pos), onError: (e) { debugPrint('Ошибка стрима геолокации: $e'); state = UserLocation( error: 'Не удалось отслеживать позицию: $e', issue: UserLocationIssue.unavailable, updatedAt: state.updatedAt, ); }, ); } /// Остановить отслеживание. Вызывать из dispose экрана. void stopWatching() { _watchers = (_watchers - 1).clamp(0, 999); if (_watchers == 0) { _sub?.cancel(); _sub = null; } } Future refresh() async { final locationService = ref.read(locationPlatformServiceProvider); final permissionState = await _ensurePermission(requestIfDenied: false); if (!permissionState.isGranted) { state = permissionState.toLocation(); return; } try { final last = await locationService.getLastKnownPosition(); if (last != null) { state = _fromPosition(last); return; } final pos = await locationService.getCurrentPosition( locationSettings: const LocationSettings( accuracy: LocationAccuracy.low, timeLimit: Duration(seconds: 10), ), ); state = _fromPosition(pos); } catch (e) { state = UserLocation( error: 'Не удалось получить позицию: $e', issue: UserLocationIssue.unavailable, updatedAt: state.updatedAt, ); } } Future requestPermission() async { final locationService = ref.read(locationPlatformServiceProvider); await locationService.requestPermission(); if (_watchers > 0 && _sub == null) { await _startWatchingIfPossible(); return; } await refresh(); } Future openAppSettings() async { await ref.read(locationPlatformServiceProvider).openAppSettings(); } Future openLocationSettings() async { await ref.read(locationPlatformServiceProvider).openLocationSettings(); } /// Проверить сервис и пермишены. Возвращает null если всё ок, /// иначе строку с причиной ошибки. Future<_LocationPermissionState> _ensurePermission({ required bool requestIfDenied, }) async { final locationService = ref.read(locationPlatformServiceProvider); if (!await locationService.isLocationServiceEnabled()) { return const _LocationPermissionState( issue: UserLocationIssue.servicesDisabled, message: 'Геолокация выключена', ); } var perm = await locationService.checkPermission(); if (perm == LocationPermission.denied && requestIfDenied) { perm = await locationService.requestPermission(); } if (perm == LocationPermission.denied) { return const _LocationPermissionState( issue: UserLocationIssue.permissionDenied, message: 'Нет разрешения на геолокацию', ); } if (perm == LocationPermission.deniedForever) { return const _LocationPermissionState( issue: UserLocationIssue.permissionDeniedForever, message: 'Доступ к геолокации запрещён навсегда', ); } return const _LocationPermissionState(); } UserLocation _fromPosition(Position position) { return UserLocation(position: position, updatedAt: position.timestamp); } } class _LocationPermissionState { final UserLocationIssue? issue; final String? message; const _LocationPermissionState({this.issue, this.message}); bool get isGranted => issue == null; UserLocation toLocation() { return UserLocation(error: message, issue: issue); } } double calculateDistanceKm(double lat1, double lon1, double lat2, double lon2) { const earthRadiusKm = 6371.0; final dLat = _degToRad(lat2 - lat1); final dLon = _degToRad(lon2 - lon1); final a = math.sin(dLat / 2) * math.sin(dLat / 2) + math.cos(_degToRad(lat1)) * math.cos(_degToRad(lat2)) * math.sin(dLon / 2) * math.sin(dLon / 2); final c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)); return earthRadiusKm * c; } double _degToRad(double deg) => deg * (math.pi / 180); /// Форматирование расстояния в человекочитаемый вид String formatDistance(double km) { if (km < 1.0) { return '${(km * 1000).round()} м'; } else if (km < 10.0) { return '${km.toStringAsFixed(1)} км'; } else { return '${km.round()} км'; } }