Separate browsing filters from review radius
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 4m2s

This commit is contained in:
Ruslan Bakiev
2026-05-14 22:34:59 +07:00
parent 0b1493a02e
commit 697f029ad2
2 changed files with 55 additions and 49 deletions

View File

@@ -53,9 +53,7 @@ class _MapContent extends ConsumerWidget {
final selected = state.selectedPlace; final selected = state.selectedPlace;
final userCoordinate = state.userCoordinate; final userCoordinate = state.userCoordinate;
final mapCenter = userCoordinate ?? selected?.coordinate ?? _fallbackCenter; final mapCenter = userCoordinate ?? selected?.coordinate ?? _fallbackCenter;
final availableTraits = { final availableTraits = PlaceTrait.values;
for (final place in state.recommendations) ...place.traits,
}.toList();
return Scaffold( return Scaffold(
body: Stack( 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( Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: SafeArea( child: SafeArea(
top: false, top: false,
child: _PlaceCarousel( child: Column(
places: state.recommendations, mainAxisSize: MainAxisSize.min,
onSelect: (place) => ref children: [
.read(placeControllerProvider.notifier) _TraitBar(
.selectPlace(place.id), 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 { class _TraitBar extends ConsumerWidget {
const _TraitBar({required this.selectedTrait, required this.traits}); const _TraitBar({required this.selectedTrait, required this.traits});
final PlaceTrait selectedTrait; final PlaceTrait? selectedTrait;
final List<PlaceTrait> traits; final List<PlaceTrait> traits;
@override @override
@@ -536,16 +533,19 @@ class _TraitBar extends ConsumerWidget {
height: 54, height: 54,
child: ListView.separated( child: ListView.separated(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
padding: const EdgeInsets.fromLTRB(68, 8, 12, 0), padding: const EdgeInsets.fromLTRB(12, 0, 12, 8),
itemCount: traits.length, itemCount: traits.length,
separatorBuilder: (_, _) => const SizedBox(width: 6), separatorBuilder: (_, _) => const SizedBox(width: 6),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = traits[index]; final item = traits[index];
final selected = item == selectedTrait;
return ChoiceChip( return ChoiceChip(
avatar: Icon(item.icon, size: 17), avatar: Icon(item.icon, size: 17),
label: Text(item.label), label: Text(item.label),
selected: item == selectedTrait, selected: selected,
onSelected: (_) => controller.selectTrait(item), onSelected: (_) => selected
? controller.clearTrait()
: controller.selectTrait(item),
backgroundColor: const Color(0xFFFFFBF5), backgroundColor: const Color(0xFFFFFBF5),
selectedColor: Theme.of(context).colorScheme.primaryContainer, selectedColor: Theme.of(context).colorScheme.primaryContainer,
side: BorderSide.none, side: BorderSide.none,
@@ -710,7 +710,7 @@ class AddExperienceFlow extends ConsumerStatefulWidget {
class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> { class _AddExperienceFlowState extends ConsumerState<AddExperienceFlow> {
static const _minimumInformationUnits = 16.0; static const _minimumInformationUnits = 16.0;
static const _nearbyPlaceRadiusMeters = 200; static const _nearbyPlaceRadiusMeters = 50;
final _api = MapflowApi(); final _api = MapflowApi();
final _waveController = WaveformRecorderController( final _waveController = WaveformRecorderController(
@@ -1279,7 +1279,7 @@ class _IntroStep extends StatelessWidget {
), ),
const SizedBox(height: 22), const SizedBox(height: 22),
Text( Text(
'Поделись ощущением от места голосом. Мы разберем запись через AI и удалим аудио после обработки.', 'Поделись ощущением от места голосом. Нужно быть в заведении или не дальше 50 м. Аудио обработаем и удалим.',
textAlign: TextAlign.left, textAlign: TextAlign.left,
style: Theme.of(context).textTheme.titleLarge?.copyWith( style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Colors.white, color: Colors.white,

View File

@@ -8,6 +8,8 @@ import '../models/place_models.dart';
final placeControllerProvider = final placeControllerProvider =
AsyncNotifierProvider<PlaceController, PlaceState>(PlaceController.new); AsyncNotifierProvider<PlaceController, PlaceState>(PlaceController.new);
const _unset = Object();
class PlaceState { class PlaceState {
const PlaceState({ const PlaceState({
required this.selectedTrait, required this.selectedTrait,
@@ -20,9 +22,8 @@ class PlaceState {
}); });
static final _distance = Distance(); static final _distance = Distance();
static const nearbyRecommendationRadiusMeters = 500.0;
final PlaceTrait selectedTrait; final PlaceTrait? selectedTrait;
final List<PlaceRecommendation> places; final List<PlaceRecommendation> places;
final String? selectedPlaceId; final String? selectedPlaceId;
final AppUser? currentUser; final AppUser? currentUser;
@@ -31,55 +32,52 @@ class PlaceState {
final VoiceReviewDraft reviewDraft; final VoiceReviewDraft reviewDraft;
List<PlaceRecommendation> get recommendations { List<PlaceRecommendation> get recommendations {
final selected = selectedTrait;
final matchingPlaces = selected == null
? places
: places.where((place) => place.traits.contains(selected));
final coordinate = userCoordinate; final coordinate = userCoordinate;
if (coordinate == null) { if (coordinate == null) {
return const []; return matchingPlaces.toList();
} }
final nearbyPlaces = places.where((place) { final ranked = [...matchingPlaces]
return _distance(coordinate, place.coordinate) <=
nearbyRecommendationRadiusMeters;
});
final ranked = [...nearbyPlaces]
..sort((a, b) { ..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( return _distance(
coordinate, coordinate,
a.coordinate, a.coordinate,
).compareTo(_distance(coordinate, b.coordinate)); ).compareTo(_distance(coordinate, b.coordinate));
}); });
return ranked.take(4).toList(); return ranked;
} }
PlaceRecommendation? get selectedPlace { PlaceRecommendation? get selectedPlace {
for (final place in places) { final visiblePlaces = recommendations;
for (final place in visiblePlaces) {
if (place.id == selectedPlaceId) { if (place.id == selectedPlaceId) {
return place; return place;
} }
} }
return recommendations.isEmpty ? null : recommendations.first; return visiblePlaces.isEmpty ? null : visiblePlaces.first;
} }
PlaceState copyWith({ PlaceState copyWith({
PlaceTrait? selectedTrait, Object? selectedTrait = _unset,
List<PlaceRecommendation>? places, List<PlaceRecommendation>? places,
String? selectedPlaceId, Object? selectedPlaceId = _unset,
AppUser? currentUser, AppUser? currentUser,
bool? hasTelegramAuth, bool? hasTelegramAuth,
LatLng? userCoordinate, LatLng? userCoordinate,
VoiceReviewDraft? reviewDraft, VoiceReviewDraft? reviewDraft,
}) { }) {
return PlaceState( return PlaceState(
selectedTrait: selectedTrait ?? this.selectedTrait, selectedTrait: identical(selectedTrait, _unset)
? this.selectedTrait
: selectedTrait as PlaceTrait?,
places: places ?? this.places, places: places ?? this.places,
selectedPlaceId: selectedPlaceId ?? this.selectedPlaceId, selectedPlaceId: identical(selectedPlaceId, _unset)
? this.selectedPlaceId
: selectedPlaceId as String?,
currentUser: currentUser ?? this.currentUser, currentUser: currentUser ?? this.currentUser,
hasTelegramAuth: hasTelegramAuth ?? this.hasTelegramAuth, hasTelegramAuth: hasTelegramAuth ?? this.hasTelegramAuth,
userCoordinate: userCoordinate ?? this.userCoordinate, userCoordinate: userCoordinate ?? this.userCoordinate,
@@ -96,7 +94,7 @@ class PlaceController extends AsyncNotifier<PlaceState> {
Future<PlaceState> build() async { Future<PlaceState> build() async {
if (!_api.hasTelegramAuth) { if (!_api.hasTelegramAuth) {
return const PlaceState( return const PlaceState(
selectedTrait: PlaceTrait.calm, selectedTrait: null,
places: [], places: [],
selectedPlaceId: null, selectedPlaceId: null,
currentUser: null, currentUser: null,
@@ -115,7 +113,7 @@ class PlaceController extends AsyncNotifier<PlaceState> {
final userCoordinate = await _location.resolve(); final userCoordinate = await _location.resolve();
final places = await _api.fetchPlaces(); final places = await _api.fetchPlaces();
return PlaceState( return PlaceState(
selectedTrait: PlaceTrait.calm, selectedTrait: null,
places: places, places: places,
selectedPlaceId: places.isEmpty ? null : places.first.id, selectedPlaceId: places.isEmpty ? null : places.first.id,
currentUser: currentUser, currentUser: currentUser,
@@ -144,6 +142,14 @@ class PlaceController extends AsyncNotifier<PlaceState> {
); );
} }
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) { void selectPlace(String placeId) {
final value = state.requireValue; final value = state.requireValue;
state = AsyncData(value.copyWith(selectedPlaceId: placeId)); state = AsyncData(value.copyWith(selectedPlaceId: placeId));