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

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