From 929d3a46d314e40602079c4ffebe3e4885e5d3ef Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev <572431+veikab@users.noreply.github.com> Date: Fri, 8 May 2026 20:23:15 +0700 Subject: [PATCH] Center map on user location --- android/app/src/main/AndroidManifest.xml | 3 + ios/Runner/Info.plist | 2 + lib/location/current_location.dart | 32 +++++ lib/screens/mapflow_shell.dart | 61 ++++++--- lib/state/place_controller.dart | 29 +++- macos/Flutter/GeneratedPluginRegistrant.swift | 4 + macos/Runner/DebugProfile.entitlements | 2 + macos/Runner/Info.plist | 2 + macos/Runner/Release.entitlements | 2 + pubspec.lock | 125 ++++++++++++++++++ pubspec.yaml | 1 + 11 files changed, 243 insertions(+), 20 deletions(-) create mode 100644 lib/location/current_location.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 35bcab6..a8ce98a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,7 @@ + + + $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + NSLocationWhenInUseUsageDescription + MapFlow uses your location to show places around you. UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/lib/location/current_location.dart b/lib/location/current_location.dart new file mode 100644 index 0000000..ba6dae6 --- /dev/null +++ b/lib/location/current_location.dart @@ -0,0 +1,32 @@ +import 'package:geolocator/geolocator.dart'; +import 'package:latlong2/latlong.dart'; + +class CurrentLocation { + Future resolve() async { + final serviceEnabled = await Geolocator.isLocationServiceEnabled(); + if (!serviceEnabled) { + return null; + } + + final permission = await _resolvePermission(); + if (permission != LocationPermission.whileInUse && + permission != LocationPermission.always) { + return null; + } + + final position = await Geolocator.getCurrentPosition( + locationSettings: const LocationSettings(accuracy: LocationAccuracy.high), + ); + + return LatLng(position.latitude, position.longitude); + } + + Future _resolvePermission() async { + final permission = await Geolocator.checkPermission(); + if (permission != LocationPermission.denied) { + return permission; + } + + return Geolocator.requestPermission(); + } +} diff --git a/lib/screens/mapflow_shell.dart b/lib/screens/mapflow_shell.dart index b78be6b..431cd02 100644 --- a/lib/screens/mapflow_shell.dart +++ b/lib/screens/mapflow_shell.dart @@ -39,13 +39,15 @@ class MapflowShell extends ConsumerWidget { class _MapContent extends ConsumerWidget { const _MapContent({required this.state}); - static const _center = LatLng(10.7718, 106.6982); + static const _fallbackCenter = LatLng(10.7718, 106.6982); final PlaceState state; @override Widget build(BuildContext context, WidgetRef ref) { final selected = state.selectedPlace; + final userCoordinate = state.userCoordinate; + final mapCenter = userCoordinate ?? selected?.coordinate ?? _fallbackCenter; final availableTraits = { for (final place in state.recommendations) ...place.traits, }.toList(); @@ -55,7 +57,7 @@ class _MapContent extends ConsumerWidget { children: [ FlutterMap( options: MapOptions( - initialCenter: selected?.coordinate ?? _center, + initialCenter: mapCenter, initialZoom: 14.2, minZoom: 3, maxZoom: 18, @@ -76,6 +78,13 @@ class _MapContent extends ConsumerWidget { .selectPlace(place.id), ), ), + if (userCoordinate != null) + Marker( + width: 30, + height: 30, + point: userCoordinate, + child: const _UserLocationMarker(), + ), ], ), const _MapAttribution(), @@ -122,7 +131,10 @@ class _MapContent extends ConsumerWidget { child: Padding( padding: const EdgeInsets.only(right: 12), child: FloatingActionButton( - onPressed: () => _openAddFlow(context, selected?.coordinate), + onPressed: () => _openAddFlow( + context, + userCoordinate ?? selected?.coordinate, + ), child: const Icon(Icons.add_location_alt_outlined), ), ), @@ -138,7 +150,7 @@ class _MapContent extends ConsumerWidget { MaterialPageRoute( fullscreenDialog: true, builder: (_) => AddExperienceFlow( - coordinate: coordinate ?? _center, + coordinate: coordinate ?? _fallbackCenter, hasTelegramAuth: state.hasTelegramAuth, ), ), @@ -146,6 +158,32 @@ class _MapContent extends ConsumerWidget { } } +class _UserLocationMarker extends StatelessWidget { + const _UserLocationMarker(); + + @override + Widget build(BuildContext context) { + final color = Theme.of(context).colorScheme.primary; + return DecoratedBox( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: color.withValues(alpha: 0.18), + ), + child: Center( + child: Container( + width: 14, + height: 14, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: color, + border: Border.all(color: Colors.white, width: 3), + ), + ), + ), + ); + } +} + class _UserAvatar extends StatelessWidget { const _UserAvatar({required this.user, required this.onLogout}); @@ -403,20 +441,7 @@ class _MapLoading extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( - body: Stack( - children: [ - FlutterMap( - options: const MapOptions( - initialCenter: LatLng(10.7718, 106.6982), - initialZoom: 14.2, - ), - children: [const _BaseMapTileLayer(), const _MapAttribution()], - ), - const Center(child: CircularProgressIndicator()), - ], - ), - ); + return const Scaffold(body: Center(child: CircularProgressIndicator())); } } diff --git a/lib/state/place_controller.dart b/lib/state/place_controller.dart index aa5d95c..e78ef33 100644 --- a/lib/state/place_controller.dart +++ b/lib/state/place_controller.dart @@ -2,6 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:latlong2/latlong.dart'; import '../api/mapflow_api.dart'; +import '../location/current_location.dart'; import '../models/place_models.dart'; final placeControllerProvider = @@ -14,14 +15,18 @@ class PlaceState { required this.selectedPlaceId, required this.currentUser, required this.hasTelegramAuth, + required this.userCoordinate, required this.reviewDraft, }); + static final _distance = Distance(); + final PlaceTrait selectedTrait; final List places; final String? selectedPlaceId; final AppUser? currentUser; final bool hasTelegramAuth; + final LatLng? userCoordinate; final VoiceReviewDraft reviewDraft; List get recommendations { @@ -29,7 +34,20 @@ class PlaceState { ..sort((a, b) { final aScore = a.traits.contains(selectedTrait) ? 1 : 0; final bScore = b.traits.contains(selectedTrait) ? 1 : 0; - return bScore.compareTo(aScore); + final scoreOrder = bScore.compareTo(aScore); + if (scoreOrder != 0) { + return scoreOrder; + } + + final coordinate = userCoordinate; + if (coordinate == null) { + return 0; + } + + return _distance( + coordinate, + a.coordinate, + ).compareTo(_distance(coordinate, b.coordinate)); }); return ranked.take(4).toList(); } @@ -49,6 +67,7 @@ class PlaceState { String? selectedPlaceId, AppUser? currentUser, bool? hasTelegramAuth, + LatLng? userCoordinate, VoiceReviewDraft? reviewDraft, }) { return PlaceState( @@ -57,6 +76,7 @@ class PlaceState { selectedPlaceId: selectedPlaceId ?? this.selectedPlaceId, currentUser: currentUser ?? this.currentUser, hasTelegramAuth: hasTelegramAuth ?? this.hasTelegramAuth, + userCoordinate: userCoordinate ?? this.userCoordinate, reviewDraft: reviewDraft ?? this.reviewDraft, ); } @@ -64,6 +84,7 @@ class PlaceState { class PlaceController extends AsyncNotifier { final _api = MapflowApi(); + final _location = CurrentLocation(); @override Future build() async { @@ -74,6 +95,7 @@ class PlaceController extends AsyncNotifier { selectedPlaceId: null, currentUser: null, hasTelegramAuth: false, + userCoordinate: null, reviewDraft: VoiceReviewDraft( placeName: '', duration: Duration.zero, @@ -84,6 +106,7 @@ class PlaceController extends AsyncNotifier { } final currentUser = await _api.authenticateTelegram(); + final userCoordinate = await _location.resolve(); final places = await _api.fetchPlaces(); return PlaceState( selectedTrait: PlaceTrait.calm, @@ -91,6 +114,7 @@ class PlaceController extends AsyncNotifier { selectedPlaceId: places.isEmpty ? null : places.first.id, currentUser: currentUser, hasTelegramAuth: _api.hasTelegramAuth, + userCoordinate: userCoordinate, reviewDraft: const VoiceReviewDraft( placeName: '', duration: Duration.zero, @@ -147,7 +171,8 @@ class PlaceController extends AsyncNotifier { final placeName = draft.placeName.trim().isEmpty ? 'Место на карте' : draft.placeName.trim(); - final point = coordinate ?? const LatLng(10.7729, 106.7004); + final point = + coordinate ?? value.userCoordinate ?? const LatLng(10.7729, 106.7004); await _api.createVoiceExperience( googlePlaceId: 'manual-${point.latitude}-${point.longitude}-$placeName', diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..d7ee0bc 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,10 @@ import FlutterMacOS import Foundation +import geolocator_apple +import package_info_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) } diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements index dddb8a3..308e7d2 100644 --- a/macos/Runner/DebugProfile.entitlements +++ b/macos/Runner/DebugProfile.entitlements @@ -8,5 +8,7 @@ com.apple.security.network.server + com.apple.security.personal-information.location + diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist index 4789daa..1e5c157 100644 --- a/macos/Runner/Info.plist +++ b/macos/Runner/Info.plist @@ -24,6 +24,8 @@ $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright $(PRODUCT_COPYRIGHT) + NSLocationWhenInUseUsageDescription + MapFlow uses your location to show places around you. NSMainNibFile MainMenu NSPrincipalClass diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements index 852fa1a..9148921 100644 --- a/macos/Runner/Release.entitlements +++ b/macos/Runner/Release.entitlements @@ -4,5 +4,7 @@ com.apple.security.app-sandbox + com.apple.security.personal-information.location + diff --git a/pubspec.lock b/pubspec.lock index d1c1291..a051bc9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -137,6 +137,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + dbus: + dependency: transitive + description: + name: dbus + sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 + url: "https://pub.dev" + source: hosted + version: "0.7.12" fake_async: dependency: transitive description: @@ -203,6 +211,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" frontend_server_client: dependency: transitive description: @@ -211,6 +224,70 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + geoclue: + dependency: transitive + description: + name: geoclue + sha256: c2a998c77474fc57aa00c6baa2928e58f4b267649057a1c76738656e9dbd2a7f + url: "https://pub.dev" + source: hosted + version: "0.1.1" + geolocator: + dependency: "direct main" + description: + name: geolocator + sha256: "79939537046c9025be47ec645f35c8090ecadb6fe98eba146a0d25e8c1357516" + url: "https://pub.dev" + source: hosted + version: "14.0.2" + geolocator_android: + dependency: transitive + description: + name: geolocator_android + sha256: "179c3cb66dfa674fc9ccbf2be872a02658724d1c067634e2c427cf6df7df901a" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + geolocator_apple: + dependency: transitive + description: + name: geolocator_apple + sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22 + url: "https://pub.dev" + source: hosted + version: "2.3.13" + geolocator_linux: + dependency: transitive + description: + name: geolocator_linux + sha256: d64112a205931926f4363bb6bd48f14cb38e7326833041d170615586cd143797 + url: "https://pub.dev" + source: hosted + version: "0.2.4" + geolocator_platform_interface: + dependency: transitive + description: + name: geolocator_platform_interface + sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67" + url: "https://pub.dev" + source: hosted + version: "4.2.6" + geolocator_web: + dependency: transitive + description: + name: geolocator_web + sha256: b1ae9bdfd90f861fde8fd4f209c37b953d65e92823cb73c7dee1fa021b06f172 + url: "https://pub.dev" + source: hosted + version: "4.1.3" + geolocator_windows: + dependency: transitive + description: + name: geolocator_windows + sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6" + url: "https://pub.dev" + source: hosted + version: "0.2.5" glob: dependency: transitive description: @@ -219,6 +296,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" + gsettings: + dependency: transitive + description: + name: gsettings + sha256: "1b0ce661f5436d2db1e51f3c4295a49849f03d304003a7ba177d01e3a858249c" + url: "https://pub.dev" + source: hosted + version: "0.2.8" hooks: dependency: transitive description: @@ -411,6 +496,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + sha256: "468c26b4254ab01979fa5e4a98cb343ea3631b9acee6f21028997419a80e1a20" + url: "https://pub.dev" + source: hosted + version: "9.0.1" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + url: "https://pub.dev" + source: hosted + version: "3.2.1" path: dependency: transitive description: @@ -467,6 +568,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" platform: dependency: transitive description: @@ -744,6 +853,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" wkt_parser: dependency: transitive description: @@ -760,6 +877,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 282fe5c..0b9d650 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,6 +39,7 @@ dependencies: latlong2: ^0.9.1 http: ^1.6.0 web: ^1.1.1 + geolocator: ^14.0.2 dev_dependencies: flutter_test: