Merge remote-tracking branch 'upstream/main' into issue-710

This commit is contained in:
misha
2025-10-16 18:23:04 +03:00
117 changed files with 4585 additions and 1484 deletions

View File

@@ -0,0 +1,52 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:vk_id/vk_id.dart';
part 'auth_result_entity.freezed.dart';
@freezed
abstract class AuthResultEntity with _$AuthResultEntity {
const factory AuthResultEntity({
required String accessToken,
required String userId,
required String clientId,
String? refreshToken,
String? firstName,
String? lastName,
String? email,
String? phone,
GenderEnum? gender,
String? birthDate,
}) = _AuthResultEntity;
}
enum GenderEnum {
male,
female,
notSet;
static GenderEnum? fromVkGender(VkSex? vkGender) {
switch (vkGender) {
case VkSex.male:
return GenderEnum.male;
case VkSex.female:
return GenderEnum.female;
case VkSex.notStated:
return GenderEnum.notSet;
case null:
return null;
}
}
static GenderEnum? fromString(String? gender) {
switch (gender) {
case 'male':
return GenderEnum.male;
case 'female':
return GenderEnum.female;
case 'notSet':
return GenderEnum.notSet;
default:
return null;
}
}
}

View File

@@ -0,0 +1,16 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'oauth_login_request.freezed.dart';
part 'oauth_login_request.g.dart';
@freezed
abstract class OAuthLoginRequest with _$OAuthLoginRequest {
const factory OAuthLoginRequest({
@JsonKey(name: 'client_id') required String clientId,
@JsonKey(name: 'access_token') required String accessToken,
@JsonKey(name: 'refresh_token') required String refreshToken,
required String provider,
}) = _OAuthLoginRequest;
factory OAuthLoginRequest.fromJson(Map<String, dynamic> json) => _$OAuthLoginRequestFromJson(json);
}

View File

@@ -0,0 +1,25 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'oauth_model.freezed.dart';
part 'oauth_model.g.dart';
@freezed
abstract class OAuthModel with _$OAuthModel {
const factory OAuthModel({
@JsonKey(name: 'access_token') required String accessToken,
@JsonKey(name: 'refresh_token') required String refreshToken,
@JsonKey(name: 'first_name') required String firstName,
@JsonKey(name: 'last_name') required String lastName,
@JsonKey(name: 'date_of_birth') required String dateOfBirth,
@JsonKey(name: 'client_id') required String clientId,
required String provider,
required String id,
required String email,
required String phone,
required String gender,
required int weight,
required int height,
}) = _OAuthModel;
factory OAuthModel.fromJson(Map<String, dynamic> json) => _$OAuthModelFromJson(json);
}

View File

@@ -0,0 +1,30 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'yandex_profile_model.freezed.dart';
part 'yandex_profile_model.g.dart';
@freezed
abstract class YandexProfileModel with _$YandexProfileModel {
factory YandexProfileModel({
required String id,
required String login,
required String birthday,
@JsonKey(name: 'client_id') required String clientId,
@JsonKey(name: 'display_name') required String displayName,
@JsonKey(name: 'real_name') required String realName,
@JsonKey(name: 'first_name') required String firstName,
@JsonKey(name: 'last_name') required String lastName,
@JsonKey(name: 'sex') required String sex,
@JsonKey(name: 'default_email') required String defaultEmail,
@JsonKey(name: 'default_phone') required YandexPhoneData defaultPhone,
}) = _YandexProfileModel;
factory YandexProfileModel.fromJson(Map<String, dynamic> json) => _$YandexProfileModelFromJson(json);
}
@freezed
abstract class YandexPhoneData with _$YandexPhoneData {
factory YandexPhoneData({required int id, required String number}) = _YandexPhoneData;
factory YandexPhoneData.fromJson(Map<String, dynamic> json) => _$YandexPhoneDataFromJson(json);
}

View File

@@ -25,19 +25,4 @@ class AuthCubit extends Cubit<AuthState> {
return false;
}
}
Future<bool> getTokenBySocial({required String provider, required String accessToken}) async {
emit(const AuthState.loading());
try {
final token = (await authRepository.getTokenBySocial(provider: provider, accessToken: accessToken)).token;
emit(AuthState.loaded(token: token));
await TokenStorage.saveTokenInStorage(token);
return true;
} catch (e) {
log(e.toString());
showErrorToast('Не удалось войти через $provider');
emit(AuthState.error(errorMessage: '$e'));
return false;
}
}
}

View File

@@ -0,0 +1,101 @@
import 'dart:developer';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart';
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class OAuthCubit extends Cubit<OAuthState> {
final AuthRepository authRepository;
OAuthCubit({required this.authRepository}) : super(const OAuthState());
Future<void> signInViaSocialMedia({required String provider, required String accessToken, required String refreshToken, required String clientId}) async {
try {
emit(const OAuthState.loading());
final tokenModel = await authRepository.signInViaSocialMedia(
provider: provider,
accessToken: accessToken,
refreshToken: refreshToken,
clientId: clientId,
);
log('OAuthCubit: Got token model: ${tokenModel.token}');
TokenStorage.saveTokenInStorage(tokenModel.token);
emit(OAuthState.loaded(token: tokenModel.token));
} catch (e) {
log('OAuthCubit: Error occurred: $e');
emit(OAuthState.error(errorMessage: e.toString()));
}
}
Future<List<RegistrationValidationErrors>?> signUpViaSocialMedia({
required String provider,
required String accessToken,
required String id,
required String refreshToken,
required String firstName,
required String lastName,
required String email,
required String phone,
required String dateOfBirth,
required String gender,
required int weight,
required int height,
required String clientId,
}) async {
try {
emit(const OAuthState.loading());
final token = await authRepository.signUpViaSocialMedia(
provider: provider,
accessToken: accessToken,
id: id,
refreshToken: refreshToken,
firstName: firstName,
lastName: lastName,
email: email,
phone: phone,
dateOfBirth: dateOfBirth,
gender: gender,
weight: weight,
height: height,
clientId: clientId,
);
TokenStorage.saveTokenInStorage(token);
emit(OAuthState.loaded(token: token));
} on DioException catch (e) {
emit(OAuthState.error(errorMessage: e.toString()));
final errors = e.response?.data['errors'] as Map<String, dynamic>?;
final emailErrors = errors?['email'] as List<dynamic>?;
final phoneErrors = errors?['phone'] as List<dynamic>?;
final dateOfBirth = errors?['date_of_birth'] as List<dynamic>?;
final errorsList = <RegistrationValidationErrors>[];
if (emailErrors != null && emailErrors.isNotEmpty) {
if (emailErrors.contains('Некорректный формат email')) {
errorsList.add(RegistrationValidationErrors.incorrectEmailFormat);
}
if (emailErrors.contains('Такой Эл. почта уже существует.')) {
errorsList.add(RegistrationValidationErrors.emailExist);
}
}
if (phoneErrors != null && phoneErrors.isNotEmpty) {
if (phoneErrors.contains('Такой phone уже существует.')) {
errorsList.add(RegistrationValidationErrors.phoneExist);
}
}
if (dateOfBirth != null && dateOfBirth.isNotEmpty) {
if (dateOfBirth.contains('Некорректный формат даты рождения')) {
errorsList.add(RegistrationValidationErrors.incorrectDateOfBirthFormat);
}
}
if (errorsList.isNotEmpty) {
return errorsList;
}
}
return null;
}
}
enum RegistrationValidationErrors { incorrectEmailFormat, emailExist, phoneExist, incorrectDateOfBirthFormat }

View File

@@ -0,0 +1,14 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'oauth_state.freezed.dart';
@freezed
abstract class OAuthState with _$OAuthState {
const factory OAuthState() = _OAuthState;
const factory OAuthState.loading() = OAuthStateLoading;
const factory OAuthState.loaded({required String token}) = OAuthStateLoaded;
const factory OAuthState.error({required String errorMessage}) = OAuthStateError;
}

View File

@@ -1,6 +1,7 @@
// ignore_for_file: unused_element, duplicate_ignore
import 'dart:async';
import 'dart:developer';
import 'package:eatvio_app/app_keys.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/firebase_api.dart';
@@ -9,9 +10,14 @@ import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/image_constant.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/core/websockets/ws_manager.dart';
import 'package:eatvio_app/core/widgets/custom_divider.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart';
import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart';
import 'package:eatvio_app/core/di/cubit_registrations.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart';
@@ -44,7 +50,7 @@ class _AuthScreenState extends State<AuthScreen> {
late StreamSubscription<bool> keyboardSubscription;
bool isFocused = false;
int tapsCount = 0;
bool _isLoading = false;
@override
void initState() {
super.initState();
@@ -87,9 +93,142 @@ class _AuthScreenState extends State<AuthScreen> {
AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false});
}
void _onVkPressed() async {
try {
log('VK button pressed');
final context = AppRouter.navigatorKey.currentContext;
if (context == null) {
log('Context is null');
return;
}
log('Getting VK auth result');
final authResult = await SocialAuthFacade.signInWithVkAndFetchProfile(context: context);
if (authResult == null) {
log('VK auth result is null');
return;
}
log('VK auth result received, trying OAuth login');
if (context.mounted) {
await _tryOAuthLogin(context, authResult, 'vk');
} else {
log('Context is not mounted');
}
} catch (e) {
log('VK button error: $e');
}
}
void _onOkPressed() async {
try {
final context = AppRouter.navigatorKey.currentContext;
if (context == null) return;
final authResult = await SocialAuthFacade.signInWithOkAndFetchProfile();
if (authResult == null) {
return;
}
if (context.mounted) {
await _tryOAuthLogin(context, authResult, 'vk');
}
} catch (e) {
log(e.toString());
}
}
void _onMailRuPressed() async {
try {
final context = AppRouter.navigatorKey.currentContext;
if (context == null) return;
final authResult = await SocialAuthFacade.signInWithMailRuAndFetchProfile();
if (authResult == null) {
return;
}
if (context.mounted) {
await _tryOAuthLogin(context, authResult, 'vk');
}
} catch (e) {
log(e.toString());
}
}
void _onYandexPressed() async {
try {
final context = AppRouter.navigatorKey.currentContext;
if (context == null) return;
final authResult = await SocialAuthFacade.signInWithYandex();
if (authResult == null) return;
if (context.mounted) {
await _tryOAuthLogin(context, authResult, 'yandex');
}
} catch (e) {
log(e.toString());
}
}
Future<void> _tryOAuthLogin(BuildContext context, AuthResultEntity authResult, String provider) async {
log('Starting OAuth login for provider: $provider');
try {
final oauthCubit = CubitRegistrations.sl<OAuthCubit>();
StreamSubscription? subscription;
subscription = oauthCubit.stream.listen((state) {
log('OAuth state changed: ${state.runtimeType}');
if (state is OAuthStateLoaded) {
log('OAuth login successful, navigating to main screen');
subscription?.cancel();
_reconnectAfterAuth();
} else if (state is OAuthStateError) {
log('OAuth login failed: ${state.errorMessage}');
subscription?.cancel();
AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: provider);
}
});
log('Calling getOAuthLogin with provider: $provider');
await oauthCubit.signInViaSocialMedia(
provider: provider,
accessToken: authResult.accessToken,
refreshToken: authResult.refreshToken ?? '',
clientId: authResult.clientId,
);
} catch (e) {
log('OAuth login error in catch block: $e');
AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: provider);
}
}
Future<void> _reconnectAfterAuth() async {
final context = AppRouter.navigatorKey.currentContext;
if (context == null) return;
try {
await sl<WsManager>().clear();
await sl<WsManager>().initialize();
await sl<ProfileAvatarCubit>().getMyProfile();
await sl<ChatsCubit>().getChatRoomsWithLoading();
await sl<ChatsCubit>().connectToWebSocket();
await sl<NotificationsCubit>().getNotifications();
await sl<NotificationsCubit>().connectToWebSocket();
await sl<FirebaseApi>().registerPushToken();
showSuccessToast('Успешный вход');
} catch (e) {
log(e.toString());
}
AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false});
}
@override
Widget build(BuildContext context) {
var shortestSide = MediaQuery.sizeOf(context).shortestSide;
final screenSize = MediaQuery.of(context).size;
final bool useMobileLayout = shortestSide < 600;
if (!useMobileLayout) isFocused = true;
final s = S.of(context);
@@ -100,9 +239,10 @@ class _AuthScreenState extends State<AuthScreen> {
builder: (BuildContext context, AuthState state) {
return Scaffold(
appBar: AppBar(
toolbarHeight: screenSize.height * 0.15,
centerTitle: true,
toolbarHeight: isFocused ? 64 : MediaQuery.of(context).size.width - 64,
title: GestureDetector(
key: AppKeys.hiddenLogoButtonAuthScreen,
onTap: () {
tapsCount++;
if (tapsCount == 7) {
@@ -110,7 +250,7 @@ class _AuthScreenState extends State<AuthScreen> {
GoRouter.of(context).pushNamed(DevToolsScreen.name);
}
},
child: SvgPicture.asset(ImageConstant.imgLogoEatvio, height: isFocused ? 64 : MediaQuery.of(context).size.width - 64, fit: BoxFit.contain),
child: SvgPicture.asset(ImageConstant.imgLogoEatvio, height: screenSize.height * 0.20),
),
),
body: SafeArea(
@@ -119,6 +259,41 @@ class _AuthScreenState extends State<AuthScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Center(child: Text('Авторизация через')),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_SocialButton(
onTap: () => _onVkPressed(),
backgroundColor: const Color(0xFF2787F5),
tooltip: 'VK ID',
child: SvgPicture.asset('assets/icons/vk_logo.svg', height: 24, width: 24),
),
_SocialButton(
onTap: _onOkPressed,
backgroundColor: const Color(0xFFF58220),
tooltip: 'Одноклассники',
child: SvgPicture.asset('assets/icons/ok_logo.svg', height: 24, width: 24),
),
_SocialButton(
onTap: _onMailRuPressed,
backgroundColor: const Color(0xFF005FF9),
tooltip: 'Mail.ru',
child: SvgPicture.asset('assets/icons/mail_logo.svg', height: 24, width: 24),
),
_SocialButton(
onTap: _onYandexPressed,
backgroundColor: const Color(0xFFFFCC00),
tooltip: 'Яндекс ID',
child: SvgPicture.asset('assets/icons/yandex_logo.svg', height: 24, width: 24),
),
],
),
const SizedBox(height: 16),
const CustomDivider(),
const SizedBox(height: 8),
const SizedBox(height: 16),
Text(s.email),
const SizedBox(height: 4),
CustomTextFormField(
@@ -126,6 +301,7 @@ class _AuthScreenState extends State<AuthScreen> {
textInputAction: TextInputAction.next,
keyboardType: TextInputType.emailAddress,
textInputType: TextInputType.emailAddress,
key: AppKeys.emailTextFieldAuthScreen,
hintText: 'Введите email',
),
const SizedBox(height: 16),
@@ -134,6 +310,7 @@ class _AuthScreenState extends State<AuthScreen> {
CustomTextFormField(
obscureText: hidePassword,
controller: passwordController,
key: AppKeys.passwordTextFieldAuthScreen,
hintText: 'Введите пароль',
suffix: IconButton(
onPressed: () {
@@ -168,52 +345,6 @@ class _AuthScreenState extends State<AuthScreen> {
),
],
),
// const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_SocialButton(
onTap: () => _onVkPressed(context),
backgroundColor: const Color(0xFF2787F5),
tooltip: 'VK ID',
child: SvgPicture.asset(
'assets/icons/vk.svg',
height: 24,
width: 24,
colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn),
),
),
_SocialButton(
onTap: _onOkPressed,
backgroundColor: const Color(0xFFF58220),
tooltip: 'Одноклассники',
child: SvgPicture.asset(
'assets/icons/ok.svg',
height: 24,
width: 24,
colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn),
),
),
const _SocialButton(
onTap: _onMailRuPressed,
backgroundColor: Color(0xFF005FF9),
tooltip: 'Mail.ru',
child: Text(
'Mail',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700),
),
),
const _SocialButton(
onTap: _onYandexPressed,
backgroundColor: Color(0xFFFFCC00),
tooltip: 'Яндекс ID',
child: Text(
'Yandex',
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w700),
),
),
],
),
const Spacer(),
Center(
child: RichText(
@@ -232,7 +363,7 @@ class _AuthScreenState extends State<AuthScreen> {
decorationColor: appTheme.blueA400,
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')),
recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('${sl<AppConfig>().baseUrl}/oferta_bs.pdf')),
),
const TextSpan(text: ' и '),
],
@@ -244,7 +375,7 @@ class _AuthScreenState extends State<AuthScreen> {
decorationColor: appTheme.blueA400,
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/docs/privacy.pdf')),
recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('${sl<AppConfig>().baseUrl}/docs/privacy.pdf')),
),
],
),
@@ -257,21 +388,28 @@ class _AuthScreenState extends State<AuthScreen> {
height: 48,
width: MediaQuery.of(context).size.width - 88,
child: FloatingActionButton.extended(
key: AppKeys.signInButtonAuthScreen,
heroTag: 'authButton',
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400,
onPressed: state is AuthStateLoading
backgroundColor: (state is AuthStateLoading || _isLoading) ? Colors.black.withAlpha(50) : appTheme.blueA400,
onPressed: (state is AuthStateLoading || _isLoading)
? null
: () async {
setState(() {
_isLoading = true;
});
final isAuthenticated = await authCubit.getTokenByCredentials(emailController.text, passwordController.text);
if (isAuthenticated) {
if (context.mounted) {
_reconnectWebSocket(context);
await _reconnectWebSocket(context);
}
}
setState(() {
_isLoading = false;
});
},
label: state is AuthStateLoading
? const CupertinoActivityIndicator()
@@ -304,7 +442,7 @@ class _AuthScreenState extends State<AuthScreen> {
children: [
const Text('Где взять QR-код?\n', style: TextStyle(fontSize: 18)),
Text(
'1. Войдите на сайте $baseUrl в свой аккаунт\n\n2. Перейдите в настройки профиля\n\n3. В разделе "Доп информация" перейдите на "QR Login"',
'1. Войдите на сайте ${sl<AppConfig>().baseUrl} в свой аккаунт\n\n2. Перейдите в настройки профиля\n\n3. В разделе "Доп информация" перейдите на "QR Login"',
),
const SizedBox(height: 12),
Row(
@@ -367,7 +505,6 @@ class _AuthScreenState extends State<AuthScreen> {
}
}
// ignore: unused_element
class _SocialButton extends StatelessWidget {
const _SocialButton({required this.onTap, required this.child, required this.backgroundColor, required this.tooltip});
@@ -381,73 +518,19 @@ class _SocialButton extends StatelessWidget {
return Tooltip(
message: tooltip,
child: InkWell(
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(20),
onTap: onTap,
child: Ink(
height: 48,
width: (MediaQuery.of(context).size.width - 16 * 2 - 12 * 3) / 4,
decoration: BoxDecoration(color: backgroundColor, borderRadius: BorderRadius.circular(12)),
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: const Color(0xFFDBDBDB), width: 1),
),
child: Center(child: child),
),
),
);
}
}
// ignore: unused_element
void _onVkPressed(BuildContext context) async {
final accessToken = await SocialAuthFacade.signInWithVk(context: context);
if (accessToken == null) return;
final isAuthenticated = await sl<AuthCubit>().getTokenBySocial(provider: 'vk', accessToken: accessToken);
if (isAuthenticated) {
if (AppRouter.navigatorKey.currentContext != null) {
// Reuse success flow
// ignore: use_build_context_synchronously
await _reconnectAfterAuth();
}
}
}
void _onOkPressed() async {
final accessToken = await SocialAuthFacade.signInWithOk();
if (accessToken == null) return;
final isAuthenticated = await sl<AuthCubit>().getTokenBySocial(provider: 'ok', accessToken: accessToken);
if (isAuthenticated) {
await _reconnectAfterAuth();
}
}
void _onMailRuPressed() async {
final accessToken = await SocialAuthFacade.signInWithMailRu();
if (accessToken == null) return;
final isAuthenticated = await sl<AuthCubit>().getTokenBySocial(provider: 'mailru', accessToken: accessToken);
if (isAuthenticated) {
await _reconnectAfterAuth();
}
}
void _onYandexPressed() async {
final accessToken = await SocialAuthFacade.signInWithYandex();
if (accessToken == null) return;
final isAuthenticated = await sl<AuthCubit>().getTokenBySocial(provider: 'yandex', accessToken: accessToken);
if (isAuthenticated) {
await _reconnectAfterAuth();
}
}
Future<void> _reconnectAfterAuth() async {
final context = AppRouter.navigatorKey.currentContext;
if (context == null) return;
try {
await sl<WsManager>().clear();
await sl<WsManager>().initialize();
await sl<ProfileAvatarCubit>().getMyProfile();
await sl<ChatsCubit>().getChatRoomsWithLoading();
await sl<ChatsCubit>().connectToWebSocket();
await sl<NotificationsCubit>().getNotifications();
await sl<NotificationsCubit>().connectToWebSocket();
await sl<FirebaseApi>().registerPushToken();
showSuccessToast('Успешный вход');
} catch (_) {}
AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false});
}

View File

@@ -0,0 +1,532 @@
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/core/di/cubit_registrations.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart';
import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/websockets/ws_manager.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
import 'package:eatvio_app/core/firebase_api.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:mask_text_input_formatter/mask_text_input_formatter.dart';
import 'package:url_launcher/url_launcher.dart';
class RegisterViaSocialScreen extends StatefulWidget {
final AuthResultEntity? authResult;
final String? provider;
const RegisterViaSocialScreen({super.key, this.authResult, this.provider});
@override
State<RegisterViaSocialScreen> createState() => _RegisterViaSocialScreenState();
}
class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
final maskFormatter = MaskTextInputFormatter(mask: '+7 (###) ### ## ##', filter: {"#": RegExp(r'[0-9]')}, type: MaskAutoCompletionType.lazy);
final maskFormatterForBirthday = MaskTextInputFormatter(mask: '##.##.####', filter: {"#": RegExp(r'[0-9]')}, type: MaskAutoCompletionType.lazy);
// Контроллеры для полей ввода
final firstNameController = TextEditingController();
final lastNameController = TextEditingController();
final emailController = TextEditingController();
final phoneController = TextEditingController();
final dateOfBirthController = TextEditingController();
final weightController = TextEditingController();
final heightController = TextEditingController();
bool emailExistError = false;
bool phoneExistError = false;
bool dateOfBirthError = false;
bool weightError = false;
bool heightError = false;
bool incorrectEmailFormatError = false;
// Состояние для выбора пола
bool isMaleSelected = true;
@override
void initState() {
super.initState();
_initializeFieldsFromAuthResult();
}
Future<void> _reconnectWebSocket(BuildContext context) async {
try {
await sl<WsManager>().clear();
await sl<WsManager>().initialize();
} catch (e) {
rethrow;
}
if (context.mounted) {
try {
await sl<ProfileAvatarCubit>().getMyProfile();
await sl<ChatsCubit>().getChatRoomsWithLoading();
await sl<ChatsCubit>().connectToWebSocket();
await sl<NotificationsCubit>().getNotifications();
await sl<NotificationsCubit>().connectToWebSocket();
await sl<FirebaseApi>().registerPushToken();
showSuccessToast('Успешная регистрация');
} catch (e) {
rethrow;
}
}
AppRouter.appRouter.go('/feed-screen', extra: {'needToInitializeWebSocket': false});
}
void _initializeFieldsFromAuthResult() {
if (widget.authResult != null) {
final authResult = widget.authResult!;
if (authResult.firstName != null) {
firstNameController.text = authResult.firstName!;
}
if (authResult.lastName != null) {
lastNameController.text = authResult.lastName!;
}
if (authResult.email != null) {
emailController.text = authResult.email!;
}
if (authResult.phone != null) {
final cleanPhone = authResult.phone!.replaceAll(RegExp(r'[^\d]'), '');
final normalizedPhone = cleanPhone.startsWith('8') ? '7${cleanPhone.substring(1)}' : cleanPhone;
final phoneWithCountryCode = normalizedPhone.startsWith('7') ? normalizedPhone : '7$normalizedPhone';
final formattedPhone = maskFormatter.maskText('+$phoneWithCountryCode');
phoneController.text = formattedPhone;
}
if (authResult.birthDate != null) {
final formattedDate = _formatBirthDate(authResult.birthDate!);
final maskedDate = maskFormatterForBirthday.maskText(formattedDate);
dateOfBirthController.text = maskedDate;
}
if (authResult.gender != null) {
isMaleSelected = authResult.gender == GenderEnum.male;
}
}
}
String _formatBirthDate(String birthDate) {
try {
// Пытаемся распарсить различные форматы даты
DateTime? date;
// Если дата в формате ISO (YYYY-MM-DD)
if (birthDate.contains('-')) {
date = DateTime.tryParse(birthDate);
}
// Если дата в формате DD.MM.YYYY
else if (birthDate.contains('.')) {
final parts = birthDate.split('.');
if (parts.length == 3) {
final day = int.tryParse(parts[0]);
final month = int.tryParse(parts[1]);
final year = int.tryParse(parts[2]);
if (day != null && month != null && year != null) {
date = DateTime(year, month, day);
}
}
}
if (date != null) {
// Форматируем в DD.MM.YYYY
final day = date.day.toString().padLeft(2, '0');
final month = date.month.toString().padLeft(2, '0');
final year = date.year.toString();
return '$day.$month.$year';
}
} catch (e) {
// Если не удалось распарсить, возвращаем исходную строку
}
return birthDate;
}
@override
void dispose() {
firstNameController.dispose();
lastNameController.dispose();
emailController.dispose();
phoneController.dispose();
dateOfBirthController.dispose();
weightController.dispose();
heightController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider<OAuthCubit>(
create: (context) => CubitRegistrations.sl<OAuthCubit>(),
child: Scaffold(
appBar: AppBar(),
body: BlocBuilder<OAuthCubit, OAuthState>(
builder: (context, state) {
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Center(
child: Text(
'Регистрация',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, height: 1.1, color: Color(0xFF323F4B)),
),
),
const SizedBox(height: 24),
Row(
children: [
SizedBox(
width: MediaQuery.of(context).size.width / 2 - 22,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text('Имя'),
const SizedBox(height: 4),
CustomTextFormField(controller: firstNameController, hintText: 'Введите имя', onChanged: (newValue) {}),
],
),
),
const SizedBox(width: 12),
SizedBox(
width: MediaQuery.of(context).size.width / 2 - 22,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Фамилия'),
const SizedBox(height: 4),
CustomTextFormField(controller: lastNameController, hintText: 'Введите фамилию', onChanged: (newValue) {}),
],
),
),
],
),
const SizedBox(height: 12),
const Text('Почта'),
const SizedBox(height: 4),
CustomTextFormField(
controller: emailController,
textInputType: TextInputType.emailAddress,
keyboardType: TextInputType.emailAddress,
hintText: 'Введите email',
textInputAction: TextInputAction.next,
hasError: emailExistError || incorrectEmailFormatError,
onChanged: (newValue) {},
),
if (emailExistError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Такая почта уже существует.')],
if (incorrectEmailFormatError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Некорректный формат email')],
const SizedBox(height: 12),
const Text('Номер телефона'),
const SizedBox(height: 4),
CustomTextFormField(
textInputAction: TextInputAction.done,
hintText: '+7 (999) 999 99 99',
controller: phoneController,
keyboardType: TextInputType.number,
inputFormatters: [maskFormatter],
hasError: phoneExistError,
onChanged: (newValue) {},
),
if (phoneExistError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Такой номер телефона уже существует.')],
const SizedBox(height: 12),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: MediaQuery.of(context).size.width / 2 - 22,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text('Дата рождения'),
const SizedBox(height: 4),
CustomTextFormField(
textInputAction: TextInputAction.done,
hintText: '01.01.2000',
controller: dateOfBirthController,
keyboardType: TextInputType.number,
hasError: dateOfBirthError,
inputFormatters: [maskFormatterForBirthday],
onChanged: (newValue) {},
),
if (dateOfBirthError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Некорректный формат даты рождения')],
],
),
),
const SizedBox(width: 12),
SizedBox(
width: MediaQuery.of(context).size.width / 2 - 22,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Пол'),
const SizedBox(height: 4),
Row(
children: [
Expanded(child: _genderButton('М', isMaleSelected)),
const SizedBox(width: 4),
Expanded(child: _genderButton('Ж', !isMaleSelected)),
],
),
],
),
),
],
),
const SizedBox(height: 12),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: MediaQuery.of(context).size.width / 2 - 22,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text('Вес'),
const SizedBox(height: 4),
CustomTextFormField(controller: weightController, hintText: 'Введите вес', onChanged: (newValue) {}, hasError: weightError),
if (weightError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Вес должен быть от 30 до 300 кг')],
],
),
),
const SizedBox(width: 12),
SizedBox(
width: MediaQuery.of(context).size.width / 2 - 22,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Рост'),
const SizedBox(height: 4),
CustomTextFormField(controller: heightController, hintText: 'Введите рост', onChanged: (newValue) {}, hasError: heightError),
if (heightError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Рост должен быть от 100 до 250 см')],
],
),
),
],
),
const SizedBox(height: 28),
const Center(
child: Text(
'Проверьте правильность заполненных\nданных. Пол, рост и вес требуются для\nрасчета вашей дневной нормы калорий',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 28),
InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () => _handleRegistration(context),
child: Ink(
width: MediaQuery.of(context).size.width - 32,
height: 52,
decoration: BoxDecoration(color: state is OAuthStateLoading ? Colors.grey : appTheme.blueA400, borderRadius: BorderRadius.circular(20)),
child: Center(
child: state is OAuthStateLoading
? const CircularProgressIndicator(color: Colors.white)
: const Text(
'Зарегистрироваться',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 18, color: Colors.white),
),
),
),
),
const SizedBox(height: 16),
Center(
child: RichText(
textAlign: TextAlign.center,
text: TextSpan(
text: 'Нажимая "Зарегистрироваться", Вы соглашаетесь\n',
style: const TextStyle(fontSize: 16, color: Colors.black),
children: [
const TextSpan(
text: 'c ',
style: TextStyle(fontSize: 16, color: Colors.black),
),
if (!hidePlans) ...[
TextSpan(
text: ' Публичной офертой',
style: TextStyle(
fontSize: 16,
color: appTheme.blueA400,
decorationColor: appTheme.blueA400,
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('${sl<AppConfig>().baseUrl}oferta_bs.pdf')),
),
const TextSpan(
text: ' и ',
style: TextStyle(fontSize: 16, color: Colors.black),
),
],
TextSpan(
text: 'Политикой конфиденциальности и защитой персональных данных',
style: TextStyle(fontSize: 16, color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline),
recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('${sl<AppConfig>().baseUrl}/docs/privacy.pdf')),
),
],
),
),
),
// Обработка успешной регистрации
if (state is OAuthStateLoaded) ...[
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.green.shade200),
),
child: const Text(
'Регистрация успешно завершена!',
style: TextStyle(color: Colors.green),
textAlign: TextAlign.center,
),
),
// Автоматический переход на главный экран после успешной регистрации
Builder(
builder: (context) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_reconnectWebSocket(context);
});
return const SizedBox.shrink();
},
),
],
],
),
);
},
),
),
);
}
Widget _genderButton(String gender, bool isActive) {
return InkWell(
onTap: () {
setState(() {
isMaleSelected = gender == 'М';
});
},
borderRadius: BorderRadius.circular(20),
child: Ink(
height: 52,
decoration: BoxDecoration(
color: const Color(0xFFF6F6F6),
borderRadius: BorderRadius.circular(20),
border: isActive ? Border.all(width: 2, color: appTheme.blueA400) : null,
),
child: Center(
child: Text(
gender,
style: TextStyle(color: isActive ? appTheme.blueA400 : const Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w500),
),
),
),
);
}
Future<void> _handleRegistration(BuildContext context) async {
final clientID = widget.authResult?.clientId;
// Валидация полей
if (firstNameController.text.isEmpty ||
lastNameController.text.isEmpty ||
emailController.text.isEmpty ||
phoneController.text.isEmpty ||
dateOfBirthController.text.isEmpty ||
weightController.text.isEmpty ||
heightController.text.isEmpty) {
showErrorToast('Пожалуйста, заполните все поля');
return;
}
// Парсим числовые значения
int? weight;
int? height;
weight = int.tryParse(weightController.text);
height = int.tryParse(heightController.text);
bool hasErrorWeightOrHeightError = false;
if (weight == null || height == null) {
setState(() {
weightError = true;
heightError = true;
});
hasErrorWeightOrHeightError = true;
}
if (weight! < 30 || weight > 300) {
setState(() {
weightError = true;
});
hasErrorWeightOrHeightError = true;
}
if (height! < 100 || height > 250) {
setState(() {
heightError = true;
});
hasErrorWeightOrHeightError = true;
}
if (hasErrorWeightOrHeightError) return;
// Очищаем номер телефона от маски для отправки
final cleanPhone = phoneController.text.replaceAll(RegExp(r'[^\d+]'), '');
if (clientID == null) return;
// Вызываем OAuth регистрацию
final errors = await context.read<OAuthCubit>().signUpViaSocialMedia(
provider: widget.provider ?? 'social',
accessToken: widget.authResult?.accessToken ?? 'dummy_token',
id: widget.authResult?.userId ?? 'dummy_id',
refreshToken: widget.authResult?.refreshToken ?? 'dummy_refresh_token',
firstName: firstNameController.text,
lastName: lastNameController.text,
email: emailController.text,
phone: cleanPhone,
dateOfBirth: dateOfBirthController.text,
gender: isMaleSelected ? 'М' : 'Ж',
weight: weight,
height: height,
clientId: clientID,
);
if (errors != null) {
for (final error in errors) {
switch (error) {
case RegistrationValidationErrors.incorrectEmailFormat:
setState(() {
incorrectEmailFormatError = true;
});
break;
case RegistrationValidationErrors.emailExist:
setState(() {
emailExistError = true;
});
break;
case RegistrationValidationErrors.phoneExist:
setState(() {
phoneExistError = true;
});
break;
case RegistrationValidationErrors.incorrectDateOfBirthFormat:
setState(() {
dateOfBirthError = true;
});
break;
}
}
return;
}
showSuccessToast('Регистрация успешна');
}
}

View File

@@ -1,8 +1,8 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/firebase_api.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/token_storage.dart';
@@ -40,17 +40,11 @@ class _ScanQRCodeScreenState extends State<ScanQRCodeScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Сканирование QR-кода'),
centerTitle: true,
),
appBar: AppBar(title: const Text('Сканирование QR-кода'), centerTitle: true),
body: Column(
children: <Widget>[
Expanded(
child: QRView(
key: qrKey,
onQRViewCreated: _onQRViewCreated,
),
child: QRView(key: qrKey, onQRViewCreated: _onQRViewCreated),
),
],
),
@@ -68,17 +62,9 @@ class _ScanQRCodeScreenState extends State<ScanQRCodeScreen> {
final scannedUrl = scanData.code;
final uri = Uri.parse(scannedUrl ?? '');
final scannedBaseUrl = (uri.hasAuthority) ? '${uri.scheme}://${uri.host}' : '';
baseUrl = scannedBaseUrl;
sl<Dio>().options.baseUrl = baseUrl;
final response = await Dio().getUri(
uri,
options: Options(
headers: {
'accept': 'application/json',
'content-type': 'application/json',
},
),
);
sl<AppConfig>().setBaseUrl(scannedBaseUrl);
sl<Dio>().options.baseUrl = scannedBaseUrl;
final response = await Dio().getUri(uri, options: Options(headers: {'accept': 'application/json', 'content-type': 'application/json'}));
final token = response.data['token'];
await TokenStorage.saveTokenInStorage(token);
try {
@@ -97,10 +83,7 @@ class _ScanQRCodeScreenState extends State<ScanQRCodeScreen> {
} catch (e) {
rethrow;
}
AppRouter.navigatorKey.currentContext?.go(
'/feed-screen',
extra: {'needToInitializeWebSocket': false},
);
AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false});
showSuccessToast('Добро пожаловать!');
} catch (e) {
await controller.resumeCamera();

View File

@@ -1,39 +1,76 @@
import 'package:eatvio_app/features/auth/models/credentials_model.dart';
import 'package:eatvio_app/features/auth/models/oauth_model.dart';
import 'package:eatvio_app/features/auth/models/oauth_login_request.dart';
import 'package:eatvio_app/features/auth/models/token_model.dart';
import 'package:eatvio_app/features/auth/services/auth_service.dart';
import 'package:eatvio_app/features/auth/models/social_exchange_request.dart';
class AuthRepository {
final AuthService authService;
AuthRepository({required this.authService});
Future<TokenModel> getTokenByCredentials(
String email,
String password,
) async {
Future<TokenModel> getTokenByCredentials(String email, String password) async {
try {
return await authService.getTokenByCredentials(
credentials: CredentialsModel(
email: email,
password: password,
),
credentials: CredentialsModel(email: email, password: password),
);
} catch (e) {
rethrow;
}
}
Future<TokenModel> getTokenBySocial({
Future<TokenModel> signInViaSocialMedia({
required String provider,
required String accessToken,
required String refreshToken,
required String clientId,
}) async {
try {
return await authService.getTokenBySocial(
body: SocialExchangeRequest(provider: provider, accessToken: accessToken),
return await authService.signInViaSocialMedia(
body: OAuthLoginRequest(provider: provider, accessToken: accessToken, refreshToken: refreshToken, clientId: clientId),
);
} catch (e) {
rethrow;
}
}
Future<String> signUpViaSocialMedia({
required String provider,
required String accessToken,
required String id,
required String refreshToken,
required String firstName,
required String lastName,
required String email,
required String phone,
required String dateOfBirth,
required String gender,
required int weight,
required int height,
required String clientId,
}) async {
try {
return (await authService.signUpViaSocialMedia(
body: OAuthModel(
provider: provider,
accessToken: accessToken,
id: id,
refreshToken: refreshToken,
firstName: firstName,
lastName: lastName,
email: email,
phone: phone,
dateOfBirth: dateOfBirth,
gender: gender == 'М' ? 'male' : 'female',
weight: weight,
height: height,
clientId: clientId,
),
))['token']!
as String;
} catch (e) {
rethrow;
}
}
}

View File

@@ -1,125 +1,214 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart';
import 'package:eatvio_app/features/auth/models/yandex_profile_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_login_yandex/flutter_login_yandex.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/env_config.dart';
import 'package:go_router/go_router.dart';
import 'package:http/http.dart' as http;
import 'package:vk_id/vk_id.dart';
import 'package:webview_flutter/webview_flutter.dart';
class VkAuthResult {
VkAuthResult({required this.accessToken, this.refreshToken, this.userId, this.firstName, this.lastName, this.email, this.phone, this.gender, this.birthDate});
final String accessToken;
final String? refreshToken;
final String? userId;
final String? firstName;
final String? lastName;
final String? email;
final String? phone;
final String? gender;
final String? birthDate;
}
class SocialAuthFacade {
static Future<String?> signInWithVk({required BuildContext context}) async {
static Future<String?> signInWithVk() async {
final context = AppRouter.navigatorKey.currentContext;
if (context == null) return null;
final result = await signInWithVkAndFetchProfile(context: context);
return result?.accessToken;
}
static Future<VkAuthResult?> signInWithVkAndFetchProfile({required BuildContext context}) async {
final clientIdStr = dotenv.maybeGet('VK_CLIENT_ID')?.trim() ?? Platform.environment['VK_CLIENT_ID'];
if (clientIdStr == null || clientIdStr.isEmpty) {
static Future<AuthResultEntity?> signInWithVkAndFetchProfile({required BuildContext context}) async {
final clientIdStr = Platform.isIOS ? EnvConfig.vkClientIdIos.trim() : EnvConfig.vkClientIdAndroid.trim();
if (clientIdStr.isEmpty) {
return null;
}
try {
dynamic controller = VkIDController(clID: int.parse(clientIdStr));
final authUri = controller.generateAuthorizeLinkWithCodeVerifier();
final redirectScheme = 'vk$clientIdStr://';
final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUri.toString(), redirectSchemePrefix: redirectScheme);
if (redirectParams == null) return null;
final code = redirectParams['code'];
final deviceId = redirectParams['device_id'];
if (code == null || deviceId == null) return null;
final controller = VkIDController(clID: int.parse(clientIdStr));
final authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: VkProvider.vkID, scopes: [VkScope.email, VkScope.phone]);
final tokenResponse = await controller.exchangeAuthorizationCodeForToken(code: code, deviceId: deviceId);
final accessToken = _extractAccessToken(tokenResponse);
if (accessToken == null) return null;
return VkAuthResult(accessToken: accessToken);
} catch (_) {
final authUrl = authUri.value.toString();
final codeVerifier = authUri.key;
final redirectScheme = 'vk$clientIdStr://';
final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUrl, redirectSchemePrefix: redirectScheme);
if (redirectParams == null) {
return null;
}
return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier, clientId: clientIdStr);
} catch (e) {
return null;
}
}
static Future<String?> signInWithOk() async {
static Future<AuthResultEntity?> _parseVKAuthResult({
required Map<String, String> redirectParams,
required VkIDController controller,
required String codeVerifier,
required String clientId,
}) async {
final code = redirectParams['code'];
final deviceId = redirectParams['device_id'];
final state = redirectParams['state'];
if (code == null || deviceId == null || state == null) {
return null;
}
final authRes = await controller.retrieveOAuthToken(authorizationCode: code, deviceId: deviceId, codeVerifier: codeVerifier, state: state);
final auth = authRes.result;
if (auth == null) {
var msg = "Retrieve OAuth token error";
final err = authRes.error?.vkErr;
if (err != null) {
msg += ": ${err.error} - ${err.description}";
}
log(msg);
return null;
}
final profileRes = await controller.getProfileInfo();
final profile = profileRes.result;
if (profile == null) {
var msg = "Profile info load error";
final err = profileRes.error?.vkErr;
if (err != null) {
msg += ": ${err.error} - ${err.description}";
}
log(msg);
return null;
}
return AuthResultEntity(
accessToken: auth.accessToken,
refreshToken: auth.refreshToken,
userId: profile.userId.toString(),
firstName: profile.firstName,
lastName: profile.lastName,
email: profile.email,
phone: profile.phone,
gender: GenderEnum.fromVkGender(profile.parsedSex),
birthDate: profile.bDayDt?.toString(),
clientId: clientId,
);
}
static Future<AuthResultEntity?> signInWithOkAndFetchProfile() async {
final context = AppRouter.navigatorKey.currentContext;
if (context == null) return null;
return _signInWithVkIdProvider(context: context, provider: 'ok');
return await _signInWithVkIdProvider(context: context, provider: VkProvider.okRu);
}
static Future<String?> signInWithMailRu() async {
static Future<AuthResultEntity?> signInWithMailRuAndFetchProfile() async {
final context = AppRouter.navigatorKey.currentContext;
if (context == null) return null;
return _signInWithVkIdProvider(context: context, provider: 'mail');
return await _signInWithVkIdProvider(context: context, provider: VkProvider.mailRu);
}
static Future<String?> signInWithYandex() async {
final clientId = dotenv.maybeGet('YANDEX_CLIENT_ID')?.trim() ?? Platform.environment['YANDEX_CLIENT_ID'];
final redirectUri = dotenv.maybeGet('YANDEX_REDIRECT_URI')?.trim() ?? Platform.environment['YANDEX_REDIRECT_URI'];
static Future<AuthResultEntity?> signInWithYandex() async {
final clientId = EnvConfig.yandexClientId.trim();
final redirectUri = EnvConfig.yandexRedirectUri.trim();
if (clientId == null || clientId.isEmpty || redirectUri == null || redirectUri.isEmpty) {
if (clientId.isEmpty || redirectUri.isEmpty) {
return null;
}
try {
final plugin = FlutterLoginYandex();
final response = await plugin.signIn();
final token = (response is Map) ? response['token'] as String? : null;
return (token != null && token.isNotEmpty) ? token : null;
} catch (_) {
return null;
}
}
static Future<String?> _signInWithVkIdProvider({required BuildContext context, required String provider}) async {
final clientIdStr = dotenv.maybeGet('VK_CLIENT_ID')?.trim() ?? Platform.environment['VK_CLIENT_ID'];
if (clientIdStr == null || clientIdStr.isEmpty) {
return null;
}
try {
dynamic controller = VkIDController(clID: int.parse(clientIdStr));
final authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider);
final redirectScheme = 'vk$clientIdStr://';
final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUri.toString(), redirectSchemePrefix: redirectScheme);
if (redirectParams == null) return null;
final code = redirectParams['code'];
final deviceId = redirectParams['device_id'];
if (code == null || deviceId == null) return null;
final tokenResponse = await controller.exchangeAuthorizationCodeForToken(code: code, deviceId: deviceId, provider: provider);
final accessToken = _extractAccessToken(tokenResponse);
return accessToken;
} catch (_) {
return null;
}
}
final context = AppRouter.navigatorKey.currentContext;
if (context == null) return null;
static String? _extractAccessToken(dynamic tokenResponse) {
if (tokenResponse == null) return null;
if (tokenResponse is Map) {
return (tokenResponse['access_token'] as String?) ?? (tokenResponse['token'] as String?);
final authorizeUrl = Uri.https('oauth.yandex.ru', '/authorize', {
'response_type': 'code',
'client_id': clientId,
'redirect_uri': redirectUri,
'force_confirm': 'true',
}).toString();
final redirectSchemePrefix = redirectUri;
final params = await _presentOAuthWebView(context: context, initialUrl: authorizeUrl, redirectSchemePrefix: redirectSchemePrefix);
if (params == null) return null;
final code = params['code'];
if (code == null || code.isEmpty) {
return null;
}
final tokens = await _getTokensFomYandex(code: code, clientId: clientId, clientSecret: EnvConfig.yandexClientSecret);
if (tokens == null) {
return null;
}
final response = await http.get(Uri.parse('https://login.yandex.ru/info?format=json'), headers: {'Authorization': 'OAuth ${tokens.accessToken}'});
final Map<String, dynamic> json = jsonDecode(response.body);
final yandexProfileData = YandexProfileModel.fromJson(json);
return AuthResultEntity(
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
userId: yandexProfileData.id,
firstName: yandexProfileData.firstName,
lastName: yandexProfileData.lastName,
email: yandexProfileData.defaultEmail,
phone: yandexProfileData.defaultPhone.number,
gender: GenderEnum.fromString(yandexProfileData.sex),
birthDate: yandexProfileData.birthday,
clientId: clientId,
);
} catch (e) {
log(e.toString());
}
try {
final token = (tokenResponse as dynamic).accessToken as String?;
if (token != null && token.isNotEmpty) return token;
} catch (_) {}
try {
final token = (tokenResponse as dynamic).token as String?;
if (token != null && token.isNotEmpty) return token;
} catch (_) {}
return null;
}
static Future<({String accessToken, String refreshToken})?> _getTokensFomYandex({
required String code,
required String clientId,
required String clientSecret,
}) async {
final response = await http.post(
Uri.parse('https://oauth.yandex.ru/token'),
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: {'grant_type': 'authorization_code', 'code': code, 'client_id': clientId, 'client_secret': clientSecret},
);
if (response.statusCode != 200) {
log('Yandex token exchange failed: ${response.body}');
return null;
}
final tokens = jsonDecode(response.body);
final accessToken = tokens['access_token'] as String;
final refreshToken = tokens['refresh_token'] as String;
return (accessToken: accessToken, refreshToken: refreshToken);
}
static Future<AuthResultEntity?> _signInWithVkIdProvider({required BuildContext context, required VkProvider provider}) async {
final clientIdStr = Platform.isIOS ? EnvConfig.vkClientIdIos.trim() : EnvConfig.vkClientIdAndroid.trim();
if (clientIdStr.isEmpty) {
return null;
}
try {
final controller = VkIDController(clID: int.parse(clientIdStr));
final authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider, scopes: [VkScope.phone, VkScope.email]);
final codeVerifier = authUri.key;
final authUrl = authUri.value.toString();
final redirectScheme = 'vk$clientIdStr://';
final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUrl, redirectSchemePrefix: redirectScheme);
if (redirectParams == null) return null;
final code = redirectParams['code'];
final deviceId = redirectParams['device_id'];
if (code == null || deviceId == null) return null;
return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier, clientId: clientIdStr);
} catch (e) {
return null;
}
}
static Future<Map<String, String>?> _presentOAuthWebView({
required BuildContext context,
required String initialUrl,
@@ -134,22 +223,49 @@ class SocialAuthFacade {
if (url.startsWith(redirectSchemePrefix)) {
final uri = Uri.parse(url);
final params = <String, String>{}..addAll(uri.queryParameters);
Navigator.of(context, rootNavigator: true).pop(params);
if (uri.fragment.isNotEmpty) {
final fragPairs = uri.fragment.split('&');
for (final pair in fragPairs) {
final eq = pair.indexOf('=');
if (eq > 0 && eq < pair.length - 1) {
final key = Uri.decodeComponent(pair.substring(0, eq));
final value = Uri.decodeComponent(pair.substring(eq + 1));
params[key] = value;
}
}
}
if (GoRouter.of(context).canPop()) {
GoRouter.of(context).pop(params);
}
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
onPageStarted: (url) {},
onPageFinished: (url) {},
onWebResourceError: (error) {},
),
)
..loadRequest(Uri.parse(initialUrl));
);
try {
await controller.loadRequest(Uri.parse(initialUrl));
} catch (e) {
return null;
}
if (!context.mounted) return null;
return showDialog<Map<String, String>>(
context: context,
barrierDismissible: false,
builder: (_) {
const borderRadius = BorderRadius.all(Radius.circular(16));
return Dialog(
insetPadding: const EdgeInsets.all(16),
child: SizedBox(height: 560, child: WebViewWidget(controller: controller)),
shape: const RoundedRectangleBorder(borderRadius: borderRadius),
elevation: 0,
child: ClipRRect(
borderRadius: borderRadius,
child: SizedBox(height: 560, child: WebViewWidget(controller: controller)),
),
);
},
);

View File

@@ -1,7 +1,8 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/auth/models/credentials_model.dart';
import 'package:eatvio_app/features/auth/models/oauth_model.dart';
import 'package:eatvio_app/features/auth/models/oauth_login_request.dart';
import 'package:eatvio_app/features/auth/models/token_model.dart';
import 'package:eatvio_app/features/auth/models/social_exchange_request.dart';
import 'package:retrofit/retrofit.dart';
part 'auth_service.g.dart';
@@ -11,12 +12,11 @@ abstract class AuthService {
factory AuthService(Dio dio, {String? baseUrl}) = _AuthService;
@POST('/api/login')
Future<TokenModel> getTokenByCredentials({
@Body() required CredentialsModel credentials,
});
Future<TokenModel> getTokenByCredentials({@Body() required CredentialsModel credentials});
@POST('/api/social/login')
Future<TokenModel> getTokenBySocial({
@Body() required SocialExchangeRequest body,
});
@POST('/api/oauth/callback')
Future<Map<String, Object?>> signUpViaSocialMedia({@Body() required OAuthModel body});
@POST('/api/oauth/login')
Future<TokenModel> signInViaSocialMedia({@Body() required OAuthLoginRequest body});
}

View File

@@ -1,6 +1,6 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/enums/complained_content.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/core/widgets/custom_popup.dart';
@@ -207,7 +207,7 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
children: [
CircleAvatar(
radius: 16,
foregroundImage: NetworkImage(baseUrl + (blogPostState.blogPostModel?.author.authorProfileIcon ?? '')),
foregroundImage: NetworkImage(sl<AppConfig>().baseUrl + (blogPostState.blogPostModel?.author.authorProfileIcon ?? '')),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
),
const SizedBox(width: 8),
@@ -239,7 +239,7 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
),
const SizedBox(height: 8),
if (blogPostState.blogPostModel?.image.isNotEmpty ?? false)
CachedNetworkImage(imageUrl: '$baseUrl/storage/${blogPostState.blogPostModel?.image ?? ''}'),
CachedNetworkImage(imageUrl: '${sl<AppConfig>().baseUrl}/storage/${blogPostState.blogPostModel?.image ?? ''}'),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
@@ -267,7 +267,7 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
final box = context.findRenderObject() as RenderBox?;
SharePlus.instance.share(
ShareParams(
uri: Uri.parse('$baseUrl/ru/blog_post/${widget.id}'),
uri: Uri.parse('${sl<AppConfig>().baseUrl}/ru/blog_post/${widget.id}'),
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
),
);

View File

@@ -1,61 +1,34 @@
import 'dart:developer';
import 'package:eatvio_app/core/cubit/base_cubit.dart';
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_state.dart';
import 'package:eatvio_app/features/blog_post_create/repositories/blog_post_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
class BlogPostCreateCubit extends Cubit<BlogPostCreateState> {
class BlogPostCreateCubit extends BaseCubit<BlogPostCreateState> {
final BlogPostRepository blogPostRepository;
BlogPostCreateCubit({
required this.blogPostRepository,
}) : super(const BlogPostCreateState());
BlogPostCreateCubit({required this.blogPostRepository}) : super(const BlogPostCreateState());
Future<bool> createBlogPost({
String? title,
String? videoUrl,
String? description,
XFile? image,
}) async {
Future<bool> createBlogPost({String? title, String? videoUrl, String? description, XFile? image}) async {
try {
emit(const BlogPostCreateState.loading());
await blogPostRepository.createBlogPost(
title,
videoUrl,
description,
image,
);
await blogPostRepository.createBlogPost(title, videoUrl, description, image);
emit(const BlogPostCreateState.loaded());
return true;
} catch (e, st) {
log(e.toString(), stackTrace: st);
} catch (error, stackTrace) {
handleError(error, stackTrace);
emit(const BlogPostCreateState.loaded());
return false;
}
}
Future<bool> updateBlogPost({
required int id,
String? title,
String? videoUrl,
String? description,
XFile? image,
String? oldImageUrl,
}) async {
Future<bool> updateBlogPost({required int id, String? title, String? videoUrl, String? description, XFile? image, String? oldImageUrl}) async {
try {
emit(const BlogPostCreateState.loading());
await blogPostRepository.updateBlogPost(
id,
title,
videoUrl,
description,
image,
oldImageUrl,
);
await blogPostRepository.updateBlogPost(id, title, videoUrl, description, image, oldImageUrl);
emit(const BlogPostCreateState.loaded());
return true;
} catch (e, st) {
log(e.toString(), stackTrace: st);
} catch (error, stackTrace) {
handleError(error, stackTrace);
emit(const BlogPostCreateState.loaded());
return false;
}
@@ -65,8 +38,8 @@ class BlogPostCreateCubit extends Cubit<BlogPostCreateState> {
try {
await blogPostRepository.deleteBlogPost(id);
return true;
} catch (e, st) {
log(e.toString(), stackTrace: st);
} catch (error, stackTrace) {
handleError(error, stackTrace);
return false;
}
}

View File

@@ -1,7 +1,7 @@
import 'dart:async';
import 'dart:io';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/permission_handler/permission_handler.dart';
import 'package:eatvio_app/core/theme/app_decoration.dart';
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
@@ -304,7 +304,7 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Image(
image: NetworkImage('$baseUrl/storage/${widget.feedBlogPostModel!.image}'),
image: NetworkImage('${sl<AppConfig>().baseUrl}/storage/${widget.feedBlogPostModel!.image}'),
),
),
),

View File

@@ -1,7 +1,8 @@
import 'dart:developer';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:http/http.dart' as http;
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
import 'package:eatvio_app/features/blog_post_create/services/blog_post_service.dart';
@@ -36,7 +37,7 @@ class BlogPostRepository {
Future<XFile?> downloadImage(String imageUrl) async {
try {
// Загрузка изображения
final response = await http.get(Uri.parse('$baseUrl/storage/$imageUrl'));
final response = await http.get(Uri.parse('${sl<AppConfig>().baseUrl}/storage/$imageUrl'));
if (response.statusCode == 200) {
// Получение временной директории

View File

@@ -1,5 +1,6 @@
import 'package:badges/badges.dart' as badges;
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/utils/image_constant.dart';
import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
@@ -102,7 +103,7 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
label: '',
icon: CircleAvatar(
foregroundImage: NetworkImage(
widget.profileModel.profilePhotoUrl.contains('https') ? widget.profileModel.profilePhotoUrl : baseUrl + widget.profileModel.profilePhotoUrl,
widget.profileModel.profilePhotoUrl.contains('https') ? widget.profileModel.profilePhotoUrl : sl<AppConfig>().baseUrl + widget.profileModel.profilePhotoUrl,
),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
radius: 16,

View File

@@ -1,6 +1,7 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:dismissible_page/dismissible_page.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/app_decoration.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
@@ -88,7 +89,7 @@ class _PhotoViewScreenState extends State<PhotoViewScreen> {
color: Colors.white,
),
title: Text(
widget.imageUrls[index].replaceAll('$baseUrl/storage/chat_files/', ''),
widget.imageUrls[index].replaceAll('${sl<AppConfig>().baseUrl}/storage/chat_files/', ''),
style: const TextStyle(
color: Colors.white,
fontSize: 12,

View File

@@ -1,4 +1,5 @@
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/localization_utils.dart';
@@ -47,7 +48,7 @@ class _OrderCardState extends State<OrderCard> {
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
'$baseUrl/storage/${widget.plan.image}',
'${sl<AppConfig>().baseUrl}/storage/${widget.plan.image}',
width: double.infinity,
fit: BoxFit.cover,
),

View File

@@ -1,4 +1,6 @@
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:flutter/material.dart';
@@ -49,7 +51,7 @@ class _LastStepScreenState extends State<LastStepScreen> {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: TextButton(
onPressed: () async => await launchUrl(Uri.parse(hidePlans ? '$baseUrl/docs/privacy.pdf' : '$baseUrl/oferta_bs.pdf')),
onPressed: () async => await launchUrl(Uri.parse(hidePlans ? '${sl<AppConfig>().baseUrl}/docs/privacy.pdf' : '${sl<AppConfig>().baseUrl}/oferta_bs.pdf')),
child: Text(
hidePlans ? 'Политика конфиденциальности и пользовательское соглашение' : 'Договор офёрты',
style: const TextStyle(height: 1, fontSize: 16, color: Color(0xFF323F4B), decoration: TextDecoration.underline),

View File

@@ -13,6 +13,7 @@ abstract class StatisticsModel with _$StatisticsModel {
@JsonKey(name: 'count_diets') int? dietsCount,
@JsonKey(name: 'count_consultants') int? consultationsCount,
@JsonKey(name: 'count_products_nutritional_values') int? qwe,
@JsonKey(name: 'referral_link') String? refLink,
}) = _StatisticsModel;
factory StatisticsModel.fromJson(Map<String, dynamic> json) => _$StatisticsModelFromJson(json);

View File

@@ -1,6 +1,7 @@
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart';
@@ -10,6 +11,7 @@ import 'package:eatvio_app/features/consultant_rating/presentation/widgets/ratin
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/statistic_widget.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class ConsultantRatingScreen extends StatelessWidget {
@@ -53,8 +55,8 @@ class ConsultantRatingPageContent extends StatelessWidget {
child: Column(
children: [
const SizedBox(height: 16),
const RatingWidget(
rating: 1058,
RatingWidget(
rating: state.absoluteRating!,
),
const SizedBox(height: 16),
Padding(
@@ -67,7 +69,10 @@ class ConsultantRatingPageContent extends StatelessWidget {
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
),
onPressed: () {},
onPressed: () {
Clipboard.setData(ClipboardData(text: state.statistics?.refLink ?? ''));
showInfoToast(s.link_was_copied);
},
),
),
const SizedBox(height: 16),

View File

@@ -2,17 +2,23 @@ import 'dart:developer';
import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_state.dart';
import 'package:eatvio_app/features/consultant_rating/repositories/consultant_rating_repository.dart';
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class ConsultantRatingCubit extends Cubit<ConsultantRatingState> {
final ConsultantRatingRepository repository;
ConsultantRatingCubit(this.repository) : super(ConsultantRatingState());
final FeedRepository feedRepository;
final ProfileRepository profileRepository;
ConsultantRatingCubit({required this.repository, required this.feedRepository, required this.profileRepository}) : super(ConsultantRatingState());
Future<void> getConsultantStatistics({String? date}) async {
try {
emit(state.copyWith(isStatisticsLoading: true));
final statistics = await repository.getConsultantStatistics(date: date);
emit(state.copyWith(isStatisticsLoading: false, statistics: statistics, error: null));
final absoluteRating = await feedRepository.getConsultants((await profileRepository.getMyProfile()).name);
emit(state.copyWith(isStatisticsLoading: false, statistics: statistics, error: null, absoluteRating: absoluteRating.first.totalScores));
} catch (e, st) {
emit(state.copyWith(isStatisticsLoading: true, error: e.toString()));
log(e.toString(), stackTrace: st);

View File

@@ -12,5 +12,6 @@ abstract class ConsultantRatingState with _$ConsultantRatingState {
@Default(null) StatisticsModel? statistics,
@Default(null) String? error,
@Default(<RatingModel>[]) List<RatingModel> ratings,
@Default(0) int? absoluteRating,
}) = _ConsultantRatingState;
}

View File

@@ -71,7 +71,7 @@ class StatisticWidget extends StatelessWidget {
children: [
Expanded(child: StatisticBouble(number: statistics?.consultationsCount ?? 0, title: s.consultations_count)),
const SizedBox(width: 8),
Expanded(child: StatisticBouble(number: statistics?.qwe ?? 0, title: s.active_consultations_count)),
Expanded(child: StatisticBouble(number: statistics?.qwe ?? 0, title: s.added_products_in_glossary)),
],
),
const SizedBox(

View File

@@ -2,8 +2,10 @@ import 'dart:convert';
import 'dart:io';
import 'dart:developer';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_state.dart';
import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
@@ -53,7 +55,7 @@ class CreateDietCubit extends Cubit<CreateDietState> {
name: diet.name,
duration: diet.duration,
type: type,
imageUrl: '$baseUrl/storage/${diet.image}',
imageUrl: '${sl<AppConfig>().baseUrl}/storage/${diet.image}',
),
);
emit(state.copyWith(dietState: diet.state, durationIndex: durationValues.indexOf(diet.duration)));

View File

@@ -1,7 +1,8 @@
import 'dart:developer';
import 'dart:io';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/features/create_recipe/entities/recipe_create_entity.dart';
import 'package:eatvio_app/features/create_recipe/models/ingredient.dart' as i;
import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart';
@@ -187,13 +188,13 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
}
Future<void> recipeToEntity(Recipe recipe) async {
final file = await downloadImageAsFile('$baseUrl/storage/${recipe.image}');
final file = await downloadImageAsFile('${sl<AppConfig>().baseUrl}/storage/${recipe.image}');
final List<StepEntity> steps = await Future.wait(
recipe.steps!.map(
(e) async => StepEntity(
description: e.description,
image: e.image.isEmpty ? null : await downloadImageAsFile('$baseUrl/storage/${e.image}'),
image: e.image.isEmpty ? null : await downloadImageAsFile('${sl<AppConfig>().baseUrl}/storage/${e.image}'),
),
),
);

View File

@@ -1,8 +1,8 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/firebase_api.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
@@ -84,7 +84,7 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
void initState() {
super.initState();
setState(() {
baseUrlForTest = baseUrl;
baseUrlForTest = sl<AppConfig>().baseUrl;
});
testsStates = List.generate(testCases.length, (index) => TestState.notRun);
}
@@ -111,8 +111,8 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
onPressed: () {
setState(() {
baseUrlForTest = controller.text;
baseUrl = baseUrlForTest.toLowerCase();
sl<Dio>().options.baseUrl = baseUrl;
sl<AppConfig>().setBaseUrl(baseUrlForTest.toLowerCase());
sl<Dio>().options.baseUrl = baseUrlForTest.toLowerCase();
controller.clear();
});
},
@@ -122,23 +122,23 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
),
const SizedBox(height: 10),
CustomOutlinedButton(
text: baseUrl == EnvConfig.devBaseUrl ? 'Set production base url' : 'Set dev2 base url',
text: sl<AppConfig>().baseUrl == EnvConfig.devBaseUrl ? 'Set production base url' : 'Set dev2 base url',
onPressed: () {
setState(() {
if (baseUrl == EnvConfig.baseUrl) {
if (sl<AppConfig>().baseUrl == EnvConfig.baseUrl) {
baseUrlForTest = EnvConfig.devBaseUrl;
} else {
baseUrlForTest = EnvConfig.baseUrl;
}
baseUrl = baseUrlForTest.toLowerCase();
sl<Dio>().options.baseUrl = baseUrl;
sl<AppConfig>().setBaseUrl(baseUrlForTest.toLowerCase());
sl<Dio>().options.baseUrl = baseUrlForTest.toLowerCase();
controller.clear();
});
},
buttonStyle: const ButtonStyle(),
),
SizedBox(height: baseUrlForTest != baseUrl ? 40 : 10),
if (baseUrl.toLowerCase() != EnvConfig.baseUrl.toLowerCase())
SizedBox(height: baseUrlForTest != sl<AppConfig>().baseUrl ? 40 : 10),
if (sl<AppConfig>().baseUrl.toLowerCase() != EnvConfig.baseUrl.toLowerCase())
ListView.builder(
shrinkWrap: true,
itemCount: testCases.length,

View File

@@ -1,3 +1,4 @@
import 'package:eatvio_app/core/routing/route_constants.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/custom_divider.dart';
@@ -6,77 +7,84 @@ import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class ChosenDietWidget extends StatelessWidget {
const ChosenDietWidget({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<DiaryCubit, DiaryState>(builder: (context, state) {
if (state is DiaryStateLoaded) {
if (state.diary.typeOfDiary.hasChosenDiet) {
return ChosenDietWidgetContent(
currentDietDay: state.diary.dayOfChosenDiet ?? 0,
dietName: state.diary.diet?.name ?? '',
dietDuration: state.diary.diet?.duration ?? 0,
);
return BlocBuilder<DiaryCubit, DiaryState>(
builder: (context, state) {
if (state is DiaryStateLoaded) {
if (state.diary.typeOfDiary.hasChosenDiet) {
return ChosenDietWidgetContent(
currentDietDay: state.diary.dayOfChosenDiet ?? 0,
dietName: state.diary.diet?.name ?? '',
dietDuration: state.diary.diet?.duration ?? 0,
dietId: state.diary.diet?.id ?? 0,
);
}
}
}
if (state is DiaryStateLoadedTomorrowWithDiet) {
if (state.diary.typeOfDiary.hasChosenDiet) {
return Column(
children: [
const CustomDivider(),
const SizedBox(height: 16),
ChosenDietWidgetContent(
currentDietDay: state.diary.dayOfChosenDiet ?? 0,
dietName: state.diary.diet?.name ?? '',
dietDuration: state.diary.diet?.duration ?? 0,
),
],
);
if (state is DiaryStateLoadedTomorrowWithDiet) {
if (state.diary.typeOfDiary.hasChosenDiet) {
return Column(
children: [
const CustomDivider(),
const SizedBox(height: 16),
ChosenDietWidgetContent(
currentDietDay: state.diary.dayOfChosenDiet ?? 0,
dietName: state.diary.diet?.name ?? '',
dietDuration: state.diary.diet?.duration ?? 0,
dietId: state.diary.diet?.id ?? 0,
),
],
);
}
}
}
return const SizedBox.shrink();
});
return const SizedBox.shrink();
},
);
}
}
class ChosenDietWidgetContent extends StatelessWidget {
const ChosenDietWidgetContent({
super.key,
required this.currentDietDay,
required this.dietName,
required this.dietDuration,
});
const ChosenDietWidgetContent({super.key, required this.currentDietDay, required this.dietName, required this.dietDuration, required this.dietId});
final int currentDietDay;
final String dietName;
final int dietDuration;
final int dietId;
@override
Widget build(BuildContext context) {
final size = MediaQuery.sizeOf(context);
final s = S.of(context);
return Column(
children: [
Row(
children: [
SizedBox(
width: size.width * 0.4,
child: Text(dietName, style: TextStyles.textLMedium.copyWith(color: appTheme.black)),
),
const Spacer(),
Container(
decoration: BoxDecoration(color: appTheme.dividerColor, borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
child: Text('${s.day} $currentDietDay из $dietDuration', style: TextStyles.textMSemibold.copyWith(color: appTheme.gray)),
)
],
),
const SizedBox(
height: 16,
)
],
return GestureDetector(
onTap: () {
GoRouter.of(context).push(RouteConstants.aboutDietScreen, extra: {'id': dietId, 'navigateToComments': false});
},
child: Column(
children: [
Row(
children: [
SizedBox(
width: size.width * 0.4,
child: Text(
dietName,
style: TextStyles.textLMedium.copyWith(color: appTheme.black, decoration: TextDecoration.underline),
),
),
const Spacer(),
Container(
decoration: BoxDecoration(color: appTheme.dividerColor, borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
child: Text('${s.day} $currentDietDay из $dietDuration', style: TextStyles.textMSemibold.copyWith(color: appTheme.gray)),
),
],
),
const SizedBox(height: 16),
],
),
);
}
}

View File

@@ -1,8 +1,9 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/permission_handler/permission_handler.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
@@ -139,7 +140,7 @@ class _PhotoGalleryWidgetState extends State<PhotoGalleryWidget> {
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: CachedNetworkImage(
imageUrl: '$baseUrl/storage/${state.diary.meals[mealIndex].photos[currentSelectedPhotoIndex].path}',
imageUrl: '${sl<AppConfig>().baseUrl}/storage/${state.diary.meals[mealIndex].photos[currentSelectedPhotoIndex].path}',
),
),
if (!state.isConsultantView && !state.isDiaryFinished && !state.isDiaryCommented)
@@ -211,7 +212,7 @@ class _PhotoGalleryWidgetState extends State<PhotoGalleryWidget> {
height: 52,
width: 52,
child: CachedNetworkImage(
imageUrl: '$baseUrl/storage/${state.diary.meals[mealIndex].photos[i].path}',
imageUrl: '${sl<AppConfig>().baseUrl}/storage/${state.diary.meals[mealIndex].photos[i].path}',
fit: BoxFit.fitWidth,
),
),

View File

@@ -19,9 +19,14 @@ class AboutDietCubit extends Cubit<AboutDietState> {
String? userStartedDietName;
List<int> _selectedIndexes = [];
Future<void> getDiet(String slug, {int? index}) async {
Future<void> getDiet({String? slug, int? id, int? index}) async {
try {
final diet = await dietsRepository.getDietBySlug(slug);
final Diet diet;
if (slug != null) {
diet = await dietsRepository.getDietBySlug(slug);
} else {
diet = await dietsRepository.getDiet(id!);
}
if (index != null) {
dietsRepository.emitCommentsStateToFeed(CommentsState(index: index, totalComments: diet.totalComments));
}
@@ -32,10 +37,16 @@ class AboutDietCubit extends Cubit<AboutDietState> {
}
}
Future<void> initDietPage(String slug) async {
Future<void> initDietPage({String? slug, int? id}) async {
try {
emit(const AboutDietState.loading());
final diet = await dietsRepository.getDietBySlug(slug);
final Diet diet;
if (slug != null) {
diet = await dietsRepository.getDietBySlug(slug);
} else {
diet = await dietsRepository.getDiet(id!);
}
final chosenDiets = await profileRepository.getChosenDiets();
final lastChosenDiet = chosenDiets.firstOrNull;
if (lastChosenDiet != null && ['inited', 'started'].contains(lastChosenDiet.state)) {
@@ -50,12 +61,7 @@ class AboutDietCubit extends Cubit<AboutDietState> {
}
}
_selectedIndexes = List.generate(diet.dailySchedule.length, (index) => 0);
emit(AboutDietState.loaded(
diet: diet,
isStarted: isCurrentDietStarted,
hasStartedDiet: hasStartedDiet,
selectedIndexes: _selectedIndexes,
));
emit(AboutDietState.loaded(diet: diet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet, selectedIndexes: _selectedIndexes));
} catch (e) {
log(e.toString());
emit(AboutDietState.error(message: e.toString()));
@@ -68,41 +74,30 @@ class AboutDietCubit extends Cubit<AboutDietState> {
final currentDiet = (state as DietStateLoaded).diet;
if (currentDiet.wasLikedByMe) {
final updatedDiet = currentDiet.copyWith(
totalLikes: currentDiet.totalLikes - 1,
wasLikedByMe: false,
);
final updatedDiet = currentDiet.copyWith(totalLikes: currentDiet.totalLikes - 1, wasLikedByMe: false);
if (dietIndexInFeed != null) {
dietsRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: updatedDiet.wasLikedByMe, totalLikes: updatedDiet.totalLikes, index: dietIndexInFeed));
}
emit(AboutDietState.loaded(diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet, selectedIndexes: _selectedIndexes));
} else {
final updatedDiet = currentDiet.copyWith(
totalLikes: currentDiet.totalLikes + 1,
wasLikedByMe: true,
);
final updatedDiet = currentDiet.copyWith(totalLikes: currentDiet.totalLikes + 1, wasLikedByMe: true);
if (dietIndexInFeed != null) {
dietsRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: updatedDiet.wasLikedByMe, totalLikes: updatedDiet.totalLikes, index: dietIndexInFeed));
}
emit(AboutDietState.loaded(diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet, selectedIndexes: _selectedIndexes));
}
EasyDebounce.debounce(
'like_diet',
const Duration(seconds: 1),
() async {
try {
await dietsRepository.likeDiet(id);
} catch (e) {
log(e.toString());
if (dietIndexInFeed != null) {
dietsRepository
.emitLikesStateToFeed(LikesState(wasLikedByMe: currentDiet.wasLikedByMe, totalLikes: currentDiet.totalLikes, index: dietIndexInFeed));
}
emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: currentDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet));
EasyDebounce.debounce('like_diet', const Duration(seconds: 1), () async {
try {
await dietsRepository.likeDiet(id);
} catch (e) {
log(e.toString());
if (dietIndexInFeed != null) {
dietsRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: currentDiet.wasLikedByMe, totalLikes: currentDiet.totalLikes, index: dietIndexInFeed));
}
},
);
emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes, diet: currentDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet));
}
});
}
Future<void> addToFavorite(int id, int? dietIndex) async {
@@ -110,30 +105,24 @@ class AboutDietCubit extends Cubit<AboutDietState> {
final currentDiet = (state as DietStateLoaded).diet;
final updatedDiet = currentDiet.copyWith(
isFavorite: true,
);
final updatedDiet = currentDiet.copyWith(isFavorite: true);
if (dietIndex != null) {
dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: updatedDiet.isFavorite, index: dietIndex));
}
emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet));
emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes, diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet));
EasyDebounce.debounce(
'add_or_remove_diet_to_favorites',
const Duration(seconds: 1),
() async {
try {
await dietsRepository.addToFavorite(id);
} catch (e) {
log(e.toString());
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
if (dietIndex != null) {
dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndex));
}
emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: currentDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet));
EasyDebounce.debounce('add_or_remove_diet_to_favorites', const Duration(seconds: 1), () async {
try {
await dietsRepository.addToFavorite(id);
} catch (e) {
log(e.toString());
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
if (dietIndex != null) {
dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndex));
}
},
);
emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes, diet: currentDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet));
}
});
}
Future<void> removeFromFavorite(int id, int? dietIndex) async {
@@ -141,30 +130,24 @@ class AboutDietCubit extends Cubit<AboutDietState> {
final currentDiet = (state as DietStateLoaded).diet;
final updatedDiet = currentDiet.copyWith(
isFavorite: false,
);
final updatedDiet = currentDiet.copyWith(isFavorite: false);
if (dietIndex != null) {
dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: updatedDiet.isFavorite, index: dietIndex));
}
emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet));
emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes, diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet));
EasyDebounce.debounce(
'add_or_remove_diet_to_favorites',
const Duration(seconds: 1),
() async {
try {
await dietsRepository.removeFromFavorite(id);
} catch (e) {
log(e.toString());
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
if (dietIndex != null) {
dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndex));
}
emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: currentDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet));
EasyDebounce.debounce('add_or_remove_diet_to_favorites', const Duration(seconds: 1), () async {
try {
await dietsRepository.removeFromFavorite(id);
} catch (e) {
log(e.toString());
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
if (dietIndex != null) {
dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndex));
}
},
);
emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes, diet: currentDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet));
}
});
}
Future<bool> startDiet(DateTime date) async {
@@ -199,13 +182,13 @@ class AboutDietCubit extends Cubit<AboutDietState> {
void clearStartedDietsState() {
if (state is! DietStateLoaded) return;
final diet = (state as DietStateLoaded).diet;
emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: diet));
emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes, diet: diet));
}
void emitCurrentStartedDietsState() {
if (state is! DietStateLoaded) return;
final diet = (state as DietStateLoaded).diet;
emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: diet, isStarted: true));
emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes, diet: diet, isStarted: true));
}
Future<Diet?> getDietBySlug(String slug) async {

View File

@@ -1,7 +1,7 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/enums/complained_content.dart';
import 'package:eatvio_app/core/enums/product_type.dart';
import 'package:eatvio_app/core/utils/localization_utils.dart';
@@ -35,11 +35,12 @@ import 'package:flutter_svg/svg.dart';
import 'package:go_router/go_router.dart';
class AboutDietScreen extends StatefulWidget {
final String slug;
final String? slug;
final int? id;
final int? index;
final bool navigateToComments;
const AboutDietScreen({super.key, required this.index, required this.navigateToComments, required this.slug});
const AboutDietScreen({super.key, required this.index, required this.navigateToComments, required this.slug, required this.id});
@override
State<AboutDietScreen> createState() => _AboutDietScreenState();
@@ -79,7 +80,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
final s = S.of(context);
return BlocProvider(
create: (context) => dietsCubit..initDietPage(widget.slug),
create: (context) => dietsCubit..initDietPage(slug: widget.slug, id: widget.id),
child: BlocProvider<CommentsCubit>(
create: (BuildContext context) => commentsCubit,
child: BlocConsumer<CommentsCubit, CommentsState>(
@@ -283,7 +284,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
replyId = null;
});
Future.delayed(const Duration(milliseconds: 500), () {
dietsCubit.getDiet(dietState.diet.slug, index: widget.index);
dietsCubit.getDiet(slug: dietState.diet.slug, index: widget.index);
});
FocusManager.instance.primaryFocus?.unfocus();
}
@@ -353,7 +354,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
Stack(
children: [
CachedNetworkImage(
imageUrl: '$baseUrl/storage/${dietState.diet.image}',
imageUrl: '${sl<AppConfig>().baseUrl}/storage/${dietState.diet.image}',
height: 300,
width: MediaQuery.sizeOf(context).width,
fit: BoxFit.fill,
@@ -526,7 +527,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
),
),
body: DataErrorWidget(
onReload: () => context.read<AboutDietCubit>().getDiet(widget.slug, index: widget.index),
onReload: () => context.read<AboutDietCubit>().getDiet(slug: widget.slug, id: widget.id, index: widget.index),
),
);
}

View File

@@ -1,4 +1,5 @@
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/features/diets/presentation/widgets/info_bouble.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
@@ -37,45 +38,25 @@ class AuthorCard extends StatelessWidget {
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
image: NetworkImage(baseUrl + author.authorProfileIcon),
image: NetworkImage(
author.authorProfileIcon.contains('https') ? author.authorProfileIcon : sl<AppConfig>().baseUrl + author.authorProfileIcon,
),
fit: BoxFit.cover,
),
),
),
subtitle: Text(
author.authorName,
style: TextStyles.textLSemibold.copyWith(
color: appTheme.black,
),
),
title: Text(
title,
style: titleStyle ?? TextStyles.textSSemibold.copyWith(
color: appTheme.gray,
),
),
subtitle: Text(author.authorName, style: TextStyles.textLSemibold.copyWith(color: appTheme.black)),
title: Text(title, style: titleStyle ?? TextStyles.textSSemibold.copyWith(color: appTheme.gray)),
),
Row(
children: [
Text(
'Консультант',
style: TextStyles.textMMedium.copyWith(
color: appTheme.black,
),
),
Text('Консультант', style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
const SizedBox(width: 10),
if (author.authorPlaceInRating != null)
InfoBouble(
radius: 12,
backgroundColor: appTheme.whiteA700,
children: [
Text(
'${author.authorPlaceInRating}-й в рейтинге',
style: TextStyles.textMMediumAuto.copyWith(
color: appTheme.blueA400,
),
),
],
children: [Text('${author.authorPlaceInRating}-й в рейтинге', style: TextStyles.textMMediumAuto.copyWith(color: appTheme.blueA400))],
),
],
),
@@ -88,18 +69,8 @@ class AuthorCard extends StatelessWidget {
child: Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: appTheme.whiteA700,
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: SvgPicture.asset(
'assets/icons/arrow.svg',
width: 12,
height: 12,
fit: BoxFit.none,
),
),
decoration: BoxDecoration(color: appTheme.whiteA700, borderRadius: BorderRadius.circular(8)),
child: Center(child: SvgPicture.asset('assets/icons/arrow.svg', width: 12, height: 12, fit: BoxFit.none)),
),
),
],

View File

@@ -1,5 +1,6 @@
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/enums/complained_content.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/features/complaining/presentation/complaining_bottom_sheet.dart';
import 'package:eatvio_app/core/widgets/custom_divider.dart';
@@ -35,7 +36,7 @@ class MoreOptions extends StatelessWidget {
onTap: disableActions == true
? null
: () {
Clipboard.setData(ClipboardData(text: '$baseUrl/ru/$path'));
Clipboard.setData(ClipboardData(text: '${sl<AppConfig>().baseUrl}/ru/$path'));
showInfoToast(s.link_was_copied);
},
child: Row(
@@ -62,7 +63,7 @@ class MoreOptions extends StatelessWidget {
final box = context.findRenderObject() as RenderBox?;
SharePlus.instance.share(
ShareParams(
uri: Uri.parse('$baseUrl/ru/$path',),
uri: Uri.parse('${sl<AppConfig>().baseUrl}/ru/$path',),
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
),
);

View File

@@ -1,7 +1,8 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:dismissible_page/dismissible_page.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/enums/complained_content.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/core/widgets/custom_popup.dart';
@@ -64,7 +65,7 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
authorName: state.feedBlogPostList[widget.index].author.authorName,
authorProfileIcon: state.feedBlogPostList[widget.index].author.authorProfileIcon.contains('https')
? state.feedBlogPostList[widget.index].author.authorProfileIcon
: baseUrl + state.feedBlogPostList[widget.index].author.authorProfileIcon,
: sl<AppConfig>().baseUrl + state.feedBlogPostList[widget.index].author.authorProfileIcon,
authorIsMe: state.feedBlogPostList[widget.index].author.authorIsMe,
),
);
@@ -75,7 +76,7 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
foregroundImage: NetworkImage(
state.feedBlogPostList[widget.index].author.authorProfileIcon.contains('https')
? state.feedBlogPostList[widget.index].author.authorProfileIcon
: baseUrl + state.feedBlogPostList[widget.index].author.authorProfileIcon,
: sl<AppConfig>().baseUrl + state.feedBlogPostList[widget.index].author.authorProfileIcon,
),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
),
@@ -157,15 +158,15 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
onTap: () {
context.pushTransparentRoute(
rootNavigator: true,
PhotoViewScreen(imageUrls: ['$baseUrl/storage/${state.feedBlogPostList[widget.index].image}'], index: 0),
PhotoViewScreen(imageUrls: ['${sl<AppConfig>().baseUrl}/storage/${state.feedBlogPostList[widget.index].image}'], index: 0),
);
},
child: Hero(
tag: '$baseUrl/storage/${state.feedBlogPostList[widget.index].image}',
tag: '${sl<AppConfig>().baseUrl}/storage/${state.feedBlogPostList[widget.index].image}',
child: CachedNetworkImage(
fit: BoxFit.fitWidth,
width: MediaQuery.of(context).size.width,
imageUrl: '$baseUrl/storage/${state.feedBlogPostList[widget.index].image}',
imageUrl: '${sl<AppConfig>().baseUrl}/storage/${state.feedBlogPostList[widget.index].image}',
errorWidget: (context, str, object) => const SizedBox.shrink(),
),
),
@@ -219,7 +220,7 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
final box = context.findRenderObject() as RenderBox?;
SharePlus.instance.share(
ShareParams(
uri: Uri.parse('$baseUrl/ru/blog_post/${state.feedBlogPostList[widget.index].id}'),
uri: Uri.parse('${sl<AppConfig>().baseUrl}/ru/blog_post/${state.feedBlogPostList[widget.index].id}'),
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
),
);

View File

@@ -1,5 +1,5 @@
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/comments/presentation/comments_actions_bottom_sheet.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/enums/complained_content.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
@@ -99,13 +99,13 @@ class _CommentsWidgetState extends State<CommentsWidget> {
extra: Author(
authorSlug: comment.authorSlug,
authorName: comment.authorName,
authorProfileIcon: comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : baseUrl + comment.authorProfileIcon,
authorProfileIcon: comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : sl<AppConfig>().baseUrl + comment.authorProfileIcon,
authorIsMe: isMe,
),
);
},
child: CircleAvatar(
foregroundImage: NetworkImage(comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : baseUrl + comment.authorProfileIcon),
foregroundImage: NetworkImage(comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : sl<AppConfig>().baseUrl + comment.authorProfileIcon),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
radius: radius,
),

View File

@@ -1,5 +1,5 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
@@ -14,10 +14,7 @@ import 'package:go_router/go_router.dart';
class ConsultantWidget extends StatelessWidget {
final FeedConsultantModel consultant;
const ConsultantWidget({
super.key,
required this.consultant,
});
const ConsultantWidget({super.key, required this.consultant});
@override
Widget build(BuildContext context) {
@@ -36,10 +33,7 @@ class ConsultantWidget extends StatelessWidget {
borderRadius: BorderRadius.circular(20),
child: Ink(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: const Color(0xFFF6F6F6),
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFF6F6F6)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -51,46 +45,32 @@ class ConsultantWidget extends StatelessWidget {
children: [
Ink(
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8)),
child: Text(
'${consultant.ratingPlace}',
style: TextStyle(
fontSize: 20,
color: appTheme.blueA400,
fontWeight: FontWeight.w600,
),
style: TextStyle(fontSize: 20, color: appTheme.blueA400, fontWeight: FontWeight.w600),
),
),
const SizedBox(width: 8),
CircleAvatar(
foregroundImage: NetworkImage(baseUrl + consultant.photo),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
),
if (consultant.photo.contains('https'))
CircleAvatar(foregroundImage: NetworkImage(consultant.photo), backgroundImage: const AssetImage('assets/images/no_avatar.png'))
else
CircleAvatar(
foregroundImage: NetworkImage(sl<AppConfig>().baseUrl + consultant.photo),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
consultant.name,
style: const TextStyle(
height: 1,
fontWeight: FontWeight.w500,
fontSize: 15,
color: Color(0xFF323F4B),
),
style: const TextStyle(height: 1, fontWeight: FontWeight.w500, fontSize: 15, color: Color(0xFF323F4B)),
),
const SizedBox(height: 4),
Text(
'Рейтинг: ${consultant.totalScores}',
style: TextStyle(
color: appTheme.blueA400,
height: 1,
fontWeight: FontWeight.w500,
fontSize: 12,
),
style: TextStyle(color: appTheme.blueA400, height: 1, fontWeight: FontWeight.w500, fontSize: 12),
),
],
),
@@ -98,21 +78,13 @@ class ConsultantWidget extends StatelessWidget {
),
Ink(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: Colors.white,
),
child: SvgPicture.asset(
'assets/icons/go_to_arrow.svg',
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(4), color: Colors.white),
child: SvgPicture.asset('assets/icons/go_to_arrow.svg'),
),
],
),
const SizedBox(height: 8),
Text(
consultant.description,
maxLines: 2,
),
Text(consultant.description, maxLines: 2),
if (consultant.badges.isNotEmpty) const SizedBox(height: 8),
_badges(consultant.badges, context),
],
@@ -128,16 +100,8 @@ class ConsultantWidget extends StatelessWidget {
children: [
for (int i = 0; i < badges.length; i++) ...[
badges[i].image.contains('svg')
? SvgPicture.network(
'$baseUrl/storage/${badges[i].image}',
width: 28,
height: 28,
)
: CachedNetworkImage(
imageUrl: '$baseUrl/storage/${badges[i].image}',
width: 28,
height: 28,
),
? SvgPicture.network('${sl<AppConfig>().baseUrl}/storage/${badges[i].image}', width: 28, height: 28)
: CachedNetworkImage(imageUrl: '${sl<AppConfig>().baseUrl}/storage/${badges[i].image}', width: 28, height: 28),
i < badges.length - 1 ? const SizedBox(width: 8) : const SizedBox.shrink(),
],
],

View File

@@ -1,7 +1,8 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/enums/complained_content.dart';
import 'package:eatvio_app/core/enums/product_type.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/widgets/bookmark_button.dart';
import 'package:eatvio_app/core/widgets/custom_popup.dart';
import 'package:eatvio_app/core/widgets/info_row.dart';
@@ -56,7 +57,7 @@ class _DietsWidgetState extends State<DietsWidget> {
ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
child: CachedNetworkImage(
imageUrl: '$baseUrl/storage/${widget.diet.image}',
imageUrl: '${sl<AppConfig>().baseUrl}/storage/${widget.diet.image}',
fit: BoxFit.fitWidth,
width: MediaQuery.of(context).size.width,
),
@@ -189,7 +190,7 @@ class _DietsWidgetState extends State<DietsWidget> {
final box = context.findRenderObject() as RenderBox?;
final result = await SharePlus.instance.share(
ShareParams(
uri: Uri.parse('$baseUrl/ru/diet/${widget.diet.id}'),
uri: Uri.parse('${sl<AppConfig>().baseUrl}/ru/diet/${widget.diet.id}'),
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
),
);

View File

@@ -1,7 +1,8 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/enums/complained_content.dart';
import 'package:eatvio_app/core/enums/product_type.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/widgets/bookmark_button.dart';
import 'package:eatvio_app/core/widgets/custom_popup.dart';
import 'package:eatvio_app/core/widgets/info_row.dart';
@@ -61,7 +62,7 @@ class _PlanWidgetState extends State<PlanWidget> {
ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
child: CachedNetworkImage(
imageUrl: '$baseUrl/storage/${widget.plan.image}',
imageUrl: '${sl<AppConfig>().baseUrl}/storage/${widget.plan.image}',
fit: BoxFit.cover,
width: MediaQuery.of(context).size.width,
height: 426,
@@ -187,7 +188,7 @@ class _PlanWidgetState extends State<PlanWidget> {
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
image: NetworkImage(baseUrl + widget.plan.author.authorProfileIcon),
image: NetworkImage(sl<AppConfig>().baseUrl + widget.plan.author.authorProfileIcon),
fit: BoxFit.cover,
),
),
@@ -263,7 +264,7 @@ class _PlanWidgetState extends State<PlanWidget> {
final box = context.findRenderObject() as RenderBox?;
final result = await SharePlus.instance.share(
ShareParams(
uri: Uri.parse('$baseUrl/ru/diet/${widget.plan.id}'),
uri: Uri.parse('${sl<AppConfig>().baseUrl}/ru/diet/${widget.plan.id}'),
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
),
);

View File

@@ -1,8 +1,9 @@
// ignore_for_file: dead_code
import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/enums/complained_content.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/utils/localization_utils.dart';
import 'package:eatvio_app/core/widgets/bookmark_button.dart';
@@ -75,7 +76,7 @@ class _RecipesWidgetState extends State<RecipesWidget> {
ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
child: CachedNetworkImage(
imageUrl: '$baseUrl/storage/${widget.recipe.image}',
imageUrl: '${sl<AppConfig>().baseUrl}/storage/${widget.recipe.image}',
fit: BoxFit.fitWidth,
width: MediaQuery.of(context).size.width,
),
@@ -231,7 +232,7 @@ class _RecipesWidgetState extends State<RecipesWidget> {
final box = context.findRenderObject() as RenderBox?;
SharePlus.instance.share(
ShareParams(
uri: Uri.parse('$baseUrl/ru/recipe/${widget.recipe.id}'),
uri: Uri.parse('${sl<AppConfig>().baseUrl}/ru/recipe/${widget.recipe.id}'),
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
),
);

View File

@@ -133,9 +133,9 @@ class FeedRepository {
}
}
Future<List<FeedConsultantModel>> getConsultants() async {
Future<List<FeedConsultantModel>> getConsultants([String? name]) async {
try {
return (await feedService.getConsultants()).data;
return (await feedService.getConsultants(name: name)).data;
} catch (e) {
rethrow;
}

View File

@@ -16,15 +16,10 @@ abstract class FeedService {
factory FeedService(Dio dio, {String baseUrl}) = _FeedService;
@GET('/api/blogPosts/index')
Future<FeedBlogPostSettingsModel> getBlogPostList({
@Query('page') required int page,
@Query('query_search') String? query,
});
Future<FeedBlogPostSettingsModel> getBlogPostList({@Query('page') required int page, @Query('query_search') String? query});
@GET('/api/blogPosts/show/{postId}')
Future<FeedBlogPostModel> getBlogPost({
@Path('postId') required int postId,
});
Future<FeedBlogPostModel> getBlogPost({@Path('postId') required int postId});
@GET('/api/recipes/index')
Future<RecipesPagination> getRecipes({
@@ -53,88 +48,50 @@ abstract class FeedService {
});
@GET('/api/blogPosts/{id}/comments/show')
Future<CommentModel> getBlogPostComments({
@Path() required String id,
});
Future<CommentModel> getBlogPostComments({@Path() required String id});
@POST('/api/blogPosts/{id}/comment/create')
Future<void> sendBlogPostComment({
@Path() required String id,
@Part() required String body,
@Part() int? parentCommentId,
});
Future<void> sendBlogPostComment({@Path() required String id, @Part() required String body, @Part() int? parentCommentId});
@GET('/api/diets/{id}/comments/show')
Future<CommentModel> getDietsComments({
@Path() required String id,
});
Future<CommentModel> getDietsComments({@Path() required String id});
@POST('/api/diets/{id}/comment/create')
Future<void> sendDietsComment({
@Path() required String id,
@Part() required String body,
@Part() int? parentCommentId,
});
Future<void> sendDietsComment({@Path() required String id, @Part() required String body, @Part() int? parentCommentId});
@GET('/api/plans/{slug}/comments/show')
Future<CommentModel> getPlansComments({
@Path() required String slug,
});
Future<CommentModel> getPlansComments({@Path() required String slug});
@POST('/api/plans/{slug}/comment/create')
Future<void> sendPlansComment({
@Path() required String slug,
@Part() required String body,
@Part() int? parentCommentId,
});
Future<void> sendPlansComment({@Path() required String slug, @Part() required String body, @Part() int? parentCommentId});
@GET('/api/recipes/{id}/comments/show')
Future<CommentModel> getRecipesComments({
@Path() required String id,
});
Future<CommentModel> getRecipesComments({@Path() required String id});
@POST('/api/recipes/{id}/comment/create')
Future<void> sendRecipesComment({
@Path() required String id,
@Part() required String body,
@Part() int? parentCommentId,
});
Future<void> sendRecipesComment({@Path() required String id, @Part() required String body, @Part() int? parentCommentId});
@POST('/api/blogPosts/{id}/like')
Future<void> likeBlogPost({
@Path() required int id,
});
Future<void> likeBlogPost({@Path() required int id});
@POST('/api/blogPosts/{id}/unliked')
Future<void> unlikedBlogPost({
@Path() required int id,
});
Future<void> unlikedBlogPost({@Path() required int id});
@GET('/api/consultants/index')
Future<FeedConsultantPaginationModel> getConsultants();
Future<FeedConsultantPaginationModel> getConsultants({@Query('filters[name][\$startsWith]') String? name});
@POST('/api/blogPosts/{id}/view')
Future<void> viewPost({
@Path() required int id,
});
Future<void> viewPost({@Path() required int id});
@POST('/api/recipes/{slug}/view')
Future<void> viewRecipe({
@Path() required String slug,
});
Future<void> viewRecipe({@Path() required String slug});
@POST('/api/diets/{id}/view')
Future<void> viewDiet({
@Path() required int id,
});
Future<void> viewDiet({@Path() required int id});
@POST('/api/plans/{slug}/view')
Future<void> viewPlan({
@Path() required String slug,
});
Future<void> viewPlan({@Path() required String slug});
@POST('/api/views')
Future<void> viewContent({
@Body() required ViewModel viewModel,
});
Future<void> viewContent({@Body() required ViewModel viewModel});
}

View File

@@ -1,4 +1,5 @@
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
@@ -18,7 +19,7 @@ class _GlossaryScreenState extends State<GlossaryScreen> {
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..loadRequest(Uri.parse('$baseUrl/login_with_pass_key?id=${widget.passKey}'));
..loadRequest(Uri.parse('${sl<AppConfig>().baseUrl}/login_with_pass_key?id=${widget.passKey}'));
}
@override

View File

@@ -1,4 +1,5 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/token_storage.dart';
@@ -64,7 +65,7 @@ class MenuScreen extends StatelessWidget {
if (profileModel.roles.contains('consultant')) _divider(context),
if (profileModel.roles.contains('consultant'))
_tile(
leading: 'assets/icons/finance.svg',
leading: 'assets/icons/consultant_rating_icon.svg',
title: 'Мой рейтинг',
onTap: () {
GoRouter.of(context).push(ConsultantRatingScreen.path);
@@ -139,7 +140,7 @@ class MenuScreen extends StatelessWidget {
CupertinoDialogAction(
onPressed: () async {
try {
await sl<Dio>().post('$baseUrl/api/profile/logout');
await sl<Dio>().post('${sl<AppConfig>().baseUrl}/api/profile/logout');
} finally {
await TokenStorage.removeTokenInStorage();
if (context.mounted) {

View File

@@ -1,4 +1,5 @@
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
@@ -33,7 +34,7 @@ class _BlockedUserWidgetState extends State<BlockedUserWidget> {
child: Row(
children: [
CircleAvatar(
foregroundImage: NetworkImage(baseUrl + widget.profile.profilePhotoUrl),
foregroundImage: NetworkImage(sl<AppConfig>().baseUrl + widget.profile.profilePhotoUrl),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
radius: 28,
),

View File

@@ -1,6 +1,7 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/enums/product_type.dart';
@@ -56,7 +57,7 @@ class _MyDietWidgetState extends State<MyDietWidget> {
child: ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
child: CachedNetworkImage(
imageUrl: '$baseUrl/storage/${widget.diet.image}',
imageUrl: '${sl<AppConfig>().baseUrl}/storage/${widget.diet.image}',
fit: BoxFit.fitWidth,
width: MediaQuery.of(context).size.width,
),
@@ -153,7 +154,7 @@ class _MyDietWidgetState extends State<MyDietWidget> {
final box = context.findRenderObject() as RenderBox?;
await SharePlus.instance.share(
ShareParams(
uri: Uri.parse('$baseUrl/ru/diet/${widget.diet.id}'),
uri: Uri.parse('${sl<AppConfig>().baseUrl}/ru/diet/${widget.diet.id}'),
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
),
);

View File

@@ -1,5 +1,6 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/enums/product_type.dart';
@@ -42,7 +43,7 @@ class _MyPlanWidgetState extends State<MyPlanWidget> {
onTap: () => GoRouter.of(context).pushNamed('aboutPlanScreen', extra: {'slug': widget.plan.slug, 'navigateToComments': false}),
child: ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
child: CachedNetworkImage(imageUrl: '$baseUrl/storage/${widget.plan.image}', fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width),
child: CachedNetworkImage(imageUrl: '${sl<AppConfig>().baseUrl}/storage/${widget.plan.image}', fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width),
),
),
Positioned(
@@ -157,7 +158,7 @@ class _MyPlanWidgetState extends State<MyPlanWidget> {
final box = context.findRenderObject() as RenderBox?;
await SharePlus.instance.share(
ShareParams(
uri: Uri.parse('$baseUrl/ru/plan/${widget.plan.id}'),
uri: Uri.parse('${sl<AppConfig>().baseUrl}/ru/plan/${widget.plan.id}'),
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
),
);

View File

@@ -1,5 +1,6 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/main_background_bouble.dart';
@@ -31,7 +32,7 @@ class PlanInfo extends StatelessWidget {
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: CachedNetworkImage(
imageUrl: '$baseUrl/storage/$imagePath',
imageUrl: '${sl<AppConfig>().baseUrl}/storage/$imagePath',
height: 220,
width: MediaQuery.sizeOf(context).width,
fit: BoxFit.fill,

View File

@@ -1,7 +1,7 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/enums/complained_content.dart';
import 'package:eatvio_app/core/enums/product_type.dart';
import 'package:eatvio_app/core/utils/localization_utils.dart';
@@ -401,7 +401,7 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
Stack(
children: [
CachedNetworkImage(
imageUrl: '$baseUrl/storage/${state.planModel?.image}',
imageUrl: '${sl<AppConfig>().baseUrl}/storage/${state.planModel?.image}',
height: 300,
width: MediaQuery.sizeOf(context).width,
fit: BoxFit.fill,

View File

@@ -1,6 +1,7 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:dismissible_page/dismissible_page.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/enums/complained_content.dart';
@@ -655,7 +656,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
borderRadius: BorderRadius.circular(4),
boxShadow: [BoxShadow(color: Colors.black.withAlpha((255 * 0.2).toInt()), blurRadius: 32)],
),
child: SvgPicture.network('$baseUrl/storage/${state.profile.badges[i].image}', width: 40, height: 40),
child: SvgPicture.network('${sl<AppConfig>().baseUrl}/storage/${state.profile.badges[i].image}', width: 40, height: 40),
)
: Tooltip(
message: state.profile.badges[i].description,
@@ -667,7 +668,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
borderRadius: BorderRadius.circular(4),
boxShadow: [BoxShadow(color: Colors.black.withAlpha((255 * 0.2).toInt()), blurRadius: 32)],
),
child: CachedNetworkImage(imageUrl: '$baseUrl/storage/${state.profile.badges[i].image}', width: 40, height: 40),
child: CachedNetworkImage(imageUrl: '${sl<AppConfig>().baseUrl}/storage/${state.profile.badges[i].image}', width: 40, height: 40),
),
i < state.profile.badges.length - 1 ? const SizedBox(width: 8) : const SizedBox.shrink(),
],
@@ -803,7 +804,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
icon: 'assets/icons/eye.svg',
text: 'Посмотреть',
onTap: () async {
context.pushTransparentRoute(rootNavigator: true, PhotoViewScreen(imageUrls: [baseUrl + state.profile.profilePhotoUrl], index: 0));
context.pushTransparentRoute(rootNavigator: true, PhotoViewScreen(imageUrls: [sl<AppConfig>().baseUrl + state.profile.profilePhotoUrl], index: 0));
},
),
if (widget.author == null || widget.author!.authorIsMe) ...[
@@ -885,7 +886,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
child: CircleAvatar(
radius: 32,
foregroundImage: NetworkImage(
state.profile.profilePhotoUrl.contains('https') ? state.profile.profilePhotoUrl : baseUrl + state.profile.profilePhotoUrl,
state.profile.profilePhotoUrl.contains('https') ? state.profile.profilePhotoUrl : sl<AppConfig>().baseUrl + state.profile.profilePhotoUrl,
),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
),
@@ -960,7 +961,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
onTap: () {
final box = context.findRenderObject() as RenderBox?;
SharePlus.instance.share(
ShareParams(uri: Uri.parse('$baseUrl/ru/profile/${state.profile.slug}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size),
ShareParams(uri: Uri.parse('${sl<AppConfig>().baseUrl}/ru/profile/${state.profile.slug}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size),
);
},
),

View File

@@ -1,7 +1,8 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:dismissible_page/dismissible_page.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/enums/complained_content.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/core/widgets/custom_popup.dart';
import 'package:eatvio_app/core/widgets/feed_icon_button.dart';
@@ -69,7 +70,7 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
foregroundImage: NetworkImage(
state.blogPosts[widget.index].author.authorProfileIcon.contains('https')
? state.blogPosts[widget.index].author.authorProfileIcon
: baseUrl + state.blogPosts[widget.index].author.authorProfileIcon,
: sl<AppConfig>().baseUrl + state.blogPosts[widget.index].author.authorProfileIcon,
),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
),
@@ -157,17 +158,17 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
context.pushTransparentRoute(
rootNavigator: true,
PhotoViewScreen(
imageUrls: ['$baseUrl/storage/${state.blogPosts[widget.index].image}'],
imageUrls: ['${sl<AppConfig>().baseUrl}/storage/${state.blogPosts[widget.index].image}'],
index: 0,
),
);
},
child: Hero(
tag: '$baseUrl/storage/${state.blogPosts[widget.index].image}',
tag: '${sl<AppConfig>().baseUrl}/storage/${state.blogPosts[widget.index].image}',
child: CachedNetworkImage(
fit: BoxFit.fitWidth,
width: MediaQuery.of(context).size.width,
imageUrl: '$baseUrl/storage/${state.blogPosts[widget.index].image}',
imageUrl: '${sl<AppConfig>().baseUrl}/storage/${state.blogPosts[widget.index].image}',
errorWidget: (context, str, object) => const SizedBox.shrink(),
),
),
@@ -226,7 +227,7 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
final box = context.findRenderObject() as RenderBox?;
SharePlus.instance.share(
ShareParams(
uri: Uri.parse('$baseUrl/ru/blog_post/${state.blogPosts[widget.index].id}'),
uri: Uri.parse('${sl<AppConfig>().baseUrl}/ru/blog_post/${state.blogPosts[widget.index].id}'),
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
),
);

View File

@@ -1,4 +1,5 @@
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/profile/models/sub_model.dart';
import 'package:eatvio_app/features/profile/presentation/widgets/sub_action_button_widget.dart';
@@ -36,7 +37,7 @@ class _SubWidgetState extends State<SubWidget> {
child: Row(
children: [
CircleAvatar(
foregroundImage: NetworkImage(baseUrl + widget.subModel.profilePhotoUrl),
foregroundImage: NetworkImage(sl<AppConfig>().baseUrl + widget.subModel.profilePhotoUrl),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
radius: 28,
),

View File

@@ -2,7 +2,7 @@ import 'dart:developer';
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/enums/complained_content.dart';
import 'package:eatvio_app/core/enums/product_type.dart';
import 'package:eatvio_app/core/utils/localization_utils.dart';
@@ -247,7 +247,7 @@ class _AboutRecipeScreenState extends State<AboutRecipeScreen> {
Stack(
children: [
CachedNetworkImage(
imageUrl: '$baseUrl/storage/${recipeState.recipe.image}',
imageUrl: '${sl<AppConfig>().baseUrl}/storage/${recipeState.recipe.image}',
height: 300,
width: MediaQuery.sizeOf(context).width,
fit: BoxFit.fill,

View File

@@ -1,5 +1,5 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
@@ -71,7 +71,7 @@ class _MyRecipeWidgetState extends State<MyRecipeWidget> {
child: ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
child: CachedNetworkImage(
imageUrl: '$baseUrl/storage/${widget.recipe.image}',
imageUrl: '${sl<AppConfig>().baseUrl}/storage/${widget.recipe.image}',
fit: BoxFit.fitWidth,
width: MediaQuery.of(context).size.width,
),
@@ -162,7 +162,7 @@ class _MyRecipeWidgetState extends State<MyRecipeWidget> {
final box = context.findRenderObject() as RenderBox?;
await SharePlus.instance.share(
ShareParams(
uri: Uri.parse('$baseUrl/ru/recipe/${widget.recipe.slug}'),
uri: Uri.parse('${sl<AppConfig>().baseUrl}/ru/recipe/${widget.recipe.slug}'),
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
),
);

View File

@@ -1,4 +1,5 @@
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/main_background_bouble.dart';
@@ -44,7 +45,7 @@ class StepsWidget extends StatelessWidget {
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Image.network(
'$baseUrl/storage/${recipe.steps![index].image}',
'${sl<AppConfig>().baseUrl}/storage/${recipe.steps![index].image}',
fit: BoxFit.cover,
),
),

View File

@@ -1,4 +1,5 @@
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart';
@@ -89,7 +90,7 @@ class _VerifyScreenState extends State<VerifyScreen> {
},
),
)
..loadRequest(Uri.parse('$baseUrl/login_with_pass_key?id=${widget.passKey}'));
..loadRequest(Uri.parse('${sl<AppConfig>().baseUrl}/login_with_pass_key?id=${widget.passKey}'));
}
@override

View File

@@ -1,7 +1,7 @@
import 'dart:convert';
import 'dart:developer';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart';
import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart';
@@ -54,7 +54,7 @@ class _WebViewRegistrationScreenState extends State<WebViewRegistrationScreen> {
},
),
)
..loadRequest(Uri.parse('$baseUrl/registration/step1'));
..loadRequest(Uri.parse('${sl<AppConfig>().baseUrl}/registration/step1'));
_injectJavaScript();
}

View File

@@ -1,4 +1,6 @@
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/core/widgets/custom_divider.dart';
import 'package:eatvio_app/generated/l10n.dart';
@@ -71,7 +73,7 @@ class SupportScreen extends StatelessWidget {
const SizedBox(height: 12),
if (!hidePlans) ...[
InkWell(
onTap: () async => await launchUrl(Uri.parse('$baseUrl/docs/public_offer.pdf')),
onTap: () async => await launchUrl(Uri.parse('${sl<AppConfig>().baseUrl}/docs/public_offer.pdf')),
borderRadius: BorderRadius.circular(16),
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
@@ -81,7 +83,7 @@ class SupportScreen extends StatelessWidget {
const SizedBox(height: 8),
],
InkWell(
onTap: () async => await launchUrl(Uri.parse('$baseUrl/docs/privacy.pdf')),
onTap: () async => await launchUrl(Uri.parse('${sl<AppConfig>().baseUrl}/docs/privacy.pdf')),
borderRadius: BorderRadius.circular(16),
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
@@ -90,7 +92,7 @@ class SupportScreen extends StatelessWidget {
),
const SizedBox(height: 8),
InkWell(
onTap: () async => await launchUrl(Uri.parse('$baseUrl/company-details')),
onTap: () async => await launchUrl(Uri.parse('${sl<AppConfig>().baseUrl}/company-details')),
borderRadius: BorderRadius.circular(16),
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),

View File

@@ -1,3 +1,4 @@
import 'package:eatvio_app/app_keys.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:flutter/material.dart';
@@ -79,6 +80,7 @@ class _WelcomeScreenState extends State<WelcomeScreen> {
),
const SizedBox(height: 24),
InkWell(
key: AppKeys.welcomeScreenButton,
onTap: () async {
final sharedPreferences = await SharedPreferences.getInstance();
await sharedPreferences.setBool('welcomeScreenWasShowed', true);