Use nearby Google places for reviews
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 1m48s

This commit is contained in:
Ruslan Bakiev
2026-05-09 15:19:30 +07:00
parent 02b3521320
commit b93f7ec4ec
3 changed files with 139 additions and 40 deletions

View File

@@ -191,6 +191,55 @@ class MapflowApi {
}).toList();
}
Future<List<PlaceRecommendation>> 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<dynamic>;
return places.map((item) {
final place = item as Map<String, dynamic>;
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<dynamic>),
);
}).toList();
}
Future<void> createVoiceExperience({
required String googlePlaceId,
required String googleName,

View File

@@ -153,7 +153,7 @@ class _MapContent extends ConsumerWidget {
MaterialPageRoute<void>(
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<AddExperienceFlow> {
static const _minimumVoiceSeconds = 30;
static const _nearbyPlaceRadiusMeters = 200;
final _api = MapflowApi();
final _recorder = AudioRecorder();
final _waveSamples = List<double>.filled(64, 0.04);
late final Future<List<PlaceRecommendation>> _nearbyPlacesFuture;
Timer? _timer;
StreamSubscription<Uint8List>? _audioStreamSub;
var _step = 0;
@@ -736,6 +739,7 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
@override
void initState() {
super.initState();
_nearbyPlacesFuture = _loadNearbyPlaces();
}
@override
@@ -755,6 +759,18 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
await _startRecording();
}
Future<List<PlaceRecommendation>> _loadNearbyPlaces() async {
final coordinate = widget.coordinate;
if (coordinate == null) {
return const [];
}
return _api.fetchNearbyPlaces(
coordinate: coordinate,
radiusMeters: _nearbyPlaceRadiusMeters,
);
}
Future<void> _startRecording() async {
final hasPermission = await _recorder.hasPermission();
if (!hasPermission) {
@@ -833,12 +849,11 @@ class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
@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<PlaceRecommendation> places;
final Future<List<PlaceRecommendation>> placesFuture;
final int radiusMeters;
final ValueChanged<PlaceRecommendation> 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<List<PlaceRecommendation>>(
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 <PlaceRecommendation>[];
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),
),
);
},
);
},
),
),
],
),

View File

@@ -20,6 +20,7 @@ class PlaceState {
});
static final _distance = Distance();
static const nearbyRecommendationRadiusMeters = 500.0;
final PlaceTrait selectedTrait;
final List<PlaceRecommendation> places;
@@ -30,7 +31,17 @@ class PlaceState {
final VoiceReviewDraft reviewDraft;
List<PlaceRecommendation> 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,