merge upstream

This commit is contained in:
2026-01-15 22:01:54 +00:00
47 changed files with 2522 additions and 2542 deletions

View File

@@ -33,15 +33,16 @@ class AppConfig {
} }
/// Список глобальных BlocProvider'ов /// Список глобальных BlocProvider'ов
/// Используются lazy: true для отложенной инициализации, чтобы не блокировать старт приложения
List<BlocProvider> get globalBlocProviders => [ List<BlocProvider> get globalBlocProviders => [
BlocProvider<NewMessagesCubit>(create: (context) => sl<NewMessagesCubit>()), BlocProvider<NewMessagesCubit>(lazy: true, create: (context) => sl<NewMessagesCubit>()),
BlocProvider<NotificationsCubit>(create: (context) => sl<NotificationsCubit>()..getNotifications()), BlocProvider<NotificationsCubit>(lazy: true, create: (context) => sl<NotificationsCubit>()),
BlocProvider<FeedCubit>(create: (context) => sl<FeedCubit>()..loadFeedBlogPostList()), BlocProvider<FeedCubit>(lazy: true, create: (context) => sl<FeedCubit>()),
BlocProvider<ProfileAvatarCubit>(create: (context) => sl<ProfileAvatarCubit>()..getMyProfile()), BlocProvider<ProfileAvatarCubit>(lazy: true, create: (context) => sl<ProfileAvatarCubit>()),
BlocProvider<ChatsCubit>(create: (context) => sl<ChatsCubit>()..getChatRoomsWithLoading()), BlocProvider<ChatsCubit>(lazy: true, create: (context) => sl<ChatsCubit>()),
BlocProvider<ViewsCubit>(create: (context) => sl<ViewsCubit>()), BlocProvider<ViewsCubit>(lazy: true, create: (context) => sl<ViewsCubit>()),
BlocProvider<InternetConnectionCubit>(lazy: false, create: (context) => sl<InternetConnectionCubit>()), BlocProvider<InternetConnectionCubit>(lazy: false, create: (context) => sl<InternetConnectionCubit>()),
BlocProvider<MarketingCubit>(create: (context) => sl<MarketingCubit>()), BlocProvider<MarketingCubit>(lazy: true, create: (context) => sl<MarketingCubit>()),
]; ];
/// Делегаты локализации /// Делегаты локализации

View File

@@ -1,3 +1,5 @@
import 'dart:async';
import 'dart:developer';
import 'dart:io' as io; import 'dart:io' as io;
import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/env_config.dart'; import 'package:eatvio_app/core/env_config.dart';
@@ -37,9 +39,21 @@ class AppInitializer {
/// Инициализация Firebase /// Инициализация Firebase
static Future<void> _initializeFirebase() async { static Future<void> _initializeFirebase() async {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform).timeout(
const Duration(seconds: 10),
onTimeout: () {
throw TimeoutException('Firebase initialization timeout');
},
);
if (sl<AppConfig>().buildType == AppBuildType.prod) { if (sl<AppConfig>().buildType == AppBuildType.prod) {
await FirebaseMessaging.instance.requestPermission(); try {
await FirebaseMessaging.instance.requestPermission().timeout(
const Duration(seconds: 5),
);
} catch (e) {
// Разрешение не критично, продолжаем работу
log('Failed to request Firebase messaging permission: $e');
}
} }
} }

View File

@@ -20,13 +20,19 @@ class ExternalRegistrations {
static void _registerDio() { static void _registerDio() {
sl.registerLazySingleton( sl.registerLazySingleton(
() => () =>
Dio(BaseOptions(baseUrl: sl<AppConfig>().baseUrl, headers: {'accept': 'application/json', 'content-type': 'application/json'})) Dio(BaseOptions(
baseUrl: sl<AppConfig>().baseUrl,
headers: {'accept': 'application/json', 'content-type': 'application/json'},
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
sendTimeout: const Duration(seconds: 10),
))
..interceptors.addAll([TokenInterceptor(), PhoneVerifyErrorInterceptor()]), ..interceptors.addAll([TokenInterceptor(), PhoneVerifyErrorInterceptor()]),
); );
} }
/// Регистрация Prefs /// Регистрация Prefs
static void _registerPrefs() { static void _registerPrefs() {
sl.registerSingletonAsync<Prefs>(()async => Prefs(await SharedPreferences.getInstance())); sl.registerSingletonAsync<Prefs>(() async => Prefs(await SharedPreferences.getInstance()));
} }
} }

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:developer'; import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_config.dart';
@@ -19,12 +20,25 @@ class FirebaseApi {
String? token; String? token;
if (await _isHuaweiDevice()) { if (await _isHuaweiDevice()) {
token = await _getHuaweiPushTokenNative(); token = await _getHuaweiPushTokenNative().timeout(
const Duration(seconds: 5),
onTimeout: () => null,
);
} else { } else {
if ((sl<AppConfig>().buildType == AppBuildType.prod)) { if ((sl<AppConfig>().buildType == AppBuildType.prod)) {
await _firebaseMessaging.requestPermission(); try {
await _firebaseMessaging.requestPermission().timeout(
const Duration(seconds: 5),
);
} catch (e) {
// Разрешение не критично, продолжаем
log('Failed to request Firebase messaging permission: $e');
}
} }
token = await _firebaseMessaging.getToken(); token = await _firebaseMessaging.getToken().timeout(
const Duration(seconds: 5),
onTimeout: () => null,
);
} }
return token; return token;
} }
@@ -39,9 +53,15 @@ class FirebaseApi {
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id, 'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
'token': await getToken(), 'token': await getToken(),
}, },
).timeout(
const Duration(seconds: 10),
onTimeout: () {
throw TimeoutException('registerAnonymousPushToken timeout');
},
); );
} catch (e, st) { } catch (e, st) {
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
// Не пробрасываем ошибку дальше, так как это не критичная операция
} }
} }
@@ -57,9 +77,15 @@ class FirebaseApi {
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id, 'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
'token': await getToken(), 'token': await getToken(),
}, },
).timeout(
const Duration(seconds: 10),
onTimeout: () {
throw TimeoutException('registerPushToken timeout');
},
); );
} catch (e, st) { } catch (e, st) {
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
// Не пробрасываем ошибку дальше, так как это не критичная операция
} }
} }
} }

View File

@@ -4,6 +4,7 @@ import 'package:eatvio_app/core/di/cubit_registrations.dart';
import 'package:eatvio_app/core/di/external_registrations.dart'; import 'package:eatvio_app/core/di/external_registrations.dart';
import 'package:eatvio_app/core/di/repository_registrations.dart'; import 'package:eatvio_app/core/di/repository_registrations.dart';
import 'package:eatvio_app/core/di/service_registrations.dart'; import 'package:eatvio_app/core/di/service_registrations.dart';
import 'package:eatvio_app/core/prefs.dart';
import 'package:eatvio_app/core/websockets/ws_manager.dart'; import 'package:eatvio_app/core/websockets/ws_manager.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
@@ -18,6 +19,9 @@ Future<void> init({required AppBuildType buildType}) async {
// Регистрация внешних зависимостей (Dio, etc.) // Регистрация внешних зависимостей (Dio, etc.)
ExternalRegistrations.registerAll(); ExternalRegistrations.registerAll();
// Ожидание инициализации асинхронных зависимостей
await sl.getAsync<Prefs>();
// Регистрация сервисов // Регистрация сервисов
ServiceRegistrations.registerAll(); ServiceRegistrations.registerAll();

View File

@@ -1,5 +1,3 @@
import 'package:eatvio_app/core/firebase_api.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/routing/route_constants.dart'; import 'package:eatvio_app/core/routing/route_constants.dart';
import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart'; import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart';
import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart'; import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart';
@@ -21,10 +19,8 @@ class AuthRoutes {
path: RouteConstants.authScreen, path: RouteConstants.authScreen,
name: 'authScreen', name: 'authScreen',
builder: (context, state) => const AuthScreen(), builder: (context, state) => const AuthScreen(),
redirect: (context, state) { // registerAnonymousPushToken вызывается в AuthCubit.initializeUser()
sl<FirebaseApi>().registerAnonymousPushToken(); // и не должен блокировать навигацию
return null;
},
routes: [ routes: [
GoRoute(path: RouteConstants.registrationScreen, name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()), GoRoute(path: RouteConstants.registrationScreen, name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()),
GoRoute(path: RouteConstants.registrationScreenV2, name: 'registrationScreenV2', builder: (context, state) => const RegistrationV2Page()), GoRoute(path: RouteConstants.registrationScreenV2, name: 'registrationScreenV2', builder: (context, state) => const RegistrationV2Page()),

View File

@@ -23,14 +23,16 @@ class MainNavigationRoutes {
return [ return [
ShellRoute( ShellRoute(
builder: (context, state, child) { builder: (context, state, child) {
return BlocBuilder<ProfileAvatarCubit, ProfileAvatarState>( // Инициализация ProfileAvatarCubit при первом использовании ShellRoute
builder: (BuildContext context, ProfileAvatarState state) { final profileAvatarCubit = context.read<ProfileAvatarCubit>();
if (state is ProfileAvatarStateLoaded) { if (profileAvatarCubit.state is ProfileAvatarStateLoading) {
return BottomNavigationBarWidget(profileModel: state.profile, child: child); // Вызываем getMyProfile() только если состояние loading (первый запуск)
} WidgetsBinding.instance.addPostFrameCallback((_) {
return const SizedBox.shrink(); profileAvatarCubit.getMyProfile();
}, });
); }
return BottomNavigationBarWidget(child: child);
}, },
routes: [ routes: [
GoRoute( GoRoute(

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/locator_service.dart';
@@ -56,7 +57,8 @@ class OtherRoutes {
builder: (context, state) => const WelcomeScreen(), builder: (context, state) => const WelcomeScreen(),
redirect: (context, state) async { redirect: (context, state) async {
try { try {
final prefs = sl<Prefs>(); // Prefs уже инициализирован в locator_service, но на всякий случай проверяем
final prefs = await sl.getAsync<Prefs>();
final welcomeScreenWasShowed = prefs.getBool(PrefsKeys.welcomeScreenWasShowed) ?? false; final welcomeScreenWasShowed = prefs.getBool(PrefsKeys.welcomeScreenWasShowed) ?? false;
if (!welcomeScreenWasShowed) { if (!welcomeScreenWasShowed) {
return null; return null;
@@ -74,7 +76,27 @@ class OtherRoutes {
builder: (context, state) => const UpdateScreen(), builder: (context, state) => const UpdateScreen(),
redirect: (context, state) async { redirect: (context, state) async {
try { try {
final versions = (await sl<Dio>().get('${sl<AppConfig>().baseUrl}/api/app_versions')).data; // Проверка версии с таймаутом, чтобы не блокировать запуск приложения
final versions = await sl<Dio>()
.get('${sl<AppConfig>().baseUrl}/api/app_versions')
.timeout(
const Duration(seconds: 5),
onTimeout: () {
// При таймауте пропускаем проверку и идем дальше
throw TimeoutException('Version check timeout');
},
)
.then((response) => response.data)
.catchError((e) {
// При любой ошибке пропускаем проверку версии
return null;
});
if (versions == null) {
// Если проверка не удалась, пропускаем экран обновления
return RouteConstants.welcomeScreen;
}
PackageInfo packageInfo = await PackageInfo.fromPlatform(); PackageInfo packageInfo = await PackageInfo.fromPlatform();
String version = packageInfo.version; String version = packageInfo.version;
Version currentVersion = Version.parse(version); Version currentVersion = Version.parse(version);
@@ -85,6 +107,7 @@ class OtherRoutes {
return RouteConstants.welcomeScreen; return RouteConstants.welcomeScreen;
} }
} catch (e) { } catch (e) {
// При любой ошибке пропускаем экран обновления
return RouteConstants.welcomeScreen; return RouteConstants.welcomeScreen;
} }
}, },

View File

@@ -1,9 +1,24 @@
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
abstract class TokenStorage { abstract class TokenStorage {
static SharedPreferences? _cachedPrefs;
static Future<SharedPreferences> _getPrefs() async {
_cachedPrefs ??= await SharedPreferences.getInstance();
return _cachedPrefs!;
}
/// Clears the cached SharedPreferences instance.
/// This is primarily used for testing to ensure fresh state between tests.
@visibleForTesting
static void clearCache() {
_cachedPrefs = null;
}
static Future<bool> haveTokenInStorage() async { static Future<bool> haveTokenInStorage() async {
final sharedPreferences = await SharedPreferences.getInstance();
try { try {
final sharedPreferences = await _getPrefs();
if (sharedPreferences.getString('token') == null) { if (sharedPreferences.getString('token') == null) {
return false; return false;
} else { } else {
@@ -15,25 +30,25 @@ abstract class TokenStorage {
} }
static Future<String?> getTokenFromStorage() async { static Future<String?> getTokenFromStorage() async {
final sharedPreferences = await SharedPreferences.getInstance(); final sharedPreferences = await _getPrefs();
return sharedPreferences.getString('token'); return sharedPreferences.getString('token');
} }
static Future<void> saveTokenInStorage(String token) async { static Future<void> saveTokenInStorage(String token) async {
final sharedPreferences = await SharedPreferences.getInstance(); final sharedPreferences = await _getPrefs();
await sharedPreferences.setString('token', token); await sharedPreferences.setString('token', token);
} }
static Future<void> removeTokenInStorage() async { static Future<void> removeTokenInStorage() async {
final sharedPreferences = await SharedPreferences.getInstance(); final sharedPreferences = await _getPrefs();
await sharedPreferences.remove('token'); await sharedPreferences.remove('token');
} }
} }
abstract class SocketIdStorage { abstract class SocketIdStorage {
static Future<bool> haveSocketIdInStorage() async { static Future<bool> haveSocketIdInStorage() async {
final sharedPreferences = await SharedPreferences.getInstance();
try { try {
final sharedPreferences = await TokenStorage._getPrefs();
if (sharedPreferences.getString('socketId') == null) { if (sharedPreferences.getString('socketId') == null) {
return false; return false;
} else { } else {
@@ -45,17 +60,17 @@ abstract class SocketIdStorage {
} }
static Future<String?> getSocketIdFromStorage() async { static Future<String?> getSocketIdFromStorage() async {
final sharedPreferences = await SharedPreferences.getInstance(); final sharedPreferences = await TokenStorage._getPrefs();
return sharedPreferences.getString('socketId'); return sharedPreferences.getString('socketId');
} }
static Future<void> saveSocketIdInStorage(String socketId) async { static Future<void> saveSocketIdInStorage(String socketId) async {
final sharedPreferences = await SharedPreferences.getInstance(); final sharedPreferences = await TokenStorage._getPrefs();
await sharedPreferences.setString('socketId', socketId); await sharedPreferences.setString('socketId', socketId);
} }
static Future<void> removeSocketIdInStorage() async { static Future<void> removeSocketIdInStorage() async {
final sharedPreferences = await SharedPreferences.getInstance(); final sharedPreferences = await TokenStorage._getPrefs();
await sharedPreferences.remove('socketId'); await sharedPreferences.remove('socketId');
} }
} }

View File

@@ -0,0 +1,67 @@
/// Утилиты для обработки markdown текста
class MarkdownUtils {
/// Обрабатывает markdown текст:
/// - Исправляет неправильный синтаксис markdown ссылок
/// - Автоматически преобразует URL в markdown ссылки
/// - Преобразует **текст** в <b>текст</b> для надежного отображения жирного текста
static String processMarkdown(String text) {
// Сначала исправляем неправильный синтаксис markdown ссылок
// Например: [http://example.com](url) -> [http://example.com](http://example.com)
final wrongLinkRegex = RegExp(r'\[(https?://[^\]]+|www\.[^\]]+)\]\(([^\)]+)\)');
text = text.replaceAllMapped(wrongLinkRegex, (match) {
final urlInText = match.group(1)!;
if (!urlInText.startsWith('http://') && !urlInText.startsWith('https://') && !urlInText.startsWith('www.')) {
return match.group(0)!;
}
final fullUrl = urlInText.startsWith('www.') ? 'https://$urlInText' : urlInText;
return '[$urlInText]($fullUrl)';
});
// Находим все существующие markdown ссылки и их позиции
final linkRegex = RegExp(r'\[([^\]]+)\]\(([^\)]+)\)');
final existingLinks = <_LinkRange>[];
for (final linkMatch in linkRegex.allMatches(text)) {
existingLinks.add(_LinkRange(linkMatch.start, linkMatch.end));
}
// Регулярное выражение для поиска URL (http://, https://, www.)
final urlRegex = RegExp(r'(https?://[^\s\)]+|www\.[^\s\)]+)', caseSensitive: false);
// Заменяем URL на markdown ссылки, если они еще не являются ссылками
String result = text;
int offset = 0;
for (final urlMatch in urlRegex.allMatches(text)) {
// Проверяем, не находится ли URL внутри существующей markdown ссылки
bool isInsideLink = existingLinks.any((link) =>
urlMatch.start >= link.start && urlMatch.end <= link.end);
if (!isInsideLink) {
final url = urlMatch.group(0)!;
final fullUrl = url.startsWith('www.') ? 'https://$url' : url;
final replacement = '[$url]($fullUrl)';
result = result.replaceRange(
urlMatch.start + offset,
urlMatch.end + offset,
replacement,
);
offset += replacement.length - url.length;
}
}
// Гарантируем, что жирный текст будет распознан
// Заменяем **текст** на <b>текст</b> вручную для надежности
final boldRegex = RegExp(r'\*\*([^*]+)\*\*');
result = result.replaceAllMapped(boldRegex, (match) {
return '<b>${match.group(1)}</b>';
});
return result;
}
}
/// Вспомогательный класс для хранения диапазона позиций ссылки
class _LinkRange {
final int start;
final int end;
_LinkRange(this.start, this.end);
}

View File

@@ -0,0 +1,124 @@
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:url_launcher/url_launcher.dart';
import 'package:eatvio_app/core/utils/markdown_utils.dart';
/// Переиспользуемый виджет для отображения HTML из markdown текста
class MarkdownHtmlWidget extends StatelessWidget {
/// Markdown текст для отображения
final String markdownText;
/// Ширина изображений (по умолчанию ширина экрана минус 32)
final double? imageWidth;
/// Цвет текста для body и параграфов
final Color? textColor;
/// Размер шрифта (по умолчанию 16)
final double? fontSize;
/// Callback для обработки нажатий на ссылки
final OnTap? onLinkTap;
const MarkdownHtmlWidget({
super.key,
required this.markdownText,
this.imageWidth,
this.textColor,
this.fontSize,
this.onLinkTap,
});
@override
Widget build(BuildContext context) {
// Обрабатываем markdown текст
final processedText = MarkdownUtils.processMarkdown(markdownText);
// Конвертируем markdown в HTML
final htmlData = md.markdownToHtml(
processedText,
extensionSet: md.ExtensionSet.gitHubFlavored,
);
final screenWidth = MediaQuery.maybeOf(context)?.size.width ?? 400.0;
final defaultImageWidth = imageWidth ?? (screenWidth - 32);
final defaultTextColor = textColor ?? Colors.black87;
final defaultFontSize = fontSize ?? 16.0;
return Html(
data: htmlData,
style: {
"body": Style(
margin: Margins.zero,
padding: HtmlPaddings.zero,
fontSize: FontSize(defaultFontSize),
lineHeight: const LineHeight(1.5),
color: defaultTextColor,
),
"p": Style(
margin: Margins.only(bottom: 12),
color: defaultTextColor,
),
"b, strong": Style(
fontWeight: FontWeight.bold,
color: Colors.black,
),
"h1, h2, h3, h4, h5, h6": Style(
margin: Margins.only(top: 16, bottom: 8),
color: Colors.black,
fontWeight: FontWeight.bold,
),
"h1": Style(fontSize: FontSize(24)),
"h2": Style(fontSize: FontSize(20)),
"h3": Style(fontSize: FontSize(18)),
"ul, ol": Style(
margin: Margins.only(bottom: 12),
padding: HtmlPaddings.only(left: 16),
),
"li": Style(
margin: Margins.only(bottom: 4),
color: defaultTextColor,
),
"strong, b": Style(
fontWeight: FontWeight.bold,
color: Colors.black,
),
"em, i": Style(fontStyle: FontStyle.italic),
"code": Style(
backgroundColor: Colors.grey.shade100,
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
fontFamily: 'monospace',
),
"pre": Style(
backgroundColor: Colors.grey.shade100,
padding: HtmlPaddings.all(12),
margin: Margins.only(bottom: 12),
),
"blockquote": Style(
padding: HtmlPaddings.only(left: 16),
margin: Margins.only(left: 8, bottom: 12),
fontStyle: FontStyle.italic,
color: Colors.grey.shade700,
),
"a": Style(
color: Colors.blue,
textDecoration: TextDecoration.underline,
),
"img": Style(width: Width(defaultImageWidth)),
},
onLinkTap: onLinkTap ?? ((url, attributes, element) async {
if (url != null) {
try {
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
} catch (e) {
debugPrint('Error launching URL: $e');
}
}
}) as OnTap,
);
}
}

View File

@@ -38,18 +38,25 @@ class AuthCubit extends Cubit<AuthState> {
} }
Future<void> initializeUser() async { Future<void> initializeUser() async {
await registerAnonymousPushToken(); try {
if (await TokenStorage.haveTokenInStorage()) {
final profile = await profileRepository.getMyProfile();
if (profile.emailVerifiedAt != null) {
await registerPushToken();
AppRouter.navigatorKey.currentContext?.go(RouteConstants.feedScreen);
return;
}
AppRouter.navigatorKey.currentContext?.go(RouteConstants.verifyEmailScreen);
} else {
await registerAnonymousPushToken(); await registerAnonymousPushToken();
FlutterNativeSplash.remove(); if (await TokenStorage.haveTokenInStorage()) {
final profile = await profileRepository.getMyProfile();
if (profile.emailVerifiedAt != null) {
await registerPushToken();
FlutterNativeSplash.remove();
AppRouter.navigatorKey.currentContext?.go(RouteConstants.feedScreen);
return;
}
FlutterNativeSplash.remove();
AppRouter.navigatorKey.currentContext?.go(RouteConstants.verifyEmailScreen);
} else {
FlutterNativeSplash.remove();
}
} catch (e) {
log('Error in initializeUser: $e');
FlutterNativeSplash.remove(); // Убираем сплэш даже при ошибке
rethrow;
} }
} }

View File

@@ -4,6 +4,7 @@ import 'package:eatvio_app/core/enums/complained_content.dart';
import 'package:eatvio_app/core/utils/date_time_utils.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/custom_popup.dart';
import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/core/widgets/markdown_html_widget.dart';
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_cubit.dart'; import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_cubit.dart';
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_state.dart'; import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_state.dart';
import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart'; import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart';
@@ -12,13 +13,11 @@ import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit
import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart';
import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:url_launcher/url_launcher.dart';
class BlogPostScreen extends StatefulWidget { class BlogPostScreen extends StatefulWidget {
final int id; final int id;
@@ -79,98 +78,99 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
resizeToAvoidBottomInset: true, resizeToAvoidBottomInset: true,
bottomSheet: Container( bottomSheet: Container(
padding: EdgeInsets.only( padding: EdgeInsets.only(
top: 12, bottom: MediaQuery.of(context).padding.bottom + 8,
bottom: 12 + MediaQuery.of(context).padding.bottom + 56, // 56 - высота bottomNavigationBar
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))), border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
), ),
child: Row( child: Padding(
mainAxisAlignment: MainAxisAlignment.center, padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
children: [ child: Row(
if (replyId != null) mainAxisAlignment: MainAxisAlignment.center,
GestureDetector( children: [
onTap: () { if (replyId != null)
context.read<CommentsCubit>().setReplyId(null); GestureDetector(
setState(() { onTap: () {
replyId = null; context.read<CommentsCubit>().setReplyId(null);
textEditingController.clear(); setState(() {
}); replyId = null;
FocusManager.instance.primaryFocus?.unfocus(); textEditingController.clear();
}, });
child: Container( FocusManager.instance.primaryFocus?.unfocus();
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
height: 32,
width: 32,
child: const Icon(Icons.close_rounded, size: 18, color: Colors.white),
),
),
if (replyId != null) const SizedBox(width: 8),
SizedBox(
width: MediaQuery.of(context).size.width - (replyId == null ? 64 : 104),
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 52),
child: TextFormField(
textCapitalization: TextCapitalization.sentences,
onChanged: (_) {
setState(() {});
}, },
controller: textEditingController, child: Container(
maxLines: 5, decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
minLines: 1, height: 32,
keyboardType: TextInputType.multiline, width: 32,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1.2), child: const Icon(Icons.close_rounded, size: 18, color: Colors.white),
decoration: InputDecoration( ),
isDense: false, ),
// убираем true, чтобы padding учитывался корректно if (replyId != null) const SizedBox(width: 8),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), Expanded(
// вертикальный паддинг увеличен child: ConstrainedBox(
hintText: 'Комментарий', constraints: const BoxConstraints(minHeight: 52),
hintStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, height: 1.2, color: Color(0xFFAEAEB2)), child: TextFormField(
filled: true, textCapitalization: TextCapitalization.sentences,
border: OutlineInputBorder( onChanged: (_) {
borderRadius: BorderRadius.circular(16), setState(() {});
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1), },
), controller: textEditingController,
enabledBorder: OutlineInputBorder( maxLines: 5,
borderRadius: BorderRadius.circular(16), minLines: 1,
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1), keyboardType: TextInputType.multiline,
), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1.2),
focusedBorder: OutlineInputBorder( decoration: InputDecoration(
borderRadius: BorderRadius.circular(16), isDense: false,
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1), // убираем true, чтобы padding учитывался корректно
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
// вертикальный паддинг увеличен
hintText: S.of(context).comment_hint,
hintStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, height: 1.2, color: Color(0xFFAEAEB2)),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
), ),
), ),
), ),
), ),
), const SizedBox(width: 8),
const SizedBox(width: 8), GestureDetector(
GestureDetector( onTap: () {
onTap: () { if (textEditingController.text.length > 2) {
if (textEditingController.text.length > 2) { commentsCubit.sendComment(
commentsCubit.sendComment( id: widget.id.toString(),
id: widget.id.toString(), commentText: textEditingController.text,
commentText: textEditingController.text, parentCommentId: replyId,
parentCommentId: replyId, commentSourceType: CommentSourceType.blogPost,
commentSourceType: CommentSourceType.blogPost, );
); context.read<CommentsCubit>().setReplyId(null);
context.read<CommentsCubit>().setReplyId(null); setState(() {
setState(() { textEditingController.clear();
textEditingController.clear(); replyId = null;
replyId = null; });
}); FocusManager.instance.primaryFocus?.unfocus();
FocusManager.instance.primaryFocus?.unfocus(); }
} },
}, child: Container(
child: Container( decoration: BoxDecoration(color: textEditingController.text.length > 2 ? appTheme.blueA400 : Colors.grey, shape: BoxShape.circle),
decoration: BoxDecoration(color: textEditingController.text.length > 2 ? appTheme.blueA400 : Colors.grey, shape: BoxShape.circle), height: 32,
height: 32, width: 32,
width: 32, child: const Icon(Icons.send_rounded, size: 18, color: Colors.white),
child: const Icon(Icons.send_rounded, size: 18, color: Colors.white), ),
), ),
), ],
], ),
), ),
), ),
appBar: AppBar( appBar: AppBar(
@@ -236,104 +236,8 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
if (blogPostState.blogPostModel?.description.isNotEmpty ?? false) if (blogPostState.blogPostModel?.description.isNotEmpty ?? false)
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Builder( child: MarkdownHtmlWidget(
builder: (context) { markdownText: blogPostState.blogPostModel!.description,
// Автоматически преобразуем URL в markdown ссылки
String description = blogPostState.blogPostModel!.description;
// Сначала исправляем неправильный синтаксис markdown ссылок, где URL в тексте, а не в href
// Например: [http://example.com](url) -> [http://example.com](http://example.com)
final wrongLinkRegex = RegExp(r'\[(https?://[^\]]+|www\.[^\]]+)\]\(([^\)]+)\)');
description = description.replaceAllMapped(wrongLinkRegex, (match) {
final urlInText = match.group(1)!;
// Если текст ссылки является URL, используем его как href
if (!urlInText.startsWith('http://') && !urlInText.startsWith('https://') && !urlInText.startsWith('www.')) {
return match.group(0)!; // Оставляем как есть, если это не URL
}
final fullUrl = urlInText.startsWith('www.') ? 'https://$urlInText' : urlInText;
// Используем URL как href, а текст ссылки - это сам URL
return '[$urlInText]($fullUrl)';
});
// Теперь находим все существующие markdown ссылки и их позиции
final linkRegex = RegExp(r'\[([^\]]+)\]\(([^\)]+)\)');
final existingLinks = <_LinkRange>[];
for (final linkMatch in linkRegex.allMatches(description)) {
existingLinks.add(_LinkRange(linkMatch.start, linkMatch.end));
}
// Регулярное выражение для поиска URL (http://, https://, www.)
final urlRegex = RegExp(r'(https?://[^\s\)]+|www\.[^\s\)]+)', caseSensitive: false);
// Заменяем URL на markdown ссылки, если они еще не являются ссылками
String result = description;
int offset = 0;
for (final urlMatch in urlRegex.allMatches(description)) {
// Проверяем, не находится ли URL внутри существующей markdown ссылки
bool isInsideLink = existingLinks.any((link) => urlMatch.start >= link.start && urlMatch.end <= link.end);
if (!isInsideLink) {
final url = urlMatch.group(0)!;
final fullUrl = url.startsWith('www.') ? 'https://$url' : url;
final replacement = '[$url]($fullUrl)';
result = result.replaceRange(urlMatch.start + offset, urlMatch.end + offset, replacement);
offset += replacement.length - url.length;
}
}
description = result;
final htmlData = md.markdownToHtml(description.replaceAll('\n', '<br>'), extensionSet: md.ExtensionSet.commonMark);
debugPrint('Markdown input: ${blogPostState.blogPostModel!.description}');
debugPrint('Processed markdown: $description');
debugPrint('HTML output: $htmlData');
return Html(
data: htmlData,
style: {
"body": Style(margin: Margins.zero, padding: HtmlPaddings.zero, fontSize: FontSize(16), lineHeight: const LineHeight(1.5)),
"p": Style(margin: Margins.only(bottom: 12), color: Colors.black87),
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: Colors.black, fontWeight: FontWeight.bold),
"h1": Style(fontSize: FontSize(24)),
"h2": Style(fontSize: FontSize(20)),
"h3": Style(fontSize: FontSize(18)),
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
"li": Style(margin: Margins.only(bottom: 4), color: Colors.black87),
"strong, b": Style(fontWeight: FontWeight.bold, color: Colors.black),
"em, i": Style(fontStyle: FontStyle.italic),
"code": Style(
backgroundColor: Colors.grey.shade100,
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
fontFamily: 'monospace',
),
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
"blockquote": Style(
padding: HtmlPaddings.only(left: 16),
margin: Margins.only(left: 8, bottom: 12),
fontStyle: FontStyle.italic,
color: Colors.grey.shade700,
),
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
},
onLinkTap: (url, attributes, element) async {
debugPrint('Link tapped: $url');
if (url != null) {
try {
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
} else {
debugPrint('Cannot launch URL: $url');
}
} catch (e) {
debugPrint('Error launching URL: $e');
}
}
},
);
},
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
@@ -436,10 +340,3 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
); );
} }
} }
class _LinkRange {
final int start;
final int end;
_LinkRange(this.start, this.end);
}

View File

@@ -5,7 +5,8 @@ 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/dev_tools/presentation/dev_tools_screen.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_state.dart'; import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_state.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_state.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_state.dart';
import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/generated/l10n.dart';
@@ -16,13 +17,11 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
class BottomNavigationBarWidget extends StatefulWidget { class BottomNavigationBarWidget extends StatefulWidget {
final ProfileModel profileModel;
final Widget child; final Widget child;
const BottomNavigationBarWidget({ const BottomNavigationBarWidget({
super.key, super.key,
required this.child, required this.child,
required this.profileModel,
}); });
@override @override
@@ -31,15 +30,12 @@ class BottomNavigationBarWidget extends StatefulWidget {
class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> { class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
int currentBottomNavBarIndex = 1; int currentBottomNavBarIndex = 1;
late final List<String> pages; List<String>? pages;
int tapsCount = 0; int tapsCount = 0;
@override List<String> _getPages(bool isConsultant) {
void initState() { if (isConsultant) {
super.initState(); return [
if (widget.profileModel.roles.contains('consultant')) {
pages = [
'/my-profile-screen', '/my-profile-screen',
'/feed-screen', '/feed-screen',
'/diary-screen', '/diary-screen',
@@ -49,7 +45,7 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
'/menu-screen', '/menu-screen',
]; ];
} else { } else {
pages = [ return [
'/my-profile-screen', '/my-profile-screen',
'/feed-screen', '/feed-screen',
'/diary-screen', '/diary-screen',
@@ -61,7 +57,8 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
} }
void _onItemTapped(int index) async { void _onItemTapped(int index) async {
if (pages.length == 7) { if (pages == null) return;
if (pages!.length == 7) {
if (index == 6) { if (index == 6) {
tapsCount++; tapsCount++;
if (tapsCount == 7) { if (tapsCount == 7) {
@@ -82,108 +79,117 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
setState(() { setState(() {
currentBottomNavBarIndex = index; currentBottomNavBarIndex = index;
}); });
GoRouter.of(context).replace(pages[currentBottomNavBarIndex]); GoRouter.of(context).replace(pages![currentBottomNavBarIndex]);
} }
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final s = S.of(context); final s = S.of(context);
return Scaffold( return BlocBuilder<ProfileAvatarCubit, ProfileAvatarState>(
body: widget.child, builder: (BuildContext context, ProfileAvatarState state) {
bottomNavigationBar: BottomNavigationBar( if (state is ProfileAvatarStateLoaded) {
type: BottomNavigationBarType.fixed, final profileModel = state.profile;
onTap: _onItemTapped, final isConsultant = profileModel.roles.contains('consultant');
selectedFontSize: 10,
unselectedFontSize: 10, // Инициализируем pages на основе profileModel
selectedItemColor: appTheme.blueA400, pages ??= _getPages(isConsultant);
currentIndex: currentBottomNavBarIndex,
items: [ return Scaffold(
BottomNavigationBarItem( body: widget.child,
label: '', bottomNavigationBar: BottomNavigationBar(
icon: CircleAvatar( type: BottomNavigationBarType.fixed,
foregroundImage: NetworkImage( onTap: _onItemTapped,
widget.profileModel.profilePhotoUrl.contains('https') ? widget.profileModel.profilePhotoUrl : sl<AppConfig>().baseUrl + widget.profileModel.profilePhotoUrl, selectedFontSize: 10,
), unselectedFontSize: 10,
backgroundImage: const AssetImage('assets/images/no_avatar.png'), selectedItemColor: appTheme.blueA400,
radius: 16, currentIndex: currentBottomNavBarIndex,
), items: [
), BottomNavigationBarItem(
BottomNavigationBarItem( label: '',
label: 'Лента', icon: CircleAvatar(
icon: Padding( foregroundImage: NetworkImage(
padding: const EdgeInsets.only(bottom: 2), profileModel.profilePhotoUrl.contains('https') ? profileModel.profilePhotoUrl : sl<AppConfig>().baseUrl + profileModel.profilePhotoUrl,
child: SvgPicture.asset( ),
colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn), backgroundImage: const AssetImage('assets/images/no_avatar.png'),
width: 20, radius: 16,
height: 20,
ImageConstant.icnFeed,
),
),
activeIcon: Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(
ImageConstant.icnFeed,
width: 20,
height: 20,
colorFilter: ColorFilter.mode(
appTheme.blueA400,
BlendMode.srcIn,
),
),
),
),
BottomNavigationBarItem(
label: s.diary,
icon: Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(
colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
ImageConstant.icnDiary,
width: 20,
height: 20,
),
),
activeIcon: Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(
ImageConstant.icnDiary,
width: 20,
height: 20,
colorFilter: ColorFilter.mode(
appTheme.blueA400,
BlendMode.srcIn,
),
),
),
),
if (widget.profileModel.roles.contains('consultant'))
BottomNavigationBarItem(
label: 'Консультации',
icon: Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(
colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
'assets/icons/consultations.svg',
width: 20,
height: 20,
),
),
activeIcon: Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(
'assets/icons/consultations.svg',
width: 20,
height: 20,
colorFilter: ColorFilter.mode(
appTheme.blueA400,
BlendMode.srcIn,
), ),
), ),
BottomNavigationBarItem(
label: 'Лента',
icon: Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(
colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
width: 20,
height: 20,
ImageConstant.icnFeed,
), ),
), ),
BottomNavigationBarItem( activeIcon: Padding(
label: s.chat, padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(
ImageConstant.icnFeed,
width: 20,
height: 20,
colorFilter: ColorFilter.mode(
appTheme.blueA400,
BlendMode.srcIn,
),
),
),
),
BottomNavigationBarItem(
label: s.diary,
icon: Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(
colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
ImageConstant.icnDiary,
width: 20,
height: 20,
),
),
activeIcon: Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(
ImageConstant.icnDiary,
width: 20,
height: 20,
colorFilter: ColorFilter.mode(
appTheme.blueA400,
BlendMode.srcIn,
),
),
),
),
if (profileModel.roles.contains('consultant'))
BottomNavigationBarItem(
label: 'Консультации',
icon: Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(
colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
'assets/icons/consultations.svg',
width: 20,
height: 20,
),
),
activeIcon: Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(
'assets/icons/consultations.svg',
width: 20,
height: 20,
colorFilter: ColorFilter.mode(
appTheme.blueA400,
BlendMode.srcIn,
),
),
),
),
BottomNavigationBarItem(
label: s.chat,
icon: BlocBuilder<ChatsCubit, ChatsState>( icon: BlocBuilder<ChatsCubit, ChatsState>(
builder: (BuildContext context, ChatsState state) { builder: (BuildContext context, ChatsState state) {
final hasUnreadMessages = state.chatsWithUnreadMessagesCount > 0; final hasUnreadMessages = state.chatsWithUnreadMessagesCount > 0;
@@ -206,7 +212,7 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
ImageConstant.icnChat, ImageConstant.icnChat,
width: 20, width: 20,
height: 20, height: 20,
colorFilter: pages[currentBottomNavBarIndex] == '/chat-select-screen' colorFilter: pages![currentBottomNavBarIndex] == '/chat-select-screen'
? ColorFilter.mode( ? ColorFilter.mode(
appTheme.blueA400, appTheme.blueA400,
BlendMode.srcIn, BlendMode.srcIn,
@@ -217,9 +223,9 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
); );
}, },
), ),
), ),
BottomNavigationBarItem( BottomNavigationBarItem(
label: 'Уведомления', label: 'Уведомления',
icon: BlocBuilder<NotificationsCubit, NotificationsState>( icon: BlocBuilder<NotificationsCubit, NotificationsState>(
builder: (BuildContext context, NotificationsState state) { builder: (BuildContext context, NotificationsState state) {
return badges.Badge( return badges.Badge(
@@ -237,7 +243,7 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
padding: const EdgeInsets.only(bottom: 2), padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset( child: SvgPicture.asset(
ImageConstant.icnBell, ImageConstant.icnBell,
colorFilter: pages[currentBottomNavBarIndex] == '/notifications-screen' colorFilter: pages![currentBottomNavBarIndex] == '/notifications-screen'
? ColorFilter.mode( ? ColorFilter.mode(
appTheme.blueA400, appTheme.blueA400,
BlendMode.srcIn, BlendMode.srcIn,
@@ -251,15 +257,22 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
}, },
), ),
), ),
BottomNavigationBarItem( BottomNavigationBarItem(
label: '', label: '',
icon: SvgPicture.asset( icon: SvgPicture.asset(
ImageConstant.imgLogo, ImageConstant.imgLogo,
height: 32, height: 32,
),
),
],
), ),
), );
], }
), // Показываем child даже когда профиль еще загружается, чтобы экраны могли инициализироваться
return Scaffold(
body: widget.child,
);
},
); );
} }
} }

View File

@@ -20,6 +20,19 @@ class ChatsScreen extends StatefulWidget {
class _ChatsScreenState extends State<ChatsScreen> { class _ChatsScreenState extends State<ChatsScreen> {
final searchController = TextEditingController(); final searchController = TextEditingController();
@override
void initState() {
super.initState();
// Инициализация ChatsCubit при первом открытии экрана
WidgetsBinding.instance.addPostFrameCallback((_) {
final cubit = context.read<ChatsCubit>();
final state = cubit.state;
if (state.state == BaseState.initial) {
cubit.getChatRoomsWithLoading();
}
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<ChatsCubit, ChatsState>( return BlocBuilder<ChatsCubit, ChatsState>(

View File

@@ -106,15 +106,14 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
resizeToAvoidBottomInset: true, resizeToAvoidBottomInset: true,
bottomSheet: Container( bottomSheet: Container(
padding: EdgeInsets.only( padding: EdgeInsets.only(
top: 12, bottom: MediaQuery.of(context).padding.bottom + 8,
bottom: 12 + MediaQuery.of(context).padding.bottom + 56, // 56 - высота bottomNavigationBar
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))), border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [

View File

@@ -3,6 +3,7 @@ import 'dart:math' as math;
import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/generated/l10n.dart';
import 'package:eatvio_app/core/theme/custom_text_style.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/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/markdown_html_widget.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
@@ -35,7 +36,9 @@ class _ExpandableTextState extends State<ExpandableText> {
void _checkTextOverflow() { void _checkTextOverflow() {
final textRenderObject = _textKey.currentContext?.findRenderObject(); final textRenderObject = _textKey.currentContext?.findRenderObject();
if (textRenderObject is RenderBox) { if (textRenderObject is RenderBox) {
final maxLinesHeight = widget.maxLines * TextStyles.textMMediumAuto.fontSize! * 1.2; final fontSize = TextStyles.textMMediumAuto.fontSize ?? 16;
final lineHeight = fontSize * 1.5;
final maxLinesHeight = widget.maxLines * lineHeight;
final actualHeight = textRenderObject.size.height; final actualHeight = textRenderObject.size.height;
if (actualHeight > maxLinesHeight && !showExpandButton) { if (actualHeight > maxLinesHeight && !showExpandButton) {
@@ -55,6 +58,7 @@ class _ExpandableTextState extends State<ExpandableText> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final s = S.of(context); final s = S.of(context);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -65,12 +69,18 @@ class _ExpandableTextState extends State<ExpandableText> {
curve: Curves.easeInOut, curve: Curves.easeInOut,
child: Stack( child: Stack(
children: [ children: [
Text( ClipRect(
widget.text, child: ConstrainedBox(
key: _textKey, constraints: BoxConstraints(
style: TextStyles.textMMediumAuto.copyWith(color: appTheme.black), maxHeight: isExpanded ? double.infinity : (widget.maxLines * (TextStyles.textMMediumAuto.fontSize ?? 16) * 1.5),
maxLines: isExpanded ? null : widget.maxLines, ),
overflow: TextOverflow.clip, child: MarkdownHtmlWidget(
key: _textKey,
markdownText: widget.text,
fontSize: TextStyles.textMMediumAuto.fontSize ?? 16,
textColor: appTheme.black,
),
),
), ),
if (!isExpanded && showExpandButton) if (!isExpanded && showExpandButton)
Positioned.fill( Positioned.fill(

View File

@@ -11,6 +11,7 @@ import 'package:eatvio_app/features/create_plan/presentation/create_plan_screen.
import 'package:eatvio_app/features/feed/presentation/bloc/consultants_cubit/consultants_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/consultants_cubit/consultants_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_state.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/profile_cubit/profile_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/profile_cubit/profile_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/profile_cubit/profile_state.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/profile_cubit/profile_state.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart';
@@ -74,6 +75,16 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
chipsLabelList = hidePlans ? ['Все', 'Рецепты', 'Диеты', 'Консультанты'] : ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты']; chipsLabelList = hidePlans ? ['Все', 'Рецепты', 'Диеты', 'Консультанты'] : ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты'];
consultantsScrollController.addListener(_onScrollConsultants); consultantsScrollController.addListener(_onScrollConsultants);
_checkRegistrationDate(context); _checkRegistrationDate(context);
// Инициализация глобальных кубитов при первом открытии экрана
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
final feedCubit = context.read<FeedCubit>();
final state = feedCubit.state;
// Загружаем данные, если они еще не загружены
if (state is! FeedStateLoaded) {
feedCubit.loadFeedBlogPostList();
}
});
super.initState(); super.initState();
} }

View File

@@ -7,17 +7,16 @@ import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/date_time_utils.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/custom_popup.dart';
import 'package:eatvio_app/core/widgets/feed_icon_button.dart'; import 'package:eatvio_app/core/widgets/feed_icon_button.dart';
import 'package:eatvio_app/core/widgets/markdown_html_widget.dart';
import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart'; import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_state.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_state.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/subscribe_button_widget.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/subscribe_button_widget.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'package:markdown/markdown.dart' as md;
class BlogPostWidget extends StatefulWidget { class BlogPostWidget extends StatefulWidget {
final int index; final int index;
@@ -125,37 +124,46 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
if (state.feedBlogPostList[widget.index].description.isNotEmpty) if (state.feedBlogPostList[widget.index].description.isNotEmpty)
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: 12),
child: Html( child: Builder(
data: md.markdownToHtml( builder: (context) {
(state.feedBlogPostList[widget.index].description.length > 200 String description = state.feedBlogPostList[widget.index].description;
? state.feedBlogPostList[widget.index].description.substring(0, 200).replaceAll('\n', '<br>') final isLong = description.length > 200;
: state.feedBlogPostList[widget.index].description.replaceAll('\n', '<br>')),
), if (isLong) {
style: { // Умная обрезка: обрезаем markdown текст, не ломая синтаксис
"body": Style(margin: Margins.zero, padding: HtmlPaddings.zero, fontSize: FontSize(16), lineHeight: const LineHeight(1.5)), int cutPoint = 200;
"p": Style(margin: Margins.only(bottom: 12), color: Colors.black87),
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: Colors.black, fontWeight: FontWeight.bold), // Проверяем, есть ли незакрытые markdown теги
"h1": Style(fontSize: FontSize(24)), String preview = description.substring(0, cutPoint);
"h2": Style(fontSize: FontSize(20)),
"h3": Style(fontSize: FontSize(18)), // Считаем открывающие и закрывающие теги
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)), int boldOpen = (preview.split('**').length - 1) ~/ 2;
"li": Style(margin: Margins.only(bottom: 4), color: Colors.black87), int boldClose = preview.split('**').length - 1 - boldOpen * 2;
"strong, b": Style(fontWeight: FontWeight.bold, color: Colors.black), bool hasUnclosedBold = boldOpen > boldClose;
"em, i": Style(fontStyle: FontStyle.italic),
"code": Style( // Если есть незакрытый bold, ищем закрытие
backgroundColor: Colors.grey.shade100, if (hasUnclosedBold) {
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2), int searchEnd = (cutPoint + 100).clamp(0, description.length);
fontFamily: 'monospace', int boldClosePos = description.indexOf('**', cutPoint);
), if (boldClosePos != -1 && boldClosePos < searchEnd) {
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)), cutPoint = boldClosePos + 2;
"blockquote": Style( } else {
padding: HtmlPaddings.only(left: 16), // Если не нашли, обрезаем до последнего пробела
margin: Margins.only(left: 8, bottom: 12), cutPoint = description.lastIndexOf(' ', 200);
fontStyle: FontStyle.italic, if (cutPoint == -1 || cutPoint < 150) cutPoint = 200;
color: Colors.grey.shade700, }
), } else {
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline), // Обрезаем до последнего пробела для красоты
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)), cutPoint = description.lastIndexOf(' ', cutPoint);
if (cutPoint == -1 || cutPoint < 150) cutPoint = 200;
}
description = description.substring(0, cutPoint);
}
return MarkdownHtmlWidget(
markdownText: description,
);
}, },
), ),
), ),

View File

@@ -13,12 +13,10 @@ import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_html/flutter_html.dart'; import 'package:eatvio_app/core/widgets/markdown_html_widget.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:url_launcher/url_launcher.dart';
class DietsWidget extends StatefulWidget { class DietsWidget extends StatefulWidget {
final Diet diet; final Diet diet;
@@ -117,86 +115,11 @@ class _DietsWidgetState extends State<DietsWidget> {
String details = widget.diet.details.length > 200 String details = widget.diet.details.length > 200
? '${widget.diet.details.substring(0, 200)}...' ? '${widget.diet.details.substring(0, 200)}...'
: widget.diet.details; : widget.diet.details;
// Автоматически преобразуем URL в markdown ссылки
// Сначала исправляем неправильный синтаксис markdown ссылок return MarkdownHtmlWidget(
final wrongLinkRegex = RegExp(r'\[(https?://[^\]]+|www\.[^\]]+)\]\(([^\)]+)\)'); markdownText: details,
details = details.replaceAllMapped(wrongLinkRegex, (match) { fontSize: TextStyles.textSMedium.fontSize ?? 14,
final urlInText = match.group(1)!; textColor: appTheme.black,
if (!urlInText.startsWith('http://') && !urlInText.startsWith('https://') && !urlInText.startsWith('www.')) {
return match.group(0)!;
}
final fullUrl = urlInText.startsWith('www.') ? 'https://$urlInText' : urlInText;
return '[$urlInText]($fullUrl)';
});
final linkRegex = RegExp(r'\[([^\]]+)\]\(([^\)]+)\)');
final existingLinks = <_LinkRange>[];
for (final linkMatch in linkRegex.allMatches(details)) {
existingLinks.add(_LinkRange(linkMatch.start, linkMatch.end));
}
final urlRegex = RegExp(r'(https?://[^\s\)]+|www\.[^\s\)]+)', caseSensitive: false);
String result = details;
int offset = 0;
for (final urlMatch in urlRegex.allMatches(details)) {
bool isInsideLink = existingLinks.any((link) =>
urlMatch.start >= link.start && urlMatch.end <= link.end);
if (!isInsideLink) {
final url = urlMatch.group(0)!;
final fullUrl = url.startsWith('www.') ? 'https://$url' : url;
final replacement = '[$url]($fullUrl)';
result = result.replaceRange(
urlMatch.start + offset,
urlMatch.end + offset,
replacement,
);
offset += replacement.length - url.length;
}
}
details = result;
return Html(
data: md.markdownToHtml(
details.replaceAll('\n', '<br>'),
extensionSet: md.ExtensionSet.commonMark,
),
style: {
"body": Style(
margin: Margins.zero,
padding: HtmlPaddings.zero,
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
lineHeight: const LineHeight(1.5),
),
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold),
"h1": Style(fontSize: FontSize(24)),
"h2": Style(fontSize: FontSize(20)),
"h3": Style(fontSize: FontSize(18)),
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
"li": Style(margin: Margins.only(bottom: 4), color: appTheme.black),
"strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black),
"em, i": Style(fontStyle: FontStyle.italic),
"code": Style(
backgroundColor: Colors.grey.shade100,
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
fontFamily: 'monospace',
),
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
"blockquote": Style(
padding: HtmlPaddings.only(left: 16),
margin: Margins.only(left: 8, bottom: 12),
fontStyle: FontStyle.italic,
color: Colors.grey.shade700,
),
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
},
onLinkTap: (url, attributes, element) async {
if (url != null) {
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
}
},
); );
}, },
), ),
@@ -267,8 +190,3 @@ class _DietsWidgetState extends State<DietsWidget> {
} }
} }
class _LinkRange {
final int start;
final int end;
_LinkRange(this.start, this.end);
}

View File

@@ -7,6 +7,7 @@ import 'package:eatvio_app/core/widgets/bookmark_button.dart';
import 'package:eatvio_app/core/widgets/custom_popup.dart'; import 'package:eatvio_app/core/widgets/custom_popup.dart';
import 'package:eatvio_app/core/widgets/info_row.dart'; import 'package:eatvio_app/core/widgets/info_row.dart';
import 'package:eatvio_app/core/widgets/feed_icon_button.dart'; import 'package:eatvio_app/core/widgets/feed_icon_button.dart';
import 'package:eatvio_app/core/widgets/markdown_html_widget.dart';
import 'package:eatvio_app/features/diets/presentation/widgets/info_bouble.dart'; import 'package:eatvio_app/features/diets/presentation/widgets/info_bouble.dart';
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart';
@@ -15,12 +16,9 @@ import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cub
import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:url_launcher/url_launcher.dart';
class PlanWidget extends StatefulWidget { class PlanWidget extends StatefulWidget {
final PlanModel plan; final PlanModel plan;
@@ -117,88 +115,11 @@ class _PlanWidgetState extends State<PlanWidget> {
const SizedBox(height: 12), const SizedBox(height: 12),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: Builder( child: MarkdownHtmlWidget(
builder: (context) { markdownText: widget.plan.details.length > 200
String details = widget.plan.details.length > 200 ? '${widget.plan.details.substring(0, 200)}...'
? '${widget.plan.details.substring(0, 200)}...' : widget.plan.details,
: widget.plan.details; textColor: appTheme.black,
// Автоматически преобразуем URL в markdown ссылки
// Сначала исправляем неправильный синтаксис markdown ссылок
final wrongLinkRegex = RegExp(r'\[(https?://[^\]]+|www\.[^\]]+)\]\(([^\)]+)\)');
details = details.replaceAllMapped(wrongLinkRegex, (match) {
final urlInText = match.group(1)!;
if (!urlInText.startsWith('http://') && !urlInText.startsWith('https://') && !urlInText.startsWith('www.')) {
return match.group(0)!;
}
final fullUrl = urlInText.startsWith('www.') ? 'https://$urlInText' : urlInText;
return '[$urlInText]($fullUrl)';
});
final linkRegex = RegExp(r'\[([^\]]+)\]\(([^\)]+)\)');
final existingLinks = <_LinkRange>[];
for (final linkMatch in linkRegex.allMatches(details)) {
existingLinks.add(_LinkRange(linkMatch.start, linkMatch.end));
}
final urlRegex = RegExp(r'(https?://[^\s\)]+|www\.[^\s\)]+)', caseSensitive: false);
String result = details;
int offset = 0;
for (final urlMatch in urlRegex.allMatches(details)) {
bool isInsideLink = existingLinks.any((link) =>
urlMatch.start >= link.start && urlMatch.end <= link.end);
if (!isInsideLink) {
final url = urlMatch.group(0)!;
final fullUrl = url.startsWith('www.') ? 'https://$url' : url;
final replacement = '[$url]($fullUrl)';
result = result.replaceRange(
urlMatch.start + offset,
urlMatch.end + offset,
replacement,
);
offset += replacement.length - url.length;
}
}
details = result;
return Html(
data: md.markdownToHtml(
details.replaceAll('\n', '<br>'),
extensionSet: md.ExtensionSet.commonMark,
),
style: {
"body": Style(margin: Margins.zero, padding: HtmlPaddings.zero, fontSize: FontSize(16), lineHeight: const LineHeight(1.5)),
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold),
"h1": Style(fontSize: FontSize(24)),
"h2": Style(fontSize: FontSize(20)),
"h3": Style(fontSize: FontSize(18)),
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
"li": Style(margin: Margins.only(bottom: 4), color: appTheme.black),
"strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black),
"em, i": Style(fontStyle: FontStyle.italic),
"code": Style(
backgroundColor: Colors.grey.shade100,
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
fontFamily: 'monospace',
),
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
"blockquote": Style(
padding: HtmlPaddings.only(left: 16),
margin: Margins.only(left: 8, bottom: 12),
fontStyle: FontStyle.italic,
color: Colors.grey.shade700,
),
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
},
onLinkTap: (url, attributes, element) async {
if (url != null) {
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
}
},
);
},
), ),
), ),
if (widget.plan.details.length > 200) if (widget.plan.details.length > 200)
@@ -312,9 +233,3 @@ class _PlanWidgetState extends State<PlanWidget> {
); );
} }
} }
class _LinkRange {
final int start;
final int end;
_LinkRange(this.start, this.end);
}

View File

@@ -16,12 +16,10 @@ import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes
import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_html/flutter_html.dart'; import 'package:eatvio_app/core/widgets/markdown_html_widget.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:url_launcher/url_launcher.dart';
class RecipesWidget extends StatefulWidget { class RecipesWidget extends StatefulWidget {
final int index; final int index;
@@ -155,88 +153,11 @@ class _RecipesWidgetState extends State<RecipesWidget> {
String description = widget.recipe.description.length > 200 String description = widget.recipe.description.length > 200
? widget.recipe.description.substring(0, 200) ? widget.recipe.description.substring(0, 200)
: widget.recipe.description; : widget.recipe.description;
// Автоматически преобразуем URL в markdown ссылки
// Сначала исправляем неправильный синтаксис markdown ссылок return MarkdownHtmlWidget(
final wrongLinkRegex = RegExp(r'\[(https?://[^\]]+|www\.[^\]]+)\]\(([^\)]+)\)'); markdownText: description,
description = description.replaceAllMapped(wrongLinkRegex, (match) { fontSize: TextStyles.textSMedium.fontSize ?? 14,
final urlInText = match.group(1)!; textColor: appTheme.black,
if (!urlInText.startsWith('http://') && !urlInText.startsWith('https://') && !urlInText.startsWith('www.')) {
return match.group(0)!;
}
final fullUrl = urlInText.startsWith('www.') ? 'https://$urlInText' : urlInText;
return '[$urlInText]($fullUrl)';
});
// Находим все существующие markdown ссылки и их позиции
final linkRegex = RegExp(r'\[([^\]]+)\]\(([^\)]+)\)');
final existingLinks = <_LinkRange>[];
for (final linkMatch in linkRegex.allMatches(description)) {
existingLinks.add(_LinkRange(linkMatch.start, linkMatch.end));
}
final urlRegex = RegExp(r'(https?://[^\s\)]+|www\.[^\s\)]+)', caseSensitive: false);
String result = description;
int offset = 0;
for (final urlMatch in urlRegex.allMatches(description)) {
bool isInsideLink = existingLinks.any((link) =>
urlMatch.start >= link.start && urlMatch.end <= link.end);
if (!isInsideLink) {
final url = urlMatch.group(0)!;
final fullUrl = url.startsWith('www.') ? 'https://$url' : url;
final replacement = '[$url]($fullUrl)';
result = result.replaceRange(
urlMatch.start + offset,
urlMatch.end + offset,
replacement,
);
offset += replacement.length - url.length;
}
}
description = result;
return Html(
data: md.markdownToHtml(
description.replaceAll('\n', '<br>'),
extensionSet: md.ExtensionSet.commonMark,
),
style: {
"body": Style(
margin: Margins.zero,
padding: HtmlPaddings.zero,
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
lineHeight: const LineHeight(1.5),
),
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold),
"h1": Style(fontSize: FontSize(24)),
"h2": Style(fontSize: FontSize(20)),
"h3": Style(fontSize: FontSize(18)),
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
"li": Style(margin: Margins.only(bottom: 4), color: appTheme.black),
"strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black),
"em, i": Style(fontStyle: FontStyle.italic),
"code": Style(
backgroundColor: Colors.grey.shade100,
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
fontFamily: 'monospace',
),
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
"blockquote": Style(
padding: HtmlPaddings.only(left: 16),
margin: Margins.only(left: 8, bottom: 12),
fontStyle: FontStyle.italic,
color: Colors.grey.shade700,
),
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
},
onLinkTap: (url, attributes, element) async {
if (url != null) {
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
}
},
); );
}, },
), ),
@@ -330,8 +251,3 @@ class _RecipesWidgetState extends State<RecipesWidget> {
} }
} }
class _LinkRange {
final int start;
final int end;
_LinkRange(this.start, this.end);
}

View File

@@ -21,12 +21,10 @@ import 'package:eatvio_app/features/personal_diet_suggestion/presentation/person
import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_html/flutter_html.dart'; import 'package:eatvio_app/core/widgets/markdown_html_widget.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:url_launcher/url_launcher.dart';
class MyDietWidget extends StatefulWidget { class MyDietWidget extends StatefulWidget {
final Diet diet; final Diet diet;
@@ -86,86 +84,11 @@ class _MyDietWidgetState extends State<MyDietWidget> {
String details = widget.diet.details.length > 200 String details = widget.diet.details.length > 200
? '${widget.diet.details.substring(0, 200)}...' ? '${widget.diet.details.substring(0, 200)}...'
: widget.diet.details; : widget.diet.details;
// Автоматически преобразуем URL в markdown ссылки
// Сначала исправляем неправильный синтаксис markdown ссылок return MarkdownHtmlWidget(
final wrongLinkRegex = RegExp(r'\[(https?://[^\]]+|www\.[^\]]+)\]\(([^\)]+)\)'); markdownText: details,
details = details.replaceAllMapped(wrongLinkRegex, (match) { fontSize: TextStyles.textSMedium.fontSize ?? 14,
final urlInText = match.group(1)!; textColor: appTheme.black,
if (!urlInText.startsWith('http://') && !urlInText.startsWith('https://') && !urlInText.startsWith('www.')) {
return match.group(0)!;
}
final fullUrl = urlInText.startsWith('www.') ? 'https://$urlInText' : urlInText;
return '[$urlInText]($fullUrl)';
});
final linkRegex = RegExp(r'\[([^\]]+)\]\(([^\)]+)\)');
final existingLinks = <_LinkRange>[];
for (final linkMatch in linkRegex.allMatches(details)) {
existingLinks.add(_LinkRange(linkMatch.start, linkMatch.end));
}
final urlRegex = RegExp(r'(https?://[^\s\)]+|www\.[^\s\)]+)', caseSensitive: false);
String result = details;
int offset = 0;
for (final urlMatch in urlRegex.allMatches(details)) {
bool isInsideLink = existingLinks.any((link) =>
urlMatch.start >= link.start && urlMatch.end <= link.end);
if (!isInsideLink) {
final url = urlMatch.group(0)!;
final fullUrl = url.startsWith('www.') ? 'https://$url' : url;
final replacement = '[$url]($fullUrl)';
result = result.replaceRange(
urlMatch.start + offset,
urlMatch.end + offset,
replacement,
);
offset += replacement.length - url.length;
}
}
details = result;
return Html(
data: md.markdownToHtml(
details.replaceAll('\n', '<br>'),
extensionSet: md.ExtensionSet.commonMark,
),
style: {
"body": Style(
margin: Margins.zero,
padding: HtmlPaddings.zero,
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
lineHeight: const LineHeight(1.5),
),
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold),
"h1": Style(fontSize: FontSize(24)),
"h2": Style(fontSize: FontSize(20)),
"h3": Style(fontSize: FontSize(18)),
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
"li": Style(margin: Margins.only(bottom: 4), color: appTheme.black),
"strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black),
"em, i": Style(fontStyle: FontStyle.italic),
"code": Style(
backgroundColor: Colors.grey.shade100,
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
fontFamily: 'monospace',
),
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
"blockquote": Style(
padding: HtmlPaddings.only(left: 16),
margin: Margins.only(left: 8, bottom: 12),
fontStyle: FontStyle.italic,
color: Colors.grey.shade700,
),
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
},
onLinkTap: (url, attributes, element) async {
if (url != null) {
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
}
},
); );
}, },
), ),
@@ -323,8 +246,3 @@ class _MyDietWidgetState extends State<MyDietWidget> {
} }
} }
class _LinkRange {
final int start;
final int end;
_LinkRange(this.start, this.end);
}

View File

@@ -14,12 +14,10 @@ import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.d
import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_html/flutter_html.dart'; import 'package:eatvio_app/core/widgets/markdown_html_widget.dart';
// Removed SVG icon usage after switching to text buttons // Removed SVG icon usage after switching to text buttons
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:url_launcher/url_launcher.dart';
class MyPlanWidget extends StatefulWidget { class MyPlanWidget extends StatefulWidget {
final PlanModel plan; final PlanModel plan;
@@ -78,86 +76,11 @@ class _MyPlanWidgetState extends State<MyPlanWidget> {
String details = widget.plan.details.length > 200 String details = widget.plan.details.length > 200
? '${widget.plan.details.substring(0, 200)}...' ? '${widget.plan.details.substring(0, 200)}...'
: widget.plan.details; : widget.plan.details;
// Автоматически преобразуем URL в markdown ссылки
// Сначала исправляем неправильный синтаксис markdown ссылок return MarkdownHtmlWidget(
final wrongLinkRegex = RegExp(r'\[(https?://[^\]]+|www\.[^\]]+)\]\(([^\)]+)\)'); markdownText: details,
details = details.replaceAllMapped(wrongLinkRegex, (match) { fontSize: TextStyles.textSMedium.fontSize ?? 14,
final urlInText = match.group(1)!; textColor: appTheme.black,
if (!urlInText.startsWith('http://') && !urlInText.startsWith('https://') && !urlInText.startsWith('www.')) {
return match.group(0)!;
}
final fullUrl = urlInText.startsWith('www.') ? 'https://$urlInText' : urlInText;
return '[$urlInText]($fullUrl)';
});
final linkRegex = RegExp(r'\[([^\]]+)\]\(([^\)]+)\)');
final existingLinks = <_LinkRange>[];
for (final linkMatch in linkRegex.allMatches(details)) {
existingLinks.add(_LinkRange(linkMatch.start, linkMatch.end));
}
final urlRegex = RegExp(r'(https?://[^\s\)]+|www\.[^\s\)]+)', caseSensitive: false);
String result = details;
int offset = 0;
for (final urlMatch in urlRegex.allMatches(details)) {
bool isInsideLink = existingLinks.any((link) =>
urlMatch.start >= link.start && urlMatch.end <= link.end);
if (!isInsideLink) {
final url = urlMatch.group(0)!;
final fullUrl = url.startsWith('www.') ? 'https://$url' : url;
final replacement = '[$url]($fullUrl)';
result = result.replaceRange(
urlMatch.start + offset,
urlMatch.end + offset,
replacement,
);
offset += replacement.length - url.length;
}
}
details = result;
return Html(
data: md.markdownToHtml(
details.replaceAll('\n', '<br>'),
extensionSet: md.ExtensionSet.commonMark,
),
style: {
"body": Style(
margin: Margins.zero,
padding: HtmlPaddings.zero,
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
lineHeight: const LineHeight(1.5),
),
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold),
"h1": Style(fontSize: FontSize(24)),
"h2": Style(fontSize: FontSize(20)),
"h3": Style(fontSize: FontSize(18)),
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
"li": Style(margin: Margins.only(bottom: 4), color: appTheme.black),
"strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black),
"em, i": Style(fontStyle: FontStyle.italic),
"code": Style(
backgroundColor: Colors.grey.shade100,
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
fontFamily: 'monospace',
),
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
"blockquote": Style(
padding: HtmlPaddings.only(left: 16),
margin: Margins.only(left: 8, bottom: 12),
fontStyle: FontStyle.italic,
color: Colors.grey.shade700,
),
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
},
onLinkTap: (url, attributes, element) async {
if (url != null) {
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
}
},
); );
}, },
), ),
@@ -268,8 +191,3 @@ class _MyPlanWidgetState extends State<MyPlanWidget> {
} }
} }
class _LinkRange {
final int start;
final int end;
_LinkRange(this.start, this.end);
}

View File

@@ -24,6 +24,14 @@ class _NotificationsScreenState extends State<NotificationsScreen> {
void initState() { void initState() {
super.initState(); super.initState();
_scrollController.addListener(_onScroll); _scrollController.addListener(_onScroll);
// Инициализация NotificationsCubit при первом открытии экрана
WidgetsBinding.instance.addPostFrameCallback((_) {
final cubit = context.read<NotificationsCubit>();
final state = cubit.state;
if (state is! NotificationsStateLoaded && state is! NotificationsStateError) {
cubit.getNotifications();
}
});
} }
@override @override

View File

@@ -153,16 +153,13 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
backgroundColor: appTheme.gray100, backgroundColor: appTheme.gray100,
resizeToAvoidBottomInset: true, resizeToAvoidBottomInset: true,
bottomSheet: Container( bottomSheet: Container(
padding: EdgeInsets.only( padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom + 8),
top: 12,
bottom: 12 + MediaQuery.of(context).padding.bottom + 56, // 56 - высота bottomNavigationBar
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))), border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [

View File

@@ -13,10 +13,9 @@ import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/prof
import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_state.dart'; import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_html/flutter_html.dart'; import 'package:eatvio_app/core/widgets/markdown_html_widget.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
class ProfileBlogPostWidget extends StatefulWidget { class ProfileBlogPostWidget extends StatefulWidget {
@@ -107,37 +106,48 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
if (state.blogPosts[widget.index].description.isNotEmpty) if (state.blogPosts[widget.index].description.isNotEmpty)
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: 12),
child: Html( child: Builder(
data: md.markdownToHtml( builder: (context) {
state.blogPosts[widget.index].description.length > 200 String description = state.blogPosts[widget.index].description;
? state.blogPosts[widget.index].description.substring(0, 200).replaceAll('\n', '<br>') final isLong = description.length > 200;
: state.blogPosts[widget.index].description.replaceAll('\n', '<br>'),
), if (isLong) {
style: { // Умная обрезка: обрезаем markdown текст, не ломая синтаксис
"body": Style(margin: Margins.zero, padding: HtmlPaddings.zero, fontSize: FontSize(16), lineHeight: const LineHeight(1.5)), int cutPoint = 200;
"p": Style(margin: Margins.only(bottom: 12), color: Colors.black87),
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: Colors.black, fontWeight: FontWeight.bold), // Проверяем, есть ли незакрытые markdown теги
"h1": Style(fontSize: FontSize(24)), String preview = description.substring(0, cutPoint);
"h2": Style(fontSize: FontSize(20)),
"h3": Style(fontSize: FontSize(18)), // Считаем открывающие и закрывающие теги
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)), int boldOpen = (preview.split('**').length - 1) ~/ 2;
"li": Style(margin: Margins.only(bottom: 4), color: Colors.black87), int boldClose = preview.split('**').length - 1 - boldOpen * 2;
"strong, b": Style(fontWeight: FontWeight.bold, color: Colors.black), bool hasUnclosedBold = boldOpen > boldClose;
"em, i": Style(fontStyle: FontStyle.italic),
"code": Style( // Если есть незакрытый bold, ищем закрытие
backgroundColor: Colors.grey.shade100, if (hasUnclosedBold) {
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2), int searchEnd = (cutPoint + 100).clamp(0, description.length);
fontFamily: 'monospace', int boldClosePos = description.indexOf('**', cutPoint);
), if (boldClosePos != -1 && boldClosePos < searchEnd) {
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)), cutPoint = boldClosePos + 2;
"blockquote": Style( } else {
padding: HtmlPaddings.only(left: 16), // Если не нашли, обрезаем до последнего пробела
margin: Margins.only(left: 8, bottom: 12), cutPoint = description.lastIndexOf(' ', 200);
fontStyle: FontStyle.italic, if (cutPoint == -1 || cutPoint < 150) cutPoint = 200;
color: Colors.grey.shade700, }
), } else {
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline), // Обрезаем до последнего пробела для красоты
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)), cutPoint = description.lastIndexOf(' ', cutPoint);
if (cutPoint == -1 || cutPoint < 150) cutPoint = 200;
}
description = description.substring(0, cutPoint);
}
return MarkdownHtmlWidget(
markdownText: description,
fontSize: 16,
textColor: Colors.black87,
);
}, },
), ),
), ),

View File

@@ -99,108 +99,102 @@ class _AboutRecipeScreenState extends State<AboutRecipeScreen> {
resizeToAvoidBottomInset: true, resizeToAvoidBottomInset: true,
bottomSheet: Container( bottomSheet: Container(
padding: EdgeInsets.only( padding: EdgeInsets.only(
top: 12, bottom: MediaQuery.of(context).padding.bottom + 8,
bottom: 12 + MediaQuery.of(context).padding.bottom + 56, // 56 - высота bottomNavigationBar
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))), border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Column( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Row( if (replyId != null)
mainAxisAlignment: MainAxisAlignment.center, GestureDetector(
children: [ onTap: () {
if (replyId != null) context.read<CommentsCubit>().setReplyId(null);
GestureDetector( setState(() {
onTap: () { replyId = null;
context.read<CommentsCubit>().setReplyId(null); textEditingController.clear();
setState(() { });
replyId = null; FocusManager.instance.primaryFocus?.unfocus();
textEditingController.clear(); },
}); child: Container(
FocusManager.instance.primaryFocus?.unfocus(); decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
}, height: 32,
child: Container( width: 32,
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle), child: const Icon(Icons.close_rounded, size: 18, color: Colors.white),
height: 32,
width: 32,
child: const Icon(Icons.close_rounded, size: 18, color: Colors.white),
),
),
if (replyId != null) const SizedBox(width: 8),
Expanded(
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 52),
child: TextFormField(
textCapitalization: TextCapitalization.sentences,
onChanged: (_) {
setState(() {});
},
controller: textEditingController,
maxLines: 5,
minLines: 1,
keyboardType: TextInputType.multiline,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1.2),
decoration: InputDecoration(
isDense: false,
// убираем true, чтобы padding учитывался корректно
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
// вертикальный паддинг увеличен
hintText: 'Комментарий',
hintStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, height: 1.2, color: Color(0xFFAEAEB2)),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
),
),
),
), ),
const SizedBox(width: 8), ),
GestureDetector( if (replyId != null) const SizedBox(width: 8),
onTap: () { Expanded(
if (textEditingController.text.length > 2) { child: ConstrainedBox(
commentsCubit.sendComment( constraints: const BoxConstraints(minHeight: 52),
id: widget.recipeSlug, child: TextFormField(
commentText: textEditingController.text, textCapitalization: TextCapitalization.sentences,
parentCommentId: replyId, onChanged: (_) {
commentSourceType: CommentSourceType.recipe, setState(() {});
);
context.read<CommentsCubit>().setReplyId(null);
setState(() {
textEditingController.clear();
replyId = null;
});
Future.delayed(const Duration(milliseconds: 500), () {
recipeCubit.getRecipe(widget.recipeSlug, index: widget.index);
});
FocusManager.instance.primaryFocus?.unfocus();
}
}, },
child: Container( controller: textEditingController,
decoration: BoxDecoration( maxLines: 5,
color: textEditingController.text.length > 2 ? appTheme.blueA400 : Colors.grey, minLines: 1,
shape: BoxShape.circle, keyboardType: TextInputType.multiline,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1.2),
decoration: InputDecoration(
isDense: false,
// убираем true, чтобы padding учитывался корректно
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
// вертикальный паддинг увеличен
hintText: s.comment_hint,
hintStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, height: 1.2, color: Color(0xFFAEAEB2)),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
), ),
height: 32,
width: 32,
child: const Icon(Icons.send_rounded, size: 18, color: Colors.white),
), ),
), ),
], ),
),
const SizedBox(width: 8),
GestureDetector(
onTap: () {
if (textEditingController.text.length > 2) {
commentsCubit.sendComment(
id: widget.recipeSlug,
commentText: textEditingController.text,
parentCommentId: replyId,
commentSourceType: CommentSourceType.recipe,
);
context.read<CommentsCubit>().setReplyId(null);
setState(() {
textEditingController.clear();
replyId = null;
});
Future.delayed(const Duration(milliseconds: 500), () {
recipeCubit.getRecipe(widget.recipeSlug, index: widget.index);
});
FocusManager.instance.primaryFocus?.unfocus();
}
},
child: Container(
decoration: BoxDecoration(
color: textEditingController.text.length > 2 ? appTheme.blueA400 : Colors.grey,
shape: BoxShape.circle,
),
height: 32,
width: 32,
child: const Icon(Icons.send_rounded, size: 18, color: Colors.white),
),
), ),
], ],
), ),

View File

@@ -12,12 +12,10 @@ import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/m
import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_html/flutter_html.dart'; import 'package:eatvio_app/core/widgets/markdown_html_widget.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'package:url_launcher/url_launcher.dart';
class MyRecipeWidget extends StatefulWidget { class MyRecipeWidget extends StatefulWidget {
final MyRecipeModel recipe; final MyRecipeModel recipe;
@@ -100,86 +98,11 @@ class _MyRecipeWidgetState extends State<MyRecipeWidget> {
String description = widget.recipe.description.length > 200 String description = widget.recipe.description.length > 200
? '${widget.recipe.description.substring(0, 200)}...' ? '${widget.recipe.description.substring(0, 200)}...'
: widget.recipe.description; : widget.recipe.description;
// Автоматически преобразуем URL в markdown ссылки
// Сначала исправляем неправильный синтаксис markdown ссылок return MarkdownHtmlWidget(
final wrongLinkRegex = RegExp(r'\[(https?://[^\]]+|www\.[^\]]+)\]\(([^\)]+)\)'); markdownText: description,
description = description.replaceAllMapped(wrongLinkRegex, (match) { fontSize: TextStyles.textSMedium.fontSize ?? 14,
final urlInText = match.group(1)!; textColor: appTheme.black,
if (!urlInText.startsWith('http://') && !urlInText.startsWith('https://') && !urlInText.startsWith('www.')) {
return match.group(0)!;
}
final fullUrl = urlInText.startsWith('www.') ? 'https://$urlInText' : urlInText;
return '[$urlInText]($fullUrl)';
});
final linkRegex = RegExp(r'\[([^\]]+)\]\(([^\)]+)\)');
final existingLinks = <_LinkRange>[];
for (final linkMatch in linkRegex.allMatches(description)) {
existingLinks.add(_LinkRange(linkMatch.start, linkMatch.end));
}
final urlRegex = RegExp(r'(https?://[^\s\)]+|www\.[^\s\)]+)', caseSensitive: false);
String result = description;
int offset = 0;
for (final urlMatch in urlRegex.allMatches(description)) {
bool isInsideLink = existingLinks.any((link) =>
urlMatch.start >= link.start && urlMatch.end <= link.end);
if (!isInsideLink) {
final url = urlMatch.group(0)!;
final fullUrl = url.startsWith('www.') ? 'https://$url' : url;
final replacement = '[$url]($fullUrl)';
result = result.replaceRange(
urlMatch.start + offset,
urlMatch.end + offset,
replacement,
);
offset += replacement.length - url.length;
}
}
description = result;
return Html(
data: md.markdownToHtml(
description.replaceAll('\n', '<br>'),
extensionSet: md.ExtensionSet.commonMark,
),
style: {
"body": Style(
margin: Margins.zero,
padding: HtmlPaddings.zero,
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
lineHeight: const LineHeight(1.5),
),
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold),
"h1": Style(fontSize: FontSize(24)),
"h2": Style(fontSize: FontSize(20)),
"h3": Style(fontSize: FontSize(18)),
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
"li": Style(margin: Margins.only(bottom: 4), color: appTheme.black),
"strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black),
"em, i": Style(fontStyle: FontStyle.italic),
"code": Style(
backgroundColor: Colors.grey.shade100,
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
fontFamily: 'monospace',
),
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
"blockquote": Style(
padding: HtmlPaddings.only(left: 16),
margin: Margins.only(left: 8, bottom: 12),
fontStyle: FontStyle.italic,
color: Colors.grey.shade700,
),
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
},
onLinkTap: (url, attributes, element) async {
if (url != null) {
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
}
},
); );
}, },
), ),
@@ -270,8 +193,3 @@ class _MyRecipeWidgetState extends State<MyRecipeWidget> {
} }
} }
class _LinkRange {
final int start;
final int end;
_LinkRange(this.start, this.end);
}

View File

@@ -11,7 +11,6 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart'; import 'package:intl/message_lookup_by_library.dart';
import 'package:intl/src/intl_helpers.dart'; import 'package:intl/src/intl_helpers.dart';
@@ -21,8 +20,8 @@ import 'messages_ru.dart' as messages_ru;
typedef Future<dynamic> LibraryLoader(); typedef Future<dynamic> LibraryLoader();
Map<String, LibraryLoader> _deferredLibraries = { Map<String, LibraryLoader> _deferredLibraries = {
'en': () => new SynchronousFuture(null), 'en': () => new Future.value(null),
'ru': () => new SynchronousFuture(null), 'ru': () => new Future.value(null),
}; };
MessageLookupByLibrary? _findExact(String localeName) { MessageLookupByLibrary? _findExact(String localeName) {
@@ -37,20 +36,19 @@ MessageLookupByLibrary? _findExact(String localeName) {
} }
/// User programs should call this before using [localeName] for messages. /// User programs should call this before using [localeName] for messages.
Future<bool> initializeMessages(String localeName) { Future<bool> initializeMessages(String localeName) async {
var availableLocale = Intl.verifiedLocale( var availableLocale = Intl.verifiedLocale(
localeName, localeName,
(locale) => _deferredLibraries[locale] != null, (locale) => _deferredLibraries[locale] != null,
onFailure: (_) => null, onFailure: (_) => null);
);
if (availableLocale == null) { if (availableLocale == null) {
return new SynchronousFuture(false); return new Future.value(false);
} }
var lib = _deferredLibraries[availableLocale]; var lib = _deferredLibraries[availableLocale];
lib == null ? new SynchronousFuture(false) : lib(); await (lib == null ? new Future.value(false) : lib());
initializeInternalMessageLookup(() => new CompositeMessageLookup()); initializeInternalMessageLookup(() => new CompositeMessageLookup());
messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor); messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor);
return new SynchronousFuture(true); return new Future.value(true);
} }
bool _messagesExistFor(String locale) { bool _messagesExistFor(String locale) {
@@ -62,11 +60,8 @@ bool _messagesExistFor(String locale) {
} }
MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) { MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) {
var actualLocale = Intl.verifiedLocale( var actualLocale = Intl.verifiedLocale(locale, _messagesExistFor,
locale, onFailure: (_) => null);
_messagesExistFor,
onFailure: (_) => null,
);
if (actualLocale == null) return null; if (actualLocale == null) return null;
return _findExact(actualLocale); return _findExact(actualLocale);
} }

File diff suppressed because it is too large Load Diff

View File

@@ -7,8 +7,7 @@
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new // ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering // ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases // ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes // ignore_for_file:unused_import, file_names
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart'; import 'package:intl/message_lookup_by_library.dart';
@@ -21,362 +20,223 @@ class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'ru'; String get localeName => 'ru';
final messages = _notInlinedMessages(_notInlinedMessages); final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{ static _notInlinedMessages(_) => <String, Function> {
"Moscow": MessageLookupByLibrary.simpleMessage("Москва"), "Moscow" : MessageLookupByLibrary.simpleMessage("Москва"),
"Russia": MessageLookupByLibrary.simpleMessage("Россия"), "Russia" : MessageLookupByLibrary.simpleMessage("Россия"),
"about_diet": MessageLookupByLibrary.simpleMessage("Об этой диете"), "about_diet" : MessageLookupByLibrary.simpleMessage("Об этой диете"),
"about_plan": MessageLookupByLibrary.simpleMessage("Об этом плане"), "about_plan" : MessageLookupByLibrary.simpleMessage("Об этом плане"),
"active": MessageLookupByLibrary.simpleMessage("Активен"), "active" : MessageLookupByLibrary.simpleMessage("Активен"),
"active_diet_state": MessageLookupByLibrary.simpleMessage("Активная"), "active_diet_state" : MessageLookupByLibrary.simpleMessage("Активная"),
"add": MessageLookupByLibrary.simpleMessage("Добавить"), "add" : MessageLookupByLibrary.simpleMessage("Добавить"),
"add_cover": MessageLookupByLibrary.simpleMessage("Добавьте обложку"), "add_cover" : MessageLookupByLibrary.simpleMessage("Добавьте обложку"),
"add_measure": MessageLookupByLibrary.simpleMessage("Добавить замер"), "add_measure" : MessageLookupByLibrary.simpleMessage("Добавить замер"),
"adding_plan": MessageLookupByLibrary.simpleMessage("Добавление плана"), "adding_plan" : MessageLookupByLibrary.simpleMessage("Добавление плана"),
"additional_information": MessageLookupByLibrary.simpleMessage( "additional_information" : MessageLookupByLibrary.simpleMessage("Дополнительная информация"),
"Дополнительная информация", "admin" : MessageLookupByLibrary.simpleMessage("Админ"),
), "all" : MessageLookupByLibrary.simpleMessage("Все"),
"admin": MessageLookupByLibrary.simpleMessage("Админ"), "allergies_presence" : MessageLookupByLibrary.simpleMessage("Наличие аллергии"),
"all": MessageLookupByLibrary.simpleMessage("Все"), "another_form_of_comment" : MessageLookupByLibrary.simpleMessage("комментария"),
"allergies_presence": MessageLookupByLibrary.simpleMessage( "another_form_of_day" : MessageLookupByLibrary.simpleMessage("дня"),
"Наличие аллергии", "answer" : MessageLookupByLibrary.simpleMessage("Ответить"),
), "archived" : MessageLookupByLibrary.simpleMessage("В архиве"),
"another_form_of_comment": MessageLookupByLibrary.simpleMessage( "authentificate" : MessageLookupByLibrary.simpleMessage("Войти"),
"комментария", "average_pressure" : MessageLookupByLibrary.simpleMessage("Средние показатели давления:"),
), "bodymass_chart" : MessageLookupByLibrary.simpleMessage("График ИМТ"),
"another_form_of_day": MessageLookupByLibrary.simpleMessage("дня"), "by_default" : MessageLookupByLibrary.simpleMessage("По умолчанию"),
"answer": MessageLookupByLibrary.simpleMessage("Ответить"), "calories" : MessageLookupByLibrary.simpleMessage("Калории"),
"archived": MessageLookupByLibrary.simpleMessage("В архиве"), "carb_type" : MessageLookupByLibrary.simpleMessage("Мучное"),
"authentificate": MessageLookupByLibrary.simpleMessage("Войти"), "carb_type_desc" : MessageLookupByLibrary.simpleMessage("Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы."),
"average_pressure": MessageLookupByLibrary.simpleMessage( "carbohydrates" : MessageLookupByLibrary.simpleMessage("Углеводы"),
"Средние показатели давления:", "chat" : MessageLookupByLibrary.simpleMessage("Чат"),
), "chest" : MessageLookupByLibrary.simpleMessage("Грудь"),
"bodymass_chart": MessageLookupByLibrary.simpleMessage("График ИМТ"), "chest_circumference" : MessageLookupByLibrary.simpleMessage("Обхват груди"),
"by_default": MessageLookupByLibrary.simpleMessage("По умолчанию"), "choose_diet_duration" : MessageLookupByLibrary.simpleMessage("Выберите срок диеты"),
"calories": MessageLookupByLibrary.simpleMessage("Калории"), "choose_plan_duration" : MessageLookupByLibrary.simpleMessage("Выберите срок плана"),
"carb_type": MessageLookupByLibrary.simpleMessage("Мучное"), "choose_start_diet_date" : MessageLookupByLibrary.simpleMessage("Выберите дату старты диеты. Она отобразиться у Вас в дневнике"),
"carb_type_desc": MessageLookupByLibrary.simpleMessage( "circumference_empty" : MessageLookupByLibrary.simpleMessage("Все замеры обязательны к заполнению"),
"Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.", "circumference_is_not_a_number" : MessageLookupByLibrary.simpleMessage("Все замеры должны быть числами"),
), "circumferences_chart" : MessageLookupByLibrary.simpleMessage("График обхватов"),
"carbohydrates": MessageLookupByLibrary.simpleMessage("Углеводы"), "city" : MessageLookupByLibrary.simpleMessage("Город"),
"chat": MessageLookupByLibrary.simpleMessage("Чат"), "city_of_living" : MessageLookupByLibrary.simpleMessage("Город проживания"),
"chest": MessageLookupByLibrary.simpleMessage("Грудь"), "comfort" : MessageLookupByLibrary.simpleMessage("Комфорт"),
"chest_circumference": MessageLookupByLibrary.simpleMessage("Обхват груди"), "comment" : MessageLookupByLibrary.simpleMessage("комментарий"),
"choose_diet_duration": MessageLookupByLibrary.simpleMessage( "comment_hint" : MessageLookupByLibrary.simpleMessage("Комментарий"),
"Выберите срок диеты", "comments" : MessageLookupByLibrary.simpleMessage("Комментарии"),
), "comments_plural" : MessageLookupByLibrary.simpleMessage("комментариев"),
"choose_plan_duration": MessageLookupByLibrary.simpleMessage( "complain" : MessageLookupByLibrary.simpleMessage("Пожаловаться"),
"Выберите срок плана", "continueButton" : MessageLookupByLibrary.simpleMessage("Продолжить"),
), "continue_" : MessageLookupByLibrary.simpleMessage("Продолжить"),
"choose_start_diet_date": MessageLookupByLibrary.simpleMessage( "copied" : MessageLookupByLibrary.simpleMessage("cкопирован"),
"Выберите дату старты диеты. Она отобразиться у Вас в дневнике", "copy_link" : MessageLookupByLibrary.simpleMessage("Скопировать ссылку"),
), "country" : MessageLookupByLibrary.simpleMessage("Страна"),
"circumference_empty": MessageLookupByLibrary.simpleMessage( "create_diet" : MessageLookupByLibrary.simpleMessage("Создание диеты"),
"Все замеры обязательны к заполнению", "customer_price" : MessageLookupByLibrary.simpleMessage("Цена для покупателя"),
), "date_of_birth" : MessageLookupByLibrary.simpleMessage("Дата рождения"),
"circumference_is_not_a_number": MessageLookupByLibrary.simpleMessage( "day" : MessageLookupByLibrary.simpleMessage("День"),
"Все замеры должны быть числами", "days" : MessageLookupByLibrary.simpleMessage("дней"),
), "deleted" : MessageLookupByLibrary.simpleMessage("Удалена"),
"circumferences_chart": MessageLookupByLibrary.simpleMessage( "diary" : MessageLookupByLibrary.simpleMessage("Дневник"),
"График обхватов", "diet" : MessageLookupByLibrary.simpleMessage("Диета"),
), "diet_author" : MessageLookupByLibrary.simpleMessage("Автор диеты"),
"city": MessageLookupByLibrary.simpleMessage("Город"), "diet_plan" : MessageLookupByLibrary.simpleMessage("План диеты"),
"city_of_living": MessageLookupByLibrary.simpleMessage("Город проживания"), "diet_state" : MessageLookupByLibrary.simpleMessage("Статус диеты"),
"comfort": MessageLookupByLibrary.simpleMessage("Комфорт"), "diet_type" : MessageLookupByLibrary.simpleMessage("Тип диеты"),
"comment": MessageLookupByLibrary.simpleMessage("комментарий"), "do_you_have_hypertension_tendency" : MessageLookupByLibrary.simpleMessage("Есть ли у вас гипертония?"),
"comments": MessageLookupByLibrary.simpleMessage("Комментарии"), "do_you_have_hypotension_tendency" : MessageLookupByLibrary.simpleMessage("Есть ли у вас гипотония?"),
"comments_plural": MessageLookupByLibrary.simpleMessage("комментариев"), "do_you_take_pills" : MessageLookupByLibrary.simpleMessage("Принимаете ли вы лекарства или бады?"),
"complain": MessageLookupByLibrary.simpleMessage("Пожаловаться"), "down_pressure" : MessageLookupByLibrary.simpleMessage("Нижние показатели давления:"),
"continueButton": MessageLookupByLibrary.simpleMessage("Продолжить"), "draft" : MessageLookupByLibrary.simpleMessage("Черновик"),
"continue_": MessageLookupByLibrary.simpleMessage("Продолжить"), "drafted" : MessageLookupByLibrary.simpleMessage("Черновики"),
"copied": MessageLookupByLibrary.simpleMessage("cкопирован"), "eating_behavior" : MessageLookupByLibrary.simpleMessage("Пищевое поведение"),
"copy_link": MessageLookupByLibrary.simpleMessage("Скопировать ссылку"), "eating_behavior_type" : MessageLookupByLibrary.simpleMessage("Формирование пищевого поведения"),
"country": MessageLookupByLibrary.simpleMessage("Страна"), "eating_behavior_type_desc" : MessageLookupByLibrary.simpleMessage("Формирование здорового пищевого поведения — это процесс, который включает в себя изменение привычек и отношения к еде. Он направлен на то, чтобы помочь людям научиться правильно питаться и поддерживать здоровый образ жизни.Основные принципы формирования здорового пищевого поведения: \n● Разнообразие продуктов; \n● Сбалансированный рацион; \n● Питьевой режим; \n● Ограничение вредных продуктов; \n● Регулярные приёмы пищи; \n● Умеренность;Осознанное питание; \n● Физическая активность;"),
"create_diet": MessageLookupByLibrary.simpleMessage("Создание диеты"), "economy" : MessageLookupByLibrary.simpleMessage("Эконом"),
"customer_price": MessageLookupByLibrary.simpleMessage( "edit" : MessageLookupByLibrary.simpleMessage("Редактировать"),
"Цена для покупателя", "email" : MessageLookupByLibrary.simpleMessage("Электронная почта"),
), "email_empty" : MessageLookupByLibrary.simpleMessage("Необходимо ввести адрес электронной почты"),
"date_of_birth": MessageLookupByLibrary.simpleMessage("Дата рождения"), "email_invalid" : MessageLookupByLibrary.simpleMessage("Некорректный адрес электронной почты"),
"day": MessageLookupByLibrary.simpleMessage("День"), "enter_additional_information" : MessageLookupByLibrary.simpleMessage("Введите любую дополнительную информацию на своё усмотрение:"),
"days": MessageLookupByLibrary.simpleMessage("дней"), "enter_diet_description" : MessageLookupByLibrary.simpleMessage("Введите описание диеты"),
"deleted": MessageLookupByLibrary.simpleMessage("Удалена"), "enter_diet_name" : MessageLookupByLibrary.simpleMessage("Выведите название диеты"),
"diary": MessageLookupByLibrary.simpleMessage("Дневник"), "enter_information" : MessageLookupByLibrary.simpleMessage("Введите информацию..."),
"diet": MessageLookupByLibrary.simpleMessage("Диета"), "exit" : MessageLookupByLibrary.simpleMessage("Выйти"),
"diet_author": MessageLookupByLibrary.simpleMessage("Автор диеты"), "expand" : MessageLookupByLibrary.simpleMessage("Показать полностью"),
"diet_plan": MessageLookupByLibrary.simpleMessage("План диеты"), "fats" : MessageLookupByLibrary.simpleMessage("Жиры"),
"diet_state": MessageLookupByLibrary.simpleMessage("Статус диеты"), "favorite_foods" : MessageLookupByLibrary.simpleMessage("Любимые продукты"),
"diet_type": MessageLookupByLibrary.simpleMessage("Тип диеты"), "favorite_unfavorite_foods" : MessageLookupByLibrary.simpleMessage("Ваши любимые/нелюбимые продукты питания?"),
"do_you_have_hypertension_tendency": MessageLookupByLibrary.simpleMessage( "female" : MessageLookupByLibrary.simpleMessage("Женский"),
"Есть ли у вас гипертония?", "fill_in_information" : MessageLookupByLibrary.simpleMessage("Заполните информацию:"),
), "finish" : MessageLookupByLibrary.simpleMessage("Завершить"),
"do_you_have_hypotension_tendency": MessageLookupByLibrary.simpleMessage( "first_name" : MessageLookupByLibrary.simpleMessage("Имя"),
"Есть ли у вас гипотония?", "first_name_empty" : MessageLookupByLibrary.simpleMessage("Имя обязательно для заполнения"),
), "first_name_short" : MessageLookupByLibrary.simpleMessage("В имени должен быть минимум 1 символ"),
"do_you_take_pills": MessageLookupByLibrary.simpleMessage( "fit_type" : MessageLookupByLibrary.simpleMessage("Поддержание формы"),
"Принимаете ли вы лекарства или бады?", "fit_type_desc" : MessageLookupByLibrary.simpleMessage("План питания для поддержания формы сбалансированный и разнообразный, чтобы обеспечить организм всеми необходимыми питательными веществами. Общие принципы: \n● Дробное питание; \n● Разнообразие продуктов; \n● Контроль порций; \n● Питьевой режим; \n● Ограничение вредных продуктов;"),
), "fitness" : MessageLookupByLibrary.simpleMessage("Поддержка формы"),
"down_pressure": MessageLookupByLibrary.simpleMessage( "from" : MessageLookupByLibrary.simpleMessage("от"),
"Нижние показатели давления:", "from_a_to_z" : MessageLookupByLibrary.simpleMessage("Название: А-Я"),
), "from_z_to_a" : MessageLookupByLibrary.simpleMessage("Название: Я-А"),
"draft": MessageLookupByLibrary.simpleMessage("Черновик"), "gallery" : MessageLookupByLibrary.simpleMessage("Галерея"),
"drafted": MessageLookupByLibrary.simpleMessage("Черновики"), "gr" : MessageLookupByLibrary.simpleMessage("гр"),
"eating_behavior": MessageLookupByLibrary.simpleMessage( "grams" : MessageLookupByLibrary.simpleMessage("гр"),
"Пищевое поведение", "height_measurements" : MessageLookupByLibrary.simpleMessage("Замер роста"),
), "hide" : MessageLookupByLibrary.simpleMessage("Свернуть"),
"eating_behavior_type": MessageLookupByLibrary.simpleMessage( "hip_circumference" : MessageLookupByLibrary.simpleMessage("Обхват бедер"),
"Формирование пищевого поведения", "hips" : MessageLookupByLibrary.simpleMessage("Бедра"),
), "in_progress" : MessageLookupByLibrary.simpleMessage("В процессе"),
"eating_behavior_type_desc": MessageLookupByLibrary.simpleMessage( "inactive_diet_state" : MessageLookupByLibrary.simpleMessage("Черновик"),
"Формирование здорового пищевого поведения — это процесс, который включает в себя изменение привычек и отношения к еде. Он направлен на то, чтобы помочь людям научиться правильно питаться и поддерживать здоровый образ жизни.Основные принципы формирования здорового пищевого поведения: \n● Разнообразие продуктов; \n● Сбалансированный рацион; \n● Питьевой режим; \n● Ограничение вредных продуктов; \n● Регулярные приёмы пищи; \n● Умеренность;Осознанное питание; \n● Физическая активность;", "infoAboutOrder" : MessageLookupByLibrary.simpleMessage("Информация о заказе"),
), "initial_measurements" : MessageLookupByLibrary.simpleMessage("Первичные замеры"),
"economy": MessageLookupByLibrary.simpleMessage("Эконом"), "label" : MessageLookupByLibrary.simpleMessage("Название"),
"edit": MessageLookupByLibrary.simpleMessage("Редактировать"), "language" : MessageLookupByLibrary.simpleMessage("Язык"),
"email": MessageLookupByLibrary.simpleMessage("Электронная почта"), "last_name" : MessageLookupByLibrary.simpleMessage("Фамилия"),
"email_empty": MessageLookupByLibrary.simpleMessage( "last_name_empty" : MessageLookupByLibrary.simpleMessage("Фамилия обязательна для заполнения"),
"Необходимо ввести адрес электронной почты", "last_name_short" : MessageLookupByLibrary.simpleMessage("В фамилии должен быть минимум 6 символов"),
), "lbl_english" : MessageLookupByLibrary.simpleMessage("English"),
"email_invalid": MessageLookupByLibrary.simpleMessage( "light_type" : MessageLookupByLibrary.simpleMessage("Легкая"),
"Некорректный адрес электронной почты", "light_type_desc" : MessageLookupByLibrary.simpleMessage("С некоторыми видами диет можно употреблять алкоголь в умеренных количествах. Рекомендуется совмещать вместе с низкокалорийными закусками."),
), "link_was_copied" : MessageLookupByLibrary.simpleMessage("Ссылка успешно скопирована"),
"enter_additional_information": MessageLookupByLibrary.simpleMessage( "loading" : MessageLookupByLibrary.simpleMessage("Загрузка..."),
"Введите любую дополнительную информацию на своё усмотрение:", "mail_was_copied" : MessageLookupByLibrary.simpleMessage("Почта успешно скопирована"),
), "male" : MessageLookupByLibrary.simpleMessage("Мужской"),
"enter_diet_description": MessageLookupByLibrary.simpleMessage( "meal" : MessageLookupByLibrary.simpleMessage("Приём пищи"),
"Введите описание диеты", "meat" : MessageLookupByLibrary.simpleMessage("Мясо"),
), "meat_type" : MessageLookupByLibrary.simpleMessage("С мясом"),
"enter_diet_name": MessageLookupByLibrary.simpleMessage( "meat_type_desc" : MessageLookupByLibrary.simpleMessage("Диета с мясом полезная и сбалансированная. Включает в себя нежирное мясо: курицу, индейку, кролика или говядину. В сочетании с овощами получаете все необходимые витамины и минералы. Не забывайте о порциях и старайтесь есть медленно, прислушиваясь к своему организму."),
"Выведите название диеты", "milliliter" : MessageLookupByLibrary.simpleMessage("мл"),
), "minimal_customer_price" : MessageLookupByLibrary.simpleMessage("Минимальная цена для покупателя: 1200₽"),
"enter_information": MessageLookupByLibrary.simpleMessage( "most_new" : MessageLookupByLibrary.simpleMessage("Самые новые"),
"Введите информацию...", "most_old" : MessageLookupByLibrary.simpleMessage("Самые старые"),
), "msg_network_err" : MessageLookupByLibrary.simpleMessage("Сетевая ошибка!"),
"exit": MessageLookupByLibrary.simpleMessage("Выйти"), "msg_something_went_wrong" : MessageLookupByLibrary.simpleMessage("Что то пошло не так!"),
"expand": MessageLookupByLibrary.simpleMessage("Показать полностью"), "my_blog" : MessageLookupByLibrary.simpleMessage("Мой блог"),
"fats": MessageLookupByLibrary.simpleMessage("Жиры"), "my_diets" : MessageLookupByLibrary.simpleMessage("Мои диеты"),
"favorite_foods": MessageLookupByLibrary.simpleMessage("Любимые продукты"), "my_plans" : MessageLookupByLibrary.simpleMessage("Мои планы"),
"favorite_unfavorite_foods": MessageLookupByLibrary.simpleMessage( "my_settings" : MessageLookupByLibrary.simpleMessage("Мои настройки"),
"Ваши любимые/нелюбимые продукты питания?", "name_empty" : MessageLookupByLibrary.simpleMessage("Имя пользователя обязательно для заполнения"),
), "name_short" : MessageLookupByLibrary.simpleMessage("В имени пользователя должно быть минимум 6 символов"),
"female": MessageLookupByLibrary.simpleMessage("Женский"), "no" : MessageLookupByLibrary.simpleMessage("Нет"),
"fill_in_information": MessageLookupByLibrary.simpleMessage( "no_account" : MessageLookupByLibrary.simpleMessage("У меня нет аккаунта"),
"Заполните информацию:", "notifications" : MessageLookupByLibrary.simpleMessage("Уведомления"),
), "one_option" : MessageLookupByLibrary.simpleMessage("1 опция"),
"finish": MessageLookupByLibrary.simpleMessage("Завершить"), "option" : MessageLookupByLibrary.simpleMessage("Опция"),
"first_name": MessageLookupByLibrary.simpleMessage("Имя"), "option_content" : MessageLookupByLibrary.simpleMessage("Содержание опции"),
"first_name_empty": MessageLookupByLibrary.simpleMessage( "option_content_hint_text" : MessageLookupByLibrary.simpleMessage("На связи 7 дней в неделю"),
"Имя обязательно для заполнения", "option_desc" : MessageLookupByLibrary.simpleMessage("Описание опции"),
), "option_desc_hint_text" : MessageLookupByLibrary.simpleMessage("Этот план питания для похудения на 7 дней сбалансированный и разнообразный"),
"first_name_short": MessageLookupByLibrary.simpleMessage( "option_duration" : MessageLookupByLibrary.simpleMessage("Продолжительность опции"),
"В имени должен быть минимум 1 символ", "option_personal_diet_desc" : MessageLookupByLibrary.simpleMessage("Опция подразумевает создание индивидуальной диеты"),
), "option_title" : MessageLookupByLibrary.simpleMessage("Заголовок опции"),
"fit_type": MessageLookupByLibrary.simpleMessage("Поддержание формы"), "order" : MessageLookupByLibrary.simpleMessage("Порядок"),
"fit_type_desc": MessageLookupByLibrary.simpleMessage( "password" : MessageLookupByLibrary.simpleMessage("Пароль"),
"План питания для поддержания формы сбалансированный и разнообразный, чтобы обеспечить организм всеми необходимыми питательными веществами. Общие принципы: \n● Дробное питание; \n● Разнообразие продуктов; \n● Контроль порций; \n● Питьевой режим; \n● Ограничение вредных продуктов;", "password_confirmation" : MessageLookupByLibrary.simpleMessage("Подтвердите пароль"),
), "password_confirmation_mismatch" : MessageLookupByLibrary.simpleMessage("Пароли не совпадают"),
"fitness": MessageLookupByLibrary.simpleMessage("Поддержка формы"), "password_empty" : MessageLookupByLibrary.simpleMessage("Пароль обязателен для заполнения"),
"from": MessageLookupByLibrary.simpleMessage("от"), "password_short" : MessageLookupByLibrary.simpleMessage("В пароле должно быть минимум 6 символов"),
"from_a_to_z": MessageLookupByLibrary.simpleMessage("Название: А-Я"), "pcs" : MessageLookupByLibrary.simpleMessage("шт"),
"from_z_to_a": MessageLookupByLibrary.simpleMessage("Название: Я-А"), "personal" : MessageLookupByLibrary.simpleMessage("Персональные"),
"gallery": MessageLookupByLibrary.simpleMessage("Галерея"), "personal_diet_state" : MessageLookupByLibrary.simpleMessage("Персональная"),
"gr": MessageLookupByLibrary.simpleMessage("гр"), "photo" : MessageLookupByLibrary.simpleMessage("Фото"),
"grams": MessageLookupByLibrary.simpleMessage("гр"), "place_of_living" : MessageLookupByLibrary.simpleMessage("Место проживания"),
"height_measurements": MessageLookupByLibrary.simpleMessage("Замер роста"), "plan_author" : MessageLookupByLibrary.simpleMessage("Автор плана"),
"hide": MessageLookupByLibrary.simpleMessage("Свернуть"), "plan_desc" : MessageLookupByLibrary.simpleMessage("Описание плана"),
"hip_circumference": MessageLookupByLibrary.simpleMessage("Обхват бедер"), "plan_desc_example" : MessageLookupByLibrary.simpleMessage("Большое подробное, привлекательное описание плана"),
"hips": MessageLookupByLibrary.simpleMessage("Бедра"), "plan_duration" : MessageLookupByLibrary.simpleMessage("Срок плана"),
"in_progress": MessageLookupByLibrary.simpleMessage("В процессе"), "plan_label_desc" : MessageLookupByLibrary.simpleMessage("План для поддержания формы"),
"inactive_diet_state": MessageLookupByLibrary.simpleMessage("Черновик"), "plans_complete" : MessageLookupByLibrary.simpleMessage("Пройденные планы"),
"infoAboutOrder": MessageLookupByLibrary.simpleMessage( "popular" : MessageLookupByLibrary.simpleMessage("Популярные"),
"Информация о заказе", "preferred_language" : MessageLookupByLibrary.simpleMessage("Предпочитаемый язык общения"),
), "pressure_empty" : MessageLookupByLibrary.simpleMessage("Все показатели давления обязательны к заполнению"),
"initial_measurements": MessageLookupByLibrary.simpleMessage( "pressure_is_not_a_number" : MessageLookupByLibrary.simpleMessage("Все показатели давления должны быть числами"),
"Первичные замеры", "preview" : MessageLookupByLibrary.simpleMessage("Превью"),
), "price" : MessageLookupByLibrary.simpleMessage("Цена"),
"label": MessageLookupByLibrary.simpleMessage("Название"), "price_explanation" : MessageLookupByLibrary.simpleMessage("Разница в ценах, это комиссия Итвио"),
"language": MessageLookupByLibrary.simpleMessage("Язык"), "profile" : MessageLookupByLibrary.simpleMessage("Профиль"),
"last_name": MessageLookupByLibrary.simpleMessage("Фамилия"), "proteins" : MessageLookupByLibrary.simpleMessage("Белки"),
"last_name_empty": MessageLookupByLibrary.simpleMessage( "public_diet_state" : MessageLookupByLibrary.simpleMessage("Публичная"),
"Фамилия обязательна для заполнения", "publish" : MessageLookupByLibrary.simpleMessage("Опубликовать"),
), "published" : MessageLookupByLibrary.simpleMessage("Опубликованные"),
"last_name_short": MessageLookupByLibrary.simpleMessage( "removeFromActive" : MessageLookupByLibrary.simpleMessage("Снять с публикации"),
"В фамилии должен быть минимум 6 символов", "save" : MessageLookupByLibrary.simpleMessage("Сохранить"),
), "services" : MessageLookupByLibrary.simpleMessage("Оказание услуг"),
"lbl_english": MessageLookupByLibrary.simpleMessage("English"), "settings" : MessageLookupByLibrary.simpleMessage("Настройки"),
"light_type": MessageLookupByLibrary.simpleMessage("Легкая"), "share" : MessageLookupByLibrary.simpleMessage("Поделиться"),
"light_type_desc": MessageLookupByLibrary.simpleMessage( "skip" : MessageLookupByLibrary.simpleMessage("Пропустить"),
"С некоторыми видами диет можно употреблять алкоголь в умеренных количествах. Рекомендуется совмещать вместе с низкокалорийными закусками.", "slim" : MessageLookupByLibrary.simpleMessage("Похудение"),
), "slim_type" : MessageLookupByLibrary.simpleMessage("Похудение"),
"link_was_copied": MessageLookupByLibrary.simpleMessage( "slim_type_desc" : MessageLookupByLibrary.simpleMessage("План питания для похудения должен быть разработан с учётом индивидуальных особенностей организма, возраста, пола, уровня физической активности и состояния здоровья. Общие принципы: \n● Снижение калорийности; \n● Сбалансированный рацион; \n● Разнообразие продуктов; \n● Питьевой режим; \n● Ограничение вредных продуктов;"),
"Ссылка успешно скопирована", "standard" : MessageLookupByLibrary.simpleMessage("Стандарт"),
), "startDiet" : MessageLookupByLibrary.simpleMessage("Начать диету"),
"loading": MessageLookupByLibrary.simpleMessage("Загрузка..."), "tell_your_schedule" : MessageLookupByLibrary.simpleMessage("Расскажите в свободной форме о том как просыпаетесь, когда едите, когда ложитесь спать:"),
"mail_was_copied": MessageLookupByLibrary.simpleMessage( "text_was_copied" : MessageLookupByLibrary.simpleMessage("Текст успешно скопирован"),
"Почта успешно скопирована", "three_options" : MessageLookupByLibrary.simpleMessage("3 опции"),
), "toOffer" : MessageLookupByLibrary.simpleMessage("Предложить"),
"male": MessageLookupByLibrary.simpleMessage("Мужской"), "to_draft" : MessageLookupByLibrary.simpleMessage("В черновик"),
"meal": MessageLookupByLibrary.simpleMessage("Приём пищи"), "unfavorite_foods" : MessageLookupByLibrary.simpleMessage("Нелюбимые продукты"),
"meat": MessageLookupByLibrary.simpleMessage("Мясо"), "up_pressure" : MessageLookupByLibrary.simpleMessage("Верхние показатели давления:"),
"meat_type": MessageLookupByLibrary.simpleMessage("С мясом"), "user_authorization" : MessageLookupByLibrary.simpleMessage("Аутентификация"),
"meat_type_desc": MessageLookupByLibrary.simpleMessage( "user_data" : MessageLookupByLibrary.simpleMessage("Данные пользователя"),
"Диета с мясом полезная и сбалансированная. Включает в себя нежирное мясо: курицу, индейку, кролика или говядину. В сочетании с овощами получаете все необходимые витамины и минералы. Не забывайте о порциях и старайтесь есть медленно, прислушиваясь к своему организму.", "user_registration" : MessageLookupByLibrary.simpleMessage("Регистрация пользователя"),
), "username" : MessageLookupByLibrary.simpleMessage("Имя пользователя"),
"milliliter": MessageLookupByLibrary.simpleMessage("мл"), "vegan" : MessageLookupByLibrary.simpleMessage("Веган."),
"minimal_customer_price": MessageLookupByLibrary.simpleMessage( "vegan_type" : MessageLookupByLibrary.simpleMessage("Вегетарианская"),
"Минимальная цена для покупателя: 1200₽", "vegan_type_desc" : MessageLookupByLibrary.simpleMessage("Диета основана на потреблении растительной пищи и отказе от продуктов животного происхождения. Такой тип питания может быть разнообразным и вкусным, а также полезным для здоровья."),
), "waist" : MessageLookupByLibrary.simpleMessage("Талия"),
"most_new": MessageLookupByLibrary.simpleMessage("Самые новые"), "waist_circumference" : MessageLookupByLibrary.simpleMessage("Обхват талии"),
"most_old": MessageLookupByLibrary.simpleMessage("Самые старые"), "weight" : MessageLookupByLibrary.simpleMessage("Вес"),
"msg_network_err": MessageLookupByLibrary.simpleMessage("Сетевая ошибка!"), "weight_chart" : MessageLookupByLibrary.simpleMessage("График веса"),
"msg_something_went_wrong": MessageLookupByLibrary.simpleMessage( "weight_measurements" : MessageLookupByLibrary.simpleMessage("Замер веса"),
"Что то пошло не так!", "wellness" : MessageLookupByLibrary.simpleMessage("Улучшение самочувствия"),
), "wellness_type" : MessageLookupByLibrary.simpleMessage("Улучшение самочувствия"),
"my_blog": MessageLookupByLibrary.simpleMessage("Мой блог"), "wellness_type_desc" : MessageLookupByLibrary.simpleMessage("Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы."),
"my_diets": MessageLookupByLibrary.simpleMessage("Мои диеты"), "what_is_your_name" : MessageLookupByLibrary.simpleMessage("Как вас зовут?"),
"my_plans": MessageLookupByLibrary.simpleMessage("Мои планы"), "wrong_credentials" : MessageLookupByLibrary.simpleMessage("Неверные данные пользователя"),
"my_settings": MessageLookupByLibrary.simpleMessage("Мои настройки"), "you_will_get" : MessageLookupByLibrary.simpleMessage("Вы получите"),
"name_empty": MessageLookupByLibrary.simpleMessage( "your_blood_pressure" : MessageLookupByLibrary.simpleMessage("Ваше артериальное давление?"),
"Имя пользователя обязательно для заполнения", "your_favorite_sports" : MessageLookupByLibrary.simpleMessage("Ваши спортивные увлечения?"),
), "your_gender" : MessageLookupByLibrary.simpleMessage("Ваш пол?"),
"name_short": MessageLookupByLibrary.simpleMessage( "your_height" : MessageLookupByLibrary.simpleMessage("Ваш рост:"),
"В имени пользователя должно быть минимум 6 символов", "your_schedule" : MessageLookupByLibrary.simpleMessage("Ваш режим дня?"),
), "your_weight" : MessageLookupByLibrary.simpleMessage("Ваш вес:")
"no": MessageLookupByLibrary.simpleMessage("Нет"),
"no_account": MessageLookupByLibrary.simpleMessage("У меня нет аккаунта"),
"notifications": MessageLookupByLibrary.simpleMessage("Уведомления"),
"one_option": MessageLookupByLibrary.simpleMessage("1 опция"),
"option": MessageLookupByLibrary.simpleMessage("Опция"),
"option_content": MessageLookupByLibrary.simpleMessage("Содержание опции"),
"option_content_hint_text": MessageLookupByLibrary.simpleMessage(
"На связи 7 дней в неделю",
),
"option_desc": MessageLookupByLibrary.simpleMessage("Описание опции"),
"option_desc_hint_text": MessageLookupByLibrary.simpleMessage(
"Этот план питания для похудения на 7 дней сбалансированный и разнообразный",
),
"option_duration": MessageLookupByLibrary.simpleMessage(
"Продолжительность опции",
),
"option_personal_diet_desc": MessageLookupByLibrary.simpleMessage(
"Опция подразумевает создание индивидуальной диеты",
),
"option_title": MessageLookupByLibrary.simpleMessage("Заголовок опции"),
"order": MessageLookupByLibrary.simpleMessage("Порядок"),
"password": MessageLookupByLibrary.simpleMessage("Пароль"),
"password_confirmation": MessageLookupByLibrary.simpleMessage(
"Подтвердите пароль",
),
"password_confirmation_mismatch": MessageLookupByLibrary.simpleMessage(
"Пароли не совпадают",
),
"password_empty": MessageLookupByLibrary.simpleMessage(
"Пароль обязателен для заполнения",
),
"password_short": MessageLookupByLibrary.simpleMessage(
"В пароле должно быть минимум 6 символов",
),
"pcs": MessageLookupByLibrary.simpleMessage("шт"),
"personal": MessageLookupByLibrary.simpleMessage("Персональные"),
"personal_diet_state": MessageLookupByLibrary.simpleMessage("Персональная"),
"photo": MessageLookupByLibrary.simpleMessage("Фото"),
"place_of_living": MessageLookupByLibrary.simpleMessage("Место проживания"),
"plan_author": MessageLookupByLibrary.simpleMessage("Автор плана"),
"plan_desc": MessageLookupByLibrary.simpleMessage("Описание плана"),
"plan_desc_example": MessageLookupByLibrary.simpleMessage(
"Большое подробное, привлекательное описание плана",
),
"plan_duration": MessageLookupByLibrary.simpleMessage("Срок плана"),
"plan_label_desc": MessageLookupByLibrary.simpleMessage(
"План для поддержания формы",
),
"plans_complete": MessageLookupByLibrary.simpleMessage("Пройденные планы"),
"popular": MessageLookupByLibrary.simpleMessage("Популярные"),
"preferred_language": MessageLookupByLibrary.simpleMessage(
"Предпочитаемый язык общения",
),
"pressure_empty": MessageLookupByLibrary.simpleMessage(
"Все показатели давления обязательны к заполнению",
),
"pressure_is_not_a_number": MessageLookupByLibrary.simpleMessage(
"Все показатели давления должны быть числами",
),
"preview": MessageLookupByLibrary.simpleMessage("Превью"),
"price": MessageLookupByLibrary.simpleMessage("Цена"),
"price_explanation": MessageLookupByLibrary.simpleMessage(
"Разница в ценах, это комиссия Итвио",
),
"profile": MessageLookupByLibrary.simpleMessage("Профиль"),
"proteins": MessageLookupByLibrary.simpleMessage("Белки"),
"public_diet_state": MessageLookupByLibrary.simpleMessage("Публичная"),
"publish": MessageLookupByLibrary.simpleMessage("Опубликовать"),
"published": MessageLookupByLibrary.simpleMessage("Опубликованные"),
"removeFromActive": MessageLookupByLibrary.simpleMessage(
"Снять с публикации",
),
"save": MessageLookupByLibrary.simpleMessage("Сохранить"),
"services": MessageLookupByLibrary.simpleMessage("Оказание услуг"),
"settings": MessageLookupByLibrary.simpleMessage("Настройки"),
"share": MessageLookupByLibrary.simpleMessage("Поделиться"),
"skip": MessageLookupByLibrary.simpleMessage("Пропустить"),
"slim": MessageLookupByLibrary.simpleMessage("Похудение"),
"slim_type": MessageLookupByLibrary.simpleMessage("Похудение"),
"slim_type_desc": MessageLookupByLibrary.simpleMessage(
"План питания для похудения должен быть разработан с учётом индивидуальных особенностей организма, возраста, пола, уровня физической активности и состояния здоровья. Общие принципы: \n● Снижение калорийности; \n● Сбалансированный рацион; \n● Разнообразие продуктов; \n● Питьевой режим; \n● Ограничение вредных продуктов;",
),
"standard": MessageLookupByLibrary.simpleMessage("Стандарт"),
"startDiet": MessageLookupByLibrary.simpleMessage("Начать диету"),
"tell_your_schedule": MessageLookupByLibrary.simpleMessage(
"Расскажите в свободной форме о том как просыпаетесь, когда едите, когда ложитесь спать:",
),
"text_was_copied": MessageLookupByLibrary.simpleMessage(
"Текст успешно скопирован",
),
"three_options": MessageLookupByLibrary.simpleMessage("3 опции"),
"toOffer": MessageLookupByLibrary.simpleMessage("Предложить"),
"to_draft": MessageLookupByLibrary.simpleMessage("В черновик"),
"unfavorite_foods": MessageLookupByLibrary.simpleMessage(
"Нелюбимые продукты",
),
"up_pressure": MessageLookupByLibrary.simpleMessage(
"Верхние показатели давления:",
),
"user_authorization": MessageLookupByLibrary.simpleMessage(
"Аутентификация",
),
"user_data": MessageLookupByLibrary.simpleMessage("Данные пользователя"),
"user_registration": MessageLookupByLibrary.simpleMessage(
"Регистрация пользователя",
),
"username": MessageLookupByLibrary.simpleMessage("Имя пользователя"),
"vegan": MessageLookupByLibrary.simpleMessage("Веган."),
"vegan_type": MessageLookupByLibrary.simpleMessage("Вегетарианская"),
"vegan_type_desc": MessageLookupByLibrary.simpleMessage(
"Диета основана на потреблении растительной пищи и отказе от продуктов животного происхождения. Такой тип питания может быть разнообразным и вкусным, а также полезным для здоровья.",
),
"waist": MessageLookupByLibrary.simpleMessage("Талия"),
"waist_circumference": MessageLookupByLibrary.simpleMessage("Обхват талии"),
"weight": MessageLookupByLibrary.simpleMessage("Вес"),
"weight_chart": MessageLookupByLibrary.simpleMessage("График веса"),
"weight_measurements": MessageLookupByLibrary.simpleMessage("Замер веса"),
"wellness": MessageLookupByLibrary.simpleMessage("Улучшение самочувствия"),
"wellness_type": MessageLookupByLibrary.simpleMessage(
"Улучшение самочувствия",
),
"wellness_type_desc": MessageLookupByLibrary.simpleMessage(
"Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.",
),
"what_is_your_name": MessageLookupByLibrary.simpleMessage("Как вас зовут?"),
"wrong_credentials": MessageLookupByLibrary.simpleMessage(
"Неверные данные пользователя",
),
"you_will_get": MessageLookupByLibrary.simpleMessage("Вы получите"),
"your_blood_pressure": MessageLookupByLibrary.simpleMessage(
"Ваше артериальное давление?",
),
"your_favorite_sports": MessageLookupByLibrary.simpleMessage(
"Ваши спортивные увлечения?",
),
"your_gender": MessageLookupByLibrary.simpleMessage("Ваш пол?"),
"your_height": MessageLookupByLibrary.simpleMessage("Ваш рост:"),
"your_schedule": MessageLookupByLibrary.simpleMessage("Ваш режим дня?"),
"your_weight": MessageLookupByLibrary.simpleMessage("Ваш вес:"),
}; };
} }

File diff suppressed because it is too large Load Diff

View File

@@ -466,6 +466,7 @@
"weAreLookingFor": "Мы собираем самое большое сообщество людей, желающих вести здоровый образ жизни!", "weAreLookingFor": "Мы собираем самое большое сообщество людей, желающих вести здоровый образ жизни!",
"letsInvite": "Давайте пригласим близких в итвио", "letsInvite": "Давайте пригласим близких в итвио",
"eatvioIsAHelthyHabbit": "Итвио — это полезная привычка", "eatvioIsAHelthyHabbit": "Итвио — это полезная привычка",
"invite": "Пригласить" "invite": "Пригласить",
"comment_hint": "Комментарий"
} }

View File

@@ -215,5 +215,6 @@
"from": "от", "from": "от",
"user_data": "Данные пользователя", "user_data": "Данные пользователя",
"infoAboutOrder": "Информация о заказе", "infoAboutOrder": "Информация о заказе",
"notifications": "Уведомления" "notifications": "Уведомления",
"comment_hint": "Комментарий"
} }

View File

@@ -1,6 +1,6 @@
name: eatvio_app name: eatvio_app
description: A new Flutter project. description: A new Flutter project.
version: 1.3.11+92 version: 1.3.12+93
publish_to: 'none' publish_to: 'none'
environment: environment:

View File

@@ -12,10 +12,7 @@ void main() {
test('createRoutes содержит маршрут authScreen', () { test('createRoutes содержит маршрут authScreen', () {
final routes = AuthRoutes.createRoutes(); final routes = AuthRoutes.createRoutes();
final authRoute = routes.firstWhere( final authRoute = routes.firstWhere((route) => route is GoRoute && route.path == '/auth-screen', orElse: () => throw Exception('Route not found'));
(route) => route is GoRoute && route.path == '/auth-screen',
orElse: () => throw Exception('Route not found'),
);
expect(authRoute, isA<GoRoute>()); expect(authRoute, isA<GoRoute>());
}); });
@@ -67,10 +64,7 @@ void main() {
test('createRoutes содержит маршрут verifyScreen', () { test('createRoutes содержит маршрут verifyScreen', () {
final routes = AuthRoutes.createRoutes(); final routes = AuthRoutes.createRoutes();
final verifyRoute = routes.firstWhere( final verifyRoute = routes.firstWhere((route) => route is GoRoute && route.path == '/verify-screen', orElse: () => throw Exception('Route not found'));
(route) => route is GoRoute && route.path == '/verify-screen',
orElse: () => throw Exception('Route not found'),
);
expect(verifyRoute, isA<GoRoute>()); expect(verifyRoute, isA<GoRoute>());
}); });
@@ -92,44 +86,25 @@ void main() {
expect(registerViaSocialRoute, isA<GoRoute>()); expect(registerViaSocialRoute, isA<GoRoute>());
}); });
test('authScreen имеет redirect функцию', () {
final routes = AuthRoutes.createRoutes();
final authRoute = routes.first as GoRoute;
expect(authRoute.redirect, isNotNull);
});
test('forgetPasswordScreen builder обрабатывает extra данные', () { test('forgetPasswordScreen builder обрабатывает extra данные', () {
final routes = AuthRoutes.createRoutes(); final routes = AuthRoutes.createRoutes();
final authRoute = routes.first as GoRoute; final authRoute = routes.first as GoRoute;
final forgetPasswordRoute = authRoute.routes.firstWhere( final forgetPasswordRoute =
(route) => route is GoRoute && route.path == 'forget-password-screen', authRoute.routes.firstWhere((route) => route is GoRoute && route.path == 'forget-password-screen', orElse: () => throw Exception('Route not found'))
orElse: () => throw Exception('Route not found'), as GoRoute;
) as GoRoute;
expect(forgetPasswordRoute.builder, isNotNull); expect(forgetPasswordRoute.builder, isNotNull);
}); });
test('verifyScreen builder обрабатывает extra данные', () { test('verifyScreen builder обрабатывает extra данные', () {
final routes = AuthRoutes.createRoutes(); final routes = AuthRoutes.createRoutes();
final verifyRoute = routes.firstWhere( final verifyRoute = routes.firstWhere((route) => route is GoRoute && route.path == '/verify-screen') as GoRoute;
(route) => route is GoRoute && route.path == '/verify-screen',
) as GoRoute;
expect(verifyRoute.builder, isNotNull); expect(verifyRoute.builder, isNotNull);
}); });
test('registerViaSocialScreen builder обрабатывает extra данные', () { test('registerViaSocialScreen builder обрабатывает extra данные', () {
final routes = AuthRoutes.createRoutes(); final routes = AuthRoutes.createRoutes();
final registerViaSocialRoute = routes.firstWhere( final registerViaSocialRoute = routes.firstWhere((route) => route is GoRoute && route.path == '/register-via-social-screen') as GoRoute;
(route) => route is GoRoute && route.path == '/register-via-social-screen',
) as GoRoute;
expect(registerViaSocialRoute.builder, isNotNull); expect(registerViaSocialRoute.builder, isNotNull);
}); });
}); });
} }

View File

@@ -6,6 +6,10 @@ void main() {
group('TokenStorage', () { group('TokenStorage', () {
setUp(() async { setUp(() async {
SharedPreferences.setMockInitialValues({}); SharedPreferences.setMockInitialValues({});
// Clear cached instance to ensure fresh state for each test
TokenStorage.clearCache();
final prefs = await SharedPreferences.getInstance();
await prefs.clear();
}); });
test('haveTokenInStorage returns false when no token is stored', () async { test('haveTokenInStorage returns false when no token is stored', () async {
@@ -66,6 +70,10 @@ void main() {
group('SocketIdStorage', () { group('SocketIdStorage', () {
setUp(() async { setUp(() async {
SharedPreferences.setMockInitialValues({}); SharedPreferences.setMockInitialValues({});
// Clear cached instance to ensure fresh state for each test
TokenStorage.clearCache();
final prefs = await SharedPreferences.getInstance();
await prefs.clear();
}); });
test('haveSocketIdInStorage returns false when no socketId is stored', () async { test('haveSocketIdInStorage returns false when no socketId is stored', () async {

View File

@@ -29,6 +29,7 @@ void main() {
const MarketingBottomSheet(), const MarketingBottomSheet(),
), ),
); );
await tester.pumpAndSettle();
expect(find.byType(MarketingBottomSheet), findsOneWidget); expect(find.byType(MarketingBottomSheet), findsOneWidget);
}); });
@@ -59,6 +60,7 @@ void main() {
), ),
); );
await tester.pump();
await tester.tap(find.text('Show')); await tester.tap(find.text('Show'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();

View File

@@ -24,6 +24,7 @@ void main() {
testWidgets('returns SizedBox.shrink when option is null', (tester) async { testWidgets('returns SizedBox.shrink when option is null', (tester) async {
await tester.pumpWidget(createTestWidget(const OptionWidget(option: null, duration: 7))); await tester.pumpWidget(createTestWidget(const OptionWidget(option: null, duration: 7)));
await tester.pumpAndSettle();
expect(find.byType(OptionWidget), findsOneWidget); expect(find.byType(OptionWidget), findsOneWidget);
expect(find.byType(SizedBox), findsWidgets); expect(find.byType(SizedBox), findsWidgets);
@@ -33,6 +34,7 @@ void main() {
const option = PacketOptions(items: ['Item 1', 'Item 2'], price: 1000.0, short: 'Short', description: 'Description', hasPersonalDiet: false); const option = PacketOptions(items: ['Item 1', 'Item 2'], price: 1000.0, short: 'Short', description: 'Description', hasPersonalDiet: false);
await tester.pumpWidget(createTestWidget(const OptionWidget(option: option, duration: 7))); await tester.pumpWidget(createTestWidget(const OptionWidget(option: option, duration: 7)));
await tester.pumpAndSettle();
expect(find.byType(OptionWidget), findsOneWidget); expect(find.byType(OptionWidget), findsOneWidget);
expect(find.text('Short'), findsOneWidget); expect(find.text('Short'), findsOneWidget);
@@ -43,6 +45,7 @@ void main() {
const option = PacketOptions(items: ['Item 1'], price: 1000.0, short: 'Short', description: 'Description', hasPersonalDiet: true); const option = PacketOptions(items: ['Item 1'], price: 1000.0, short: 'Short', description: 'Description', hasPersonalDiet: true);
await tester.pumpWidget(createTestWidget(const OptionWidget(option: option, duration: 7))); await tester.pumpWidget(createTestWidget(const OptionWidget(option: option, duration: 7)));
await tester.pumpAndSettle();
expect(find.byType(OptionWidget), findsOneWidget); expect(find.byType(OptionWidget), findsOneWidget);
}); });
@@ -51,6 +54,7 @@ void main() {
const option = PacketOptions(items: ['Item 1'], price: 1000.0, short: 'Short', description: 'Description', hasPersonalDiet: false); const option = PacketOptions(items: ['Item 1'], price: 1000.0, short: 'Short', description: 'Description', hasPersonalDiet: false);
await tester.pumpWidget(createTestWidget(const OptionWidget(option: option, duration: 7, disableDuration: true))); await tester.pumpWidget(createTestWidget(const OptionWidget(option: option, duration: 7, disableDuration: true)));
await tester.pumpAndSettle();
expect(find.byType(OptionWidget), findsOneWidget); expect(find.byType(OptionWidget), findsOneWidget);
}); });

View File

@@ -31,6 +31,7 @@ void main() {
), ),
); );
await tester.pump();
await tester.tap(find.text('Show Error')); await tester.tap(find.text('Show Error'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
@@ -68,6 +69,7 @@ void main() {
), ),
); );
await tester.pump();
await tester.tap(find.text('Show Error')); await tester.tap(find.text('Show Error'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
@@ -103,6 +105,7 @@ void main() {
), ),
); );
await tester.pump();
await tester.tap(find.text('Show Error')); await tester.tap(find.text('Show Error'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
@@ -134,6 +137,7 @@ void main() {
), ),
); );
await tester.pump();
await tester.tap(find.text('Show Error')); await tester.tap(find.text('Show Error'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();

View File

@@ -2,6 +2,8 @@ import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/app_initializer.dart'; import 'package:eatvio_app/core/app_initializer.dart';
import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.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';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_state.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_state.dart';
@@ -20,7 +22,7 @@ import 'package:network_image_mock/network_image_mock.dart';
import 'bottom_navigation_bar_widget_test.mocks.dart'; import 'bottom_navigation_bar_widget_test.mocks.dart';
@GenerateNiceMocks([MockSpec<NotificationsCubit>(), MockSpec<ChatsCubit>()]) @GenerateNiceMocks([MockSpec<NotificationsCubit>(), MockSpec<ChatsCubit>(), MockSpec<ProfileAvatarCubit>()])
void main() { void main() {
group('BottomNavigationBarWidget smoke', () { group('BottomNavigationBarWidget smoke', () {
setUpAll(() async { setUpAll(() async {
@@ -86,6 +88,7 @@ void main() {
mockNetworkImagesFor(() async { mockNetworkImagesFor(() async {
final notificationsCubit = MockNotificationsCubit(); final notificationsCubit = MockNotificationsCubit();
final chatsCubit = MockChatsCubit(); final chatsCubit = MockChatsCubit();
final profileAvatarCubit = MockProfileAvatarCubit();
when(notificationsCubit.state).thenReturn(const NotificationsState()); when(notificationsCubit.state).thenReturn(const NotificationsState());
when(notificationsCubit.stream).thenAnswer((_) => const Stream.empty()); when(notificationsCubit.stream).thenAnswer((_) => const Stream.empty());
@@ -93,16 +96,20 @@ void main() {
when(chatsCubit.state).thenReturn(ChatsState()); when(chatsCubit.state).thenReturn(ChatsState());
when(chatsCubit.stream).thenAnswer((_) => const Stream.empty()); when(chatsCubit.stream).thenAnswer((_) => const Stream.empty());
when(chatsCubit.close()).thenAnswer((_) async => Future<void>.value()); when(chatsCubit.close()).thenAnswer((_) async => Future<void>.value());
when(profileAvatarCubit.state).thenReturn(const ProfileAvatarState.loaded(profile: mockUserProfile));
when(profileAvatarCubit.stream).thenAnswer((_) => const Stream.empty());
when(profileAvatarCubit.close()).thenAnswer((_) async => Future<void>.value());
await tester.pumpWidget( await tester.pumpWidget(
MultiBlocProvider( MultiBlocProvider(
providers: [ providers: [
BlocProvider<NotificationsCubit>.value(value: notificationsCubit), BlocProvider<NotificationsCubit>.value(value: notificationsCubit),
BlocProvider<ChatsCubit>.value(value: chatsCubit), BlocProvider<ChatsCubit>.value(value: chatsCubit),
BlocProvider<ProfileAvatarCubit>.value(value: profileAvatarCubit),
], ],
child: wrap( child: wrap(
const Scaffold( const Scaffold(
body: BottomNavigationBarWidget(profileModel: mockUserProfile, child: Text('Test Content')), body: BottomNavigationBarWidget(child: Text('Test Content')),
), ),
), ),
), ),
@@ -117,6 +124,7 @@ void main() {
mockNetworkImagesFor(() async { mockNetworkImagesFor(() async {
final notificationsCubit = MockNotificationsCubit(); final notificationsCubit = MockNotificationsCubit();
final chatsCubit = MockChatsCubit(); final chatsCubit = MockChatsCubit();
final profileAvatarCubit = MockProfileAvatarCubit();
when(notificationsCubit.state).thenReturn(const NotificationsState()); when(notificationsCubit.state).thenReturn(const NotificationsState());
when(notificationsCubit.stream).thenAnswer((_) => const Stream.empty()); when(notificationsCubit.stream).thenAnswer((_) => const Stream.empty());
@@ -124,16 +132,20 @@ void main() {
when(chatsCubit.state).thenReturn(ChatsState()); when(chatsCubit.state).thenReturn(ChatsState());
when(chatsCubit.stream).thenAnswer((_) => const Stream.empty()); when(chatsCubit.stream).thenAnswer((_) => const Stream.empty());
when(chatsCubit.close()).thenAnswer((_) async => Future<void>.value()); when(chatsCubit.close()).thenAnswer((_) async => Future<void>.value());
when(profileAvatarCubit.state).thenReturn(const ProfileAvatarState.loaded(profile: mockConsultantProfile));
when(profileAvatarCubit.stream).thenAnswer((_) => const Stream.empty());
when(profileAvatarCubit.close()).thenAnswer((_) async => Future<void>.value());
await tester.pumpWidget( await tester.pumpWidget(
MultiBlocProvider( MultiBlocProvider(
providers: [ providers: [
BlocProvider<NotificationsCubit>.value(value: notificationsCubit), BlocProvider<NotificationsCubit>.value(value: notificationsCubit),
BlocProvider<ChatsCubit>.value(value: chatsCubit), BlocProvider<ChatsCubit>.value(value: chatsCubit),
BlocProvider<ProfileAvatarCubit>.value(value: profileAvatarCubit),
], ],
child: wrap( child: wrap(
const Scaffold( const Scaffold(
body: BottomNavigationBarWidget(profileModel: mockConsultantProfile, child: Text('Test Content')), body: BottomNavigationBarWidget(child: Text('Test Content')),
), ),
), ),
), ),

View File

@@ -34,6 +34,7 @@ void main() {
), ),
), ),
); );
await tester.pumpAndSettle();
expect(find.byType(ChosenDietWidgetContent), findsOneWidget); expect(find.byType(ChosenDietWidgetContent), findsOneWidget);
expect(find.text('Test Diet'), findsOneWidget); expect(find.text('Test Diet'), findsOneWidget);
@@ -50,6 +51,7 @@ void main() {
), ),
), ),
); );
await tester.pumpAndSettle();
expect(find.byType(ChosenDietWidgetContent), findsOneWidget); expect(find.byType(ChosenDietWidgetContent), findsOneWidget);
}); });
@@ -65,6 +67,7 @@ void main() {
), ),
), ),
); );
await tester.pumpAndSettle();
expect(find.text('My Diet'), findsOneWidget); expect(find.text('My Diet'), findsOneWidget);
}); });

View File

@@ -37,6 +37,7 @@ void main() {
planName: 'Test Plan', planName: 'Test Plan',
planDuration: 14, planDuration: 14,
)); ));
await tester.pumpAndSettle();
expect(find.byType(ChosenPlanWidgetContent), findsOneWidget); expect(find.byType(ChosenPlanWidgetContent), findsOneWidget);
expect(find.text('Test Plan'), findsOneWidget); expect(find.text('Test Plan'), findsOneWidget);
@@ -48,6 +49,7 @@ void main() {
planName: 'My Plan', planName: 'My Plan',
planDuration: 7, planDuration: 7,
)); ));
await tester.pumpAndSettle();
expect(find.byType(ChosenPlanWidgetContent), findsOneWidget); expect(find.byType(ChosenPlanWidgetContent), findsOneWidget);
expect(find.textContaining('3'), findsOneWidget); expect(find.textContaining('3'), findsOneWidget);
@@ -60,6 +62,7 @@ void main() {
planName: 'Long Plan', planName: 'Long Plan',
planDuration: 30, planDuration: 30,
)); ));
await tester.pumpAndSettle();
expect(find.byType(ChosenPlanWidgetContent), findsOneWidget); expect(find.byType(ChosenPlanWidgetContent), findsOneWidget);
expect(find.text('Long Plan'), findsOneWidget); expect(find.text('Long Plan'), findsOneWidget);

View File

@@ -78,6 +78,7 @@ void main() {
), ),
), ),
); );
await tester.pumpAndSettle();
expect(find.byType(CommentsWidget), findsOneWidget); expect(find.byType(CommentsWidget), findsOneWidget);
}); });

View File

@@ -63,6 +63,7 @@ void main() {
// Act // Act
await tester.pumpWidget(createTestWidget(const SizedBox(), mockCubit)); await tester.pumpWidget(createTestWidget(const SizedBox(), mockCubit));
await tester.pump();
await tester.tap(find.text('Show Bottom Sheet')); await tester.tap(find.text('Show Bottom Sheet'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
@@ -107,6 +108,7 @@ void main() {
// Act // Act
await tester.pumpWidget(createTestWidgetError(const SizedBox(), mockCubit)); await tester.pumpWidget(createTestWidgetError(const SizedBox(), mockCubit));
await tester.pump();
await tester.tap(find.text('Show Bottom Sheet')); await tester.tap(find.text('Show Bottom Sheet'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();

View File

@@ -54,6 +54,7 @@ void main() {
home: ChangePasswordScreen(settingsEntity: settingsEntity), home: ChangePasswordScreen(settingsEntity: settingsEntity),
), ),
); );
await tester.pumpAndSettle();
expect(find.byType(ChangePasswordScreen), findsOneWidget); expect(find.byType(ChangePasswordScreen), findsOneWidget);
expect(find.text('Смена пароля'), findsOneWidget); expect(find.text('Смена пароля'), findsOneWidget);
@@ -80,6 +81,7 @@ void main() {
home: ChangePasswordScreen(settingsEntity: settingsEntity), home: ChangePasswordScreen(settingsEntity: settingsEntity),
), ),
); );
await tester.pumpAndSettle();
expect(find.byType(AppBar), findsOneWidget); expect(find.byType(AppBar), findsOneWidget);
expect(find.text('Смена пароля'), findsOneWidget); expect(find.text('Смена пароля'), findsOneWidget);