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); const _unset = Object(); 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(); 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 selected = selectedTrait; final matchingPlaces = selected == null ? places : places.where((place) => place.traits.contains(selected)); final coordinate = userCoordinate; if (coordinate == null) { return matchingPlaces.toList(); } final ranked = [...matchingPlaces] ..sort((a, b) { return _distance( coordinate, a.coordinate, ).compareTo(_distance(coordinate, b.coordinate)); }); return ranked; } PlaceRecommendation? get selectedPlace { final visiblePlaces = recommendations; for (final place in visiblePlaces) { if (place.id == selectedPlaceId) { return place; } } return visiblePlaces.isEmpty ? null : visiblePlaces.first; } PlaceState copyWith({ Object? selectedTrait = _unset, List? places, Object? selectedPlaceId = _unset, AppUser? currentUser, bool? hasTelegramAuth, LatLng? userCoordinate, VoiceReviewDraft? reviewDraft, }) { return PlaceState( selectedTrait: identical(selectedTrait, _unset) ? this.selectedTrait : selectedTrait as PlaceTrait?, places: places ?? this.places, selectedPlaceId: identical(selectedPlaceId, _unset) ? this.selectedPlaceId : selectedPlaceId as String?, 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: null, 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: null, 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 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)); } 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: [], ), ), ); } }