All checks were successful
Build and deploy Flutter Web / build (push) Successful in 1m49s
196 lines
5.4 KiB
Dart
196 lines
5.4 KiB
Dart
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, PlaceState>(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();
|
|
|
|
final PlaceTrait selectedTrait;
|
|
final List<PlaceRecommendation> places;
|
|
final String? selectedPlaceId;
|
|
final AppUser? currentUser;
|
|
final bool hasTelegramAuth;
|
|
final LatLng? userCoordinate;
|
|
final VoiceReviewDraft reviewDraft;
|
|
|
|
List<PlaceRecommendation> get recommendations {
|
|
final ranked = [...places]
|
|
..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;
|
|
}
|
|
|
|
final coordinate = userCoordinate;
|
|
if (coordinate == null) {
|
|
return 0;
|
|
}
|
|
|
|
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<PlaceRecommendation>? 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<PlaceState> {
|
|
final _api = MapflowApi();
|
|
final _location = CurrentLocation();
|
|
|
|
@override
|
|
Future<PlaceState> 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<void> publishReview({required PlaceRecommendation place}) 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: 'web-recording-${DateTime.now().microsecondsSinceEpoch}',
|
|
);
|
|
|
|
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: [],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|