Stabilize location driven review flow
Build and deploy Flutter Web / build (push) Successful in 6m5s
Build and deploy Flutter Web / build (push) Successful in 6m5s
This commit is contained in:
@@ -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<String> voiceFilterTags;
|
||||
@@ -75,6 +79,7 @@ class PlaceState {
|
||||
Object? currentUser = _unset,
|
||||
bool? hasTelegramAuth,
|
||||
Object? userCoordinate = _unset,
|
||||
LocationResolutionStatus? locationStatus,
|
||||
VoiceReviewDraft? reviewDraft,
|
||||
String? voiceFilterTranscript,
|
||||
List<String>? 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<PlaceViewState> {
|
||||
currentUser: currentUser,
|
||||
hasTelegramAuth: _authRepository.hasTelegramAuth,
|
||||
userCoordinate: null,
|
||||
locationStatus: LocationResolutionStatus.resolving,
|
||||
reviewDraft: _emptyDraft,
|
||||
voiceFilterTranscript: '',
|
||||
voiceFilterTags: const [],
|
||||
@@ -358,12 +365,21 @@ class PlaceCubit extends Cubit<PlaceViewState> {
|
||||
}
|
||||
|
||||
Future<void> _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<PlaceViewState> {
|
||||
currentUser: null,
|
||||
hasTelegramAuth: hasTelegramAuth,
|
||||
userCoordinate: null,
|
||||
locationStatus: LocationResolutionStatus.unavailable,
|
||||
reviewDraft: _emptyDraft,
|
||||
voiceFilterTranscript: '',
|
||||
voiceFilterTags: const [],
|
||||
|
||||
@@ -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<AddExperienceFlow> {
|
||||
await _startRecording();
|
||||
}
|
||||
|
||||
Future<List<PlaceRecommendation>> _loadNearbyPlaces() async {
|
||||
final coordinate = widget.coordinate;
|
||||
if (coordinate == null) {
|
||||
return const [];
|
||||
}
|
||||
|
||||
Future<List<PlaceRecommendation>> _loadNearbyPlaces(LatLng coordinate) async {
|
||||
return context
|
||||
.read<PlacesRepository>()
|
||||
.fetchNearbyPlaces(
|
||||
@@ -2490,9 +2517,29 @@ class _AddExperienceFlowState extends State<AddExperienceFlow> {
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = context.mapflowTokens;
|
||||
final controller = context.read<PlaceCubit>();
|
||||
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<AddExperienceFlow> {
|
||||
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<List<PlaceRecommendation>>? 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<List<PlaceRecommendation>>(
|
||||
future: placesFuture,
|
||||
builder: (context, snapshot) {
|
||||
|
||||
Reference in New Issue
Block a user