From bb49a13b65d2b86c0c7a0005f361f2dd2cde2fef Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev <572431+veikab@users.noreply.github.com> Date: Wed, 24 Jun 2026 14:19:35 +0700 Subject: [PATCH] Stabilize location driven review flow --- .../mapflow/application/place_cubit.dart | 23 ++- .../mapflow/presentation/mapflow_shell.dart | 140 +++++++++++++----- 2 files changed, 120 insertions(+), 43 deletions(-) diff --git a/lib/features/mapflow/application/place_cubit.dart b/lib/features/mapflow/application/place_cubit.dart index 1361073..da2ec65 100644 --- a/lib/features/mapflow/application/place_cubit.dart +++ b/lib/features/mapflow/application/place_cubit.dart @@ -13,6 +13,8 @@ const _unset = Object(); enum PlaceLoadStatus { loading, ready, failure } +enum LocationResolutionStatus { resolving, resolved, unavailable } + class PlaceState { const PlaceState({ required this.selectedTrait, @@ -21,6 +23,7 @@ class PlaceState { required this.currentUser, required this.hasTelegramAuth, required this.userCoordinate, + required this.locationStatus, required this.reviewDraft, required this.voiceFilterTranscript, required this.voiceFilterTags, @@ -34,6 +37,7 @@ class PlaceState { final AppUser? currentUser; final bool hasTelegramAuth; final LatLng? userCoordinate; + final LocationResolutionStatus locationStatus; final VoiceReviewDraft reviewDraft; final String voiceFilterTranscript; final List voiceFilterTags; @@ -75,6 +79,7 @@ class PlaceState { Object? currentUser = _unset, bool? hasTelegramAuth, Object? userCoordinate = _unset, + LocationResolutionStatus? locationStatus, VoiceReviewDraft? reviewDraft, String? voiceFilterTranscript, List? voiceFilterTags, @@ -94,6 +99,7 @@ class PlaceState { userCoordinate: identical(userCoordinate, _unset) ? this.userCoordinate : userCoordinate as LatLng?, + locationStatus: locationStatus ?? this.locationStatus, reviewDraft: reviewDraft ?? this.reviewDraft, voiceFilterTranscript: voiceFilterTranscript ?? this.voiceFilterTranscript, @@ -169,6 +175,7 @@ class PlaceCubit extends Cubit { currentUser: currentUser, hasTelegramAuth: _authRepository.hasTelegramAuth, userCoordinate: null, + locationStatus: LocationResolutionStatus.resolving, reviewDraft: _emptyDraft, voiceFilterTranscript: '', voiceFilterTags: const [], @@ -358,12 +365,21 @@ class PlaceCubit extends Cubit { } Future _resolveLocation() async { - final coordinate = await _location.resolve().catchError((Object _) => null); + final coordinate = await _location.resolve(); final value = state.placeState; - if (value == null || coordinate == null) { + if (value == null) { return; } - emit(PlaceViewState.ready(value.copyWith(userCoordinate: coordinate))); + emit( + PlaceViewState.ready( + value.copyWith( + userCoordinate: coordinate, + locationStatus: coordinate == null + ? LocationResolutionStatus.unavailable + : LocationResolutionStatus.resolved, + ), + ), + ); } PlaceState _requireReady() { @@ -382,6 +398,7 @@ class PlaceCubit extends Cubit { currentUser: null, hasTelegramAuth: hasTelegramAuth, userCoordinate: null, + locationStatus: LocationResolutionStatus.unavailable, reviewDraft: _emptyDraft, voiceFilterTranscript: '', voiceFilterTags: const [], diff --git a/lib/features/mapflow/presentation/mapflow_shell.dart b/lib/features/mapflow/presentation/mapflow_shell.dart index 2f01aa7..b26c0c3 100644 --- a/lib/features/mapflow/presentation/mapflow_shell.dart +++ b/lib/features/mapflow/presentation/mapflow_shell.dart @@ -329,18 +329,35 @@ class _MapboxMapLayer extends StatefulWidget { } class _MapboxMapLayerState extends State<_MapboxMapLayer> { - static const _worldCenter = LatLng(0, 0); + static const _fallbackCenter = LatLng(16.0544, 108.2022); + final _viewportController = mbx.ViewportController(); + late final mbx.CameraViewportState _initialViewport; mbx.MapboxMap? _mapboxMap; var _mapLoaded = false; var _didInitialFlyIn = false; var _cameraAnimationSequence = 0; var _projectionVersion = 0; - Timer? _initialFlyInTimer; LatLng? _lastFocusCenter; List<_ProjectedMarker> _projectedPlaces = const []; Offset? _projectedUserCoordinate; + @override + void initState() { + super.initState(); + final initialFocus = widget.focusCenter; + _initialViewport = _cameraViewport( + initialFocus ?? _fallbackCenter, + zoom: initialFocus == null ? 11.2 : 15.0, + bearing: initialFocus == null ? 0 : -12, + pitch: initialFocus == null ? 0 : 60, + ); + if (initialFocus != null) { + _didInitialFlyIn = true; + _lastFocusCenter = initialFocus; + } + } + @override void didUpdateWidget(covariant _MapboxMapLayer oldWidget) { super.didUpdateWidget(oldWidget); @@ -355,12 +372,8 @@ class _MapboxMapLayerState extends State<_MapboxMapLayer> { children: [ mbx.MapWidget( styleUri: _mapboxStyleUri, - viewport: mbx.CameraViewportState( - center: _mapboxPoint(_worldCenter), - zoom: 0.48, - bearing: 0, - pitch: 0, - ), + viewport: _initialViewport, + viewportController: _viewportController, onMapCreated: _onMapCreated, onStyleLoadedListener: (_) { if (!kIsWeb) { @@ -404,7 +417,7 @@ class _MapboxMapLayerState extends State<_MapboxMapLayer> { @override void dispose() { - _initialFlyInTimer?.cancel(); + _viewportController.dispose(); super.dispose(); } @@ -429,46 +442,65 @@ class _MapboxMapLayerState extends State<_MapboxMapLayer> { } _lastFocusCenter = focusCenter; - final camera = mbx.CameraOptions( - center: _mapboxPoint(focusCenter), + final camera = _cameraViewport( + focusCenter, zoom: 15.0, bearing: -12, pitch: 60, ); if (_didInitialFlyIn) { _cameraAnimationSequence++; - unawaited( - mapboxMap.easeTo(camera, mbx.MapAnimationOptions(duration: 650)), + _viewportController.moveTo( + camera, + transition: const mbx.EasingViewportTransition( + duration: Duration(milliseconds: 650), + ), ); return; } _didInitialFlyIn = true; final animationSequence = ++_cameraAnimationSequence; - unawaited( - mapboxMap.easeTo( - mbx.CameraOptions( - center: _mapboxPoint(_targetOrbitCenter(focusCenter)), - zoom: 0.72, - bearing: _targetOrbitBearing(focusCenter), - pitch: 0, - ), - mbx.MapAnimationOptions(duration: 850), + _viewportController.moveTo( + _cameraViewport( + _targetOrbitCenter(focusCenter), + zoom: 0.72, + bearing: _targetOrbitBearing(focusCenter), + pitch: 0, + ), + transition: const mbx.EasingViewportTransition( + duration: Duration(milliseconds: 850), ), ); - _initialFlyInTimer?.cancel(); - _initialFlyInTimer = Timer(const Duration(milliseconds: 520), () { + Timer(const Duration(milliseconds: 520), () { if (!mounted || animationSequence != _cameraAnimationSequence || _mapboxMap != mapboxMap) { return; } - unawaited( - mapboxMap.flyTo(camera, mbx.MapAnimationOptions(duration: 2800)), + _viewportController.moveTo( + camera, + transition: const mbx.FlyViewportTransition( + duration: Duration(milliseconds: 2800), + ), ); }); } + mbx.CameraViewportState _cameraViewport( + LatLng center, { + required double zoom, + required double bearing, + required double pitch, + }) { + return mbx.CameraViewportState( + center: _mapboxPoint(center), + zoom: zoom, + bearing: bearing, + pitch: pitch, + ); + } + LatLng _targetOrbitCenter(LatLng target) { final latitude = (target.latitude * 0.18).clamp(-18.0, 18.0); return LatLng(latitude, target.longitude); @@ -2396,12 +2428,7 @@ class _AddExperienceFlowState extends State { await _startRecording(); } - Future> _loadNearbyPlaces() async { - final coordinate = widget.coordinate; - if (coordinate == null) { - return const []; - } - + Future> _loadNearbyPlaces(LatLng coordinate) async { return context .read() .fetchNearbyPlaces( @@ -2490,9 +2517,29 @@ class _AddExperienceFlowState extends State { Widget build(BuildContext context) { final tokens = context.mapflowTokens; final controller = context.read(); + final locationState = context.select((PlaceCubit cubit) { + final state = cubit.state.placeState; + return (coordinate: state?.userCoordinate, status: state?.locationStatus); + }); + final effectiveCoordinate = widget.coordinate ?? locationState.coordinate; + final coordinateResolving = + effectiveCoordinate == null && + locationState.status == LocationResolutionStatus.resolving; final hasTelegramAuth = context.select( (PlaceCubit cubit) => cubit.state.placeState?.hasTelegramAuth ?? false, ); + if (_step == 1 && + _nearbyPlacesFuture == null && + effectiveCoordinate != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted || _step != 1 || _nearbyPlacesFuture != null) { + return; + } + setState(() { + _nearbyPlacesFuture = _loadNearbyPlaces(effectiveCoordinate); + }); + }); + } final informationProgress = (_informationUnits / _minimumInformationUnits) .clamp(0.0, 1.0); final content = switch (_step) { @@ -2512,19 +2559,24 @@ class _AddExperienceFlowState extends State { await _stopRecording(); } setState(() { - _nearbyPlacesFuture = _loadNearbyPlaces(); + _nearbyPlacesFuture = effectiveCoordinate == null + ? null + : _loadNearbyPlaces(effectiveCoordinate); _step = 1; }); }, ), 1 => _PlaceStep( placesFuture: _nearbyPlacesFuture, - hasCoordinate: widget.coordinate != null, + hasCoordinate: effectiveCoordinate != null, + coordinateResolving: coordinateResolving, radiusMeters: _nearbyPlaceRadiusMeters, isSubmitting: _submitting, onRetry: () { setState(() { - _nearbyPlacesFuture = _loadNearbyPlaces(); + _nearbyPlacesFuture = effectiveCoordinate == null + ? null + : _loadNearbyPlaces(effectiveCoordinate); }); }, onSelect: (place) async { @@ -2626,6 +2678,7 @@ class _PlaceStep extends StatelessWidget { const _PlaceStep({ required this.placesFuture, required this.hasCoordinate, + required this.coordinateResolving, required this.radiusMeters, required this.isSubmitting, required this.onRetry, @@ -2634,6 +2687,7 @@ class _PlaceStep extends StatelessWidget { final Future>? placesFuture; final bool hasCoordinate; + final bool coordinateResolving; final int radiusMeters; final bool isSubmitting; final VoidCallback onRetry; @@ -2658,11 +2712,17 @@ class _PlaceStep extends StatelessWidget { const SizedBox(height: 16), Expanded( child: placesFuture == null - ? _PlaceUnavailable( - icon: Icons.location_off_outlined, - message: 'Нет геопозиции', - tokens: tokens, - ) + ? coordinateResolving + ? Center( + child: CircularProgressIndicator( + color: tokens.voiceAccent, + ), + ) + : _PlaceUnavailable( + icon: Icons.location_off_outlined, + message: 'Нет геопозиции', + tokens: tokens, + ) : FutureBuilder>( future: placesFuture, builder: (context, snapshot) {