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 PlaceLoadStatus { loading, ready, failure }
|
||||||
|
|
||||||
|
enum LocationResolutionStatus { resolving, resolved, unavailable }
|
||||||
|
|
||||||
class PlaceState {
|
class PlaceState {
|
||||||
const PlaceState({
|
const PlaceState({
|
||||||
required this.selectedTrait,
|
required this.selectedTrait,
|
||||||
@@ -21,6 +23,7 @@ class PlaceState {
|
|||||||
required this.currentUser,
|
required this.currentUser,
|
||||||
required this.hasTelegramAuth,
|
required this.hasTelegramAuth,
|
||||||
required this.userCoordinate,
|
required this.userCoordinate,
|
||||||
|
required this.locationStatus,
|
||||||
required this.reviewDraft,
|
required this.reviewDraft,
|
||||||
required this.voiceFilterTranscript,
|
required this.voiceFilterTranscript,
|
||||||
required this.voiceFilterTags,
|
required this.voiceFilterTags,
|
||||||
@@ -34,6 +37,7 @@ class PlaceState {
|
|||||||
final AppUser? currentUser;
|
final AppUser? currentUser;
|
||||||
final bool hasTelegramAuth;
|
final bool hasTelegramAuth;
|
||||||
final LatLng? userCoordinate;
|
final LatLng? userCoordinate;
|
||||||
|
final LocationResolutionStatus locationStatus;
|
||||||
final VoiceReviewDraft reviewDraft;
|
final VoiceReviewDraft reviewDraft;
|
||||||
final String voiceFilterTranscript;
|
final String voiceFilterTranscript;
|
||||||
final List<String> voiceFilterTags;
|
final List<String> voiceFilterTags;
|
||||||
@@ -75,6 +79,7 @@ class PlaceState {
|
|||||||
Object? currentUser = _unset,
|
Object? currentUser = _unset,
|
||||||
bool? hasTelegramAuth,
|
bool? hasTelegramAuth,
|
||||||
Object? userCoordinate = _unset,
|
Object? userCoordinate = _unset,
|
||||||
|
LocationResolutionStatus? locationStatus,
|
||||||
VoiceReviewDraft? reviewDraft,
|
VoiceReviewDraft? reviewDraft,
|
||||||
String? voiceFilterTranscript,
|
String? voiceFilterTranscript,
|
||||||
List<String>? voiceFilterTags,
|
List<String>? voiceFilterTags,
|
||||||
@@ -94,6 +99,7 @@ class PlaceState {
|
|||||||
userCoordinate: identical(userCoordinate, _unset)
|
userCoordinate: identical(userCoordinate, _unset)
|
||||||
? this.userCoordinate
|
? this.userCoordinate
|
||||||
: userCoordinate as LatLng?,
|
: userCoordinate as LatLng?,
|
||||||
|
locationStatus: locationStatus ?? this.locationStatus,
|
||||||
reviewDraft: reviewDraft ?? this.reviewDraft,
|
reviewDraft: reviewDraft ?? this.reviewDraft,
|
||||||
voiceFilterTranscript:
|
voiceFilterTranscript:
|
||||||
voiceFilterTranscript ?? this.voiceFilterTranscript,
|
voiceFilterTranscript ?? this.voiceFilterTranscript,
|
||||||
@@ -169,6 +175,7 @@ class PlaceCubit extends Cubit<PlaceViewState> {
|
|||||||
currentUser: currentUser,
|
currentUser: currentUser,
|
||||||
hasTelegramAuth: _authRepository.hasTelegramAuth,
|
hasTelegramAuth: _authRepository.hasTelegramAuth,
|
||||||
userCoordinate: null,
|
userCoordinate: null,
|
||||||
|
locationStatus: LocationResolutionStatus.resolving,
|
||||||
reviewDraft: _emptyDraft,
|
reviewDraft: _emptyDraft,
|
||||||
voiceFilterTranscript: '',
|
voiceFilterTranscript: '',
|
||||||
voiceFilterTags: const [],
|
voiceFilterTags: const [],
|
||||||
@@ -358,12 +365,21 @@ class PlaceCubit extends Cubit<PlaceViewState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _resolveLocation() async {
|
Future<void> _resolveLocation() async {
|
||||||
final coordinate = await _location.resolve().catchError((Object _) => null);
|
final coordinate = await _location.resolve();
|
||||||
final value = state.placeState;
|
final value = state.placeState;
|
||||||
if (value == null || coordinate == null) {
|
if (value == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
emit(PlaceViewState.ready(value.copyWith(userCoordinate: coordinate)));
|
emit(
|
||||||
|
PlaceViewState.ready(
|
||||||
|
value.copyWith(
|
||||||
|
userCoordinate: coordinate,
|
||||||
|
locationStatus: coordinate == null
|
||||||
|
? LocationResolutionStatus.unavailable
|
||||||
|
: LocationResolutionStatus.resolved,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaceState _requireReady() {
|
PlaceState _requireReady() {
|
||||||
@@ -382,6 +398,7 @@ class PlaceCubit extends Cubit<PlaceViewState> {
|
|||||||
currentUser: null,
|
currentUser: null,
|
||||||
hasTelegramAuth: hasTelegramAuth,
|
hasTelegramAuth: hasTelegramAuth,
|
||||||
userCoordinate: null,
|
userCoordinate: null,
|
||||||
|
locationStatus: LocationResolutionStatus.unavailable,
|
||||||
reviewDraft: _emptyDraft,
|
reviewDraft: _emptyDraft,
|
||||||
voiceFilterTranscript: '',
|
voiceFilterTranscript: '',
|
||||||
voiceFilterTags: const [],
|
voiceFilterTags: const [],
|
||||||
|
|||||||
@@ -329,18 +329,35 @@ class _MapboxMapLayer extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MapboxMapLayerState extends State<_MapboxMapLayer> {
|
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;
|
mbx.MapboxMap? _mapboxMap;
|
||||||
var _mapLoaded = false;
|
var _mapLoaded = false;
|
||||||
var _didInitialFlyIn = false;
|
var _didInitialFlyIn = false;
|
||||||
var _cameraAnimationSequence = 0;
|
var _cameraAnimationSequence = 0;
|
||||||
var _projectionVersion = 0;
|
var _projectionVersion = 0;
|
||||||
Timer? _initialFlyInTimer;
|
|
||||||
LatLng? _lastFocusCenter;
|
LatLng? _lastFocusCenter;
|
||||||
List<_ProjectedMarker> _projectedPlaces = const [];
|
List<_ProjectedMarker> _projectedPlaces = const [];
|
||||||
Offset? _projectedUserCoordinate;
|
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
|
@override
|
||||||
void didUpdateWidget(covariant _MapboxMapLayer oldWidget) {
|
void didUpdateWidget(covariant _MapboxMapLayer oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
@@ -355,12 +372,8 @@ class _MapboxMapLayerState extends State<_MapboxMapLayer> {
|
|||||||
children: [
|
children: [
|
||||||
mbx.MapWidget(
|
mbx.MapWidget(
|
||||||
styleUri: _mapboxStyleUri,
|
styleUri: _mapboxStyleUri,
|
||||||
viewport: mbx.CameraViewportState(
|
viewport: _initialViewport,
|
||||||
center: _mapboxPoint(_worldCenter),
|
viewportController: _viewportController,
|
||||||
zoom: 0.48,
|
|
||||||
bearing: 0,
|
|
||||||
pitch: 0,
|
|
||||||
),
|
|
||||||
onMapCreated: _onMapCreated,
|
onMapCreated: _onMapCreated,
|
||||||
onStyleLoadedListener: (_) {
|
onStyleLoadedListener: (_) {
|
||||||
if (!kIsWeb) {
|
if (!kIsWeb) {
|
||||||
@@ -404,7 +417,7 @@ class _MapboxMapLayerState extends State<_MapboxMapLayer> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_initialFlyInTimer?.cancel();
|
_viewportController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,46 +442,65 @@ class _MapboxMapLayerState extends State<_MapboxMapLayer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_lastFocusCenter = focusCenter;
|
_lastFocusCenter = focusCenter;
|
||||||
final camera = mbx.CameraOptions(
|
final camera = _cameraViewport(
|
||||||
center: _mapboxPoint(focusCenter),
|
focusCenter,
|
||||||
zoom: 15.0,
|
zoom: 15.0,
|
||||||
bearing: -12,
|
bearing: -12,
|
||||||
pitch: 60,
|
pitch: 60,
|
||||||
);
|
);
|
||||||
if (_didInitialFlyIn) {
|
if (_didInitialFlyIn) {
|
||||||
_cameraAnimationSequence++;
|
_cameraAnimationSequence++;
|
||||||
unawaited(
|
_viewportController.moveTo(
|
||||||
mapboxMap.easeTo(camera, mbx.MapAnimationOptions(duration: 650)),
|
camera,
|
||||||
|
transition: const mbx.EasingViewportTransition(
|
||||||
|
duration: Duration(milliseconds: 650),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_didInitialFlyIn = true;
|
_didInitialFlyIn = true;
|
||||||
final animationSequence = ++_cameraAnimationSequence;
|
final animationSequence = ++_cameraAnimationSequence;
|
||||||
unawaited(
|
_viewportController.moveTo(
|
||||||
mapboxMap.easeTo(
|
_cameraViewport(
|
||||||
mbx.CameraOptions(
|
_targetOrbitCenter(focusCenter),
|
||||||
center: _mapboxPoint(_targetOrbitCenter(focusCenter)),
|
zoom: 0.72,
|
||||||
zoom: 0.72,
|
bearing: _targetOrbitBearing(focusCenter),
|
||||||
bearing: _targetOrbitBearing(focusCenter),
|
pitch: 0,
|
||||||
pitch: 0,
|
),
|
||||||
),
|
transition: const mbx.EasingViewportTransition(
|
||||||
mbx.MapAnimationOptions(duration: 850),
|
duration: Duration(milliseconds: 850),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
_initialFlyInTimer?.cancel();
|
Timer(const Duration(milliseconds: 520), () {
|
||||||
_initialFlyInTimer = Timer(const Duration(milliseconds: 520), () {
|
|
||||||
if (!mounted ||
|
if (!mounted ||
|
||||||
animationSequence != _cameraAnimationSequence ||
|
animationSequence != _cameraAnimationSequence ||
|
||||||
_mapboxMap != mapboxMap) {
|
_mapboxMap != mapboxMap) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
unawaited(
|
_viewportController.moveTo(
|
||||||
mapboxMap.flyTo(camera, mbx.MapAnimationOptions(duration: 2800)),
|
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) {
|
LatLng _targetOrbitCenter(LatLng target) {
|
||||||
final latitude = (target.latitude * 0.18).clamp(-18.0, 18.0);
|
final latitude = (target.latitude * 0.18).clamp(-18.0, 18.0);
|
||||||
return LatLng(latitude, target.longitude);
|
return LatLng(latitude, target.longitude);
|
||||||
@@ -2396,12 +2428,7 @@ class _AddExperienceFlowState extends State<AddExperienceFlow> {
|
|||||||
await _startRecording();
|
await _startRecording();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<PlaceRecommendation>> _loadNearbyPlaces() async {
|
Future<List<PlaceRecommendation>> _loadNearbyPlaces(LatLng coordinate) async {
|
||||||
final coordinate = widget.coordinate;
|
|
||||||
if (coordinate == null) {
|
|
||||||
return const [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return context
|
return context
|
||||||
.read<PlacesRepository>()
|
.read<PlacesRepository>()
|
||||||
.fetchNearbyPlaces(
|
.fetchNearbyPlaces(
|
||||||
@@ -2490,9 +2517,29 @@ class _AddExperienceFlowState extends State<AddExperienceFlow> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final tokens = context.mapflowTokens;
|
final tokens = context.mapflowTokens;
|
||||||
final controller = context.read<PlaceCubit>();
|
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(
|
final hasTelegramAuth = context.select(
|
||||||
(PlaceCubit cubit) => cubit.state.placeState?.hasTelegramAuth ?? false,
|
(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)
|
final informationProgress = (_informationUnits / _minimumInformationUnits)
|
||||||
.clamp(0.0, 1.0);
|
.clamp(0.0, 1.0);
|
||||||
final content = switch (_step) {
|
final content = switch (_step) {
|
||||||
@@ -2512,19 +2559,24 @@ class _AddExperienceFlowState extends State<AddExperienceFlow> {
|
|||||||
await _stopRecording();
|
await _stopRecording();
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
_nearbyPlacesFuture = _loadNearbyPlaces();
|
_nearbyPlacesFuture = effectiveCoordinate == null
|
||||||
|
? null
|
||||||
|
: _loadNearbyPlaces(effectiveCoordinate);
|
||||||
_step = 1;
|
_step = 1;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
1 => _PlaceStep(
|
1 => _PlaceStep(
|
||||||
placesFuture: _nearbyPlacesFuture,
|
placesFuture: _nearbyPlacesFuture,
|
||||||
hasCoordinate: widget.coordinate != null,
|
hasCoordinate: effectiveCoordinate != null,
|
||||||
|
coordinateResolving: coordinateResolving,
|
||||||
radiusMeters: _nearbyPlaceRadiusMeters,
|
radiusMeters: _nearbyPlaceRadiusMeters,
|
||||||
isSubmitting: _submitting,
|
isSubmitting: _submitting,
|
||||||
onRetry: () {
|
onRetry: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
_nearbyPlacesFuture = _loadNearbyPlaces();
|
_nearbyPlacesFuture = effectiveCoordinate == null
|
||||||
|
? null
|
||||||
|
: _loadNearbyPlaces(effectiveCoordinate);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onSelect: (place) async {
|
onSelect: (place) async {
|
||||||
@@ -2626,6 +2678,7 @@ class _PlaceStep extends StatelessWidget {
|
|||||||
const _PlaceStep({
|
const _PlaceStep({
|
||||||
required this.placesFuture,
|
required this.placesFuture,
|
||||||
required this.hasCoordinate,
|
required this.hasCoordinate,
|
||||||
|
required this.coordinateResolving,
|
||||||
required this.radiusMeters,
|
required this.radiusMeters,
|
||||||
required this.isSubmitting,
|
required this.isSubmitting,
|
||||||
required this.onRetry,
|
required this.onRetry,
|
||||||
@@ -2634,6 +2687,7 @@ class _PlaceStep extends StatelessWidget {
|
|||||||
|
|
||||||
final Future<List<PlaceRecommendation>>? placesFuture;
|
final Future<List<PlaceRecommendation>>? placesFuture;
|
||||||
final bool hasCoordinate;
|
final bool hasCoordinate;
|
||||||
|
final bool coordinateResolving;
|
||||||
final int radiusMeters;
|
final int radiusMeters;
|
||||||
final bool isSubmitting;
|
final bool isSubmitting;
|
||||||
final VoidCallback onRetry;
|
final VoidCallback onRetry;
|
||||||
@@ -2658,11 +2712,17 @@ class _PlaceStep extends StatelessWidget {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: placesFuture == null
|
child: placesFuture == null
|
||||||
? _PlaceUnavailable(
|
? coordinateResolving
|
||||||
icon: Icons.location_off_outlined,
|
? Center(
|
||||||
message: 'Нет геопозиции',
|
child: CircularProgressIndicator(
|
||||||
tokens: tokens,
|
color: tokens.voiceAccent,
|
||||||
)
|
),
|
||||||
|
)
|
||||||
|
: _PlaceUnavailable(
|
||||||
|
icon: Icons.location_off_outlined,
|
||||||
|
message: 'Нет геопозиции',
|
||||||
|
tokens: tokens,
|
||||||
|
)
|
||||||
: FutureBuilder<List<PlaceRecommendation>>(
|
: FutureBuilder<List<PlaceRecommendation>>(
|
||||||
future: placesFuture,
|
future: placesFuture,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
|
|||||||
Reference in New Issue
Block a user