import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:latlong2/latlong.dart'; import '../api/mapflow_api.dart'; import '../location/current_location.dart'; import '../models/place_models.dart'; final placeControllerProvider = AsyncNotifierProvider(PlaceController.new); class PlaceState { const PlaceState({ required this.selectedTrait, required this.places, required this.selectedPlaceId, required this.currentUser, required this.hasTelegramAuth, required this.userCoordinate, required this.reviewDraft, }); static final _distance = Distance(); static const nearbyRecommendationRadiusMeters = 500.0; final PlaceTrait selectedTrait; final List places; final String? selectedPlaceId; final AppUser? currentUser; final bool hasTelegramAuth; final LatLng? userCoordinate; final VoiceReviewDraft reviewDraft; List get recommendations { 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; 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(); } PlaceRecommendation? get selectedPlace { for (final place in places) { if (place.id == selectedPlaceId) { return place; } } return recommendations.isEmpty ? null : recommendations.first; } PlaceState copyWith({ PlaceTrait? selectedTrait, List? places, String? selectedPlaceId, AppUser? currentUser, bool? hasTelegramAuth, LatLng? userCoordinate, VoiceReviewDraft? reviewDraft, }) { return PlaceState( selectedTrait: selectedTrait ?? this.selectedTrait, places: places ?? this.places, selectedPlaceId: selectedPlaceId ?? this.selectedPlaceId, currentUser: currentUser ?? this.currentUser, hasTelegramAuth: hasTelegramAuth ?? this.hasTelegramAuth, userCoordinate: userCoordinate ?? this.userCoordinate, reviewDraft: reviewDraft ?? this.reviewDraft, ); } } class PlaceController extends AsyncNotifier { final _api = MapflowApi(); final _location = CurrentLocation(); @override Future build() async { if (!_api.hasTelegramAuth) { return const PlaceState( selectedTrait: PlaceTrait.calm, places: [], selectedPlaceId: null, currentUser: null, hasTelegramAuth: false, userCoordinate: null, reviewDraft: VoiceReviewDraft( placeName: '', duration: Duration.zero, extractedTraits: {}, evidence: [], ), ); } final currentUser = await _api.authenticateTelegram(); final userCoordinate = await _location.resolve(); final places = await _api.fetchPlaces(); return PlaceState( selectedTrait: PlaceTrait.calm, places: places, selectedPlaceId: places.isEmpty ? null : places.first.id, currentUser: currentUser, hasTelegramAuth: _api.hasTelegramAuth, userCoordinate: userCoordinate, reviewDraft: const VoiceReviewDraft( placeName: '', duration: Duration.zero, extractedTraits: {}, evidence: [], ), ); } void selectTrait(PlaceTrait trait) { final value = state.requireValue; PlaceRecommendation? next; for (final place in value.places) { if (place.traits.contains(trait)) { next = place; break; } } state = AsyncData( value.copyWith(selectedTrait: trait, selectedPlaceId: next?.id), ); } void selectPlace(String placeId) { final value = state.requireValue; state = AsyncData(value.copyWith(selectedPlaceId: placeId)); } void setReviewPlace(String placeName) { final value = state.requireValue; state = AsyncData( value.copyWith( reviewDraft: value.reviewDraft.copyWith(placeName: placeName), ), ); } void setReviewDuration(Duration duration) { final value = state.requireValue; state = AsyncData( value.copyWith( reviewDraft: value.reviewDraft.copyWith(duration: duration), ), ); } Future publishReview({ required PlaceRecommendation place, required String audioObjectKey, required String audioContentBase64, required String audioMimeType, }) async { final value = state.requireValue; if (!value.hasTelegramAuth) { throw StateError('Открой через Telegram, чтобы оставить голос.'); } final draft = value.reviewDraft; await _api.createVoiceExperience( googlePlaceId: place.googlePlaceId, googleName: place.name, coordinate: place.coordinate, durationSeconds: draft.duration.inSeconds, audioObjectKey: audioObjectKey, audioContentBase64: audioContentBase64, audioMimeType: audioMimeType, ); final places = await _api.fetchPlaces(); final selectedPlace = places.isEmpty ? null : places.first.id; state = AsyncData( value.copyWith( places: places, selectedPlaceId: selectedPlace, reviewDraft: const VoiceReviewDraft( placeName: '', duration: Duration.zero, extractedTraits: {}, evidence: [], ), ), ); } }