This commit is contained in:
+29
-4
@@ -5,7 +5,10 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import '../features/mapflow/application/place_cubit.dart';
|
import '../features/mapflow/application/place_cubit.dart';
|
||||||
import '../features/mapflow/data/mapflow_api.dart';
|
import '../features/mapflow/data/auth_repository.dart';
|
||||||
|
import '../features/mapflow/data/mapflow_graphql_client.dart';
|
||||||
|
import '../features/mapflow/data/places_repository.dart';
|
||||||
|
import '../features/mapflow/data/voice_experiences_repository.dart';
|
||||||
import '../shared/location/current_location.dart';
|
import '../shared/location/current_location.dart';
|
||||||
import 'router/app_router.dart';
|
import 'router/app_router.dart';
|
||||||
import 'theme/mapflow_theme.dart';
|
import 'theme/mapflow_theme.dart';
|
||||||
@@ -18,14 +21,28 @@ class MapflowApp extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MapflowAppState extends State<MapflowApp> {
|
class _MapflowAppState extends State<MapflowApp> {
|
||||||
|
late final MapflowGraphqlClient _graphqlClient;
|
||||||
|
late final AuthRepository _authRepository;
|
||||||
|
late final PlacesRepository _placesRepository;
|
||||||
|
late final VoiceExperiencesRepository _voiceExperiencesRepository;
|
||||||
late final PlaceCubit _placeCubit;
|
late final PlaceCubit _placeCubit;
|
||||||
late final GoRouter _router;
|
late final GoRouter _router;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_placeCubit = PlaceCubit(api: MapflowApi(), location: CurrentLocation())
|
_graphqlClient = MapflowGraphqlClient();
|
||||||
..load();
|
_authRepository = AuthRepository(client: _graphqlClient);
|
||||||
|
_placesRepository = PlacesRepository(client: _graphqlClient);
|
||||||
|
_voiceExperiencesRepository = VoiceExperiencesRepository(
|
||||||
|
client: _graphqlClient,
|
||||||
|
);
|
||||||
|
_placeCubit = PlaceCubit(
|
||||||
|
authRepository: _authRepository,
|
||||||
|
placesRepository: _placesRepository,
|
||||||
|
voiceExperiencesRepository: _voiceExperiencesRepository,
|
||||||
|
location: CurrentLocation(),
|
||||||
|
)..load();
|
||||||
_router = createAppRouter();
|
_router = createAppRouter();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +50,7 @@ class _MapflowAppState extends State<MapflowApp> {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
_router.dispose();
|
_router.dispose();
|
||||||
_placeCubit.close();
|
_placeCubit.close();
|
||||||
|
_graphqlClient.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +63,13 @@ class _MapflowAppState extends State<MapflowApp> {
|
|||||||
..[const SingleActivator(LogicalKeyboardKey.tab, shift: true)] =
|
..[const SingleActivator(LogicalKeyboardKey.tab, shift: true)] =
|
||||||
const PreviousFocusIntent();
|
const PreviousFocusIntent();
|
||||||
|
|
||||||
return BlocProvider.value(
|
return MultiRepositoryProvider(
|
||||||
|
providers: [
|
||||||
|
RepositoryProvider.value(value: _authRepository),
|
||||||
|
RepositoryProvider.value(value: _placesRepository),
|
||||||
|
RepositoryProvider.value(value: _voiceExperiencesRepository),
|
||||||
|
],
|
||||||
|
child: BlocProvider.value(
|
||||||
value: _placeCubit,
|
value: _placeCubit,
|
||||||
child: MaterialApp.router(
|
child: MaterialApp.router(
|
||||||
title: 'MapFlow',
|
title: 'MapFlow',
|
||||||
@@ -71,6 +95,7 @@ class _MapflowAppState extends State<MapflowApp> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
import '../../../shared/location/current_location.dart';
|
import '../../../shared/location/current_location.dart';
|
||||||
import '../data/mapflow_api.dart';
|
import '../data/auth_repository.dart';
|
||||||
|
import '../data/places_repository.dart';
|
||||||
|
import '../data/voice_experiences_repository.dart';
|
||||||
import '../domain/place_models.dart';
|
import '../domain/place_models.dart';
|
||||||
|
|
||||||
const _unset = Object();
|
const _unset = Object();
|
||||||
@@ -117,25 +119,33 @@ class PlaceViewState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class PlaceCubit extends Cubit<PlaceViewState> {
|
class PlaceCubit extends Cubit<PlaceViewState> {
|
||||||
PlaceCubit({required MapflowApi api, required CurrentLocation location})
|
PlaceCubit({
|
||||||
: _api = api,
|
required AuthRepository authRepository,
|
||||||
|
required PlacesRepository placesRepository,
|
||||||
|
required VoiceExperiencesRepository voiceExperiencesRepository,
|
||||||
|
required CurrentLocation location,
|
||||||
|
}) : _authRepository = authRepository,
|
||||||
|
_placesRepository = placesRepository,
|
||||||
|
_voiceExperiencesRepository = voiceExperiencesRepository,
|
||||||
_location = location,
|
_location = location,
|
||||||
super(const PlaceViewState.loading());
|
super(const PlaceViewState.loading());
|
||||||
|
|
||||||
final MapflowApi _api;
|
final AuthRepository _authRepository;
|
||||||
|
final PlacesRepository _placesRepository;
|
||||||
|
final VoiceExperiencesRepository _voiceExperiencesRepository;
|
||||||
final CurrentLocation _location;
|
final CurrentLocation _location;
|
||||||
|
|
||||||
Future<void> load() async {
|
Future<void> load() async {
|
||||||
emit(const PlaceViewState.loading());
|
emit(const PlaceViewState.loading());
|
||||||
|
|
||||||
if (!_api.hasTelegramAuth) {
|
if (!_authRepository.hasTelegramAuth) {
|
||||||
emit(PlaceViewState.ready(_emptyState(hasTelegramAuth: false)));
|
emit(PlaceViewState.ready(_emptyState(hasTelegramAuth: false)));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final currentUser = await _api.authenticateTelegram();
|
final currentUser = await _authRepository.authenticateTelegram();
|
||||||
final userCoordinate = await _location.resolve();
|
final userCoordinate = await _location.resolve();
|
||||||
final places = await _api.fetchPlaces();
|
final places = await _placesRepository.fetchPlaces();
|
||||||
emit(
|
emit(
|
||||||
PlaceViewState.ready(
|
PlaceViewState.ready(
|
||||||
PlaceState(
|
PlaceState(
|
||||||
@@ -143,7 +153,7 @@ class PlaceCubit extends Cubit<PlaceViewState> {
|
|||||||
places: places,
|
places: places,
|
||||||
selectedPlaceId: places.isEmpty ? null : places.first.id,
|
selectedPlaceId: places.isEmpty ? null : places.first.id,
|
||||||
currentUser: currentUser,
|
currentUser: currentUser,
|
||||||
hasTelegramAuth: _api.hasTelegramAuth,
|
hasTelegramAuth: _authRepository.hasTelegramAuth,
|
||||||
userCoordinate: userCoordinate,
|
userCoordinate: userCoordinate,
|
||||||
reviewDraft: _emptyDraft,
|
reviewDraft: _emptyDraft,
|
||||||
),
|
),
|
||||||
@@ -216,7 +226,7 @@ class PlaceCubit extends Cubit<PlaceViewState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final draft = value.reviewDraft;
|
final draft = value.reviewDraft;
|
||||||
await _api.createVoiceExperience(
|
await _voiceExperiencesRepository.createVoiceExperience(
|
||||||
googlePlaceId: place.googlePlaceId,
|
googlePlaceId: place.googlePlaceId,
|
||||||
googleName: place.name,
|
googleName: place.name,
|
||||||
coordinate: place.coordinate,
|
coordinate: place.coordinate,
|
||||||
@@ -226,7 +236,7 @@ class PlaceCubit extends Cubit<PlaceViewState> {
|
|||||||
audioMimeType: audioMimeType,
|
audioMimeType: audioMimeType,
|
||||||
);
|
);
|
||||||
|
|
||||||
final places = await _api.fetchPlaces();
|
final places = await _placesRepository.fetchPlaces();
|
||||||
final selectedPlace = places.isEmpty ? null : places.first.id;
|
final selectedPlace = places.isEmpty ? null : places.first.id;
|
||||||
emit(
|
emit(
|
||||||
PlaceViewState.ready(
|
PlaceViewState.ready(
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import '../domain/place_models.dart';
|
||||||
|
import 'graphql/__generated__/authenticate_telegram.req.gql.dart';
|
||||||
|
import 'graphql/__generated__/authenticate_telegram_login.req.gql.dart';
|
||||||
|
import 'graphql/__generated__/complete_telegram_bot_login.req.gql.dart';
|
||||||
|
import 'graphql/__generated__/me.req.gql.dart';
|
||||||
|
import 'graphql/__generated__/start_telegram_bot_login.req.gql.dart';
|
||||||
|
import 'mapflow_data_mappers.dart';
|
||||||
|
import 'mapflow_graphql_client.dart';
|
||||||
|
|
||||||
|
class AuthRepository {
|
||||||
|
const AuthRepository({required MapflowGraphqlClient client})
|
||||||
|
: _client = client;
|
||||||
|
|
||||||
|
final MapflowGraphqlClient _client;
|
||||||
|
|
||||||
|
bool get hasTelegramAuth => _client.hasTelegramAuth;
|
||||||
|
|
||||||
|
Future<AppUser?> authenticateTelegram() async {
|
||||||
|
if (!hasTelegramAuth) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_client.mapflowSessionToken.isNotEmpty) {
|
||||||
|
final data = await _client.request(GMeReq());
|
||||||
|
return appUserFromMe(data.me);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_client.telegramLoginData.isNotEmpty) {
|
||||||
|
final loginData =
|
||||||
|
jsonDecode(_client.telegramLoginData) as Map<String, dynamic>;
|
||||||
|
final data = await _client.request(
|
||||||
|
GAuthenticateTelegramLoginReq((b) {
|
||||||
|
b.vars.input
|
||||||
|
..id = _requiredDouble(loginData, 'id')
|
||||||
|
..first_name = _optionalString(loginData, 'first_name')
|
||||||
|
..last_name = _optionalString(loginData, 'last_name')
|
||||||
|
..username = _optionalString(loginData, 'username')
|
||||||
|
..photo_url = _optionalString(loginData, 'photo_url')
|
||||||
|
..auth_date = _requiredDouble(loginData, 'auth_date')
|
||||||
|
..hash = _requiredString(loginData, 'hash');
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return appUserFromAuthenticateTelegramLogin(
|
||||||
|
data.authenticateTelegramLogin.user,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = await _client.request(
|
||||||
|
GAuthenticateTelegramReq((b) {
|
||||||
|
b.vars.input.initData = _client.telegramInitData;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return appUserFromAuthenticateTelegram(data.authenticateTelegram.user);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<TelegramBotLogin> startTelegramBotLogin() async {
|
||||||
|
final data = await _client.request(GStartTelegramBotLoginReq());
|
||||||
|
final payload = data.startTelegramBotLogin;
|
||||||
|
return TelegramBotLogin(
|
||||||
|
token: payload.token,
|
||||||
|
botUrl: payload.botUrl,
|
||||||
|
expiresAt: DateTime.parse(payload.expiresAt),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<TelegramBotLoginSession> completeTelegramBotLogin(String token) async {
|
||||||
|
final data = await _client.request(
|
||||||
|
GCompleteTelegramBotLoginReq((b) {
|
||||||
|
b.vars.token = token;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
final payload = data.completeTelegramBotLogin;
|
||||||
|
return TelegramBotLoginSession(
|
||||||
|
sessionToken: payload.sessionToken,
|
||||||
|
user: appUserFromCompleteTelegramBotLogin(payload.user),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
double _requiredDouble(Map<String, dynamic> json, String key) {
|
||||||
|
final value = json[key];
|
||||||
|
if (value is num) {
|
||||||
|
return value.toDouble();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw StateError('Telegram login data "$key" must be a number.');
|
||||||
|
}
|
||||||
|
|
||||||
|
String _requiredString(Map<String, dynamic> json, String key) {
|
||||||
|
final value = json[key];
|
||||||
|
if (value is String && value.isNotEmpty) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw StateError('Telegram login data "$key" must be a non-empty string.');
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _optionalString(Map<String, dynamic> json, String key) {
|
||||||
|
final value = json[key];
|
||||||
|
if (value == null || value is String) {
|
||||||
|
return value as String?;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw StateError('Telegram login data "$key" must be a string.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,347 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:built_value/json_object.dart';
|
|
||||||
import 'package:ferry/ferry.dart';
|
|
||||||
import 'package:gql_http_link/gql_http_link.dart';
|
|
||||||
import 'package:latlong2/latlong.dart';
|
|
||||||
|
|
||||||
import '../../../shared/auth/telegram_session.dart' as telegram_auth;
|
|
||||||
import '../domain/place_models.dart';
|
|
||||||
import 'graphql/__generated__/authenticate_telegram.req.gql.dart';
|
|
||||||
import 'graphql/__generated__/authenticate_telegram_login.req.gql.dart';
|
|
||||||
import 'graphql/__generated__/complete_telegram_bot_login.req.gql.dart';
|
|
||||||
import 'graphql/__generated__/create_voice_experience.req.gql.dart';
|
|
||||||
import 'graphql/__generated__/me.req.gql.dart';
|
|
||||||
import 'graphql/__generated__/nearby_places.req.gql.dart';
|
|
||||||
import 'graphql/__generated__/places.req.gql.dart';
|
|
||||||
import 'graphql/__generated__/schema.schema.gql.dart';
|
|
||||||
import 'graphql/__generated__/start_telegram_bot_login.req.gql.dart';
|
|
||||||
import 'graphql/__generated__/voice_experiences.req.gql.dart';
|
|
||||||
|
|
||||||
class MapflowApi {
|
|
||||||
MapflowApi({
|
|
||||||
Client? client,
|
|
||||||
String? telegramInitData,
|
|
||||||
String? telegramLoginData,
|
|
||||||
String? mapflowSessionToken,
|
|
||||||
String endpoint = const String.fromEnvironment(
|
|
||||||
'API_BASE_URL',
|
|
||||||
defaultValue: '/graphql',
|
|
||||||
),
|
|
||||||
}) : _telegramInitData = telegramInitData ?? telegram_auth.telegramInitData(),
|
|
||||||
_telegramLoginData =
|
|
||||||
telegramLoginData ?? telegram_auth.telegramLoginData(),
|
|
||||||
_mapflowSessionToken =
|
|
||||||
mapflowSessionToken ?? telegram_auth.mapflowSessionToken(),
|
|
||||||
_client =
|
|
||||||
client ??
|
|
||||||
Client(
|
|
||||||
link: HttpLink(
|
|
||||||
Uri.base.resolve(endpoint).toString(),
|
|
||||||
defaultHeaders: _headers(
|
|
||||||
telegramInitData ?? telegram_auth.telegramInitData(),
|
|
||||||
telegramLoginData ?? telegram_auth.telegramLoginData(),
|
|
||||||
mapflowSessionToken ?? telegram_auth.mapflowSessionToken(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final Client _client;
|
|
||||||
final String _telegramInitData;
|
|
||||||
final String _telegramLoginData;
|
|
||||||
final String _mapflowSessionToken;
|
|
||||||
|
|
||||||
bool get hasTelegramAuth =>
|
|
||||||
_telegramInitData.isNotEmpty ||
|
|
||||||
_telegramLoginData.isNotEmpty ||
|
|
||||||
_mapflowSessionToken.isNotEmpty;
|
|
||||||
|
|
||||||
Future<AppUser?> authenticateTelegram() async {
|
|
||||||
if (!hasTelegramAuth) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_mapflowSessionToken.isNotEmpty) {
|
|
||||||
final data = await _request(GMeReq());
|
|
||||||
return _appUserFromTyped(data.me);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_telegramLoginData.isNotEmpty) {
|
|
||||||
final loginData = jsonDecode(_telegramLoginData) as Map<String, dynamic>;
|
|
||||||
final data = await _request(
|
|
||||||
GAuthenticateTelegramLoginReq((b) {
|
|
||||||
b.vars.input
|
|
||||||
..id = _requiredDouble(loginData, 'id')
|
|
||||||
..first_name = _optionalString(loginData, 'first_name')
|
|
||||||
..last_name = _optionalString(loginData, 'last_name')
|
|
||||||
..username = _optionalString(loginData, 'username')
|
|
||||||
..photo_url = _optionalString(loginData, 'photo_url')
|
|
||||||
..auth_date = _requiredDouble(loginData, 'auth_date')
|
|
||||||
..hash = _requiredString(loginData, 'hash');
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return _appUserFromTyped(data.authenticateTelegramLogin.user);
|
|
||||||
}
|
|
||||||
|
|
||||||
final data = await _request(
|
|
||||||
GAuthenticateTelegramReq((b) {
|
|
||||||
b.vars.input.initData = _telegramInitData;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return _appUserFromTyped(data.authenticateTelegram.user);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<TelegramBotLogin> startTelegramBotLogin() async {
|
|
||||||
final data = await _request(GStartTelegramBotLoginReq());
|
|
||||||
final payload = data.startTelegramBotLogin;
|
|
||||||
return TelegramBotLogin(
|
|
||||||
token: payload.token,
|
|
||||||
botUrl: payload.botUrl,
|
|
||||||
expiresAt: DateTime.parse(payload.expiresAt),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<TelegramBotLoginSession> completeTelegramBotLogin(String token) async {
|
|
||||||
final data = await _request(
|
|
||||||
GCompleteTelegramBotLoginReq((b) {
|
|
||||||
b.vars.token = token;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
final payload = data.completeTelegramBotLogin;
|
|
||||||
return TelegramBotLoginSession(
|
|
||||||
sessionToken: payload.sessionToken,
|
|
||||||
user: _appUserFromTyped(payload.user),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<PlaceRecommendation>> fetchPlaces() async {
|
|
||||||
final data = await _request(GPlacesReq());
|
|
||||||
return data.places.map(_placeRecommendationFromTyped).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<PlaceRecommendation>> fetchNearbyPlaces({
|
|
||||||
required LatLng coordinate,
|
|
||||||
required int radiusMeters,
|
|
||||||
}) async {
|
|
||||||
final data = await _request(
|
|
||||||
GNearbyPlacesReq((b) {
|
|
||||||
b.vars.input
|
|
||||||
..latitude = coordinate.latitude
|
|
||||||
..longitude = coordinate.longitude
|
|
||||||
..radiusMeters = radiusMeters;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return data.nearbyPlaces.map(_placeRecommendationFromTyped).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> createVoiceExperience({
|
|
||||||
required String googlePlaceId,
|
|
||||||
required String googleName,
|
|
||||||
required LatLng coordinate,
|
|
||||||
required int durationSeconds,
|
|
||||||
required String audioObjectKey,
|
|
||||||
required String audioContentBase64,
|
|
||||||
required String audioMimeType,
|
|
||||||
}) async {
|
|
||||||
if (!hasTelegramAuth) {
|
|
||||||
throw StateError('Telegram authorization is required.');
|
|
||||||
}
|
|
||||||
|
|
||||||
await _request(
|
|
||||||
GCreateVoiceExperienceReq((b) {
|
|
||||||
b.vars.input
|
|
||||||
..googlePlaceId = googlePlaceId
|
|
||||||
..googleName = googleName
|
|
||||||
..latitude = coordinate.latitude
|
|
||||||
..longitude = coordinate.longitude
|
|
||||||
..durationSeconds = durationSeconds
|
|
||||||
..audioObjectKey = audioObjectKey
|
|
||||||
..audioContentBase64 = audioContentBase64
|
|
||||||
..audioMimeType = audioMimeType;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<VoiceExperienceDebug>> fetchVoiceExperiences() async {
|
|
||||||
final data = await _request(GVoiceExperiencesReq());
|
|
||||||
return data.voiceExperiences.map(_voiceExperienceDebugFromTyped).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<TData> _request<TData, TVars>(
|
|
||||||
OperationRequest<TData, TVars> request,
|
|
||||||
) async {
|
|
||||||
final response = await _client
|
|
||||||
.request(request)
|
|
||||||
.firstWhere((item) => !item.loading);
|
|
||||||
|
|
||||||
if (response.hasErrors) {
|
|
||||||
throw StateError(_errorMessage(response));
|
|
||||||
}
|
|
||||||
|
|
||||||
final data = response.data;
|
|
||||||
if (data == null) {
|
|
||||||
throw StateError('GraphQL response data is empty.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Map<String, String> _headers(
|
|
||||||
String telegramInitData,
|
|
||||||
String telegramLoginData,
|
|
||||||
String mapflowSessionToken,
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
if (telegramInitData.isNotEmpty) 'x-telegram-init-data': telegramInitData,
|
|
||||||
if (telegramLoginData.isNotEmpty)
|
|
||||||
'x-telegram-login-data': telegramLoginData,
|
|
||||||
if (mapflowSessionToken.isNotEmpty)
|
|
||||||
'x-mapflow-session-token': mapflowSessionToken,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
String _errorMessage(OperationResponse<dynamic, dynamic> response) {
|
|
||||||
final graphQLErrors = response.graphqlErrors;
|
|
||||||
if (graphQLErrors != null && graphQLErrors.isNotEmpty) {
|
|
||||||
return graphQLErrors.map((error) => error.message).join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
final linkException = response.linkException;
|
|
||||||
if (linkException != null) {
|
|
||||||
return linkException.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'GraphQL request failed.';
|
|
||||||
}
|
|
||||||
|
|
||||||
AppUser _appUserFromTyped(dynamic user) {
|
|
||||||
return AppUser(
|
|
||||||
id: user.id as String,
|
|
||||||
telegramId: user.telegramId as String,
|
|
||||||
username: user.username as String?,
|
|
||||||
firstName: user.firstName as String?,
|
|
||||||
lastName: user.lastName as String?,
|
|
||||||
photoUrl: user.photoUrl as String?,
|
|
||||||
languageCode: user.languageCode as String?,
|
|
||||||
isAdmin: user.isAdmin as bool,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
PlaceRecommendation _placeRecommendationFromTyped(dynamic place) {
|
|
||||||
return PlaceRecommendation(
|
|
||||||
id: place.id as String,
|
|
||||||
googlePlaceId: place.googlePlaceId as String,
|
|
||||||
name: place.name as String,
|
|
||||||
area: '',
|
|
||||||
photoUrls: const [],
|
|
||||||
coordinate: LatLng(place.latitude as double, place.longitude as double),
|
|
||||||
traits: _traitsFromAnalyses(
|
|
||||||
place.experiences.map((dynamic experience) {
|
|
||||||
return experience.analysis as JsonObject?;
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
googlePrimaryType: place.googlePrimaryType as String?,
|
|
||||||
googleTypes: place.googleTypes.toList().cast<String>(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
VoiceExperienceDebug _voiceExperienceDebugFromTyped(dynamic experience) {
|
|
||||||
return VoiceExperienceDebug(
|
|
||||||
id: experience.id as String,
|
|
||||||
placeName: experience.place.name as String,
|
|
||||||
userName: _userDisplayName(experience.user),
|
|
||||||
status: (experience.status as GVoiceExperienceStatus).name,
|
|
||||||
durationSeconds: experience.durationSeconds as int,
|
|
||||||
transcript: experience.transcript as String?,
|
|
||||||
analysis: _jsonMap(experience.analysis as JsonObject?),
|
|
||||||
createdAt: DateTime.parse(experience.createdAt as String),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _userDisplayName(dynamic user) {
|
|
||||||
if (user == null) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
final firstName = user.firstName as String?;
|
|
||||||
final username = user.username as String?;
|
|
||||||
final telegramId = user.telegramId as String;
|
|
||||||
if (firstName != null && firstName.trim().isNotEmpty) {
|
|
||||||
return firstName;
|
|
||||||
}
|
|
||||||
if (username != null && username.trim().isNotEmpty) {
|
|
||||||
return '@$username';
|
|
||||||
}
|
|
||||||
return telegramId;
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<PlaceTrait> _traitsFromAnalyses(Iterable<JsonObject?> analyses) {
|
|
||||||
final traits = <PlaceTrait>{};
|
|
||||||
for (final analysisObject in analyses) {
|
|
||||||
final analysis = _jsonMap(analysisObject);
|
|
||||||
if (analysis == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
final tags = analysis['tags'];
|
|
||||||
if (tags is! List) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final tag in tags) {
|
|
||||||
final trait = _traitByTag(tag.toString());
|
|
||||||
if (trait != null) {
|
|
||||||
traits.add(trait);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return traits;
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic>? _jsonMap(JsonObject? object) {
|
|
||||||
if (object == null || !object.isMap) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return object.asMap.map((key, value) => MapEntry(key.toString(), value));
|
|
||||||
}
|
|
||||||
|
|
||||||
double _requiredDouble(Map<String, dynamic> json, String key) {
|
|
||||||
final value = json[key];
|
|
||||||
if (value is num) {
|
|
||||||
return value.toDouble();
|
|
||||||
}
|
|
||||||
|
|
||||||
throw StateError('Telegram login data "$key" must be a number.');
|
|
||||||
}
|
|
||||||
|
|
||||||
String _requiredString(Map<String, dynamic> json, String key) {
|
|
||||||
final value = json[key];
|
|
||||||
if (value is String && value.isNotEmpty) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw StateError('Telegram login data "$key" must be a non-empty string.');
|
|
||||||
}
|
|
||||||
|
|
||||||
String? _optionalString(Map<String, dynamic> json, String key) {
|
|
||||||
final value = json[key];
|
|
||||||
if (value == null || value is String) {
|
|
||||||
return value as String?;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw StateError('Telegram login data "$key" must be a string.');
|
|
||||||
}
|
|
||||||
|
|
||||||
PlaceTrait? _traitByTag(String tag) {
|
|
||||||
final name = tag.contains(':') ? tag.split(':').last : tag;
|
|
||||||
for (final trait in PlaceTrait.values) {
|
|
||||||
if (trait.name == name) {
|
|
||||||
return trait;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
import 'package:built_value/json_object.dart';
|
||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
|
import '../domain/place_models.dart';
|
||||||
|
import 'graphql/__generated__/authenticate_telegram.data.gql.dart';
|
||||||
|
import 'graphql/__generated__/authenticate_telegram_login.data.gql.dart';
|
||||||
|
import 'graphql/__generated__/complete_telegram_bot_login.data.gql.dart';
|
||||||
|
import 'graphql/__generated__/me.data.gql.dart';
|
||||||
|
import 'graphql/__generated__/nearby_places.data.gql.dart';
|
||||||
|
import 'graphql/__generated__/places.data.gql.dart';
|
||||||
|
import 'graphql/__generated__/voice_experiences.data.gql.dart';
|
||||||
|
|
||||||
|
AppUser appUserFromMe(GMeData_me user) {
|
||||||
|
return _appUser(
|
||||||
|
id: user.id,
|
||||||
|
telegramId: user.telegramId,
|
||||||
|
username: user.username,
|
||||||
|
firstName: user.firstName,
|
||||||
|
lastName: user.lastName,
|
||||||
|
photoUrl: user.photoUrl,
|
||||||
|
languageCode: user.languageCode,
|
||||||
|
isAdmin: user.isAdmin,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AppUser appUserFromAuthenticateTelegram(
|
||||||
|
GAuthenticateTelegramData_authenticateTelegram_user user,
|
||||||
|
) {
|
||||||
|
return _appUser(
|
||||||
|
id: user.id,
|
||||||
|
telegramId: user.telegramId,
|
||||||
|
username: user.username,
|
||||||
|
firstName: user.firstName,
|
||||||
|
lastName: user.lastName,
|
||||||
|
photoUrl: user.photoUrl,
|
||||||
|
languageCode: user.languageCode,
|
||||||
|
isAdmin: user.isAdmin,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AppUser appUserFromAuthenticateTelegramLogin(
|
||||||
|
GAuthenticateTelegramLoginData_authenticateTelegramLogin_user user,
|
||||||
|
) {
|
||||||
|
return _appUser(
|
||||||
|
id: user.id,
|
||||||
|
telegramId: user.telegramId,
|
||||||
|
username: user.username,
|
||||||
|
firstName: user.firstName,
|
||||||
|
lastName: user.lastName,
|
||||||
|
photoUrl: user.photoUrl,
|
||||||
|
languageCode: user.languageCode,
|
||||||
|
isAdmin: user.isAdmin,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AppUser appUserFromCompleteTelegramBotLogin(
|
||||||
|
GCompleteTelegramBotLoginData_completeTelegramBotLogin_user user,
|
||||||
|
) {
|
||||||
|
return _appUser(
|
||||||
|
id: user.id,
|
||||||
|
telegramId: user.telegramId,
|
||||||
|
username: user.username,
|
||||||
|
firstName: user.firstName,
|
||||||
|
lastName: user.lastName,
|
||||||
|
photoUrl: user.photoUrl,
|
||||||
|
languageCode: user.languageCode,
|
||||||
|
isAdmin: user.isAdmin,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AppUser _appUser({
|
||||||
|
required String id,
|
||||||
|
required String telegramId,
|
||||||
|
required String? username,
|
||||||
|
required String? firstName,
|
||||||
|
required String? lastName,
|
||||||
|
required String? photoUrl,
|
||||||
|
required String? languageCode,
|
||||||
|
required bool isAdmin,
|
||||||
|
}) {
|
||||||
|
return AppUser(
|
||||||
|
id: id,
|
||||||
|
telegramId: telegramId,
|
||||||
|
username: username,
|
||||||
|
firstName: firstName,
|
||||||
|
lastName: lastName,
|
||||||
|
photoUrl: photoUrl,
|
||||||
|
languageCode: languageCode,
|
||||||
|
isAdmin: isAdmin,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
PlaceRecommendation placeRecommendationFromPlace(GPlacesData_places place) {
|
||||||
|
return PlaceRecommendation(
|
||||||
|
id: place.id,
|
||||||
|
googlePlaceId: place.googlePlaceId,
|
||||||
|
name: place.name,
|
||||||
|
area: '',
|
||||||
|
photoUrls: const [],
|
||||||
|
coordinate: LatLng(place.latitude, place.longitude),
|
||||||
|
traits: _traitsFromAnalyses(place.experiences.map((item) => item.analysis)),
|
||||||
|
googlePrimaryType: place.googlePrimaryType,
|
||||||
|
googleTypes: place.googleTypes.toList().cast<String>(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
PlaceRecommendation placeRecommendationFromNearbyPlace(
|
||||||
|
GNearbyPlacesData_nearbyPlaces place,
|
||||||
|
) {
|
||||||
|
return PlaceRecommendation(
|
||||||
|
id: place.id,
|
||||||
|
googlePlaceId: place.googlePlaceId,
|
||||||
|
name: place.name,
|
||||||
|
area: '',
|
||||||
|
photoUrls: const [],
|
||||||
|
coordinate: LatLng(place.latitude, place.longitude),
|
||||||
|
traits: _traitsFromAnalyses(place.experiences.map((item) => item.analysis)),
|
||||||
|
googlePrimaryType: place.googlePrimaryType,
|
||||||
|
googleTypes: place.googleTypes.toList().cast<String>(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
VoiceExperienceDebug voiceExperienceDebugFromVoiceExperience(
|
||||||
|
GVoiceExperiencesData_voiceExperiences experience,
|
||||||
|
) {
|
||||||
|
return VoiceExperienceDebug(
|
||||||
|
id: experience.id,
|
||||||
|
placeName: experience.place.name,
|
||||||
|
userName: _userDisplayName(experience.user),
|
||||||
|
status: experience.status.name,
|
||||||
|
durationSeconds: experience.durationSeconds,
|
||||||
|
transcript: experience.transcript,
|
||||||
|
analysis: _jsonMap(experience.analysis),
|
||||||
|
createdAt: DateTime.parse(experience.createdAt),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _userDisplayName(GVoiceExperiencesData_voiceExperiences_user? user) {
|
||||||
|
if (user == null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
final firstName = user.firstName;
|
||||||
|
final username = user.username;
|
||||||
|
final telegramId = user.telegramId;
|
||||||
|
if (firstName != null && firstName.trim().isNotEmpty) {
|
||||||
|
return firstName;
|
||||||
|
}
|
||||||
|
if (username != null && username.trim().isNotEmpty) {
|
||||||
|
return '@$username';
|
||||||
|
}
|
||||||
|
return telegramId;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<PlaceTrait> _traitsFromAnalyses(Iterable<JsonObject?> analyses) {
|
||||||
|
final traits = <PlaceTrait>{};
|
||||||
|
for (final analysisObject in analyses) {
|
||||||
|
final analysis = _jsonMap(analysisObject);
|
||||||
|
if (analysis == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final tags = analysis['tags'];
|
||||||
|
if (tags is! List) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final tag in tags) {
|
||||||
|
final trait = _traitByTag(tag.toString());
|
||||||
|
if (trait != null) {
|
||||||
|
traits.add(trait);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return traits;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic>? _jsonMap(JsonObject? object) {
|
||||||
|
if (object == null || !object.isMap) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return object.asMap.map((key, value) => MapEntry(key.toString(), value));
|
||||||
|
}
|
||||||
|
|
||||||
|
PlaceTrait? _traitByTag(String tag) {
|
||||||
|
final name = tag.contains(':') ? tag.split(':').last : tag;
|
||||||
|
for (final trait in PlaceTrait.values) {
|
||||||
|
if (trait.name == name) {
|
||||||
|
return trait;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:ferry/ferry.dart';
|
||||||
|
import 'package:gql_http_link/gql_http_link.dart';
|
||||||
|
|
||||||
|
import '../../../shared/auth/telegram_session.dart' as telegram_auth;
|
||||||
|
|
||||||
|
class MapflowGraphqlClient {
|
||||||
|
factory MapflowGraphqlClient({
|
||||||
|
Client? client,
|
||||||
|
String? telegramInitData,
|
||||||
|
String? telegramLoginData,
|
||||||
|
String? mapflowSessionToken,
|
||||||
|
String endpoint = const String.fromEnvironment(
|
||||||
|
'API_BASE_URL',
|
||||||
|
defaultValue: '/graphql',
|
||||||
|
),
|
||||||
|
}) {
|
||||||
|
final resolvedTelegramInitData =
|
||||||
|
telegramInitData ?? telegram_auth.telegramInitData();
|
||||||
|
final resolvedTelegramLoginData =
|
||||||
|
telegramLoginData ?? telegram_auth.telegramLoginData();
|
||||||
|
final resolvedMapflowSessionToken =
|
||||||
|
mapflowSessionToken ?? telegram_auth.mapflowSessionToken();
|
||||||
|
|
||||||
|
return MapflowGraphqlClient._(
|
||||||
|
client:
|
||||||
|
client ??
|
||||||
|
Client(
|
||||||
|
link: HttpLink(
|
||||||
|
Uri.base.resolve(endpoint).toString(),
|
||||||
|
defaultHeaders: _headers(
|
||||||
|
resolvedTelegramInitData,
|
||||||
|
resolvedTelegramLoginData,
|
||||||
|
resolvedMapflowSessionToken,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
telegramInitData: resolvedTelegramInitData,
|
||||||
|
telegramLoginData: resolvedTelegramLoginData,
|
||||||
|
mapflowSessionToken: resolvedMapflowSessionToken,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
MapflowGraphqlClient._({
|
||||||
|
required Client client,
|
||||||
|
required this.telegramInitData,
|
||||||
|
required this.telegramLoginData,
|
||||||
|
required this.mapflowSessionToken,
|
||||||
|
}) : _client = client;
|
||||||
|
|
||||||
|
final Client _client;
|
||||||
|
final String telegramInitData;
|
||||||
|
final String telegramLoginData;
|
||||||
|
final String mapflowSessionToken;
|
||||||
|
|
||||||
|
bool get hasTelegramAuth =>
|
||||||
|
telegramInitData.isNotEmpty ||
|
||||||
|
telegramLoginData.isNotEmpty ||
|
||||||
|
mapflowSessionToken.isNotEmpty;
|
||||||
|
|
||||||
|
Future<TData> request<TData, TVars>(
|
||||||
|
OperationRequest<TData, TVars> request,
|
||||||
|
) async {
|
||||||
|
final response = await _client
|
||||||
|
.request(request)
|
||||||
|
.firstWhere((item) => !item.loading);
|
||||||
|
|
||||||
|
if (response.hasErrors) {
|
||||||
|
throw StateError(_errorMessage(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = response.data;
|
||||||
|
if (data == null) {
|
||||||
|
throw StateError('GraphQL response data is empty.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
unawaited(_client.dispose());
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, String> _headers(
|
||||||
|
String telegramInitData,
|
||||||
|
String telegramLoginData,
|
||||||
|
String mapflowSessionToken,
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
if (telegramInitData.isNotEmpty) 'x-telegram-init-data': telegramInitData,
|
||||||
|
if (telegramLoginData.isNotEmpty)
|
||||||
|
'x-telegram-login-data': telegramLoginData,
|
||||||
|
if (mapflowSessionToken.isNotEmpty)
|
||||||
|
'x-mapflow-session-token': mapflowSessionToken,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
String _errorMessage(OperationResponse<dynamic, dynamic> response) {
|
||||||
|
final graphQLErrors = response.graphqlErrors;
|
||||||
|
if (graphQLErrors != null && graphQLErrors.isNotEmpty) {
|
||||||
|
return graphQLErrors.map((error) => error.message).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
final linkException = response.linkException;
|
||||||
|
if (linkException != null) {
|
||||||
|
return linkException.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'GraphQL request failed.';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
|
import '../domain/place_models.dart';
|
||||||
|
import 'graphql/__generated__/nearby_places.req.gql.dart';
|
||||||
|
import 'graphql/__generated__/places.req.gql.dart';
|
||||||
|
import 'mapflow_data_mappers.dart';
|
||||||
|
import 'mapflow_graphql_client.dart';
|
||||||
|
|
||||||
|
class PlacesRepository {
|
||||||
|
const PlacesRepository({required MapflowGraphqlClient client})
|
||||||
|
: _client = client;
|
||||||
|
|
||||||
|
final MapflowGraphqlClient _client;
|
||||||
|
|
||||||
|
Future<List<PlaceRecommendation>> fetchPlaces() async {
|
||||||
|
final data = await _client.request(GPlacesReq());
|
||||||
|
return data.places.map(placeRecommendationFromPlace).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<PlaceRecommendation>> fetchNearbyPlaces({
|
||||||
|
required LatLng coordinate,
|
||||||
|
required int radiusMeters,
|
||||||
|
}) async {
|
||||||
|
final data = await _client.request(
|
||||||
|
GNearbyPlacesReq((b) {
|
||||||
|
b.vars.input
|
||||||
|
..latitude = coordinate.latitude
|
||||||
|
..longitude = coordinate.longitude
|
||||||
|
..radiusMeters = radiusMeters;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return data.nearbyPlaces.map(placeRecommendationFromNearbyPlace).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
|
import '../domain/place_models.dart';
|
||||||
|
import 'graphql/__generated__/create_voice_experience.req.gql.dart';
|
||||||
|
import 'graphql/__generated__/voice_experiences.req.gql.dart';
|
||||||
|
import 'mapflow_data_mappers.dart';
|
||||||
|
import 'mapflow_graphql_client.dart';
|
||||||
|
|
||||||
|
class VoiceExperiencesRepository {
|
||||||
|
const VoiceExperiencesRepository({required MapflowGraphqlClient client})
|
||||||
|
: _client = client;
|
||||||
|
|
||||||
|
final MapflowGraphqlClient _client;
|
||||||
|
|
||||||
|
Future<void> createVoiceExperience({
|
||||||
|
required String googlePlaceId,
|
||||||
|
required String googleName,
|
||||||
|
required LatLng coordinate,
|
||||||
|
required int durationSeconds,
|
||||||
|
required String audioObjectKey,
|
||||||
|
required String audioContentBase64,
|
||||||
|
required String audioMimeType,
|
||||||
|
}) async {
|
||||||
|
if (!_client.hasTelegramAuth) {
|
||||||
|
throw StateError('Telegram authorization is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
await _client.request(
|
||||||
|
GCreateVoiceExperienceReq((b) {
|
||||||
|
b.vars.input
|
||||||
|
..googlePlaceId = googlePlaceId
|
||||||
|
..googleName = googleName
|
||||||
|
..latitude = coordinate.latitude
|
||||||
|
..longitude = coordinate.longitude
|
||||||
|
..durationSeconds = durationSeconds
|
||||||
|
..audioObjectKey = audioObjectKey
|
||||||
|
..audioContentBase64 = audioContentBase64
|
||||||
|
..audioMimeType = audioMimeType;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<VoiceExperienceDebug>> fetchVoiceExperiences() async {
|
||||||
|
final data = await _client.request(GVoiceExperiencesReq());
|
||||||
|
return data.voiceExperiences
|
||||||
|
.map(voiceExperienceDebugFromVoiceExperience)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../../app/theme/mapflow_theme.dart';
|
import '../../../app/theme/mapflow_theme.dart';
|
||||||
import '../data/mapflow_api.dart';
|
import '../data/voice_experiences_repository.dart';
|
||||||
import '../domain/place_models.dart';
|
import '../domain/place_models.dart';
|
||||||
|
|
||||||
class AdminVoiceExperiencesScreen extends StatefulWidget {
|
class AdminVoiceExperiencesScreen extends StatefulWidget {
|
||||||
@@ -16,7 +17,8 @@ class AdminVoiceExperiencesScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _AdminVoiceExperiencesScreenState
|
class _AdminVoiceExperiencesScreenState
|
||||||
extends State<AdminVoiceExperiencesScreen> {
|
extends State<AdminVoiceExperiencesScreen> {
|
||||||
late final Future<List<VoiceExperienceDebug>> _future = MapflowApi()
|
late final Future<List<VoiceExperienceDebug>> _future = context
|
||||||
|
.read<VoiceExperiencesRepository>()
|
||||||
.fetchVoiceExperiences();
|
.fetchVoiceExperiences();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ import '../../../app/theme/mapflow_theme.dart';
|
|||||||
import '../../../shared/auth/telegram_login_button.dart';
|
import '../../../shared/auth/telegram_login_button.dart';
|
||||||
import '../../../shared/auth/telegram_session.dart' as telegram_session;
|
import '../../../shared/auth/telegram_session.dart' as telegram_session;
|
||||||
import '../application/place_cubit.dart';
|
import '../application/place_cubit.dart';
|
||||||
import '../data/mapflow_api.dart';
|
import '../data/auth_repository.dart';
|
||||||
|
import '../data/places_repository.dart';
|
||||||
import '../domain/place_models.dart';
|
import '../domain/place_models.dart';
|
||||||
import 'widgets/place_photo_card.dart';
|
import 'widgets/place_photo_card.dart';
|
||||||
|
|
||||||
@@ -376,13 +377,14 @@ class _TelegramLoginScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _TelegramLoginScreenState extends State<_TelegramLoginScreen> {
|
class _TelegramLoginScreenState extends State<_TelegramLoginScreen> {
|
||||||
final _api = MapflowApi();
|
late final AuthRepository _authRepository;
|
||||||
var _loading = false;
|
var _loading = false;
|
||||||
var _message = '';
|
var _message = '';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_authRepository = context.read<AuthRepository>();
|
||||||
final urlToken = telegram_session.telegramLoginTokenFromUrl();
|
final urlToken = telegram_session.telegramLoginTokenFromUrl();
|
||||||
if (urlToken.isNotEmpty) {
|
if (urlToken.isNotEmpty) {
|
||||||
_completeLogin(urlToken);
|
_completeLogin(urlToken);
|
||||||
@@ -394,7 +396,7 @@ class _TelegramLoginScreenState extends State<_TelegramLoginScreen> {
|
|||||||
_loading = true;
|
_loading = true;
|
||||||
_message = '';
|
_message = '';
|
||||||
});
|
});
|
||||||
final login = await _api.startTelegramBotLogin();
|
final login = await _authRepository.startTelegramBotLogin();
|
||||||
telegram_session.openExternalUrl(login.botUrl);
|
telegram_session.openExternalUrl(login.botUrl);
|
||||||
setState(() {
|
setState(() {
|
||||||
_loading = false;
|
_loading = false;
|
||||||
@@ -407,7 +409,7 @@ class _TelegramLoginScreenState extends State<_TelegramLoginScreen> {
|
|||||||
_loading = true;
|
_loading = true;
|
||||||
_message = '';
|
_message = '';
|
||||||
});
|
});
|
||||||
final session = await _api.completeTelegramBotLogin(token);
|
final session = await _authRepository.completeTelegramBotLogin(token);
|
||||||
telegram_session.saveMapflowSessionToken(session.sessionToken);
|
telegram_session.saveMapflowSessionToken(session.sessionToken);
|
||||||
widget.onAuthenticated();
|
widget.onAuthenticated();
|
||||||
telegram_session.reloadApp();
|
telegram_session.reloadApp();
|
||||||
@@ -667,7 +669,6 @@ class _AddExperienceFlowState extends State<AddExperienceFlow> {
|
|||||||
static const _minimumInformationUnits = 16.0;
|
static const _minimumInformationUnits = 16.0;
|
||||||
static const _nearbyPlaceRadiusMeters = 50;
|
static const _nearbyPlaceRadiusMeters = 50;
|
||||||
|
|
||||||
final _api = MapflowApi();
|
|
||||||
final _waveController = WaveformRecorderController(
|
final _waveController = WaveformRecorderController(
|
||||||
interval: const Duration(milliseconds: 45),
|
interval: const Duration(milliseconds: 45),
|
||||||
config: const RecordConfig(
|
config: const RecordConfig(
|
||||||
@@ -718,7 +719,7 @@ class _AddExperienceFlowState extends State<AddExperienceFlow> {
|
|||||||
return const [];
|
return const [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return _api.fetchNearbyPlaces(
|
return context.read<PlacesRepository>().fetchNearbyPlaces(
|
||||||
coordinate: coordinate,
|
coordinate: coordinate,
|
||||||
radiusMeters: _nearbyPlaceRadiusMeters,
|
radiusMeters: _nearbyPlaceRadiusMeters,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user