diff --git a/lib/api/mapflow_api.dart b/lib/api/mapflow_api.dart index 80f6e9e..4a521d7 100644 --- a/lib/api/mapflow_api.dart +++ b/lib/api/mapflow_api.dart @@ -191,6 +191,55 @@ class MapflowApi { }).toList(); } + Future> fetchNearbyPlaces({ + required LatLng coordinate, + required int radiusMeters, + }) async { + final data = await _graphql( + ''' + query NearbyPlaces(\$input: NearbyPlacesInput!) { + nearbyPlaces(input: \$input) { + id + googlePlaceId + name + latitude + longitude + experiences { + id + status + analysis + createdAt + } + } + } + ''', + variables: { + 'input': { + 'latitude': coordinate.latitude, + 'longitude': coordinate.longitude, + 'radiusMeters': radiusMeters, + }, + }, + ); + + final places = data['nearbyPlaces'] as List; + return places.map((item) { + final place = item as Map; + return PlaceRecommendation( + id: place['id'] as String, + googlePlaceId: place['googlePlaceId'] as String, + name: place['name'] as String, + area: '', + photoUrls: const [], + coordinate: LatLng( + (place['latitude'] as num).toDouble(), + (place['longitude'] as num).toDouble(), + ), + traits: _traitsFromExperiences(place['experiences'] as List), + ); + }).toList(); + } + Future createVoiceExperience({ required String googlePlaceId, required String googleName, diff --git a/lib/screens/mapflow_shell.dart b/lib/screens/mapflow_shell.dart index 1f7e303..b3c335b 100644 --- a/lib/screens/mapflow_shell.dart +++ b/lib/screens/mapflow_shell.dart @@ -153,7 +153,7 @@ class _MapContent extends ConsumerWidget { MaterialPageRoute( fullscreenDialog: true, builder: (_) => AddExperienceFlow( - coordinate: coordinate ?? _fallbackCenter, + coordinate: coordinate, hasTelegramAuth: state.hasTelegramAuth, ), ), @@ -711,7 +711,7 @@ class AddExperienceFlow extends ConsumerStatefulWidget { required this.hasTelegramAuth, }); - final LatLng coordinate; + final LatLng? coordinate; final bool hasTelegramAuth; @override @@ -720,10 +720,13 @@ class AddExperienceFlow extends ConsumerStatefulWidget { class _AddExperienceFlowState extends ConsumerState { static const _minimumVoiceSeconds = 30; + static const _nearbyPlaceRadiusMeters = 200; + final _api = MapflowApi(); final _recorder = AudioRecorder(); final _waveSamples = List.filled(64, 0.04); + late final Future> _nearbyPlacesFuture; Timer? _timer; StreamSubscription? _audioStreamSub; var _step = 0; @@ -736,6 +739,7 @@ class _AddExperienceFlowState extends ConsumerState { @override void initState() { super.initState(); + _nearbyPlacesFuture = _loadNearbyPlaces(); } @override @@ -755,6 +759,18 @@ class _AddExperienceFlowState extends ConsumerState { await _startRecording(); } + Future> _loadNearbyPlaces() async { + final coordinate = widget.coordinate; + if (coordinate == null) { + return const []; + } + + return _api.fetchNearbyPlaces( + coordinate: coordinate, + radiusMeters: _nearbyPlaceRadiusMeters, + ); + } + Future _startRecording() async { final hasPermission = await _recorder.hasPermission(); if (!hasPermission) { @@ -833,12 +849,11 @@ class _AddExperienceFlowState extends ConsumerState { @override Widget build(BuildContext context) { final controller = ref.read(placeControllerProvider.notifier); - final places = ref.watch(placeControllerProvider).value?.places ?? []; - final content = switch (_step) { 0 => _IntroStep(onNext: () => setState(() => _step = 1)), 1 => _PlaceStep( - places: places, + placesFuture: _nearbyPlacesFuture, + radiusMeters: _nearbyPlaceRadiusMeters, onSelect: (place) { setState(() { _selectedPlace = place; @@ -949,9 +964,14 @@ class _IntroStep extends StatelessWidget { } class _PlaceStep extends StatelessWidget { - const _PlaceStep({required this.places, required this.onSelect}); + const _PlaceStep({ + required this.placesFuture, + required this.radiusMeters, + required this.onSelect, + }); - final List places; + final Future> placesFuture; + final int radiusMeters; final ValueChanged onSelect; @override @@ -969,33 +989,57 @@ class _PlaceStep extends StatelessWidget { ), const SizedBox(height: 16), Expanded( - child: places.isEmpty - ? const Center( - child: Icon(Icons.location_off_outlined, size: 42), - ) - : ListView.separated( - itemCount: places.length, - separatorBuilder: (_, _) => const SizedBox(height: 10), - itemBuilder: (context, index) { - final place = places[index]; - return ListTile( - onTap: () => onSelect(place), - contentPadding: const EdgeInsets.symmetric( - horizontal: 14, - vertical: 8, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - tileColor: const Color(0xFFFFFBF5), - leading: const Icon(Icons.place_outlined), - title: Text( - place.name, - style: const TextStyle(fontWeight: FontWeight.w800), - ), - ); - }, - ), + child: FutureBuilder>( + future: placesFuture, + builder: (context, snapshot) { + if (snapshot.connectionState != ConnectionState.done) { + return const Center(child: CircularProgressIndicator()); + } + if (snapshot.hasError) { + return const Center( + child: Icon(Icons.error_outline, size: 42), + ); + } + + final places = snapshot.data ?? const []; + if (places.isEmpty) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.location_off_outlined, size: 42), + const SizedBox(height: 10), + Text('Нет мест в $radiusMetersм'), + ], + ), + ); + } + + return ListView.separated( + itemCount: places.length, + separatorBuilder: (_, _) => const SizedBox(height: 10), + itemBuilder: (context, index) { + final place = places[index]; + return ListTile( + onTap: () => onSelect(place), + contentPadding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 8, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + tileColor: const Color(0xFFFFFBF5), + leading: const Icon(Icons.place_outlined), + title: Text( + place.name, + style: const TextStyle(fontWeight: FontWeight.w800), + ), + ); + }, + ); + }, + ), ), ], ), diff --git a/lib/state/place_controller.dart b/lib/state/place_controller.dart index 0323379..031edee 100644 --- a/lib/state/place_controller.dart +++ b/lib/state/place_controller.dart @@ -20,6 +20,7 @@ class PlaceState { }); static final _distance = Distance(); + static const nearbyRecommendationRadiusMeters = 500.0; final PlaceTrait selectedTrait; final List places; @@ -30,7 +31,17 @@ class PlaceState { final VoiceReviewDraft reviewDraft; List get recommendations { - final ranked = [...places] + final coordinate = userCoordinate; + if (coordinate == null) { + return const []; + } + + final nearbyPlaces = places.where((place) { + return _distance(coordinate, place.coordinate) <= + nearbyRecommendationRadiusMeters; + }); + + final ranked = [...nearbyPlaces] ..sort((a, b) { final aScore = a.traits.contains(selectedTrait) ? 1 : 0; final bScore = b.traits.contains(selectedTrait) ? 1 : 0; @@ -39,11 +50,6 @@ class PlaceState { return scoreOrder; } - final coordinate = userCoordinate; - if (coordinate == null) { - return 0; - } - return _distance( coordinate, a.coordinate,