Separate browsing filters from review radius
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 4m2s
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 4m2s
This commit is contained in:
@@ -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,26 +119,25 @@ 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(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
_TraitBar(
|
||||||
|
selectedTrait: state.selectedTrait,
|
||||||
|
traits: availableTraits,
|
||||||
|
),
|
||||||
|
_PlaceCarousel(
|
||||||
places: state.recommendations,
|
places: state.recommendations,
|
||||||
onSelect: (place) => ref
|
onSelect: (place) => ref
|
||||||
.read(placeControllerProvider.notifier)
|
.read(placeControllerProvider.notifier)
|
||||||
.selectPlace(place.id),
|
.selectPlace(place.id),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SafeArea(
|
SafeArea(
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
Reference in New Issue
Block a user