diff --git a/lib/screens/mapflow_shell.dart b/lib/screens/mapflow_shell.dart index b0fb882..b8f0570 100644 --- a/lib/screens/mapflow_shell.dart +++ b/lib/screens/mapflow_shell.dart @@ -53,9 +53,7 @@ class _MapContent extends ConsumerWidget { final selected = state.selectedPlace; final userCoordinate = state.userCoordinate; final mapCenter = userCoordinate ?? selected?.coordinate ?? _fallbackCenter; - final availableTraits = { - for (final place in state.recommendations) ...place.traits, - }.toList(); + final availableTraits = PlaceTrait.values; return Scaffold( body: Stack( @@ -121,25 +119,24 @@ class _MapContent extends ConsumerWidget { ), ), ), - if (availableTraits.isNotEmpty) - SafeArea( - child: Align( - alignment: Alignment.topCenter, - child: _TraitBar( - selectedTrait: state.selectedTrait, - traits: availableTraits, - ), - ), - ), Align( alignment: Alignment.bottomCenter, child: SafeArea( top: false, - child: _PlaceCarousel( - places: state.recommendations, - onSelect: (place) => ref - .read(placeControllerProvider.notifier) - .selectPlace(place.id), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _TraitBar( + selectedTrait: state.selectedTrait, + traits: availableTraits, + ), + _PlaceCarousel( + places: state.recommendations, + onSelect: (place) => ref + .read(placeControllerProvider.notifier) + .selectPlace(place.id), + ), + ], ), ), ), @@ -525,7 +522,7 @@ class _MapAttribution extends StatelessWidget { class _TraitBar extends ConsumerWidget { const _TraitBar({required this.selectedTrait, required this.traits}); - final PlaceTrait selectedTrait; + final PlaceTrait? selectedTrait; final List traits; @override @@ -536,16 +533,19 @@ class _TraitBar extends ConsumerWidget { height: 54, child: ListView.separated( scrollDirection: Axis.horizontal, - padding: const EdgeInsets.fromLTRB(68, 8, 12, 0), + padding: const EdgeInsets.fromLTRB(12, 0, 12, 8), itemCount: traits.length, separatorBuilder: (_, _) => const SizedBox(width: 6), itemBuilder: (context, index) { final item = traits[index]; + final selected = item == selectedTrait; return ChoiceChip( avatar: Icon(item.icon, size: 17), label: Text(item.label), - selected: item == selectedTrait, - onSelected: (_) => controller.selectTrait(item), + selected: selected, + onSelected: (_) => selected + ? controller.clearTrait() + : controller.selectTrait(item), backgroundColor: const Color(0xFFFFFBF5), selectedColor: Theme.of(context).colorScheme.primaryContainer, side: BorderSide.none, @@ -710,7 +710,7 @@ class AddExperienceFlow extends ConsumerStatefulWidget { class _AddExperienceFlowState extends ConsumerState { static const _minimumInformationUnits = 16.0; - static const _nearbyPlaceRadiusMeters = 200; + static const _nearbyPlaceRadiusMeters = 50; final _api = MapflowApi(); final _waveController = WaveformRecorderController( @@ -1279,7 +1279,7 @@ class _IntroStep extends StatelessWidget { ), const SizedBox(height: 22), Text( - 'Поделись ощущением от места голосом. Мы разберем запись через AI и удалим аудио после обработки.', + 'Поделись ощущением от места голосом. Нужно быть в заведении или не дальше 50 м. Аудио обработаем и удалим.', textAlign: TextAlign.left, style: Theme.of(context).textTheme.titleLarge?.copyWith( color: Colors.white, diff --git a/lib/state/place_controller.dart b/lib/state/place_controller.dart index 47e512c..820ade2 100644 --- a/lib/state/place_controller.dart +++ b/lib/state/place_controller.dart @@ -8,6 +8,8 @@ import '../models/place_models.dart'; final placeControllerProvider = AsyncNotifierProvider(PlaceController.new); +const _unset = Object(); + class PlaceState { const PlaceState({ required this.selectedTrait, @@ -20,9 +22,8 @@ class PlaceState { }); static final _distance = Distance(); - static const nearbyRecommendationRadiusMeters = 500.0; - final PlaceTrait selectedTrait; + final PlaceTrait? selectedTrait; final List places; final String? selectedPlaceId; final AppUser? currentUser; @@ -31,55 +32,52 @@ class PlaceState { final VoiceReviewDraft reviewDraft; List get recommendations { + final selected = selectedTrait; + final matchingPlaces = selected == null + ? places + : places.where((place) => place.traits.contains(selected)); final coordinate = userCoordinate; if (coordinate == null) { - return const []; + return matchingPlaces.toList(); } - final nearbyPlaces = places.where((place) { - return _distance(coordinate, place.coordinate) <= - nearbyRecommendationRadiusMeters; - }); - - final ranked = [...nearbyPlaces] + final ranked = [...matchingPlaces] ..sort((a, b) { - final aScore = a.traits.contains(selectedTrait) ? 1 : 0; - final bScore = b.traits.contains(selectedTrait) ? 1 : 0; - final scoreOrder = bScore.compareTo(aScore); - if (scoreOrder != 0) { - return scoreOrder; - } - return _distance( coordinate, a.coordinate, ).compareTo(_distance(coordinate, b.coordinate)); }); - return ranked.take(4).toList(); + return ranked; } PlaceRecommendation? get selectedPlace { - for (final place in places) { + final visiblePlaces = recommendations; + for (final place in visiblePlaces) { if (place.id == selectedPlaceId) { return place; } } - return recommendations.isEmpty ? null : recommendations.first; + return visiblePlaces.isEmpty ? null : visiblePlaces.first; } PlaceState copyWith({ - PlaceTrait? selectedTrait, + Object? selectedTrait = _unset, List? places, - String? selectedPlaceId, + Object? selectedPlaceId = _unset, AppUser? currentUser, bool? hasTelegramAuth, LatLng? userCoordinate, VoiceReviewDraft? reviewDraft, }) { return PlaceState( - selectedTrait: selectedTrait ?? this.selectedTrait, + selectedTrait: identical(selectedTrait, _unset) + ? this.selectedTrait + : selectedTrait as PlaceTrait?, places: places ?? this.places, - selectedPlaceId: selectedPlaceId ?? this.selectedPlaceId, + selectedPlaceId: identical(selectedPlaceId, _unset) + ? this.selectedPlaceId + : selectedPlaceId as String?, currentUser: currentUser ?? this.currentUser, hasTelegramAuth: hasTelegramAuth ?? this.hasTelegramAuth, userCoordinate: userCoordinate ?? this.userCoordinate, @@ -96,7 +94,7 @@ class PlaceController extends AsyncNotifier { Future build() async { if (!_api.hasTelegramAuth) { return const PlaceState( - selectedTrait: PlaceTrait.calm, + selectedTrait: null, places: [], selectedPlaceId: null, currentUser: null, @@ -115,7 +113,7 @@ class PlaceController extends AsyncNotifier { final userCoordinate = await _location.resolve(); final places = await _api.fetchPlaces(); return PlaceState( - selectedTrait: PlaceTrait.calm, + selectedTrait: null, places: places, selectedPlaceId: places.isEmpty ? null : places.first.id, currentUser: currentUser, @@ -144,6 +142,14 @@ class PlaceController extends AsyncNotifier { ); } + void clearTrait() { + final value = state.requireValue; + final selectedPlaceId = value.places.isEmpty ? null : value.places.first.id; + state = AsyncData( + value.copyWith(selectedTrait: null, selectedPlaceId: selectedPlaceId), + ); + } + void selectPlace(String placeId) { final value = state.requireValue; state = AsyncData(value.copyWith(selectedPlaceId: placeId));