forked from eatvio/eatvio_app
merge upstream
This commit is contained in:
@@ -33,15 +33,16 @@ class AppConfig {
|
||||
}
|
||||
|
||||
/// Список глобальных BlocProvider'ов
|
||||
/// Используются lazy: true для отложенной инициализации, чтобы не блокировать старт приложения
|
||||
List<BlocProvider> get globalBlocProviders => [
|
||||
BlocProvider<NewMessagesCubit>(create: (context) => sl<NewMessagesCubit>()),
|
||||
BlocProvider<NotificationsCubit>(create: (context) => sl<NotificationsCubit>()..getNotifications()),
|
||||
BlocProvider<FeedCubit>(create: (context) => sl<FeedCubit>()..loadFeedBlogPostList()),
|
||||
BlocProvider<ProfileAvatarCubit>(create: (context) => sl<ProfileAvatarCubit>()..getMyProfile()),
|
||||
BlocProvider<ChatsCubit>(create: (context) => sl<ChatsCubit>()..getChatRoomsWithLoading()),
|
||||
BlocProvider<ViewsCubit>(create: (context) => sl<ViewsCubit>()),
|
||||
BlocProvider<NewMessagesCubit>(lazy: true, create: (context) => sl<NewMessagesCubit>()),
|
||||
BlocProvider<NotificationsCubit>(lazy: true, create: (context) => sl<NotificationsCubit>()),
|
||||
BlocProvider<FeedCubit>(lazy: true, create: (context) => sl<FeedCubit>()),
|
||||
BlocProvider<ProfileAvatarCubit>(lazy: true, create: (context) => sl<ProfileAvatarCubit>()),
|
||||
BlocProvider<ChatsCubit>(lazy: true, create: (context) => sl<ChatsCubit>()),
|
||||
BlocProvider<ViewsCubit>(lazy: true, create: (context) => sl<ViewsCubit>()),
|
||||
BlocProvider<InternetConnectionCubit>(lazy: false, create: (context) => sl<InternetConnectionCubit>()),
|
||||
BlocProvider<MarketingCubit>(create: (context) => sl<MarketingCubit>()),
|
||||
BlocProvider<MarketingCubit>(lazy: true, create: (context) => sl<MarketingCubit>()),
|
||||
];
|
||||
|
||||
/// Делегаты локализации
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:io' as io;
|
||||
import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/env_config.dart';
|
||||
@@ -37,9 +39,21 @@ class AppInitializer {
|
||||
|
||||
/// Инициализация Firebase
|
||||
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) {
|
||||
await FirebaseMessaging.instance.requestPermission();
|
||||
try {
|
||||
await FirebaseMessaging.instance.requestPermission().timeout(
|
||||
const Duration(seconds: 5),
|
||||
);
|
||||
} catch (e) {
|
||||
// Разрешение не критично, продолжаем работу
|
||||
log('Failed to request Firebase messaging permission: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,13 +20,19 @@ class ExternalRegistrations {
|
||||
static void _registerDio() {
|
||||
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()]),
|
||||
);
|
||||
}
|
||||
|
||||
/// Регистрация Prefs
|
||||
static void _registerPrefs() {
|
||||
sl.registerSingletonAsync<Prefs>(()async => Prefs(await SharedPreferences.getInstance()));
|
||||
sl.registerSingletonAsync<Prefs>(() async => Prefs(await SharedPreferences.getInstance()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
import 'package:eatvio_app/core/app_config.dart';
|
||||
@@ -19,12 +20,25 @@ class FirebaseApi {
|
||||
String? token;
|
||||
|
||||
if (await _isHuaweiDevice()) {
|
||||
token = await _getHuaweiPushTokenNative();
|
||||
token = await _getHuaweiPushTokenNative().timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () => null,
|
||||
);
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
@@ -39,9 +53,15 @@ class FirebaseApi {
|
||||
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
|
||||
'token': await getToken(),
|
||||
},
|
||||
).timeout(
|
||||
const Duration(seconds: 10),
|
||||
onTimeout: () {
|
||||
throw TimeoutException('registerAnonymousPushToken timeout');
|
||||
},
|
||||
);
|
||||
} catch (e, st) {
|
||||
log(e.toString(), stackTrace: st);
|
||||
// Не пробрасываем ошибку дальше, так как это не критичная операция
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,9 +77,15 @@ class FirebaseApi {
|
||||
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
|
||||
'token': await getToken(),
|
||||
},
|
||||
).timeout(
|
||||
const Duration(seconds: 10),
|
||||
onTimeout: () {
|
||||
throw TimeoutException('registerPushToken timeout');
|
||||
},
|
||||
);
|
||||
} catch (e, st) {
|
||||
log(e.toString(), stackTrace: st);
|
||||
// Не пробрасываем ошибку дальше, так как это не критичная операция
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/repository_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:get_it/get_it.dart';
|
||||
|
||||
@@ -18,6 +19,9 @@ Future<void> init({required AppBuildType buildType}) async {
|
||||
// Регистрация внешних зависимостей (Dio, etc.)
|
||||
ExternalRegistrations.registerAll();
|
||||
|
||||
// Ожидание инициализации асинхронных зависимостей
|
||||
await sl.getAsync<Prefs>();
|
||||
|
||||
// Регистрация сервисов
|
||||
ServiceRegistrations.registerAll();
|
||||
|
||||
|
||||
@@ -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/features/auth/entities/auth_result_entity.dart';
|
||||
import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart';
|
||||
@@ -21,10 +19,8 @@ class AuthRoutes {
|
||||
path: RouteConstants.authScreen,
|
||||
name: 'authScreen',
|
||||
builder: (context, state) => const AuthScreen(),
|
||||
redirect: (context, state) {
|
||||
sl<FirebaseApi>().registerAnonymousPushToken();
|
||||
return null;
|
||||
},
|
||||
// registerAnonymousPushToken вызывается в AuthCubit.initializeUser()
|
||||
// и не должен блокировать навигацию
|
||||
routes: [
|
||||
GoRoute(path: RouteConstants.registrationScreen, name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()),
|
||||
GoRoute(path: RouteConstants.registrationScreenV2, name: 'registrationScreenV2', builder: (context, state) => const RegistrationV2Page()),
|
||||
|
||||
@@ -23,14 +23,16 @@ class MainNavigationRoutes {
|
||||
return [
|
||||
ShellRoute(
|
||||
builder: (context, state, child) {
|
||||
return BlocBuilder<ProfileAvatarCubit, ProfileAvatarState>(
|
||||
builder: (BuildContext context, ProfileAvatarState state) {
|
||||
if (state is ProfileAvatarStateLoaded) {
|
||||
return BottomNavigationBarWidget(profileModel: state.profile, child: child);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
);
|
||||
// Инициализация ProfileAvatarCubit при первом использовании ShellRoute
|
||||
final profileAvatarCubit = context.read<ProfileAvatarCubit>();
|
||||
if (profileAvatarCubit.state is ProfileAvatarStateLoading) {
|
||||
// Вызываем getMyProfile() только если состояние loading (первый запуск)
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
profileAvatarCubit.getMyProfile();
|
||||
});
|
||||
}
|
||||
|
||||
return BottomNavigationBarWidget(child: child);
|
||||
},
|
||||
routes: [
|
||||
GoRoute(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
@@ -56,7 +57,8 @@ class OtherRoutes {
|
||||
builder: (context, state) => const WelcomeScreen(),
|
||||
redirect: (context, state) async {
|
||||
try {
|
||||
final prefs = sl<Prefs>();
|
||||
// Prefs уже инициализирован в locator_service, но на всякий случай проверяем
|
||||
final prefs = await sl.getAsync<Prefs>();
|
||||
final welcomeScreenWasShowed = prefs.getBool(PrefsKeys.welcomeScreenWasShowed) ?? false;
|
||||
if (!welcomeScreenWasShowed) {
|
||||
return null;
|
||||
@@ -74,7 +76,27 @@ class OtherRoutes {
|
||||
builder: (context, state) => const UpdateScreen(),
|
||||
redirect: (context, state) async {
|
||||
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();
|
||||
String version = packageInfo.version;
|
||||
Version currentVersion = Version.parse(version);
|
||||
@@ -85,6 +107,7 @@ class OtherRoutes {
|
||||
return RouteConstants.welcomeScreen;
|
||||
}
|
||||
} catch (e) {
|
||||
// При любой ошибке пропускаем экран обновления
|
||||
return RouteConstants.welcomeScreen;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,9 +1,24 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
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 {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
try {
|
||||
final sharedPreferences = await _getPrefs();
|
||||
if (sharedPreferences.getString('token') == null) {
|
||||
return false;
|
||||
} else {
|
||||
@@ -15,25 +30,25 @@ abstract class TokenStorage {
|
||||
}
|
||||
|
||||
static Future<String?> getTokenFromStorage() async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
final sharedPreferences = await _getPrefs();
|
||||
return sharedPreferences.getString('token');
|
||||
}
|
||||
|
||||
static Future<void> saveTokenInStorage(String token) async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
final sharedPreferences = await _getPrefs();
|
||||
await sharedPreferences.setString('token', token);
|
||||
}
|
||||
|
||||
static Future<void> removeTokenInStorage() async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
final sharedPreferences = await _getPrefs();
|
||||
await sharedPreferences.remove('token');
|
||||
}
|
||||
}
|
||||
|
||||
abstract class SocketIdStorage {
|
||||
static Future<bool> haveSocketIdInStorage() async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
try {
|
||||
final sharedPreferences = await TokenStorage._getPrefs();
|
||||
if (sharedPreferences.getString('socketId') == null) {
|
||||
return false;
|
||||
} else {
|
||||
@@ -45,17 +60,17 @@ abstract class SocketIdStorage {
|
||||
}
|
||||
|
||||
static Future<String?> getSocketIdFromStorage() async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
final sharedPreferences = await TokenStorage._getPrefs();
|
||||
return sharedPreferences.getString('socketId');
|
||||
}
|
||||
|
||||
static Future<void> saveSocketIdInStorage(String socketId) async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
final sharedPreferences = await TokenStorage._getPrefs();
|
||||
await sharedPreferences.setString('socketId', socketId);
|
||||
}
|
||||
|
||||
static Future<void> removeSocketIdInStorage() async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
final sharedPreferences = await TokenStorage._getPrefs();
|
||||
await sharedPreferences.remove('socketId');
|
||||
}
|
||||
}
|
||||
|
||||
67
lib/core/utils/markdown_utils.dart
Normal file
67
lib/core/utils/markdown_utils.dart
Normal 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);
|
||||
}
|
||||
124
lib/core/widgets/markdown_html_widget.dart
Normal file
124
lib/core/widgets/markdown_html_widget.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -38,18 +38,25 @@ class AuthCubit extends Cubit<AuthState> {
|
||||
}
|
||||
|
||||
Future<void> initializeUser() async {
|
||||
await registerAnonymousPushToken();
|
||||
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 {
|
||||
try {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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/widgets/custom_popup.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_state.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/core/locator_service.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_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_html/flutter_html.dart';
|
||||
import 'package:go_router/go_router.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 {
|
||||
final int id;
|
||||
@@ -79,98 +78,99 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
|
||||
resizeToAvoidBottomInset: true,
|
||||
bottomSheet: Container(
|
||||
padding: EdgeInsets.only(
|
||||
top: 12,
|
||||
bottom: 12 + MediaQuery.of(context).padding.bottom + 56, // 56 - высота bottomNavigationBar
|
||||
bottom: MediaQuery.of(context).padding.bottom + 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (replyId != null)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context.read<CommentsCubit>().setReplyId(null);
|
||||
setState(() {
|
||||
replyId = null;
|
||||
textEditingController.clear();
|
||||
});
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
},
|
||||
child: Container(
|
||||
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(() {});
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (replyId != null)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context.read<CommentsCubit>().setReplyId(null);
|
||||
setState(() {
|
||||
replyId = null;
|
||||
textEditingController.clear();
|
||||
});
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
},
|
||||
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),
|
||||
child: Container(
|
||||
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),
|
||||
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: 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),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if (textEditingController.text.length > 2) {
|
||||
commentsCubit.sendComment(
|
||||
id: widget.id.toString(),
|
||||
commentText: textEditingController.text,
|
||||
parentCommentId: replyId,
|
||||
commentSourceType: CommentSourceType.blogPost,
|
||||
);
|
||||
context.read<CommentsCubit>().setReplyId(null);
|
||||
setState(() {
|
||||
textEditingController.clear();
|
||||
replyId = null;
|
||||
});
|
||||
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),
|
||||
const SizedBox(width: 8),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if (textEditingController.text.length > 2) {
|
||||
commentsCubit.sendComment(
|
||||
id: widget.id.toString(),
|
||||
commentText: textEditingController.text,
|
||||
parentCommentId: replyId,
|
||||
commentSourceType: CommentSourceType.blogPost,
|
||||
);
|
||||
context.read<CommentsCubit>().setReplyId(null);
|
||||
setState(() {
|
||||
textEditingController.clear();
|
||||
replyId = null;
|
||||
});
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
appBar: AppBar(
|
||||
@@ -236,104 +236,8 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
|
||||
if (blogPostState.blogPostModel?.description.isNotEmpty ?? false)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
// Автоматически преобразуем 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');
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
child: MarkdownHtmlWidget(
|
||||
markdownText: blogPostState.blogPostModel!.description,
|
||||
),
|
||||
),
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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/notifications/presentation/notifications_cubit/notifications_cubit.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_state.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';
|
||||
|
||||
class BottomNavigationBarWidget extends StatefulWidget {
|
||||
final ProfileModel profileModel;
|
||||
final Widget child;
|
||||
|
||||
const BottomNavigationBarWidget({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.profileModel,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -31,15 +30,12 @@ class BottomNavigationBarWidget extends StatefulWidget {
|
||||
|
||||
class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
||||
int currentBottomNavBarIndex = 1;
|
||||
late final List<String> pages;
|
||||
List<String>? pages;
|
||||
int tapsCount = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
if (widget.profileModel.roles.contains('consultant')) {
|
||||
pages = [
|
||||
List<String> _getPages(bool isConsultant) {
|
||||
if (isConsultant) {
|
||||
return [
|
||||
'/my-profile-screen',
|
||||
'/feed-screen',
|
||||
'/diary-screen',
|
||||
@@ -49,7 +45,7 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
||||
'/menu-screen',
|
||||
];
|
||||
} else {
|
||||
pages = [
|
||||
return [
|
||||
'/my-profile-screen',
|
||||
'/feed-screen',
|
||||
'/diary-screen',
|
||||
@@ -61,7 +57,8 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
||||
}
|
||||
|
||||
void _onItemTapped(int index) async {
|
||||
if (pages.length == 7) {
|
||||
if (pages == null) return;
|
||||
if (pages!.length == 7) {
|
||||
if (index == 6) {
|
||||
tapsCount++;
|
||||
if (tapsCount == 7) {
|
||||
@@ -82,108 +79,117 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
||||
setState(() {
|
||||
currentBottomNavBarIndex = index;
|
||||
});
|
||||
GoRouter.of(context).replace(pages[currentBottomNavBarIndex]);
|
||||
GoRouter.of(context).replace(pages![currentBottomNavBarIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return Scaffold(
|
||||
body: widget.child,
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
type: BottomNavigationBarType.fixed,
|
||||
onTap: _onItemTapped,
|
||||
selectedFontSize: 10,
|
||||
unselectedFontSize: 10,
|
||||
selectedItemColor: appTheme.blueA400,
|
||||
currentIndex: currentBottomNavBarIndex,
|
||||
items: [
|
||||
BottomNavigationBarItem(
|
||||
label: '',
|
||||
icon: CircleAvatar(
|
||||
foregroundImage: NetworkImage(
|
||||
widget.profileModel.profilePhotoUrl.contains('https') ? widget.profileModel.profilePhotoUrl : sl<AppConfig>().baseUrl + widget.profileModel.profilePhotoUrl,
|
||||
),
|
||||
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
||||
radius: 16,
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
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,
|
||||
return BlocBuilder<ProfileAvatarCubit, ProfileAvatarState>(
|
||||
builder: (BuildContext context, ProfileAvatarState state) {
|
||||
if (state is ProfileAvatarStateLoaded) {
|
||||
final profileModel = state.profile;
|
||||
final isConsultant = profileModel.roles.contains('consultant');
|
||||
|
||||
// Инициализируем pages на основе profileModel
|
||||
pages ??= _getPages(isConsultant);
|
||||
|
||||
return Scaffold(
|
||||
body: widget.child,
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
type: BottomNavigationBarType.fixed,
|
||||
onTap: _onItemTapped,
|
||||
selectedFontSize: 10,
|
||||
unselectedFontSize: 10,
|
||||
selectedItemColor: appTheme.blueA400,
|
||||
currentIndex: currentBottomNavBarIndex,
|
||||
items: [
|
||||
BottomNavigationBarItem(
|
||||
label: '',
|
||||
icon: CircleAvatar(
|
||||
foregroundImage: NetworkImage(
|
||||
profileModel.profilePhotoUrl.contains('https') ? profileModel.profilePhotoUrl : sl<AppConfig>().baseUrl + profileModel.profilePhotoUrl,
|
||||
),
|
||||
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
||||
radius: 16,
|
||||
),
|
||||
),
|
||||
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(
|
||||
label: s.chat,
|
||||
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 (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>(
|
||||
builder: (BuildContext context, ChatsState state) {
|
||||
final hasUnreadMessages = state.chatsWithUnreadMessagesCount > 0;
|
||||
@@ -206,7 +212,7 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
||||
ImageConstant.icnChat,
|
||||
width: 20,
|
||||
height: 20,
|
||||
colorFilter: pages[currentBottomNavBarIndex] == '/chat-select-screen'
|
||||
colorFilter: pages![currentBottomNavBarIndex] == '/chat-select-screen'
|
||||
? ColorFilter.mode(
|
||||
appTheme.blueA400,
|
||||
BlendMode.srcIn,
|
||||
@@ -217,9 +223,9 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
label: 'Уведомления',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
label: 'Уведомления',
|
||||
icon: BlocBuilder<NotificationsCubit, NotificationsState>(
|
||||
builder: (BuildContext context, NotificationsState state) {
|
||||
return badges.Badge(
|
||||
@@ -237,7 +243,7 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
child: SvgPicture.asset(
|
||||
ImageConstant.icnBell,
|
||||
colorFilter: pages[currentBottomNavBarIndex] == '/notifications-screen'
|
||||
colorFilter: pages![currentBottomNavBarIndex] == '/notifications-screen'
|
||||
? ColorFilter.mode(
|
||||
appTheme.blueA400,
|
||||
BlendMode.srcIn,
|
||||
@@ -251,15 +257,22 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
||||
},
|
||||
),
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
label: '',
|
||||
icon: SvgPicture.asset(
|
||||
ImageConstant.imgLogo,
|
||||
height: 32,
|
||||
BottomNavigationBarItem(
|
||||
label: '',
|
||||
icon: SvgPicture.asset(
|
||||
ImageConstant.imgLogo,
|
||||
height: 32,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
// Показываем child даже когда профиль еще загружается, чтобы экраны могли инициализироваться
|
||||
return Scaffold(
|
||||
body: widget.child,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,19 @@ class ChatsScreen extends StatefulWidget {
|
||||
class _ChatsScreenState extends State<ChatsScreen> {
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<ChatsCubit, ChatsState>(
|
||||
|
||||
@@ -106,15 +106,14 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
|
||||
resizeToAvoidBottomInset: true,
|
||||
bottomSheet: Container(
|
||||
padding: EdgeInsets.only(
|
||||
top: 12,
|
||||
bottom: 12 + MediaQuery.of(context).padding.bottom + 56, // 56 - высота bottomNavigationBar
|
||||
bottom: MediaQuery.of(context).padding.bottom + 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:math' as math;
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/widgets/markdown_html_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
@@ -35,7 +36,9 @@ class _ExpandableTextState extends State<ExpandableText> {
|
||||
void _checkTextOverflow() {
|
||||
final textRenderObject = _textKey.currentContext?.findRenderObject();
|
||||
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;
|
||||
|
||||
if (actualHeight > maxLinesHeight && !showExpandButton) {
|
||||
@@ -55,6 +58,7 @@ class _ExpandableTextState extends State<ExpandableText> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -65,12 +69,18 @@ class _ExpandableTextState extends State<ExpandableText> {
|
||||
curve: Curves.easeInOut,
|
||||
child: Stack(
|
||||
children: [
|
||||
Text(
|
||||
widget.text,
|
||||
key: _textKey,
|
||||
style: TextStyles.textMMediumAuto.copyWith(color: appTheme.black),
|
||||
maxLines: isExpanded ? null : widget.maxLines,
|
||||
overflow: TextOverflow.clip,
|
||||
ClipRect(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: isExpanded ? double.infinity : (widget.maxLines * (TextStyles.textMMediumAuto.fontSize ?? 16) * 1.5),
|
||||
),
|
||||
child: MarkdownHtmlWidget(
|
||||
key: _textKey,
|
||||
markdownText: widget.text,
|
||||
fontSize: TextStyles.textMMediumAuto.fontSize ?? 16,
|
||||
textColor: appTheme.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!isExpanded && showExpandButton)
|
||||
Positioned.fill(
|
||||
|
||||
@@ -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/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_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_state.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 ? ['Все', 'Рецепты', 'Диеты', 'Консультанты'] : ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты'];
|
||||
consultantsScrollController.addListener(_onScrollConsultants);
|
||||
_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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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/widgets/custom_popup.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/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/widgets/subscribe_button_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_html/flutter_html.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:markdown/markdown.dart' as md;
|
||||
|
||||
class BlogPostWidget extends StatefulWidget {
|
||||
final int index;
|
||||
@@ -125,37 +124,46 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
|
||||
if (state.feedBlogPostList[widget.index].description.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Html(
|
||||
data: md.markdownToHtml(
|
||||
(state.feedBlogPostList[widget.index].description.length > 200
|
||||
? state.feedBlogPostList[widget.index].description.substring(0, 200).replaceAll('\n', '<br>')
|
||||
: state.feedBlogPostList[widget.index].description.replaceAll('\n', '<br>')),
|
||||
),
|
||||
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)),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
String description = state.feedBlogPostList[widget.index].description;
|
||||
final isLong = description.length > 200;
|
||||
|
||||
if (isLong) {
|
||||
// Умная обрезка: обрезаем markdown текст, не ломая синтаксис
|
||||
int cutPoint = 200;
|
||||
|
||||
// Проверяем, есть ли незакрытые markdown теги
|
||||
String preview = description.substring(0, cutPoint);
|
||||
|
||||
// Считаем открывающие и закрывающие теги
|
||||
int boldOpen = (preview.split('**').length - 1) ~/ 2;
|
||||
int boldClose = preview.split('**').length - 1 - boldOpen * 2;
|
||||
bool hasUnclosedBold = boldOpen > boldClose;
|
||||
|
||||
// Если есть незакрытый bold, ищем закрытие
|
||||
if (hasUnclosedBold) {
|
||||
int searchEnd = (cutPoint + 100).clamp(0, description.length);
|
||||
int boldClosePos = description.indexOf('**', cutPoint);
|
||||
if (boldClosePos != -1 && boldClosePos < searchEnd) {
|
||||
cutPoint = boldClosePos + 2;
|
||||
} else {
|
||||
// Если не нашли, обрезаем до последнего пробела
|
||||
cutPoint = description.lastIndexOf(' ', 200);
|
||||
if (cutPoint == -1 || cutPoint < 150) cutPoint = 200;
|
||||
}
|
||||
} else {
|
||||
// Обрезаем до последнего пробела для красоты
|
||||
cutPoint = description.lastIndexOf(' ', cutPoint);
|
||||
if (cutPoint == -1 || cutPoint < 150) cutPoint = 200;
|
||||
}
|
||||
|
||||
description = description.substring(0, cutPoint);
|
||||
}
|
||||
|
||||
return MarkdownHtmlWidget(
|
||||
markdownText: description,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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:flutter/material.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:go_router/go_router.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 {
|
||||
final Diet diet;
|
||||
@@ -117,86 +115,11 @@ class _DietsWidgetState extends State<DietsWidget> {
|
||||
String details = widget.diet.details.length > 200
|
||||
? '${widget.diet.details.substring(0, 200)}...'
|
||||
: widget.diet.details;
|
||||
// Автоматически преобразуем 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(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);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
return MarkdownHtmlWidget(
|
||||
markdownText: details,
|
||||
fontSize: TextStyles.textSMedium.fontSize ?? 14,
|
||||
textColor: appTheme.black,
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -267,8 +190,3 @@ class _DietsWidgetState extends State<DietsWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
class _LinkRange {
|
||||
final int start;
|
||||
final int end;
|
||||
_LinkRange(this.start, this.end);
|
||||
}
|
||||
|
||||
@@ -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/info_row.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/feed/models/plan_pagination_model.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:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_html/flutter_html.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:go_router/go_router.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 {
|
||||
final PlanModel plan;
|
||||
@@ -117,88 +115,11 @@ class _PlanWidgetState extends State<PlanWidget> {
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
String details = widget.plan.details.length > 200
|
||||
? '${widget.plan.details.substring(0, 200)}...'
|
||||
: widget.plan.details;
|
||||
// Автоматически преобразуем 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);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
child: MarkdownHtmlWidget(
|
||||
markdownText: widget.plan.details.length > 200
|
||||
? '${widget.plan.details.substring(0, 200)}...'
|
||||
: widget.plan.details,
|
||||
textColor: appTheme.black,
|
||||
),
|
||||
),
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -16,12 +16,10 @@ import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.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:go_router/go_router.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 {
|
||||
final int index;
|
||||
@@ -155,88 +153,11 @@ class _RecipesWidgetState extends State<RecipesWidget> {
|
||||
String description = widget.recipe.description.length > 200
|
||||
? widget.recipe.description.substring(0, 200)
|
||||
: widget.recipe.description;
|
||||
// Автоматически преобразуем URL в markdown ссылки
|
||||
// Сначала исправляем неправильный синтаксис markdown ссылок
|
||||
final wrongLinkRegex = RegExp(r'\[(https?://[^\]]+|www\.[^\]]+)\]\(([^\)]+)\)');
|
||||
description = description.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(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);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
return MarkdownHtmlWidget(
|
||||
markdownText: description,
|
||||
fontSize: TextStyles.textSMedium.fontSize ?? 14,
|
||||
textColor: appTheme.black,
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -330,8 +251,3 @@ class _RecipesWidgetState extends State<RecipesWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
class _LinkRange {
|
||||
final int start;
|
||||
final int end;
|
||||
_LinkRange(this.start, this.end);
|
||||
}
|
||||
|
||||
@@ -21,12 +21,10 @@ import 'package:eatvio_app/features/personal_diet_suggestion/presentation/person
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.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:go_router/go_router.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 {
|
||||
final Diet diet;
|
||||
@@ -86,86 +84,11 @@ class _MyDietWidgetState extends State<MyDietWidget> {
|
||||
String details = widget.diet.details.length > 200
|
||||
? '${widget.diet.details.substring(0, 200)}...'
|
||||
: widget.diet.details;
|
||||
// Автоматически преобразуем 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(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);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
return MarkdownHtmlWidget(
|
||||
markdownText: details,
|
||||
fontSize: TextStyles.textSMedium.fontSize ?? 14,
|
||||
textColor: appTheme.black,
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -323,8 +246,3 @@ class _MyDietWidgetState extends State<MyDietWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
class _LinkRange {
|
||||
final int start;
|
||||
final int end;
|
||||
_LinkRange(this.start, this.end);
|
||||
}
|
||||
|
||||
@@ -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:flutter/material.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
|
||||
import 'package:go_router/go_router.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 {
|
||||
final PlanModel plan;
|
||||
@@ -78,86 +76,11 @@ class _MyPlanWidgetState extends State<MyPlanWidget> {
|
||||
String details = widget.plan.details.length > 200
|
||||
? '${widget.plan.details.substring(0, 200)}...'
|
||||
: widget.plan.details;
|
||||
// Автоматически преобразуем 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(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);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
return MarkdownHtmlWidget(
|
||||
markdownText: details,
|
||||
fontSize: TextStyles.textSMedium.fontSize ?? 14,
|
||||
textColor: appTheme.black,
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -268,8 +191,3 @@ class _MyPlanWidgetState extends State<MyPlanWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
class _LinkRange {
|
||||
final int start;
|
||||
final int end;
|
||||
_LinkRange(this.start, this.end);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,14 @@ class _NotificationsScreenState extends State<NotificationsScreen> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_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
|
||||
|
||||
@@ -153,16 +153,13 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
|
||||
backgroundColor: appTheme.gray100,
|
||||
resizeToAvoidBottomInset: true,
|
||||
bottomSheet: Container(
|
||||
padding: EdgeInsets.only(
|
||||
top: 12,
|
||||
bottom: 12 + MediaQuery.of(context).padding.bottom + 56, // 56 - высота bottomNavigationBar
|
||||
),
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom + 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
||||
@@ -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:flutter/material.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:go_router/go_router.dart';
|
||||
import 'package:markdown/markdown.dart' as md;
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
class ProfileBlogPostWidget extends StatefulWidget {
|
||||
@@ -107,37 +106,48 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
|
||||
if (state.blogPosts[widget.index].description.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Html(
|
||||
data: md.markdownToHtml(
|
||||
state.blogPosts[widget.index].description.length > 200
|
||||
? state.blogPosts[widget.index].description.substring(0, 200).replaceAll('\n', '<br>')
|
||||
: state.blogPosts[widget.index].description.replaceAll('\n', '<br>'),
|
||||
),
|
||||
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)),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
String description = state.blogPosts[widget.index].description;
|
||||
final isLong = description.length > 200;
|
||||
|
||||
if (isLong) {
|
||||
// Умная обрезка: обрезаем markdown текст, не ломая синтаксис
|
||||
int cutPoint = 200;
|
||||
|
||||
// Проверяем, есть ли незакрытые markdown теги
|
||||
String preview = description.substring(0, cutPoint);
|
||||
|
||||
// Считаем открывающие и закрывающие теги
|
||||
int boldOpen = (preview.split('**').length - 1) ~/ 2;
|
||||
int boldClose = preview.split('**').length - 1 - boldOpen * 2;
|
||||
bool hasUnclosedBold = boldOpen > boldClose;
|
||||
|
||||
// Если есть незакрытый bold, ищем закрытие
|
||||
if (hasUnclosedBold) {
|
||||
int searchEnd = (cutPoint + 100).clamp(0, description.length);
|
||||
int boldClosePos = description.indexOf('**', cutPoint);
|
||||
if (boldClosePos != -1 && boldClosePos < searchEnd) {
|
||||
cutPoint = boldClosePos + 2;
|
||||
} else {
|
||||
// Если не нашли, обрезаем до последнего пробела
|
||||
cutPoint = description.lastIndexOf(' ', 200);
|
||||
if (cutPoint == -1 || cutPoint < 150) cutPoint = 200;
|
||||
}
|
||||
} else {
|
||||
// Обрезаем до последнего пробела для красоты
|
||||
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,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -99,108 +99,102 @@ class _AboutRecipeScreenState extends State<AboutRecipeScreen> {
|
||||
resizeToAvoidBottomInset: true,
|
||||
bottomSheet: Container(
|
||||
padding: EdgeInsets.only(
|
||||
top: 12,
|
||||
bottom: 12 + MediaQuery.of(context).padding.bottom + 56, // 56 - высота bottomNavigationBar
|
||||
bottom: MediaQuery.of(context).padding.bottom + 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (replyId != null)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context.read<CommentsCubit>().setReplyId(null);
|
||||
setState(() {
|
||||
replyId = null;
|
||||
textEditingController.clear();
|
||||
});
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
},
|
||||
child: Container(
|
||||
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),
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (replyId != null)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context.read<CommentsCubit>().setReplyId(null);
|
||||
setState(() {
|
||||
replyId = null;
|
||||
textEditingController.clear();
|
||||
});
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
|
||||
height: 32,
|
||||
width: 32,
|
||||
child: const Icon(Icons.close_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();
|
||||
}
|
||||
),
|
||||
if (replyId != null) const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minHeight: 52),
|
||||
child: TextFormField(
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
onChanged: (_) {
|
||||
setState(() {});
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: textEditingController.text.length > 2 ? appTheme.blueA400 : Colors.grey,
|
||||
shape: BoxShape.circle,
|
||||
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: 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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:flutter/material.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:go_router/go_router.dart';
|
||||
import 'package:markdown/markdown.dart' as md;
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class MyRecipeWidget extends StatefulWidget {
|
||||
final MyRecipeModel recipe;
|
||||
@@ -100,86 +98,11 @@ class _MyRecipeWidgetState extends State<MyRecipeWidget> {
|
||||
String description = widget.recipe.description.length > 200
|
||||
? '${widget.recipe.description.substring(0, 200)}...'
|
||||
: widget.recipe.description;
|
||||
// Автоматически преобразуем URL в markdown ссылки
|
||||
// Сначала исправляем неправильный синтаксис markdown ссылок
|
||||
final wrongLinkRegex = RegExp(r'\[(https?://[^\]]+|www\.[^\]]+)\]\(([^\)]+)\)');
|
||||
description = description.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(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);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
return MarkdownHtmlWidget(
|
||||
markdownText: description,
|
||||
fontSize: TextStyles.textSMedium.fontSize ?? 14,
|
||||
textColor: appTheme.black,
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -270,8 +193,3 @@ class _MyRecipeWidgetState extends State<MyRecipeWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
class _LinkRange {
|
||||
final int start;
|
||||
final int end;
|
||||
_LinkRange(this.start, this.end);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:intl/message_lookup_by_library.dart';
|
||||
import 'package:intl/src/intl_helpers.dart';
|
||||
@@ -21,8 +20,8 @@ import 'messages_ru.dart' as messages_ru;
|
||||
|
||||
typedef Future<dynamic> LibraryLoader();
|
||||
Map<String, LibraryLoader> _deferredLibraries = {
|
||||
'en': () => new SynchronousFuture(null),
|
||||
'ru': () => new SynchronousFuture(null),
|
||||
'en': () => new Future.value(null),
|
||||
'ru': () => new Future.value(null),
|
||||
};
|
||||
|
||||
MessageLookupByLibrary? _findExact(String localeName) {
|
||||
@@ -37,20 +36,19 @@ MessageLookupByLibrary? _findExact(String localeName) {
|
||||
}
|
||||
|
||||
/// 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(
|
||||
localeName,
|
||||
(locale) => _deferredLibraries[locale] != null,
|
||||
onFailure: (_) => null,
|
||||
);
|
||||
onFailure: (_) => null);
|
||||
if (availableLocale == null) {
|
||||
return new SynchronousFuture(false);
|
||||
return new Future.value(false);
|
||||
}
|
||||
var lib = _deferredLibraries[availableLocale];
|
||||
lib == null ? new SynchronousFuture(false) : lib();
|
||||
await (lib == null ? new Future.value(false) : lib());
|
||||
initializeInternalMessageLookup(() => new CompositeMessageLookup());
|
||||
messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor);
|
||||
return new SynchronousFuture(true);
|
||||
return new Future.value(true);
|
||||
}
|
||||
|
||||
bool _messagesExistFor(String locale) {
|
||||
@@ -62,11 +60,8 @@ bool _messagesExistFor(String locale) {
|
||||
}
|
||||
|
||||
MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) {
|
||||
var actualLocale = Intl.verifiedLocale(
|
||||
locale,
|
||||
_messagesExistFor,
|
||||
onFailure: (_) => null,
|
||||
);
|
||||
var actualLocale = Intl.verifiedLocale(locale, _messagesExistFor,
|
||||
onFailure: (_) => null);
|
||||
if (actualLocale == null) return null;
|
||||
return _findExact(actualLocale);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,7 @@
|
||||
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
|
||||
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
|
||||
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
|
||||
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
|
||||
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
|
||||
// ignore_for_file:unused_import, file_names
|
||||
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:intl/message_lookup_by_library.dart';
|
||||
@@ -21,362 +20,223 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
String get localeName => 'ru';
|
||||
|
||||
final messages = _notInlinedMessages(_notInlinedMessages);
|
||||
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
||||
"Moscow": MessageLookupByLibrary.simpleMessage("Москва"),
|
||||
"Russia": MessageLookupByLibrary.simpleMessage("Россия"),
|
||||
"about_diet": MessageLookupByLibrary.simpleMessage("Об этой диете"),
|
||||
"about_plan": MessageLookupByLibrary.simpleMessage("Об этом плане"),
|
||||
"active": MessageLookupByLibrary.simpleMessage("Активен"),
|
||||
"active_diet_state": MessageLookupByLibrary.simpleMessage("Активная"),
|
||||
"add": MessageLookupByLibrary.simpleMessage("Добавить"),
|
||||
"add_cover": MessageLookupByLibrary.simpleMessage("Добавьте обложку"),
|
||||
"add_measure": MessageLookupByLibrary.simpleMessage("Добавить замер"),
|
||||
"adding_plan": MessageLookupByLibrary.simpleMessage("Добавление плана"),
|
||||
"additional_information": MessageLookupByLibrary.simpleMessage(
|
||||
"Дополнительная информация",
|
||||
),
|
||||
"admin": MessageLookupByLibrary.simpleMessage("Админ"),
|
||||
"all": MessageLookupByLibrary.simpleMessage("Все"),
|
||||
"allergies_presence": MessageLookupByLibrary.simpleMessage(
|
||||
"Наличие аллергии",
|
||||
),
|
||||
"another_form_of_comment": MessageLookupByLibrary.simpleMessage(
|
||||
"комментария",
|
||||
),
|
||||
"another_form_of_day": MessageLookupByLibrary.simpleMessage("дня"),
|
||||
"answer": MessageLookupByLibrary.simpleMessage("Ответить"),
|
||||
"archived": MessageLookupByLibrary.simpleMessage("В архиве"),
|
||||
"authentificate": MessageLookupByLibrary.simpleMessage("Войти"),
|
||||
"average_pressure": MessageLookupByLibrary.simpleMessage(
|
||||
"Средние показатели давления:",
|
||||
),
|
||||
"bodymass_chart": MessageLookupByLibrary.simpleMessage("График ИМТ"),
|
||||
"by_default": MessageLookupByLibrary.simpleMessage("По умолчанию"),
|
||||
"calories": MessageLookupByLibrary.simpleMessage("Калории"),
|
||||
"carb_type": MessageLookupByLibrary.simpleMessage("Мучное"),
|
||||
"carb_type_desc": MessageLookupByLibrary.simpleMessage(
|
||||
"Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.",
|
||||
),
|
||||
"carbohydrates": MessageLookupByLibrary.simpleMessage("Углеводы"),
|
||||
"chat": MessageLookupByLibrary.simpleMessage("Чат"),
|
||||
"chest": MessageLookupByLibrary.simpleMessage("Грудь"),
|
||||
"chest_circumference": MessageLookupByLibrary.simpleMessage("Обхват груди"),
|
||||
"choose_diet_duration": MessageLookupByLibrary.simpleMessage(
|
||||
"Выберите срок диеты",
|
||||
),
|
||||
"choose_plan_duration": MessageLookupByLibrary.simpleMessage(
|
||||
"Выберите срок плана",
|
||||
),
|
||||
"choose_start_diet_date": MessageLookupByLibrary.simpleMessage(
|
||||
"Выберите дату старты диеты. Она отобразиться у Вас в дневнике",
|
||||
),
|
||||
"circumference_empty": MessageLookupByLibrary.simpleMessage(
|
||||
"Все замеры обязательны к заполнению",
|
||||
),
|
||||
"circumference_is_not_a_number": MessageLookupByLibrary.simpleMessage(
|
||||
"Все замеры должны быть числами",
|
||||
),
|
||||
"circumferences_chart": MessageLookupByLibrary.simpleMessage(
|
||||
"График обхватов",
|
||||
),
|
||||
"city": MessageLookupByLibrary.simpleMessage("Город"),
|
||||
"city_of_living": MessageLookupByLibrary.simpleMessage("Город проживания"),
|
||||
"comfort": MessageLookupByLibrary.simpleMessage("Комфорт"),
|
||||
"comment": MessageLookupByLibrary.simpleMessage("комментарий"),
|
||||
"comments": MessageLookupByLibrary.simpleMessage("Комментарии"),
|
||||
"comments_plural": MessageLookupByLibrary.simpleMessage("комментариев"),
|
||||
"complain": MessageLookupByLibrary.simpleMessage("Пожаловаться"),
|
||||
"continueButton": MessageLookupByLibrary.simpleMessage("Продолжить"),
|
||||
"continue_": MessageLookupByLibrary.simpleMessage("Продолжить"),
|
||||
"copied": MessageLookupByLibrary.simpleMessage("cкопирован"),
|
||||
"copy_link": MessageLookupByLibrary.simpleMessage("Скопировать ссылку"),
|
||||
"country": MessageLookupByLibrary.simpleMessage("Страна"),
|
||||
"create_diet": MessageLookupByLibrary.simpleMessage("Создание диеты"),
|
||||
"customer_price": MessageLookupByLibrary.simpleMessage(
|
||||
"Цена для покупателя",
|
||||
),
|
||||
"date_of_birth": MessageLookupByLibrary.simpleMessage("Дата рождения"),
|
||||
"day": MessageLookupByLibrary.simpleMessage("День"),
|
||||
"days": MessageLookupByLibrary.simpleMessage("дней"),
|
||||
"deleted": MessageLookupByLibrary.simpleMessage("Удалена"),
|
||||
"diary": MessageLookupByLibrary.simpleMessage("Дневник"),
|
||||
"diet": MessageLookupByLibrary.simpleMessage("Диета"),
|
||||
"diet_author": MessageLookupByLibrary.simpleMessage("Автор диеты"),
|
||||
"diet_plan": MessageLookupByLibrary.simpleMessage("План диеты"),
|
||||
"diet_state": MessageLookupByLibrary.simpleMessage("Статус диеты"),
|
||||
"diet_type": MessageLookupByLibrary.simpleMessage("Тип диеты"),
|
||||
"do_you_have_hypertension_tendency": MessageLookupByLibrary.simpleMessage(
|
||||
"Есть ли у вас гипертония?",
|
||||
),
|
||||
"do_you_have_hypotension_tendency": MessageLookupByLibrary.simpleMessage(
|
||||
"Есть ли у вас гипотония?",
|
||||
),
|
||||
"do_you_take_pills": MessageLookupByLibrary.simpleMessage(
|
||||
"Принимаете ли вы лекарства или бады?",
|
||||
),
|
||||
"down_pressure": MessageLookupByLibrary.simpleMessage(
|
||||
"Нижние показатели давления:",
|
||||
),
|
||||
"draft": MessageLookupByLibrary.simpleMessage("Черновик"),
|
||||
"drafted": MessageLookupByLibrary.simpleMessage("Черновики"),
|
||||
"eating_behavior": MessageLookupByLibrary.simpleMessage(
|
||||
"Пищевое поведение",
|
||||
),
|
||||
"eating_behavior_type": MessageLookupByLibrary.simpleMessage(
|
||||
"Формирование пищевого поведения",
|
||||
),
|
||||
"eating_behavior_type_desc": MessageLookupByLibrary.simpleMessage(
|
||||
"Формирование здорового пищевого поведения — это процесс, который включает в себя изменение привычек и отношения к еде. Он направлен на то, чтобы помочь людям научиться правильно питаться и поддерживать здоровый образ жизни.Основные принципы формирования здорового пищевого поведения: \n● Разнообразие продуктов; \n● Сбалансированный рацион; \n● Питьевой режим; \n● Ограничение вредных продуктов; \n● Регулярные приёмы пищи; \n● Умеренность;Осознанное питание; \n● Физическая активность;",
|
||||
),
|
||||
"economy": MessageLookupByLibrary.simpleMessage("Эконом"),
|
||||
"edit": MessageLookupByLibrary.simpleMessage("Редактировать"),
|
||||
"email": MessageLookupByLibrary.simpleMessage("Электронная почта"),
|
||||
"email_empty": MessageLookupByLibrary.simpleMessage(
|
||||
"Необходимо ввести адрес электронной почты",
|
||||
),
|
||||
"email_invalid": MessageLookupByLibrary.simpleMessage(
|
||||
"Некорректный адрес электронной почты",
|
||||
),
|
||||
"enter_additional_information": MessageLookupByLibrary.simpleMessage(
|
||||
"Введите любую дополнительную информацию на своё усмотрение:",
|
||||
),
|
||||
"enter_diet_description": MessageLookupByLibrary.simpleMessage(
|
||||
"Введите описание диеты",
|
||||
),
|
||||
"enter_diet_name": MessageLookupByLibrary.simpleMessage(
|
||||
"Выведите название диеты",
|
||||
),
|
||||
"enter_information": MessageLookupByLibrary.simpleMessage(
|
||||
"Введите информацию...",
|
||||
),
|
||||
"exit": MessageLookupByLibrary.simpleMessage("Выйти"),
|
||||
"expand": MessageLookupByLibrary.simpleMessage("Показать полностью"),
|
||||
"fats": MessageLookupByLibrary.simpleMessage("Жиры"),
|
||||
"favorite_foods": MessageLookupByLibrary.simpleMessage("Любимые продукты"),
|
||||
"favorite_unfavorite_foods": MessageLookupByLibrary.simpleMessage(
|
||||
"Ваши любимые/нелюбимые продукты питания?",
|
||||
),
|
||||
"female": MessageLookupByLibrary.simpleMessage("Женский"),
|
||||
"fill_in_information": MessageLookupByLibrary.simpleMessage(
|
||||
"Заполните информацию:",
|
||||
),
|
||||
"finish": MessageLookupByLibrary.simpleMessage("Завершить"),
|
||||
"first_name": MessageLookupByLibrary.simpleMessage("Имя"),
|
||||
"first_name_empty": MessageLookupByLibrary.simpleMessage(
|
||||
"Имя обязательно для заполнения",
|
||||
),
|
||||
"first_name_short": MessageLookupByLibrary.simpleMessage(
|
||||
"В имени должен быть минимум 1 символ",
|
||||
),
|
||||
"fit_type": MessageLookupByLibrary.simpleMessage("Поддержание формы"),
|
||||
"fit_type_desc": MessageLookupByLibrary.simpleMessage(
|
||||
"План питания для поддержания формы сбалансированный и разнообразный, чтобы обеспечить организм всеми необходимыми питательными веществами. Общие принципы: \n● Дробное питание; \n● Разнообразие продуктов; \n● Контроль порций; \n● Питьевой режим; \n● Ограничение вредных продуктов;",
|
||||
),
|
||||
"fitness": MessageLookupByLibrary.simpleMessage("Поддержка формы"),
|
||||
"from": MessageLookupByLibrary.simpleMessage("от"),
|
||||
"from_a_to_z": MessageLookupByLibrary.simpleMessage("Название: А-Я"),
|
||||
"from_z_to_a": MessageLookupByLibrary.simpleMessage("Название: Я-А"),
|
||||
"gallery": MessageLookupByLibrary.simpleMessage("Галерея"),
|
||||
"gr": MessageLookupByLibrary.simpleMessage("гр"),
|
||||
"grams": MessageLookupByLibrary.simpleMessage("гр"),
|
||||
"height_measurements": MessageLookupByLibrary.simpleMessage("Замер роста"),
|
||||
"hide": MessageLookupByLibrary.simpleMessage("Свернуть"),
|
||||
"hip_circumference": MessageLookupByLibrary.simpleMessage("Обхват бедер"),
|
||||
"hips": MessageLookupByLibrary.simpleMessage("Бедра"),
|
||||
"in_progress": MessageLookupByLibrary.simpleMessage("В процессе"),
|
||||
"inactive_diet_state": MessageLookupByLibrary.simpleMessage("Черновик"),
|
||||
"infoAboutOrder": MessageLookupByLibrary.simpleMessage(
|
||||
"Информация о заказе",
|
||||
),
|
||||
"initial_measurements": MessageLookupByLibrary.simpleMessage(
|
||||
"Первичные замеры",
|
||||
),
|
||||
"label": MessageLookupByLibrary.simpleMessage("Название"),
|
||||
"language": MessageLookupByLibrary.simpleMessage("Язык"),
|
||||
"last_name": MessageLookupByLibrary.simpleMessage("Фамилия"),
|
||||
"last_name_empty": MessageLookupByLibrary.simpleMessage(
|
||||
"Фамилия обязательна для заполнения",
|
||||
),
|
||||
"last_name_short": MessageLookupByLibrary.simpleMessage(
|
||||
"В фамилии должен быть минимум 6 символов",
|
||||
),
|
||||
"lbl_english": MessageLookupByLibrary.simpleMessage("English"),
|
||||
"light_type": MessageLookupByLibrary.simpleMessage("Легкая"),
|
||||
"light_type_desc": MessageLookupByLibrary.simpleMessage(
|
||||
"С некоторыми видами диет можно употреблять алкоголь в умеренных количествах. Рекомендуется совмещать вместе с низкокалорийными закусками.",
|
||||
),
|
||||
"link_was_copied": MessageLookupByLibrary.simpleMessage(
|
||||
"Ссылка успешно скопирована",
|
||||
),
|
||||
"loading": MessageLookupByLibrary.simpleMessage("Загрузка..."),
|
||||
"mail_was_copied": MessageLookupByLibrary.simpleMessage(
|
||||
"Почта успешно скопирована",
|
||||
),
|
||||
"male": MessageLookupByLibrary.simpleMessage("Мужской"),
|
||||
"meal": MessageLookupByLibrary.simpleMessage("Приём пищи"),
|
||||
"meat": MessageLookupByLibrary.simpleMessage("Мясо"),
|
||||
"meat_type": MessageLookupByLibrary.simpleMessage("С мясом"),
|
||||
"meat_type_desc": MessageLookupByLibrary.simpleMessage(
|
||||
"Диета с мясом полезная и сбалансированная. Включает в себя нежирное мясо: курицу, индейку, кролика или говядину. В сочетании с овощами получаете все необходимые витамины и минералы. Не забывайте о порциях и старайтесь есть медленно, прислушиваясь к своему организму.",
|
||||
),
|
||||
"milliliter": MessageLookupByLibrary.simpleMessage("мл"),
|
||||
"minimal_customer_price": MessageLookupByLibrary.simpleMessage(
|
||||
"Минимальная цена для покупателя: 1200₽",
|
||||
),
|
||||
"most_new": MessageLookupByLibrary.simpleMessage("Самые новые"),
|
||||
"most_old": MessageLookupByLibrary.simpleMessage("Самые старые"),
|
||||
"msg_network_err": MessageLookupByLibrary.simpleMessage("Сетевая ошибка!"),
|
||||
"msg_something_went_wrong": MessageLookupByLibrary.simpleMessage(
|
||||
"Что то пошло не так!",
|
||||
),
|
||||
"my_blog": MessageLookupByLibrary.simpleMessage("Мой блог"),
|
||||
"my_diets": MessageLookupByLibrary.simpleMessage("Мои диеты"),
|
||||
"my_plans": MessageLookupByLibrary.simpleMessage("Мои планы"),
|
||||
"my_settings": MessageLookupByLibrary.simpleMessage("Мои настройки"),
|
||||
"name_empty": MessageLookupByLibrary.simpleMessage(
|
||||
"Имя пользователя обязательно для заполнения",
|
||||
),
|
||||
"name_short": MessageLookupByLibrary.simpleMessage(
|
||||
"В имени пользователя должно быть минимум 6 символов",
|
||||
),
|
||||
"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("Ваш вес:"),
|
||||
static _notInlinedMessages(_) => <String, Function> {
|
||||
"Moscow" : MessageLookupByLibrary.simpleMessage("Москва"),
|
||||
"Russia" : MessageLookupByLibrary.simpleMessage("Россия"),
|
||||
"about_diet" : MessageLookupByLibrary.simpleMessage("Об этой диете"),
|
||||
"about_plan" : MessageLookupByLibrary.simpleMessage("Об этом плане"),
|
||||
"active" : MessageLookupByLibrary.simpleMessage("Активен"),
|
||||
"active_diet_state" : MessageLookupByLibrary.simpleMessage("Активная"),
|
||||
"add" : MessageLookupByLibrary.simpleMessage("Добавить"),
|
||||
"add_cover" : MessageLookupByLibrary.simpleMessage("Добавьте обложку"),
|
||||
"add_measure" : MessageLookupByLibrary.simpleMessage("Добавить замер"),
|
||||
"adding_plan" : MessageLookupByLibrary.simpleMessage("Добавление плана"),
|
||||
"additional_information" : MessageLookupByLibrary.simpleMessage("Дополнительная информация"),
|
||||
"admin" : MessageLookupByLibrary.simpleMessage("Админ"),
|
||||
"all" : MessageLookupByLibrary.simpleMessage("Все"),
|
||||
"allergies_presence" : MessageLookupByLibrary.simpleMessage("Наличие аллергии"),
|
||||
"another_form_of_comment" : MessageLookupByLibrary.simpleMessage("комментария"),
|
||||
"another_form_of_day" : MessageLookupByLibrary.simpleMessage("дня"),
|
||||
"answer" : MessageLookupByLibrary.simpleMessage("Ответить"),
|
||||
"archived" : MessageLookupByLibrary.simpleMessage("В архиве"),
|
||||
"authentificate" : MessageLookupByLibrary.simpleMessage("Войти"),
|
||||
"average_pressure" : MessageLookupByLibrary.simpleMessage("Средние показатели давления:"),
|
||||
"bodymass_chart" : MessageLookupByLibrary.simpleMessage("График ИМТ"),
|
||||
"by_default" : MessageLookupByLibrary.simpleMessage("По умолчанию"),
|
||||
"calories" : MessageLookupByLibrary.simpleMessage("Калории"),
|
||||
"carb_type" : MessageLookupByLibrary.simpleMessage("Мучное"),
|
||||
"carb_type_desc" : MessageLookupByLibrary.simpleMessage("Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы."),
|
||||
"carbohydrates" : MessageLookupByLibrary.simpleMessage("Углеводы"),
|
||||
"chat" : MessageLookupByLibrary.simpleMessage("Чат"),
|
||||
"chest" : MessageLookupByLibrary.simpleMessage("Грудь"),
|
||||
"chest_circumference" : MessageLookupByLibrary.simpleMessage("Обхват груди"),
|
||||
"choose_diet_duration" : MessageLookupByLibrary.simpleMessage("Выберите срок диеты"),
|
||||
"choose_plan_duration" : MessageLookupByLibrary.simpleMessage("Выберите срок плана"),
|
||||
"choose_start_diet_date" : MessageLookupByLibrary.simpleMessage("Выберите дату старты диеты. Она отобразиться у Вас в дневнике"),
|
||||
"circumference_empty" : MessageLookupByLibrary.simpleMessage("Все замеры обязательны к заполнению"),
|
||||
"circumference_is_not_a_number" : MessageLookupByLibrary.simpleMessage("Все замеры должны быть числами"),
|
||||
"circumferences_chart" : MessageLookupByLibrary.simpleMessage("График обхватов"),
|
||||
"city" : MessageLookupByLibrary.simpleMessage("Город"),
|
||||
"city_of_living" : MessageLookupByLibrary.simpleMessage("Город проживания"),
|
||||
"comfort" : MessageLookupByLibrary.simpleMessage("Комфорт"),
|
||||
"comment" : MessageLookupByLibrary.simpleMessage("комментарий"),
|
||||
"comment_hint" : MessageLookupByLibrary.simpleMessage("Комментарий"),
|
||||
"comments" : MessageLookupByLibrary.simpleMessage("Комментарии"),
|
||||
"comments_plural" : MessageLookupByLibrary.simpleMessage("комментариев"),
|
||||
"complain" : MessageLookupByLibrary.simpleMessage("Пожаловаться"),
|
||||
"continueButton" : MessageLookupByLibrary.simpleMessage("Продолжить"),
|
||||
"continue_" : MessageLookupByLibrary.simpleMessage("Продолжить"),
|
||||
"copied" : MessageLookupByLibrary.simpleMessage("cкопирован"),
|
||||
"copy_link" : MessageLookupByLibrary.simpleMessage("Скопировать ссылку"),
|
||||
"country" : MessageLookupByLibrary.simpleMessage("Страна"),
|
||||
"create_diet" : MessageLookupByLibrary.simpleMessage("Создание диеты"),
|
||||
"customer_price" : MessageLookupByLibrary.simpleMessage("Цена для покупателя"),
|
||||
"date_of_birth" : MessageLookupByLibrary.simpleMessage("Дата рождения"),
|
||||
"day" : MessageLookupByLibrary.simpleMessage("День"),
|
||||
"days" : MessageLookupByLibrary.simpleMessage("дней"),
|
||||
"deleted" : MessageLookupByLibrary.simpleMessage("Удалена"),
|
||||
"diary" : MessageLookupByLibrary.simpleMessage("Дневник"),
|
||||
"diet" : MessageLookupByLibrary.simpleMessage("Диета"),
|
||||
"diet_author" : MessageLookupByLibrary.simpleMessage("Автор диеты"),
|
||||
"diet_plan" : MessageLookupByLibrary.simpleMessage("План диеты"),
|
||||
"diet_state" : MessageLookupByLibrary.simpleMessage("Статус диеты"),
|
||||
"diet_type" : MessageLookupByLibrary.simpleMessage("Тип диеты"),
|
||||
"do_you_have_hypertension_tendency" : MessageLookupByLibrary.simpleMessage("Есть ли у вас гипертония?"),
|
||||
"do_you_have_hypotension_tendency" : MessageLookupByLibrary.simpleMessage("Есть ли у вас гипотония?"),
|
||||
"do_you_take_pills" : MessageLookupByLibrary.simpleMessage("Принимаете ли вы лекарства или бады?"),
|
||||
"down_pressure" : MessageLookupByLibrary.simpleMessage("Нижние показатели давления:"),
|
||||
"draft" : MessageLookupByLibrary.simpleMessage("Черновик"),
|
||||
"drafted" : MessageLookupByLibrary.simpleMessage("Черновики"),
|
||||
"eating_behavior" : MessageLookupByLibrary.simpleMessage("Пищевое поведение"),
|
||||
"eating_behavior_type" : MessageLookupByLibrary.simpleMessage("Формирование пищевого поведения"),
|
||||
"eating_behavior_type_desc" : MessageLookupByLibrary.simpleMessage("Формирование здорового пищевого поведения — это процесс, который включает в себя изменение привычек и отношения к еде. Он направлен на то, чтобы помочь людям научиться правильно питаться и поддерживать здоровый образ жизни.Основные принципы формирования здорового пищевого поведения: \n● Разнообразие продуктов; \n● Сбалансированный рацион; \n● Питьевой режим; \n● Ограничение вредных продуктов; \n● Регулярные приёмы пищи; \n● Умеренность;Осознанное питание; \n● Физическая активность;"),
|
||||
"economy" : MessageLookupByLibrary.simpleMessage("Эконом"),
|
||||
"edit" : MessageLookupByLibrary.simpleMessage("Редактировать"),
|
||||
"email" : MessageLookupByLibrary.simpleMessage("Электронная почта"),
|
||||
"email_empty" : MessageLookupByLibrary.simpleMessage("Необходимо ввести адрес электронной почты"),
|
||||
"email_invalid" : MessageLookupByLibrary.simpleMessage("Некорректный адрес электронной почты"),
|
||||
"enter_additional_information" : MessageLookupByLibrary.simpleMessage("Введите любую дополнительную информацию на своё усмотрение:"),
|
||||
"enter_diet_description" : MessageLookupByLibrary.simpleMessage("Введите описание диеты"),
|
||||
"enter_diet_name" : MessageLookupByLibrary.simpleMessage("Выведите название диеты"),
|
||||
"enter_information" : MessageLookupByLibrary.simpleMessage("Введите информацию..."),
|
||||
"exit" : MessageLookupByLibrary.simpleMessage("Выйти"),
|
||||
"expand" : MessageLookupByLibrary.simpleMessage("Показать полностью"),
|
||||
"fats" : MessageLookupByLibrary.simpleMessage("Жиры"),
|
||||
"favorite_foods" : MessageLookupByLibrary.simpleMessage("Любимые продукты"),
|
||||
"favorite_unfavorite_foods" : MessageLookupByLibrary.simpleMessage("Ваши любимые/нелюбимые продукты питания?"),
|
||||
"female" : MessageLookupByLibrary.simpleMessage("Женский"),
|
||||
"fill_in_information" : MessageLookupByLibrary.simpleMessage("Заполните информацию:"),
|
||||
"finish" : MessageLookupByLibrary.simpleMessage("Завершить"),
|
||||
"first_name" : MessageLookupByLibrary.simpleMessage("Имя"),
|
||||
"first_name_empty" : MessageLookupByLibrary.simpleMessage("Имя обязательно для заполнения"),
|
||||
"first_name_short" : MessageLookupByLibrary.simpleMessage("В имени должен быть минимум 1 символ"),
|
||||
"fit_type" : MessageLookupByLibrary.simpleMessage("Поддержание формы"),
|
||||
"fit_type_desc" : MessageLookupByLibrary.simpleMessage("План питания для поддержания формы сбалансированный и разнообразный, чтобы обеспечить организм всеми необходимыми питательными веществами. Общие принципы: \n● Дробное питание; \n● Разнообразие продуктов; \n● Контроль порций; \n● Питьевой режим; \n● Ограничение вредных продуктов;"),
|
||||
"fitness" : MessageLookupByLibrary.simpleMessage("Поддержка формы"),
|
||||
"from" : MessageLookupByLibrary.simpleMessage("от"),
|
||||
"from_a_to_z" : MessageLookupByLibrary.simpleMessage("Название: А-Я"),
|
||||
"from_z_to_a" : MessageLookupByLibrary.simpleMessage("Название: Я-А"),
|
||||
"gallery" : MessageLookupByLibrary.simpleMessage("Галерея"),
|
||||
"gr" : MessageLookupByLibrary.simpleMessage("гр"),
|
||||
"grams" : MessageLookupByLibrary.simpleMessage("гр"),
|
||||
"height_measurements" : MessageLookupByLibrary.simpleMessage("Замер роста"),
|
||||
"hide" : MessageLookupByLibrary.simpleMessage("Свернуть"),
|
||||
"hip_circumference" : MessageLookupByLibrary.simpleMessage("Обхват бедер"),
|
||||
"hips" : MessageLookupByLibrary.simpleMessage("Бедра"),
|
||||
"in_progress" : MessageLookupByLibrary.simpleMessage("В процессе"),
|
||||
"inactive_diet_state" : MessageLookupByLibrary.simpleMessage("Черновик"),
|
||||
"infoAboutOrder" : MessageLookupByLibrary.simpleMessage("Информация о заказе"),
|
||||
"initial_measurements" : MessageLookupByLibrary.simpleMessage("Первичные замеры"),
|
||||
"label" : MessageLookupByLibrary.simpleMessage("Название"),
|
||||
"language" : MessageLookupByLibrary.simpleMessage("Язык"),
|
||||
"last_name" : MessageLookupByLibrary.simpleMessage("Фамилия"),
|
||||
"last_name_empty" : MessageLookupByLibrary.simpleMessage("Фамилия обязательна для заполнения"),
|
||||
"last_name_short" : MessageLookupByLibrary.simpleMessage("В фамилии должен быть минимум 6 символов"),
|
||||
"lbl_english" : MessageLookupByLibrary.simpleMessage("English"),
|
||||
"light_type" : MessageLookupByLibrary.simpleMessage("Легкая"),
|
||||
"light_type_desc" : MessageLookupByLibrary.simpleMessage("С некоторыми видами диет можно употреблять алкоголь в умеренных количествах. Рекомендуется совмещать вместе с низкокалорийными закусками."),
|
||||
"link_was_copied" : MessageLookupByLibrary.simpleMessage("Ссылка успешно скопирована"),
|
||||
"loading" : MessageLookupByLibrary.simpleMessage("Загрузка..."),
|
||||
"mail_was_copied" : MessageLookupByLibrary.simpleMessage("Почта успешно скопирована"),
|
||||
"male" : MessageLookupByLibrary.simpleMessage("Мужской"),
|
||||
"meal" : MessageLookupByLibrary.simpleMessage("Приём пищи"),
|
||||
"meat" : MessageLookupByLibrary.simpleMessage("Мясо"),
|
||||
"meat_type" : MessageLookupByLibrary.simpleMessage("С мясом"),
|
||||
"meat_type_desc" : MessageLookupByLibrary.simpleMessage("Диета с мясом полезная и сбалансированная. Включает в себя нежирное мясо: курицу, индейку, кролика или говядину. В сочетании с овощами получаете все необходимые витамины и минералы. Не забывайте о порциях и старайтесь есть медленно, прислушиваясь к своему организму."),
|
||||
"milliliter" : MessageLookupByLibrary.simpleMessage("мл"),
|
||||
"minimal_customer_price" : MessageLookupByLibrary.simpleMessage("Минимальная цена для покупателя: 1200₽"),
|
||||
"most_new" : MessageLookupByLibrary.simpleMessage("Самые новые"),
|
||||
"most_old" : MessageLookupByLibrary.simpleMessage("Самые старые"),
|
||||
"msg_network_err" : MessageLookupByLibrary.simpleMessage("Сетевая ошибка!"),
|
||||
"msg_something_went_wrong" : MessageLookupByLibrary.simpleMessage("Что то пошло не так!"),
|
||||
"my_blog" : MessageLookupByLibrary.simpleMessage("Мой блог"),
|
||||
"my_diets" : MessageLookupByLibrary.simpleMessage("Мои диеты"),
|
||||
"my_plans" : MessageLookupByLibrary.simpleMessage("Мои планы"),
|
||||
"my_settings" : MessageLookupByLibrary.simpleMessage("Мои настройки"),
|
||||
"name_empty" : MessageLookupByLibrary.simpleMessage("Имя пользователя обязательно для заполнения"),
|
||||
"name_short" : MessageLookupByLibrary.simpleMessage("В имени пользователя должно быть минимум 6 символов"),
|
||||
"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
@@ -466,6 +466,7 @@
|
||||
"weAreLookingFor": "Мы собираем самое большое сообщество людей, желающих вести здоровый образ жизни!",
|
||||
"letsInvite": "Давайте пригласим близких в итвио",
|
||||
"eatvioIsAHelthyHabbit": "Итвио — это полезная привычка",
|
||||
"invite": "Пригласить"
|
||||
"invite": "Пригласить",
|
||||
"comment_hint": "Комментарий"
|
||||
}
|
||||
|
||||
|
||||
@@ -215,5 +215,6 @@
|
||||
"from": "от",
|
||||
"user_data": "Данные пользователя",
|
||||
"infoAboutOrder": "Информация о заказе",
|
||||
"notifications": "Уведомления"
|
||||
"notifications": "Уведомления",
|
||||
"comment_hint": "Комментарий"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: eatvio_app
|
||||
description: A new Flutter project.
|
||||
version: 1.3.11+92
|
||||
version: 1.3.12+93
|
||||
publish_to: 'none'
|
||||
|
||||
environment:
|
||||
|
||||
@@ -12,10 +12,7 @@ void main() {
|
||||
|
||||
test('createRoutes содержит маршрут authScreen', () {
|
||||
final routes = AuthRoutes.createRoutes();
|
||||
final authRoute = routes.firstWhere(
|
||||
(route) => route is GoRoute && route.path == '/auth-screen',
|
||||
orElse: () => throw Exception('Route not found'),
|
||||
);
|
||||
final authRoute = routes.firstWhere((route) => route is GoRoute && route.path == '/auth-screen', orElse: () => throw Exception('Route not found'));
|
||||
expect(authRoute, isA<GoRoute>());
|
||||
});
|
||||
|
||||
@@ -67,10 +64,7 @@ void main() {
|
||||
|
||||
test('createRoutes содержит маршрут verifyScreen', () {
|
||||
final routes = AuthRoutes.createRoutes();
|
||||
final verifyRoute = routes.firstWhere(
|
||||
(route) => route is GoRoute && route.path == '/verify-screen',
|
||||
orElse: () => throw Exception('Route not found'),
|
||||
);
|
||||
final verifyRoute = routes.firstWhere((route) => route is GoRoute && route.path == '/verify-screen', orElse: () => throw Exception('Route not found'));
|
||||
expect(verifyRoute, isA<GoRoute>());
|
||||
});
|
||||
|
||||
@@ -92,44 +86,25 @@ void main() {
|
||||
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 данные', () {
|
||||
final routes = AuthRoutes.createRoutes();
|
||||
final authRoute = routes.first as GoRoute;
|
||||
final forgetPasswordRoute = authRoute.routes.firstWhere(
|
||||
(route) => route is GoRoute && route.path == 'forget-password-screen',
|
||||
orElse: () => throw Exception('Route not found'),
|
||||
) as GoRoute;
|
||||
final forgetPasswordRoute =
|
||||
authRoute.routes.firstWhere((route) => route is GoRoute && route.path == 'forget-password-screen', orElse: () => throw Exception('Route not found'))
|
||||
as GoRoute;
|
||||
expect(forgetPasswordRoute.builder, isNotNull);
|
||||
});
|
||||
|
||||
test('verifyScreen builder обрабатывает extra данные', () {
|
||||
final routes = AuthRoutes.createRoutes();
|
||||
final verifyRoute = routes.firstWhere(
|
||||
(route) => route is GoRoute && route.path == '/verify-screen',
|
||||
) as GoRoute;
|
||||
final verifyRoute = routes.firstWhere((route) => route is GoRoute && route.path == '/verify-screen') as GoRoute;
|
||||
expect(verifyRoute.builder, isNotNull);
|
||||
});
|
||||
|
||||
test('registerViaSocialScreen builder обрабатывает extra данные', () {
|
||||
final routes = AuthRoutes.createRoutes();
|
||||
final registerViaSocialRoute = routes.firstWhere(
|
||||
(route) => route is GoRoute && route.path == '/register-via-social-screen',
|
||||
) as GoRoute;
|
||||
final registerViaSocialRoute = routes.firstWhere((route) => route is GoRoute && route.path == '/register-via-social-screen') as GoRoute;
|
||||
expect(registerViaSocialRoute.builder, isNotNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@ void main() {
|
||||
group('TokenStorage', () {
|
||||
setUp(() async {
|
||||
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 {
|
||||
@@ -66,6 +70,10 @@ void main() {
|
||||
group('SocketIdStorage', () {
|
||||
setUp(() async {
|
||||
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 {
|
||||
|
||||
@@ -29,6 +29,7 @@ void main() {
|
||||
const MarketingBottomSheet(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(MarketingBottomSheet), findsOneWidget);
|
||||
});
|
||||
@@ -59,6 +60,7 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('Show'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ void main() {
|
||||
|
||||
testWidgets('returns SizedBox.shrink when option is null', (tester) async {
|
||||
await tester.pumpWidget(createTestWidget(const OptionWidget(option: null, duration: 7)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(OptionWidget), findsOneWidget);
|
||||
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);
|
||||
|
||||
await tester.pumpWidget(createTestWidget(const OptionWidget(option: option, duration: 7)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(OptionWidget), 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);
|
||||
|
||||
await tester.pumpWidget(createTestWidget(const OptionWidget(option: option, duration: 7)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
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);
|
||||
|
||||
await tester.pumpWidget(createTestWidget(const OptionWidget(option: option, duration: 7, disableDuration: true)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(OptionWidget), findsOneWidget);
|
||||
});
|
||||
|
||||
@@ -31,6 +31,7 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('Show Error'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
@@ -68,6 +69,7 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('Show Error'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
@@ -103,6 +105,7 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('Show Error'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
@@ -134,6 +137,7 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('Show Error'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
|
||||
@@ -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/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
||||
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/chat/presentation/bloc/chats_cubit/chats_cubit.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';
|
||||
|
||||
@GenerateNiceMocks([MockSpec<NotificationsCubit>(), MockSpec<ChatsCubit>()])
|
||||
@GenerateNiceMocks([MockSpec<NotificationsCubit>(), MockSpec<ChatsCubit>(), MockSpec<ProfileAvatarCubit>()])
|
||||
void main() {
|
||||
group('BottomNavigationBarWidget smoke', () {
|
||||
setUpAll(() async {
|
||||
@@ -86,6 +88,7 @@ void main() {
|
||||
mockNetworkImagesFor(() async {
|
||||
final notificationsCubit = MockNotificationsCubit();
|
||||
final chatsCubit = MockChatsCubit();
|
||||
final profileAvatarCubit = MockProfileAvatarCubit();
|
||||
|
||||
when(notificationsCubit.state).thenReturn(const NotificationsState());
|
||||
when(notificationsCubit.stream).thenAnswer((_) => const Stream.empty());
|
||||
@@ -93,16 +96,20 @@ void main() {
|
||||
when(chatsCubit.state).thenReturn(ChatsState());
|
||||
when(chatsCubit.stream).thenAnswer((_) => const Stream.empty());
|
||||
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(
|
||||
MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<NotificationsCubit>.value(value: notificationsCubit),
|
||||
BlocProvider<ChatsCubit>.value(value: chatsCubit),
|
||||
BlocProvider<ProfileAvatarCubit>.value(value: profileAvatarCubit),
|
||||
],
|
||||
child: wrap(
|
||||
const Scaffold(
|
||||
body: BottomNavigationBarWidget(profileModel: mockUserProfile, child: Text('Test Content')),
|
||||
body: BottomNavigationBarWidget(child: Text('Test Content')),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -117,6 +124,7 @@ void main() {
|
||||
mockNetworkImagesFor(() async {
|
||||
final notificationsCubit = MockNotificationsCubit();
|
||||
final chatsCubit = MockChatsCubit();
|
||||
final profileAvatarCubit = MockProfileAvatarCubit();
|
||||
|
||||
when(notificationsCubit.state).thenReturn(const NotificationsState());
|
||||
when(notificationsCubit.stream).thenAnswer((_) => const Stream.empty());
|
||||
@@ -124,16 +132,20 @@ void main() {
|
||||
when(chatsCubit.state).thenReturn(ChatsState());
|
||||
when(chatsCubit.stream).thenAnswer((_) => const Stream.empty());
|
||||
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(
|
||||
MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<NotificationsCubit>.value(value: notificationsCubit),
|
||||
BlocProvider<ChatsCubit>.value(value: chatsCubit),
|
||||
BlocProvider<ProfileAvatarCubit>.value(value: profileAvatarCubit),
|
||||
],
|
||||
child: wrap(
|
||||
const Scaffold(
|
||||
body: BottomNavigationBarWidget(profileModel: mockConsultantProfile, child: Text('Test Content')),
|
||||
body: BottomNavigationBarWidget(child: Text('Test Content')),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -34,6 +34,7 @@ void main() {
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(ChosenDietWidgetContent), findsOneWidget);
|
||||
expect(find.text('Test Diet'), findsOneWidget);
|
||||
@@ -50,6 +51,7 @@ void main() {
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(ChosenDietWidgetContent), findsOneWidget);
|
||||
});
|
||||
@@ -65,6 +67,7 @@ void main() {
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('My Diet'), findsOneWidget);
|
||||
});
|
||||
|
||||
@@ -37,6 +37,7 @@ void main() {
|
||||
planName: 'Test Plan',
|
||||
planDuration: 14,
|
||||
));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(ChosenPlanWidgetContent), findsOneWidget);
|
||||
expect(find.text('Test Plan'), findsOneWidget);
|
||||
@@ -48,6 +49,7 @@ void main() {
|
||||
planName: 'My Plan',
|
||||
planDuration: 7,
|
||||
));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(ChosenPlanWidgetContent), findsOneWidget);
|
||||
expect(find.textContaining('3'), findsOneWidget);
|
||||
@@ -60,6 +62,7 @@ void main() {
|
||||
planName: 'Long Plan',
|
||||
planDuration: 30,
|
||||
));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(ChosenPlanWidgetContent), findsOneWidget);
|
||||
expect(find.text('Long Plan'), findsOneWidget);
|
||||
|
||||
@@ -78,6 +78,7 @@ void main() {
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(CommentsWidget), findsOneWidget);
|
||||
});
|
||||
|
||||
@@ -63,6 +63,7 @@ void main() {
|
||||
|
||||
// Act
|
||||
await tester.pumpWidget(createTestWidget(const SizedBox(), mockCubit));
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('Show Bottom Sheet'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
@@ -107,6 +108,7 @@ void main() {
|
||||
|
||||
// Act
|
||||
await tester.pumpWidget(createTestWidgetError(const SizedBox(), mockCubit));
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('Show Bottom Sheet'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ void main() {
|
||||
home: ChangePasswordScreen(settingsEntity: settingsEntity),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(ChangePasswordScreen), findsOneWidget);
|
||||
expect(find.text('Смена пароля'), findsOneWidget);
|
||||
@@ -80,6 +81,7 @@ void main() {
|
||||
home: ChangePasswordScreen(settingsEntity: settingsEntity),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(AppBar), findsOneWidget);
|
||||
expect(find.text('Смена пароля'), findsOneWidget);
|
||||
|
||||
Reference in New Issue
Block a user