Center map on user location
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 2m28s

This commit is contained in:
Ruslan Bakiev
2026-05-08 20:23:15 +07:00
parent f388b7a3d2
commit 929d3a46d3
11 changed files with 243 additions and 20 deletions

View File

@@ -1,4 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<application
android:label="mapflow"
android:name="${applicationName}"

View File

@@ -24,6 +24,8 @@
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSLocationWhenInUseUsageDescription</key>
<string>MapFlow uses your location to show places around you.</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>

View File

@@ -0,0 +1,32 @@
import 'package:geolocator/geolocator.dart';
import 'package:latlong2/latlong.dart';
class CurrentLocation {
Future<LatLng?> 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<LocationPermission> _resolvePermission() async {
final permission = await Geolocator.checkPermission();
if (permission != LocationPermission.denied) {
return permission;
}
return Geolocator.requestPermission();
}
}

View File

@@ -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<void>(
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()));
}
}

View File

@@ -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<PlaceRecommendation> places;
final String? selectedPlaceId;
final AppUser? currentUser;
final bool hasTelegramAuth;
final LatLng? userCoordinate;
final VoiceReviewDraft reviewDraft;
List<PlaceRecommendation> 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<PlaceState> {
final _api = MapflowApi();
final _location = CurrentLocation();
@override
Future<PlaceState> build() async {
@@ -74,6 +95,7 @@ class PlaceController extends AsyncNotifier<PlaceState> {
selectedPlaceId: null,
currentUser: null,
hasTelegramAuth: false,
userCoordinate: null,
reviewDraft: VoiceReviewDraft(
placeName: '',
duration: Duration.zero,
@@ -84,6 +106,7 @@ class PlaceController extends AsyncNotifier<PlaceState> {
}
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<PlaceState> {
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<PlaceState> {
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',

View File

@@ -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"))
}

View File

@@ -8,5 +8,7 @@
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.personal-information.location</key>
<true/>
</dict>
</plist>

View File

@@ -24,6 +24,8 @@
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key>
<string>$(PRODUCT_COPYRIGHT)</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>MapFlow uses your location to show places around you.</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>

View File

@@ -4,5 +4,7 @@
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.personal-information.location</key>
<true/>
</dict>
</plist>

View File

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

View File

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