forked from eatvio/eatvio_app
merge upstream
This commit is contained in:
@@ -33,15 +33,16 @@ class AppConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Список глобальных BlocProvider'ов
|
/// Список глобальных BlocProvider'ов
|
||||||
|
/// Используются lazy: true для отложенной инициализации, чтобы не блокировать старт приложения
|
||||||
List<BlocProvider> get globalBlocProviders => [
|
List<BlocProvider> get globalBlocProviders => [
|
||||||
BlocProvider<NewMessagesCubit>(create: (context) => sl<NewMessagesCubit>()),
|
BlocProvider<NewMessagesCubit>(lazy: true, create: (context) => sl<NewMessagesCubit>()),
|
||||||
BlocProvider<NotificationsCubit>(create: (context) => sl<NotificationsCubit>()..getNotifications()),
|
BlocProvider<NotificationsCubit>(lazy: true, create: (context) => sl<NotificationsCubit>()),
|
||||||
BlocProvider<FeedCubit>(create: (context) => sl<FeedCubit>()..loadFeedBlogPostList()),
|
BlocProvider<FeedCubit>(lazy: true, create: (context) => sl<FeedCubit>()),
|
||||||
BlocProvider<ProfileAvatarCubit>(create: (context) => sl<ProfileAvatarCubit>()..getMyProfile()),
|
BlocProvider<ProfileAvatarCubit>(lazy: true, create: (context) => sl<ProfileAvatarCubit>()),
|
||||||
BlocProvider<ChatsCubit>(create: (context) => sl<ChatsCubit>()..getChatRoomsWithLoading()),
|
BlocProvider<ChatsCubit>(lazy: true, create: (context) => sl<ChatsCubit>()),
|
||||||
BlocProvider<ViewsCubit>(create: (context) => sl<ViewsCubit>()),
|
BlocProvider<ViewsCubit>(lazy: true, create: (context) => sl<ViewsCubit>()),
|
||||||
BlocProvider<InternetConnectionCubit>(lazy: false, create: (context) => sl<InternetConnectionCubit>()),
|
BlocProvider<InternetConnectionCubit>(lazy: false, create: (context) => sl<InternetConnectionCubit>()),
|
||||||
BlocProvider<MarketingCubit>(create: (context) => sl<MarketingCubit>()),
|
BlocProvider<MarketingCubit>(lazy: true, create: (context) => sl<MarketingCubit>()),
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Делегаты локализации
|
/// Делегаты локализации
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:developer';
|
||||||
import 'dart:io' as io;
|
import 'dart:io' as io;
|
||||||
import 'package:eatvio_app/core/app_config.dart';
|
import 'package:eatvio_app/core/app_config.dart';
|
||||||
import 'package:eatvio_app/core/env_config.dart';
|
import 'package:eatvio_app/core/env_config.dart';
|
||||||
@@ -37,9 +39,21 @@ class AppInitializer {
|
|||||||
|
|
||||||
/// Инициализация Firebase
|
/// Инициализация Firebase
|
||||||
static Future<void> _initializeFirebase() async {
|
static Future<void> _initializeFirebase() async {
|
||||||
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform).timeout(
|
||||||
|
const Duration(seconds: 10),
|
||||||
|
onTimeout: () {
|
||||||
|
throw TimeoutException('Firebase initialization timeout');
|
||||||
|
},
|
||||||
|
);
|
||||||
if (sl<AppConfig>().buildType == AppBuildType.prod) {
|
if (sl<AppConfig>().buildType == AppBuildType.prod) {
|
||||||
await FirebaseMessaging.instance.requestPermission();
|
try {
|
||||||
|
await FirebaseMessaging.instance.requestPermission().timeout(
|
||||||
|
const Duration(seconds: 5),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// Разрешение не критично, продолжаем работу
|
||||||
|
log('Failed to request Firebase messaging permission: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,13 +20,19 @@ class ExternalRegistrations {
|
|||||||
static void _registerDio() {
|
static void _registerDio() {
|
||||||
sl.registerLazySingleton(
|
sl.registerLazySingleton(
|
||||||
() =>
|
() =>
|
||||||
Dio(BaseOptions(baseUrl: sl<AppConfig>().baseUrl, headers: {'accept': 'application/json', 'content-type': 'application/json'}))
|
Dio(BaseOptions(
|
||||||
|
baseUrl: sl<AppConfig>().baseUrl,
|
||||||
|
headers: {'accept': 'application/json', 'content-type': 'application/json'},
|
||||||
|
connectTimeout: const Duration(seconds: 10),
|
||||||
|
receiveTimeout: const Duration(seconds: 10),
|
||||||
|
sendTimeout: const Duration(seconds: 10),
|
||||||
|
))
|
||||||
..interceptors.addAll([TokenInterceptor(), PhoneVerifyErrorInterceptor()]),
|
..interceptors.addAll([TokenInterceptor(), PhoneVerifyErrorInterceptor()]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Регистрация Prefs
|
/// Регистрация Prefs
|
||||||
static void _registerPrefs() {
|
static void _registerPrefs() {
|
||||||
sl.registerSingletonAsync<Prefs>(()async => Prefs(await SharedPreferences.getInstance()));
|
sl.registerSingletonAsync<Prefs>(() async => Prefs(await SharedPreferences.getInstance()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:eatvio_app/core/app_config.dart';
|
import 'package:eatvio_app/core/app_config.dart';
|
||||||
@@ -19,12 +20,25 @@ class FirebaseApi {
|
|||||||
String? token;
|
String? token;
|
||||||
|
|
||||||
if (await _isHuaweiDevice()) {
|
if (await _isHuaweiDevice()) {
|
||||||
token = await _getHuaweiPushTokenNative();
|
token = await _getHuaweiPushTokenNative().timeout(
|
||||||
|
const Duration(seconds: 5),
|
||||||
|
onTimeout: () => null,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
if ((sl<AppConfig>().buildType == AppBuildType.prod)) {
|
if ((sl<AppConfig>().buildType == AppBuildType.prod)) {
|
||||||
await _firebaseMessaging.requestPermission();
|
try {
|
||||||
|
await _firebaseMessaging.requestPermission().timeout(
|
||||||
|
const Duration(seconds: 5),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// Разрешение не критично, продолжаем
|
||||||
|
log('Failed to request Firebase messaging permission: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
token = await _firebaseMessaging.getToken();
|
token = await _firebaseMessaging.getToken().timeout(
|
||||||
|
const Duration(seconds: 5),
|
||||||
|
onTimeout: () => null,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
@@ -39,9 +53,15 @@ class FirebaseApi {
|
|||||||
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
|
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
|
||||||
'token': await getToken(),
|
'token': await getToken(),
|
||||||
},
|
},
|
||||||
|
).timeout(
|
||||||
|
const Duration(seconds: 10),
|
||||||
|
onTimeout: () {
|
||||||
|
throw TimeoutException('registerAnonymousPushToken timeout');
|
||||||
|
},
|
||||||
);
|
);
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
log(e.toString(), stackTrace: st);
|
log(e.toString(), stackTrace: st);
|
||||||
|
// Не пробрасываем ошибку дальше, так как это не критичная операция
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,9 +77,15 @@ class FirebaseApi {
|
|||||||
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
|
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
|
||||||
'token': await getToken(),
|
'token': await getToken(),
|
||||||
},
|
},
|
||||||
|
).timeout(
|
||||||
|
const Duration(seconds: 10),
|
||||||
|
onTimeout: () {
|
||||||
|
throw TimeoutException('registerPushToken timeout');
|
||||||
|
},
|
||||||
);
|
);
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
log(e.toString(), stackTrace: st);
|
log(e.toString(), stackTrace: st);
|
||||||
|
// Не пробрасываем ошибку дальше, так как это не критичная операция
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:eatvio_app/core/di/cubit_registrations.dart';
|
|||||||
import 'package:eatvio_app/core/di/external_registrations.dart';
|
import 'package:eatvio_app/core/di/external_registrations.dart';
|
||||||
import 'package:eatvio_app/core/di/repository_registrations.dart';
|
import 'package:eatvio_app/core/di/repository_registrations.dart';
|
||||||
import 'package:eatvio_app/core/di/service_registrations.dart';
|
import 'package:eatvio_app/core/di/service_registrations.dart';
|
||||||
|
import 'package:eatvio_app/core/prefs.dart';
|
||||||
import 'package:eatvio_app/core/websockets/ws_manager.dart';
|
import 'package:eatvio_app/core/websockets/ws_manager.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
|
|
||||||
@@ -18,6 +19,9 @@ Future<void> init({required AppBuildType buildType}) async {
|
|||||||
// Регистрация внешних зависимостей (Dio, etc.)
|
// Регистрация внешних зависимостей (Dio, etc.)
|
||||||
ExternalRegistrations.registerAll();
|
ExternalRegistrations.registerAll();
|
||||||
|
|
||||||
|
// Ожидание инициализации асинхронных зависимостей
|
||||||
|
await sl.getAsync<Prefs>();
|
||||||
|
|
||||||
// Регистрация сервисов
|
// Регистрация сервисов
|
||||||
ServiceRegistrations.registerAll();
|
ServiceRegistrations.registerAll();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'package:eatvio_app/core/firebase_api.dart';
|
|
||||||
import 'package:eatvio_app/core/locator_service.dart';
|
|
||||||
import 'package:eatvio_app/core/routing/route_constants.dart';
|
import 'package:eatvio_app/core/routing/route_constants.dart';
|
||||||
import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart';
|
import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart';
|
||||||
import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart';
|
import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart';
|
||||||
@@ -21,10 +19,8 @@ class AuthRoutes {
|
|||||||
path: RouteConstants.authScreen,
|
path: RouteConstants.authScreen,
|
||||||
name: 'authScreen',
|
name: 'authScreen',
|
||||||
builder: (context, state) => const AuthScreen(),
|
builder: (context, state) => const AuthScreen(),
|
||||||
redirect: (context, state) {
|
// registerAnonymousPushToken вызывается в AuthCubit.initializeUser()
|
||||||
sl<FirebaseApi>().registerAnonymousPushToken();
|
// и не должен блокировать навигацию
|
||||||
return null;
|
|
||||||
},
|
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(path: RouteConstants.registrationScreen, name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()),
|
GoRoute(path: RouteConstants.registrationScreen, name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()),
|
||||||
GoRoute(path: RouteConstants.registrationScreenV2, name: 'registrationScreenV2', builder: (context, state) => const RegistrationV2Page()),
|
GoRoute(path: RouteConstants.registrationScreenV2, name: 'registrationScreenV2', builder: (context, state) => const RegistrationV2Page()),
|
||||||
|
|||||||
@@ -23,14 +23,16 @@ class MainNavigationRoutes {
|
|||||||
return [
|
return [
|
||||||
ShellRoute(
|
ShellRoute(
|
||||||
builder: (context, state, child) {
|
builder: (context, state, child) {
|
||||||
return BlocBuilder<ProfileAvatarCubit, ProfileAvatarState>(
|
// Инициализация ProfileAvatarCubit при первом использовании ShellRoute
|
||||||
builder: (BuildContext context, ProfileAvatarState state) {
|
final profileAvatarCubit = context.read<ProfileAvatarCubit>();
|
||||||
if (state is ProfileAvatarStateLoaded) {
|
if (profileAvatarCubit.state is ProfileAvatarStateLoading) {
|
||||||
return BottomNavigationBarWidget(profileModel: state.profile, child: child);
|
// Вызываем getMyProfile() только если состояние loading (первый запуск)
|
||||||
}
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
return const SizedBox.shrink();
|
profileAvatarCubit.getMyProfile();
|
||||||
},
|
});
|
||||||
);
|
}
|
||||||
|
|
||||||
|
return BottomNavigationBarWidget(child: child);
|
||||||
},
|
},
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:eatvio_app/core/app_config.dart';
|
import 'package:eatvio_app/core/app_config.dart';
|
||||||
import 'package:eatvio_app/core/locator_service.dart';
|
import 'package:eatvio_app/core/locator_service.dart';
|
||||||
@@ -56,7 +57,8 @@ class OtherRoutes {
|
|||||||
builder: (context, state) => const WelcomeScreen(),
|
builder: (context, state) => const WelcomeScreen(),
|
||||||
redirect: (context, state) async {
|
redirect: (context, state) async {
|
||||||
try {
|
try {
|
||||||
final prefs = sl<Prefs>();
|
// Prefs уже инициализирован в locator_service, но на всякий случай проверяем
|
||||||
|
final prefs = await sl.getAsync<Prefs>();
|
||||||
final welcomeScreenWasShowed = prefs.getBool(PrefsKeys.welcomeScreenWasShowed) ?? false;
|
final welcomeScreenWasShowed = prefs.getBool(PrefsKeys.welcomeScreenWasShowed) ?? false;
|
||||||
if (!welcomeScreenWasShowed) {
|
if (!welcomeScreenWasShowed) {
|
||||||
return null;
|
return null;
|
||||||
@@ -74,7 +76,27 @@ class OtherRoutes {
|
|||||||
builder: (context, state) => const UpdateScreen(),
|
builder: (context, state) => const UpdateScreen(),
|
||||||
redirect: (context, state) async {
|
redirect: (context, state) async {
|
||||||
try {
|
try {
|
||||||
final versions = (await sl<Dio>().get('${sl<AppConfig>().baseUrl}/api/app_versions')).data;
|
// Проверка версии с таймаутом, чтобы не блокировать запуск приложения
|
||||||
|
final versions = await sl<Dio>()
|
||||||
|
.get('${sl<AppConfig>().baseUrl}/api/app_versions')
|
||||||
|
.timeout(
|
||||||
|
const Duration(seconds: 5),
|
||||||
|
onTimeout: () {
|
||||||
|
// При таймауте пропускаем проверку и идем дальше
|
||||||
|
throw TimeoutException('Version check timeout');
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.then((response) => response.data)
|
||||||
|
.catchError((e) {
|
||||||
|
// При любой ошибке пропускаем проверку версии
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (versions == null) {
|
||||||
|
// Если проверка не удалась, пропускаем экран обновления
|
||||||
|
return RouteConstants.welcomeScreen;
|
||||||
|
}
|
||||||
|
|
||||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||||
String version = packageInfo.version;
|
String version = packageInfo.version;
|
||||||
Version currentVersion = Version.parse(version);
|
Version currentVersion = Version.parse(version);
|
||||||
@@ -85,6 +107,7 @@ class OtherRoutes {
|
|||||||
return RouteConstants.welcomeScreen;
|
return RouteConstants.welcomeScreen;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// При любой ошибке пропускаем экран обновления
|
||||||
return RouteConstants.welcomeScreen;
|
return RouteConstants.welcomeScreen;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,9 +1,24 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
abstract class TokenStorage {
|
abstract class TokenStorage {
|
||||||
|
static SharedPreferences? _cachedPrefs;
|
||||||
|
|
||||||
|
static Future<SharedPreferences> _getPrefs() async {
|
||||||
|
_cachedPrefs ??= await SharedPreferences.getInstance();
|
||||||
|
return _cachedPrefs!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears the cached SharedPreferences instance.
|
||||||
|
/// This is primarily used for testing to ensure fresh state between tests.
|
||||||
|
@visibleForTesting
|
||||||
|
static void clearCache() {
|
||||||
|
_cachedPrefs = null;
|
||||||
|
}
|
||||||
|
|
||||||
static Future<bool> haveTokenInStorage() async {
|
static Future<bool> haveTokenInStorage() async {
|
||||||
final sharedPreferences = await SharedPreferences.getInstance();
|
|
||||||
try {
|
try {
|
||||||
|
final sharedPreferences = await _getPrefs();
|
||||||
if (sharedPreferences.getString('token') == null) {
|
if (sharedPreferences.getString('token') == null) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
@@ -15,25 +30,25 @@ abstract class TokenStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Future<String?> getTokenFromStorage() async {
|
static Future<String?> getTokenFromStorage() async {
|
||||||
final sharedPreferences = await SharedPreferences.getInstance();
|
final sharedPreferences = await _getPrefs();
|
||||||
return sharedPreferences.getString('token');
|
return sharedPreferences.getString('token');
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> saveTokenInStorage(String token) async {
|
static Future<void> saveTokenInStorage(String token) async {
|
||||||
final sharedPreferences = await SharedPreferences.getInstance();
|
final sharedPreferences = await _getPrefs();
|
||||||
await sharedPreferences.setString('token', token);
|
await sharedPreferences.setString('token', token);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> removeTokenInStorage() async {
|
static Future<void> removeTokenInStorage() async {
|
||||||
final sharedPreferences = await SharedPreferences.getInstance();
|
final sharedPreferences = await _getPrefs();
|
||||||
await sharedPreferences.remove('token');
|
await sharedPreferences.remove('token');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class SocketIdStorage {
|
abstract class SocketIdStorage {
|
||||||
static Future<bool> haveSocketIdInStorage() async {
|
static Future<bool> haveSocketIdInStorage() async {
|
||||||
final sharedPreferences = await SharedPreferences.getInstance();
|
|
||||||
try {
|
try {
|
||||||
|
final sharedPreferences = await TokenStorage._getPrefs();
|
||||||
if (sharedPreferences.getString('socketId') == null) {
|
if (sharedPreferences.getString('socketId') == null) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
@@ -45,17 +60,17 @@ abstract class SocketIdStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Future<String?> getSocketIdFromStorage() async {
|
static Future<String?> getSocketIdFromStorage() async {
|
||||||
final sharedPreferences = await SharedPreferences.getInstance();
|
final sharedPreferences = await TokenStorage._getPrefs();
|
||||||
return sharedPreferences.getString('socketId');
|
return sharedPreferences.getString('socketId');
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> saveSocketIdInStorage(String socketId) async {
|
static Future<void> saveSocketIdInStorage(String socketId) async {
|
||||||
final sharedPreferences = await SharedPreferences.getInstance();
|
final sharedPreferences = await TokenStorage._getPrefs();
|
||||||
await sharedPreferences.setString('socketId', socketId);
|
await sharedPreferences.setString('socketId', socketId);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> removeSocketIdInStorage() async {
|
static Future<void> removeSocketIdInStorage() async {
|
||||||
final sharedPreferences = await SharedPreferences.getInstance();
|
final sharedPreferences = await TokenStorage._getPrefs();
|
||||||
await sharedPreferences.remove('socketId');
|
await sharedPreferences.remove('socketId');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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 {
|
Future<void> initializeUser() async {
|
||||||
await registerAnonymousPushToken();
|
try {
|
||||||
if (await TokenStorage.haveTokenInStorage()) {
|
|
||||||
final profile = await profileRepository.getMyProfile();
|
|
||||||
if (profile.emailVerifiedAt != null) {
|
|
||||||
await registerPushToken();
|
|
||||||
AppRouter.navigatorKey.currentContext?.go(RouteConstants.feedScreen);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
AppRouter.navigatorKey.currentContext?.go(RouteConstants.verifyEmailScreen);
|
|
||||||
} else {
|
|
||||||
await registerAnonymousPushToken();
|
await registerAnonymousPushToken();
|
||||||
FlutterNativeSplash.remove();
|
if (await TokenStorage.haveTokenInStorage()) {
|
||||||
|
final profile = await profileRepository.getMyProfile();
|
||||||
|
if (profile.emailVerifiedAt != null) {
|
||||||
|
await registerPushToken();
|
||||||
|
FlutterNativeSplash.remove();
|
||||||
|
AppRouter.navigatorKey.currentContext?.go(RouteConstants.feedScreen);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
FlutterNativeSplash.remove();
|
||||||
|
AppRouter.navigatorKey.currentContext?.go(RouteConstants.verifyEmailScreen);
|
||||||
|
} else {
|
||||||
|
FlutterNativeSplash.remove();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log('Error in initializeUser: $e');
|
||||||
|
FlutterNativeSplash.remove(); // Убираем сплэш даже при ошибке
|
||||||
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:eatvio_app/core/enums/complained_content.dart';
|
|||||||
import 'package:eatvio_app/core/utils/date_time_utils.dart';
|
import 'package:eatvio_app/core/utils/date_time_utils.dart';
|
||||||
import 'package:eatvio_app/core/widgets/custom_popup.dart';
|
import 'package:eatvio_app/core/widgets/custom_popup.dart';
|
||||||
import 'package:eatvio_app/core/widgets/loader.dart';
|
import 'package:eatvio_app/core/widgets/loader.dart';
|
||||||
|
import 'package:eatvio_app/core/widgets/markdown_html_widget.dart';
|
||||||
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_cubit.dart';
|
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_cubit.dart';
|
||||||
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_state.dart';
|
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_state.dart';
|
||||||
import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart';
|
import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart';
|
||||||
@@ -12,13 +13,11 @@ import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit
|
|||||||
import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart';
|
import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart';
|
||||||
import 'package:eatvio_app/core/locator_service.dart';
|
import 'package:eatvio_app/core/locator_service.dart';
|
||||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||||
|
import 'package:eatvio_app/generated/l10n.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_html/flutter_html.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'package:markdown/markdown.dart' as md;
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
|
||||||
|
|
||||||
class BlogPostScreen extends StatefulWidget {
|
class BlogPostScreen extends StatefulWidget {
|
||||||
final int id;
|
final int id;
|
||||||
@@ -79,98 +78,99 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
|
|||||||
resizeToAvoidBottomInset: true,
|
resizeToAvoidBottomInset: true,
|
||||||
bottomSheet: Container(
|
bottomSheet: Container(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
top: 12,
|
bottom: MediaQuery.of(context).padding.bottom + 8,
|
||||||
bottom: 12 + MediaQuery.of(context).padding.bottom + 56, // 56 - высота bottomNavigationBar
|
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
|
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Padding(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||||
children: [
|
child: Row(
|
||||||
if (replyId != null)
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
GestureDetector(
|
children: [
|
||||||
onTap: () {
|
if (replyId != null)
|
||||||
context.read<CommentsCubit>().setReplyId(null);
|
GestureDetector(
|
||||||
setState(() {
|
onTap: () {
|
||||||
replyId = null;
|
context.read<CommentsCubit>().setReplyId(null);
|
||||||
textEditingController.clear();
|
setState(() {
|
||||||
});
|
replyId = null;
|
||||||
FocusManager.instance.primaryFocus?.unfocus();
|
textEditingController.clear();
|
||||||
},
|
});
|
||||||
child: Container(
|
FocusManager.instance.primaryFocus?.unfocus();
|
||||||
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
|
|
||||||
height: 32,
|
|
||||||
width: 32,
|
|
||||||
child: const Icon(Icons.close_rounded, size: 18, color: Colors.white),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (replyId != null) const SizedBox(width: 8),
|
|
||||||
SizedBox(
|
|
||||||
width: MediaQuery.of(context).size.width - (replyId == null ? 64 : 104),
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: const BoxConstraints(minHeight: 52),
|
|
||||||
child: TextFormField(
|
|
||||||
textCapitalization: TextCapitalization.sentences,
|
|
||||||
onChanged: (_) {
|
|
||||||
setState(() {});
|
|
||||||
},
|
},
|
||||||
controller: textEditingController,
|
child: Container(
|
||||||
maxLines: 5,
|
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
|
||||||
minLines: 1,
|
height: 32,
|
||||||
keyboardType: TextInputType.multiline,
|
width: 32,
|
||||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1.2),
|
child: const Icon(Icons.close_rounded, size: 18, color: Colors.white),
|
||||||
decoration: InputDecoration(
|
),
|
||||||
isDense: false,
|
),
|
||||||
// убираем true, чтобы padding учитывался корректно
|
if (replyId != null) const SizedBox(width: 8),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
Expanded(
|
||||||
// вертикальный паддинг увеличен
|
child: ConstrainedBox(
|
||||||
hintText: 'Комментарий',
|
constraints: const BoxConstraints(minHeight: 52),
|
||||||
hintStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, height: 1.2, color: Color(0xFFAEAEB2)),
|
child: TextFormField(
|
||||||
filled: true,
|
textCapitalization: TextCapitalization.sentences,
|
||||||
border: OutlineInputBorder(
|
onChanged: (_) {
|
||||||
borderRadius: BorderRadius.circular(16),
|
setState(() {});
|
||||||
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
},
|
||||||
),
|
controller: textEditingController,
|
||||||
enabledBorder: OutlineInputBorder(
|
maxLines: 5,
|
||||||
borderRadius: BorderRadius.circular(16),
|
minLines: 1,
|
||||||
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
keyboardType: TextInputType.multiline,
|
||||||
),
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1.2),
|
||||||
focusedBorder: OutlineInputBorder(
|
decoration: InputDecoration(
|
||||||
borderRadius: BorderRadius.circular(16),
|
isDense: false,
|
||||||
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
// убираем true, чтобы padding учитывался корректно
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||||
|
// вертикальный паддинг увеличен
|
||||||
|
hintText: S.of(context).comment_hint,
|
||||||
|
hintStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, height: 1.2, color: Color(0xFFAEAEB2)),
|
||||||
|
filled: true,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 8),
|
||||||
const SizedBox(width: 8),
|
GestureDetector(
|
||||||
GestureDetector(
|
onTap: () {
|
||||||
onTap: () {
|
if (textEditingController.text.length > 2) {
|
||||||
if (textEditingController.text.length > 2) {
|
commentsCubit.sendComment(
|
||||||
commentsCubit.sendComment(
|
id: widget.id.toString(),
|
||||||
id: widget.id.toString(),
|
commentText: textEditingController.text,
|
||||||
commentText: textEditingController.text,
|
parentCommentId: replyId,
|
||||||
parentCommentId: replyId,
|
commentSourceType: CommentSourceType.blogPost,
|
||||||
commentSourceType: CommentSourceType.blogPost,
|
);
|
||||||
);
|
context.read<CommentsCubit>().setReplyId(null);
|
||||||
context.read<CommentsCubit>().setReplyId(null);
|
setState(() {
|
||||||
setState(() {
|
textEditingController.clear();
|
||||||
textEditingController.clear();
|
replyId = null;
|
||||||
replyId = null;
|
});
|
||||||
});
|
FocusManager.instance.primaryFocus?.unfocus();
|
||||||
FocusManager.instance.primaryFocus?.unfocus();
|
}
|
||||||
}
|
},
|
||||||
},
|
child: Container(
|
||||||
child: Container(
|
decoration: BoxDecoration(color: textEditingController.text.length > 2 ? appTheme.blueA400 : Colors.grey, shape: BoxShape.circle),
|
||||||
decoration: BoxDecoration(color: textEditingController.text.length > 2 ? appTheme.blueA400 : Colors.grey, shape: BoxShape.circle),
|
height: 32,
|
||||||
height: 32,
|
width: 32,
|
||||||
width: 32,
|
child: const Icon(Icons.send_rounded, size: 18, color: Colors.white),
|
||||||
child: const Icon(Icons.send_rounded, size: 18, color: Colors.white),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
@@ -236,104 +236,8 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
|
|||||||
if (blogPostState.blogPostModel?.description.isNotEmpty ?? false)
|
if (blogPostState.blogPostModel?.description.isNotEmpty ?? false)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
child: Builder(
|
child: MarkdownHtmlWidget(
|
||||||
builder: (context) {
|
markdownText: blogPostState.blogPostModel!.description,
|
||||||
// Автоматически преобразуем URL в markdown ссылки
|
|
||||||
String description = blogPostState.blogPostModel!.description;
|
|
||||||
|
|
||||||
// Сначала исправляем неправильный синтаксис markdown ссылок, где URL в тексте, а не в href
|
|
||||||
// Например: [http://example.com](url) -> [http://example.com](http://example.com)
|
|
||||||
final wrongLinkRegex = RegExp(r'\[(https?://[^\]]+|www\.[^\]]+)\]\(([^\)]+)\)');
|
|
||||||
description = description.replaceAllMapped(wrongLinkRegex, (match) {
|
|
||||||
final urlInText = match.group(1)!;
|
|
||||||
// Если текст ссылки является URL, используем его как href
|
|
||||||
if (!urlInText.startsWith('http://') && !urlInText.startsWith('https://') && !urlInText.startsWith('www.')) {
|
|
||||||
return match.group(0)!; // Оставляем как есть, если это не URL
|
|
||||||
}
|
|
||||||
final fullUrl = urlInText.startsWith('www.') ? 'https://$urlInText' : urlInText;
|
|
||||||
// Используем URL как href, а текст ссылки - это сам URL
|
|
||||||
return '[$urlInText]($fullUrl)';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Теперь находим все существующие markdown ссылки и их позиции
|
|
||||||
final linkRegex = RegExp(r'\[([^\]]+)\]\(([^\)]+)\)');
|
|
||||||
final existingLinks = <_LinkRange>[];
|
|
||||||
|
|
||||||
for (final linkMatch in linkRegex.allMatches(description)) {
|
|
||||||
existingLinks.add(_LinkRange(linkMatch.start, linkMatch.end));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Регулярное выражение для поиска URL (http://, https://, www.)
|
|
||||||
final urlRegex = RegExp(r'(https?://[^\s\)]+|www\.[^\s\)]+)', caseSensitive: false);
|
|
||||||
|
|
||||||
// Заменяем URL на markdown ссылки, если они еще не являются ссылками
|
|
||||||
String result = description;
|
|
||||||
int offset = 0;
|
|
||||||
|
|
||||||
for (final urlMatch in urlRegex.allMatches(description)) {
|
|
||||||
// Проверяем, не находится ли URL внутри существующей markdown ссылки
|
|
||||||
bool isInsideLink = existingLinks.any((link) => urlMatch.start >= link.start && urlMatch.end <= link.end);
|
|
||||||
|
|
||||||
if (!isInsideLink) {
|
|
||||||
final url = urlMatch.group(0)!;
|
|
||||||
final fullUrl = url.startsWith('www.') ? 'https://$url' : url;
|
|
||||||
final replacement = '[$url]($fullUrl)';
|
|
||||||
result = result.replaceRange(urlMatch.start + offset, urlMatch.end + offset, replacement);
|
|
||||||
offset += replacement.length - url.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
description = result;
|
|
||||||
|
|
||||||
final htmlData = md.markdownToHtml(description.replaceAll('\n', '<br>'), extensionSet: md.ExtensionSet.commonMark);
|
|
||||||
debugPrint('Markdown input: ${blogPostState.blogPostModel!.description}');
|
|
||||||
debugPrint('Processed markdown: $description');
|
|
||||||
debugPrint('HTML output: $htmlData');
|
|
||||||
return Html(
|
|
||||||
data: htmlData,
|
|
||||||
style: {
|
|
||||||
"body": Style(margin: Margins.zero, padding: HtmlPaddings.zero, fontSize: FontSize(16), lineHeight: const LineHeight(1.5)),
|
|
||||||
"p": Style(margin: Margins.only(bottom: 12), color: Colors.black87),
|
|
||||||
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: Colors.black, fontWeight: FontWeight.bold),
|
|
||||||
"h1": Style(fontSize: FontSize(24)),
|
|
||||||
"h2": Style(fontSize: FontSize(20)),
|
|
||||||
"h3": Style(fontSize: FontSize(18)),
|
|
||||||
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
|
|
||||||
"li": Style(margin: Margins.only(bottom: 4), color: Colors.black87),
|
|
||||||
"strong, b": Style(fontWeight: FontWeight.bold, color: Colors.black),
|
|
||||||
"em, i": Style(fontStyle: FontStyle.italic),
|
|
||||||
"code": Style(
|
|
||||||
backgroundColor: Colors.grey.shade100,
|
|
||||||
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
),
|
|
||||||
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
|
|
||||||
"blockquote": Style(
|
|
||||||
padding: HtmlPaddings.only(left: 16),
|
|
||||||
margin: Margins.only(left: 8, bottom: 12),
|
|
||||||
fontStyle: FontStyle.italic,
|
|
||||||
color: Colors.grey.shade700,
|
|
||||||
),
|
|
||||||
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
|
|
||||||
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
|
|
||||||
},
|
|
||||||
onLinkTap: (url, attributes, element) async {
|
|
||||||
debugPrint('Link tapped: $url');
|
|
||||||
if (url != null) {
|
|
||||||
try {
|
|
||||||
final uri = Uri.parse(url);
|
|
||||||
if (await canLaunchUrl(uri)) {
|
|
||||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
|
||||||
} else {
|
|
||||||
debugPrint('Cannot launch URL: $url');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('Error launching URL: $e');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@@ -436,10 +340,3 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _LinkRange {
|
|
||||||
final int start;
|
|
||||||
final int end;
|
|
||||||
|
|
||||||
_LinkRange(this.start, this.end);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import 'package:eatvio_app/core/utils/image_constant.dart';
|
|||||||
import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart';
|
import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart';
|
||||||
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
||||||
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_state.dart';
|
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_state.dart';
|
||||||
import 'package:eatvio_app/features/profile/models/profile_model.dart';
|
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
||||||
|
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
|
||||||
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
|
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
|
||||||
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_state.dart';
|
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_state.dart';
|
||||||
import 'package:eatvio_app/generated/l10n.dart';
|
import 'package:eatvio_app/generated/l10n.dart';
|
||||||
@@ -16,13 +17,11 @@ import 'package:flutter_svg/flutter_svg.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
class BottomNavigationBarWidget extends StatefulWidget {
|
class BottomNavigationBarWidget extends StatefulWidget {
|
||||||
final ProfileModel profileModel;
|
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
const BottomNavigationBarWidget({
|
const BottomNavigationBarWidget({
|
||||||
super.key,
|
super.key,
|
||||||
required this.child,
|
required this.child,
|
||||||
required this.profileModel,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -31,15 +30,12 @@ class BottomNavigationBarWidget extends StatefulWidget {
|
|||||||
|
|
||||||
class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
||||||
int currentBottomNavBarIndex = 1;
|
int currentBottomNavBarIndex = 1;
|
||||||
late final List<String> pages;
|
List<String>? pages;
|
||||||
int tapsCount = 0;
|
int tapsCount = 0;
|
||||||
|
|
||||||
@override
|
List<String> _getPages(bool isConsultant) {
|
||||||
void initState() {
|
if (isConsultant) {
|
||||||
super.initState();
|
return [
|
||||||
|
|
||||||
if (widget.profileModel.roles.contains('consultant')) {
|
|
||||||
pages = [
|
|
||||||
'/my-profile-screen',
|
'/my-profile-screen',
|
||||||
'/feed-screen',
|
'/feed-screen',
|
||||||
'/diary-screen',
|
'/diary-screen',
|
||||||
@@ -49,7 +45,7 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
|||||||
'/menu-screen',
|
'/menu-screen',
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
pages = [
|
return [
|
||||||
'/my-profile-screen',
|
'/my-profile-screen',
|
||||||
'/feed-screen',
|
'/feed-screen',
|
||||||
'/diary-screen',
|
'/diary-screen',
|
||||||
@@ -61,7 +57,8 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onItemTapped(int index) async {
|
void _onItemTapped(int index) async {
|
||||||
if (pages.length == 7) {
|
if (pages == null) return;
|
||||||
|
if (pages!.length == 7) {
|
||||||
if (index == 6) {
|
if (index == 6) {
|
||||||
tapsCount++;
|
tapsCount++;
|
||||||
if (tapsCount == 7) {
|
if (tapsCount == 7) {
|
||||||
@@ -82,108 +79,117 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
currentBottomNavBarIndex = index;
|
currentBottomNavBarIndex = index;
|
||||||
});
|
});
|
||||||
GoRouter.of(context).replace(pages[currentBottomNavBarIndex]);
|
GoRouter.of(context).replace(pages![currentBottomNavBarIndex]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final s = S.of(context);
|
final s = S.of(context);
|
||||||
return Scaffold(
|
return BlocBuilder<ProfileAvatarCubit, ProfileAvatarState>(
|
||||||
body: widget.child,
|
builder: (BuildContext context, ProfileAvatarState state) {
|
||||||
bottomNavigationBar: BottomNavigationBar(
|
if (state is ProfileAvatarStateLoaded) {
|
||||||
type: BottomNavigationBarType.fixed,
|
final profileModel = state.profile;
|
||||||
onTap: _onItemTapped,
|
final isConsultant = profileModel.roles.contains('consultant');
|
||||||
selectedFontSize: 10,
|
|
||||||
unselectedFontSize: 10,
|
// Инициализируем pages на основе profileModel
|
||||||
selectedItemColor: appTheme.blueA400,
|
pages ??= _getPages(isConsultant);
|
||||||
currentIndex: currentBottomNavBarIndex,
|
|
||||||
items: [
|
return Scaffold(
|
||||||
BottomNavigationBarItem(
|
body: widget.child,
|
||||||
label: '',
|
bottomNavigationBar: BottomNavigationBar(
|
||||||
icon: CircleAvatar(
|
type: BottomNavigationBarType.fixed,
|
||||||
foregroundImage: NetworkImage(
|
onTap: _onItemTapped,
|
||||||
widget.profileModel.profilePhotoUrl.contains('https') ? widget.profileModel.profilePhotoUrl : sl<AppConfig>().baseUrl + widget.profileModel.profilePhotoUrl,
|
selectedFontSize: 10,
|
||||||
),
|
unselectedFontSize: 10,
|
||||||
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
selectedItemColor: appTheme.blueA400,
|
||||||
radius: 16,
|
currentIndex: currentBottomNavBarIndex,
|
||||||
),
|
items: [
|
||||||
),
|
BottomNavigationBarItem(
|
||||||
BottomNavigationBarItem(
|
label: '',
|
||||||
label: 'Лента',
|
icon: CircleAvatar(
|
||||||
icon: Padding(
|
foregroundImage: NetworkImage(
|
||||||
padding: const EdgeInsets.only(bottom: 2),
|
profileModel.profilePhotoUrl.contains('https') ? profileModel.profilePhotoUrl : sl<AppConfig>().baseUrl + profileModel.profilePhotoUrl,
|
||||||
child: SvgPicture.asset(
|
),
|
||||||
colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
|
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
||||||
width: 20,
|
radius: 16,
|
||||||
height: 20,
|
|
||||||
ImageConstant.icnFeed,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
activeIcon: Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 2),
|
|
||||||
child: SvgPicture.asset(
|
|
||||||
ImageConstant.icnFeed,
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
colorFilter: ColorFilter.mode(
|
|
||||||
appTheme.blueA400,
|
|
||||||
BlendMode.srcIn,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
BottomNavigationBarItem(
|
|
||||||
label: s.diary,
|
|
||||||
icon: Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 2),
|
|
||||||
child: SvgPicture.asset(
|
|
||||||
colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
|
|
||||||
ImageConstant.icnDiary,
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
activeIcon: Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 2),
|
|
||||||
child: SvgPicture.asset(
|
|
||||||
ImageConstant.icnDiary,
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
colorFilter: ColorFilter.mode(
|
|
||||||
appTheme.blueA400,
|
|
||||||
BlendMode.srcIn,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (widget.profileModel.roles.contains('consultant'))
|
|
||||||
BottomNavigationBarItem(
|
|
||||||
label: 'Консультации',
|
|
||||||
icon: Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 2),
|
|
||||||
child: SvgPicture.asset(
|
|
||||||
colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
|
|
||||||
'assets/icons/consultations.svg',
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
activeIcon: Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 2),
|
|
||||||
child: SvgPicture.asset(
|
|
||||||
'assets/icons/consultations.svg',
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
colorFilter: ColorFilter.mode(
|
|
||||||
appTheme.blueA400,
|
|
||||||
BlendMode.srcIn,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
BottomNavigationBarItem(
|
||||||
|
label: 'Лента',
|
||||||
|
icon: Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
ImageConstant.icnFeed,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
BottomNavigationBarItem(
|
activeIcon: Padding(
|
||||||
label: s.chat,
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
ImageConstant.icnFeed,
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
colorFilter: ColorFilter.mode(
|
||||||
|
appTheme.blueA400,
|
||||||
|
BlendMode.srcIn,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
BottomNavigationBarItem(
|
||||||
|
label: s.diary,
|
||||||
|
icon: Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
|
||||||
|
ImageConstant.icnDiary,
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
activeIcon: Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
ImageConstant.icnDiary,
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
colorFilter: ColorFilter.mode(
|
||||||
|
appTheme.blueA400,
|
||||||
|
BlendMode.srcIn,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (profileModel.roles.contains('consultant'))
|
||||||
|
BottomNavigationBarItem(
|
||||||
|
label: 'Консультации',
|
||||||
|
icon: Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
|
||||||
|
'assets/icons/consultations.svg',
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
activeIcon: Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
'assets/icons/consultations.svg',
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
colorFilter: ColorFilter.mode(
|
||||||
|
appTheme.blueA400,
|
||||||
|
BlendMode.srcIn,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
BottomNavigationBarItem(
|
||||||
|
label: s.chat,
|
||||||
icon: BlocBuilder<ChatsCubit, ChatsState>(
|
icon: BlocBuilder<ChatsCubit, ChatsState>(
|
||||||
builder: (BuildContext context, ChatsState state) {
|
builder: (BuildContext context, ChatsState state) {
|
||||||
final hasUnreadMessages = state.chatsWithUnreadMessagesCount > 0;
|
final hasUnreadMessages = state.chatsWithUnreadMessagesCount > 0;
|
||||||
@@ -206,7 +212,7 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
|||||||
ImageConstant.icnChat,
|
ImageConstant.icnChat,
|
||||||
width: 20,
|
width: 20,
|
||||||
height: 20,
|
height: 20,
|
||||||
colorFilter: pages[currentBottomNavBarIndex] == '/chat-select-screen'
|
colorFilter: pages![currentBottomNavBarIndex] == '/chat-select-screen'
|
||||||
? ColorFilter.mode(
|
? ColorFilter.mode(
|
||||||
appTheme.blueA400,
|
appTheme.blueA400,
|
||||||
BlendMode.srcIn,
|
BlendMode.srcIn,
|
||||||
@@ -217,9 +223,9 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
label: 'Уведомления',
|
label: 'Уведомления',
|
||||||
icon: BlocBuilder<NotificationsCubit, NotificationsState>(
|
icon: BlocBuilder<NotificationsCubit, NotificationsState>(
|
||||||
builder: (BuildContext context, NotificationsState state) {
|
builder: (BuildContext context, NotificationsState state) {
|
||||||
return badges.Badge(
|
return badges.Badge(
|
||||||
@@ -237,7 +243,7 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
|||||||
padding: const EdgeInsets.only(bottom: 2),
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
child: SvgPicture.asset(
|
child: SvgPicture.asset(
|
||||||
ImageConstant.icnBell,
|
ImageConstant.icnBell,
|
||||||
colorFilter: pages[currentBottomNavBarIndex] == '/notifications-screen'
|
colorFilter: pages![currentBottomNavBarIndex] == '/notifications-screen'
|
||||||
? ColorFilter.mode(
|
? ColorFilter.mode(
|
||||||
appTheme.blueA400,
|
appTheme.blueA400,
|
||||||
BlendMode.srcIn,
|
BlendMode.srcIn,
|
||||||
@@ -251,15 +257,22 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
label: '',
|
label: '',
|
||||||
icon: SvgPicture.asset(
|
icon: SvgPicture.asset(
|
||||||
ImageConstant.imgLogo,
|
ImageConstant.imgLogo,
|
||||||
height: 32,
|
height: 32,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
],
|
}
|
||||||
),
|
// Показываем child даже когда профиль еще загружается, чтобы экраны могли инициализироваться
|
||||||
|
return Scaffold(
|
||||||
|
body: widget.child,
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,19 @@ class ChatsScreen extends StatefulWidget {
|
|||||||
class _ChatsScreenState extends State<ChatsScreen> {
|
class _ChatsScreenState extends State<ChatsScreen> {
|
||||||
final searchController = TextEditingController();
|
final searchController = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// Инициализация ChatsCubit при первом открытии экрана
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
final cubit = context.read<ChatsCubit>();
|
||||||
|
final state = cubit.state;
|
||||||
|
if (state.state == BaseState.initial) {
|
||||||
|
cubit.getChatRoomsWithLoading();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<ChatsCubit, ChatsState>(
|
return BlocBuilder<ChatsCubit, ChatsState>(
|
||||||
|
|||||||
@@ -106,15 +106,14 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
|
|||||||
resizeToAvoidBottomInset: true,
|
resizeToAvoidBottomInset: true,
|
||||||
bottomSheet: Container(
|
bottomSheet: Container(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
top: 12,
|
bottom: MediaQuery.of(context).padding.bottom + 8,
|
||||||
bottom: 12 + MediaQuery.of(context).padding.bottom + 56, // 56 - высота bottomNavigationBar
|
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
|
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'dart:math' as math;
|
|||||||
import 'package:eatvio_app/generated/l10n.dart';
|
import 'package:eatvio_app/generated/l10n.dart';
|
||||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||||
|
import 'package:eatvio_app/core/widgets/markdown_html_widget.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
|
||||||
@@ -35,7 +36,9 @@ class _ExpandableTextState extends State<ExpandableText> {
|
|||||||
void _checkTextOverflow() {
|
void _checkTextOverflow() {
|
||||||
final textRenderObject = _textKey.currentContext?.findRenderObject();
|
final textRenderObject = _textKey.currentContext?.findRenderObject();
|
||||||
if (textRenderObject is RenderBox) {
|
if (textRenderObject is RenderBox) {
|
||||||
final maxLinesHeight = widget.maxLines * TextStyles.textMMediumAuto.fontSize! * 1.2;
|
final fontSize = TextStyles.textMMediumAuto.fontSize ?? 16;
|
||||||
|
final lineHeight = fontSize * 1.5;
|
||||||
|
final maxLinesHeight = widget.maxLines * lineHeight;
|
||||||
final actualHeight = textRenderObject.size.height;
|
final actualHeight = textRenderObject.size.height;
|
||||||
|
|
||||||
if (actualHeight > maxLinesHeight && !showExpandButton) {
|
if (actualHeight > maxLinesHeight && !showExpandButton) {
|
||||||
@@ -55,6 +58,7 @@ class _ExpandableTextState extends State<ExpandableText> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final s = S.of(context);
|
final s = S.of(context);
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -65,12 +69,18 @@ class _ExpandableTextState extends State<ExpandableText> {
|
|||||||
curve: Curves.easeInOut,
|
curve: Curves.easeInOut,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
ClipRect(
|
||||||
widget.text,
|
child: ConstrainedBox(
|
||||||
key: _textKey,
|
constraints: BoxConstraints(
|
||||||
style: TextStyles.textMMediumAuto.copyWith(color: appTheme.black),
|
maxHeight: isExpanded ? double.infinity : (widget.maxLines * (TextStyles.textMMediumAuto.fontSize ?? 16) * 1.5),
|
||||||
maxLines: isExpanded ? null : widget.maxLines,
|
),
|
||||||
overflow: TextOverflow.clip,
|
child: MarkdownHtmlWidget(
|
||||||
|
key: _textKey,
|
||||||
|
markdownText: widget.text,
|
||||||
|
fontSize: TextStyles.textMMediumAuto.fontSize ?? 16,
|
||||||
|
textColor: appTheme.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (!isExpanded && showExpandButton)
|
if (!isExpanded && showExpandButton)
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import 'package:eatvio_app/features/create_plan/presentation/create_plan_screen.
|
|||||||
import 'package:eatvio_app/features/feed/presentation/bloc/consultants_cubit/consultants_cubit.dart';
|
import 'package:eatvio_app/features/feed/presentation/bloc/consultants_cubit/consultants_cubit.dart';
|
||||||
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart';
|
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart';
|
||||||
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
|
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
|
||||||
|
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_state.dart';
|
||||||
import 'package:eatvio_app/features/feed/presentation/bloc/profile_cubit/profile_cubit.dart';
|
import 'package:eatvio_app/features/feed/presentation/bloc/profile_cubit/profile_cubit.dart';
|
||||||
import 'package:eatvio_app/features/feed/presentation/bloc/profile_cubit/profile_state.dart';
|
import 'package:eatvio_app/features/feed/presentation/bloc/profile_cubit/profile_state.dart';
|
||||||
import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart';
|
import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart';
|
||||||
@@ -74,6 +75,16 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
|
|||||||
chipsLabelList = hidePlans ? ['Все', 'Рецепты', 'Диеты', 'Консультанты'] : ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты'];
|
chipsLabelList = hidePlans ? ['Все', 'Рецепты', 'Диеты', 'Консультанты'] : ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты'];
|
||||||
consultantsScrollController.addListener(_onScrollConsultants);
|
consultantsScrollController.addListener(_onScrollConsultants);
|
||||||
_checkRegistrationDate(context);
|
_checkRegistrationDate(context);
|
||||||
|
// Инициализация глобальных кубитов при первом открытии экрана
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
final feedCubit = context.read<FeedCubit>();
|
||||||
|
final state = feedCubit.state;
|
||||||
|
// Загружаем данные, если они еще не загружены
|
||||||
|
if (state is! FeedStateLoaded) {
|
||||||
|
feedCubit.loadFeedBlogPostList();
|
||||||
|
}
|
||||||
|
});
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,17 +7,16 @@ import 'package:eatvio_app/core/theme/theme_helper.dart';
|
|||||||
import 'package:eatvio_app/core/utils/date_time_utils.dart';
|
import 'package:eatvio_app/core/utils/date_time_utils.dart';
|
||||||
import 'package:eatvio_app/core/widgets/custom_popup.dart';
|
import 'package:eatvio_app/core/widgets/custom_popup.dart';
|
||||||
import 'package:eatvio_app/core/widgets/feed_icon_button.dart';
|
import 'package:eatvio_app/core/widgets/feed_icon_button.dart';
|
||||||
|
import 'package:eatvio_app/core/widgets/markdown_html_widget.dart';
|
||||||
import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart';
|
import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart';
|
||||||
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
|
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
|
||||||
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_state.dart';
|
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_state.dart';
|
||||||
import 'package:eatvio_app/features/feed/presentation/widgets/subscribe_button_widget.dart';
|
import 'package:eatvio_app/features/feed/presentation/widgets/subscribe_button_widget.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_html/flutter_html.dart';
|
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'package:markdown/markdown.dart' as md;
|
|
||||||
|
|
||||||
class BlogPostWidget extends StatefulWidget {
|
class BlogPostWidget extends StatefulWidget {
|
||||||
final int index;
|
final int index;
|
||||||
@@ -125,37 +124,46 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
|
|||||||
if (state.feedBlogPostList[widget.index].description.isNotEmpty)
|
if (state.feedBlogPostList[widget.index].description.isNotEmpty)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
child: Html(
|
child: Builder(
|
||||||
data: md.markdownToHtml(
|
builder: (context) {
|
||||||
(state.feedBlogPostList[widget.index].description.length > 200
|
String description = state.feedBlogPostList[widget.index].description;
|
||||||
? state.feedBlogPostList[widget.index].description.substring(0, 200).replaceAll('\n', '<br>')
|
final isLong = description.length > 200;
|
||||||
: state.feedBlogPostList[widget.index].description.replaceAll('\n', '<br>')),
|
|
||||||
),
|
if (isLong) {
|
||||||
style: {
|
// Умная обрезка: обрезаем markdown текст, не ломая синтаксис
|
||||||
"body": Style(margin: Margins.zero, padding: HtmlPaddings.zero, fontSize: FontSize(16), lineHeight: const LineHeight(1.5)),
|
int cutPoint = 200;
|
||||||
"p": Style(margin: Margins.only(bottom: 12), color: Colors.black87),
|
|
||||||
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: Colors.black, fontWeight: FontWeight.bold),
|
// Проверяем, есть ли незакрытые markdown теги
|
||||||
"h1": Style(fontSize: FontSize(24)),
|
String preview = description.substring(0, cutPoint);
|
||||||
"h2": Style(fontSize: FontSize(20)),
|
|
||||||
"h3": Style(fontSize: FontSize(18)),
|
// Считаем открывающие и закрывающие теги
|
||||||
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
|
int boldOpen = (preview.split('**').length - 1) ~/ 2;
|
||||||
"li": Style(margin: Margins.only(bottom: 4), color: Colors.black87),
|
int boldClose = preview.split('**').length - 1 - boldOpen * 2;
|
||||||
"strong, b": Style(fontWeight: FontWeight.bold, color: Colors.black),
|
bool hasUnclosedBold = boldOpen > boldClose;
|
||||||
"em, i": Style(fontStyle: FontStyle.italic),
|
|
||||||
"code": Style(
|
// Если есть незакрытый bold, ищем закрытие
|
||||||
backgroundColor: Colors.grey.shade100,
|
if (hasUnclosedBold) {
|
||||||
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
|
int searchEnd = (cutPoint + 100).clamp(0, description.length);
|
||||||
fontFamily: 'monospace',
|
int boldClosePos = description.indexOf('**', cutPoint);
|
||||||
),
|
if (boldClosePos != -1 && boldClosePos < searchEnd) {
|
||||||
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
|
cutPoint = boldClosePos + 2;
|
||||||
"blockquote": Style(
|
} else {
|
||||||
padding: HtmlPaddings.only(left: 16),
|
// Если не нашли, обрезаем до последнего пробела
|
||||||
margin: Margins.only(left: 8, bottom: 12),
|
cutPoint = description.lastIndexOf(' ', 200);
|
||||||
fontStyle: FontStyle.italic,
|
if (cutPoint == -1 || cutPoint < 150) cutPoint = 200;
|
||||||
color: Colors.grey.shade700,
|
}
|
||||||
),
|
} else {
|
||||||
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
|
// Обрезаем до последнего пробела для красоты
|
||||||
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
|
cutPoint = description.lastIndexOf(' ', cutPoint);
|
||||||
|
if (cutPoint == -1 || cutPoint < 150) cutPoint = 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
description = description.substring(0, cutPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
return MarkdownHtmlWidget(
|
||||||
|
markdownText: description,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -13,12 +13,10 @@ import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
|||||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_html/flutter_html.dart';
|
import 'package:eatvio_app/core/widgets/markdown_html_widget.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'package:markdown/markdown.dart' as md;
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
|
||||||
|
|
||||||
class DietsWidget extends StatefulWidget {
|
class DietsWidget extends StatefulWidget {
|
||||||
final Diet diet;
|
final Diet diet;
|
||||||
@@ -117,86 +115,11 @@ class _DietsWidgetState extends State<DietsWidget> {
|
|||||||
String details = widget.diet.details.length > 200
|
String details = widget.diet.details.length > 200
|
||||||
? '${widget.diet.details.substring(0, 200)}...'
|
? '${widget.diet.details.substring(0, 200)}...'
|
||||||
: widget.diet.details;
|
: widget.diet.details;
|
||||||
// Автоматически преобразуем URL в markdown ссылки
|
|
||||||
// Сначала исправляем неправильный синтаксис markdown ссылок
|
return MarkdownHtmlWidget(
|
||||||
final wrongLinkRegex = RegExp(r'\[(https?://[^\]]+|www\.[^\]]+)\]\(([^\)]+)\)');
|
markdownText: details,
|
||||||
details = details.replaceAllMapped(wrongLinkRegex, (match) {
|
fontSize: TextStyles.textSMedium.fontSize ?? 14,
|
||||||
final urlInText = match.group(1)!;
|
textColor: appTheme.black,
|
||||||
if (!urlInText.startsWith('http://') && !urlInText.startsWith('https://') && !urlInText.startsWith('www.')) {
|
|
||||||
return match.group(0)!;
|
|
||||||
}
|
|
||||||
final fullUrl = urlInText.startsWith('www.') ? 'https://$urlInText' : urlInText;
|
|
||||||
return '[$urlInText]($fullUrl)';
|
|
||||||
});
|
|
||||||
|
|
||||||
final linkRegex = RegExp(r'\[([^\]]+)\]\(([^\)]+)\)');
|
|
||||||
final existingLinks = <_LinkRange>[];
|
|
||||||
for (final linkMatch in linkRegex.allMatches(details)) {
|
|
||||||
existingLinks.add(_LinkRange(linkMatch.start, linkMatch.end));
|
|
||||||
}
|
|
||||||
final urlRegex = RegExp(r'(https?://[^\s\)]+|www\.[^\s\)]+)', caseSensitive: false);
|
|
||||||
String result = details;
|
|
||||||
int offset = 0;
|
|
||||||
for (final urlMatch in urlRegex.allMatches(details)) {
|
|
||||||
bool isInsideLink = existingLinks.any((link) =>
|
|
||||||
urlMatch.start >= link.start && urlMatch.end <= link.end);
|
|
||||||
if (!isInsideLink) {
|
|
||||||
final url = urlMatch.group(0)!;
|
|
||||||
final fullUrl = url.startsWith('www.') ? 'https://$url' : url;
|
|
||||||
final replacement = '[$url]($fullUrl)';
|
|
||||||
result = result.replaceRange(
|
|
||||||
urlMatch.start + offset,
|
|
||||||
urlMatch.end + offset,
|
|
||||||
replacement,
|
|
||||||
);
|
|
||||||
offset += replacement.length - url.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
details = result;
|
|
||||||
return Html(
|
|
||||||
data: md.markdownToHtml(
|
|
||||||
details.replaceAll('\n', '<br>'),
|
|
||||||
extensionSet: md.ExtensionSet.commonMark,
|
|
||||||
),
|
|
||||||
style: {
|
|
||||||
"body": Style(
|
|
||||||
margin: Margins.zero,
|
|
||||||
padding: HtmlPaddings.zero,
|
|
||||||
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
|
|
||||||
lineHeight: const LineHeight(1.5),
|
|
||||||
),
|
|
||||||
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
|
|
||||||
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold),
|
|
||||||
"h1": Style(fontSize: FontSize(24)),
|
|
||||||
"h2": Style(fontSize: FontSize(20)),
|
|
||||||
"h3": Style(fontSize: FontSize(18)),
|
|
||||||
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
|
|
||||||
"li": Style(margin: Margins.only(bottom: 4), color: appTheme.black),
|
|
||||||
"strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black),
|
|
||||||
"em, i": Style(fontStyle: FontStyle.italic),
|
|
||||||
"code": Style(
|
|
||||||
backgroundColor: Colors.grey.shade100,
|
|
||||||
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
),
|
|
||||||
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
|
|
||||||
"blockquote": Style(
|
|
||||||
padding: HtmlPaddings.only(left: 16),
|
|
||||||
margin: Margins.only(left: 8, bottom: 12),
|
|
||||||
fontStyle: FontStyle.italic,
|
|
||||||
color: Colors.grey.shade700,
|
|
||||||
),
|
|
||||||
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
|
|
||||||
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
|
|
||||||
},
|
|
||||||
onLinkTap: (url, attributes, element) async {
|
|
||||||
if (url != null) {
|
|
||||||
final uri = Uri.parse(url);
|
|
||||||
if (await canLaunchUrl(uri)) {
|
|
||||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -267,8 +190,3 @@ class _DietsWidgetState extends State<DietsWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _LinkRange {
|
|
||||||
final int start;
|
|
||||||
final int end;
|
|
||||||
_LinkRange(this.start, this.end);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'package:eatvio_app/core/widgets/bookmark_button.dart';
|
|||||||
import 'package:eatvio_app/core/widgets/custom_popup.dart';
|
import 'package:eatvio_app/core/widgets/custom_popup.dart';
|
||||||
import 'package:eatvio_app/core/widgets/info_row.dart';
|
import 'package:eatvio_app/core/widgets/info_row.dart';
|
||||||
import 'package:eatvio_app/core/widgets/feed_icon_button.dart';
|
import 'package:eatvio_app/core/widgets/feed_icon_button.dart';
|
||||||
|
import 'package:eatvio_app/core/widgets/markdown_html_widget.dart';
|
||||||
import 'package:eatvio_app/features/diets/presentation/widgets/info_bouble.dart';
|
import 'package:eatvio_app/features/diets/presentation/widgets/info_bouble.dart';
|
||||||
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
|
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
|
||||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||||
@@ -15,12 +16,9 @@ import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cub
|
|||||||
import 'package:eatvio_app/generated/l10n.dart';
|
import 'package:eatvio_app/generated/l10n.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_html/flutter_html.dart';
|
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'package:markdown/markdown.dart' as md;
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
|
||||||
|
|
||||||
class PlanWidget extends StatefulWidget {
|
class PlanWidget extends StatefulWidget {
|
||||||
final PlanModel plan;
|
final PlanModel plan;
|
||||||
@@ -117,88 +115,11 @@ class _PlanWidgetState extends State<PlanWidget> {
|
|||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: Builder(
|
child: MarkdownHtmlWidget(
|
||||||
builder: (context) {
|
markdownText: widget.plan.details.length > 200
|
||||||
String details = widget.plan.details.length > 200
|
? '${widget.plan.details.substring(0, 200)}...'
|
||||||
? '${widget.plan.details.substring(0, 200)}...'
|
: widget.plan.details,
|
||||||
: widget.plan.details;
|
textColor: appTheme.black,
|
||||||
// Автоматически преобразуем URL в markdown ссылки
|
|
||||||
// Сначала исправляем неправильный синтаксис markdown ссылок
|
|
||||||
final wrongLinkRegex = RegExp(r'\[(https?://[^\]]+|www\.[^\]]+)\]\(([^\)]+)\)');
|
|
||||||
details = details.replaceAllMapped(wrongLinkRegex, (match) {
|
|
||||||
final urlInText = match.group(1)!;
|
|
||||||
if (!urlInText.startsWith('http://') && !urlInText.startsWith('https://') && !urlInText.startsWith('www.')) {
|
|
||||||
return match.group(0)!;
|
|
||||||
}
|
|
||||||
final fullUrl = urlInText.startsWith('www.') ? 'https://$urlInText' : urlInText;
|
|
||||||
return '[$urlInText]($fullUrl)';
|
|
||||||
});
|
|
||||||
|
|
||||||
final linkRegex = RegExp(r'\[([^\]]+)\]\(([^\)]+)\)');
|
|
||||||
final existingLinks = <_LinkRange>[];
|
|
||||||
for (final linkMatch in linkRegex.allMatches(details)) {
|
|
||||||
existingLinks.add(_LinkRange(linkMatch.start, linkMatch.end));
|
|
||||||
}
|
|
||||||
final urlRegex = RegExp(r'(https?://[^\s\)]+|www\.[^\s\)]+)', caseSensitive: false);
|
|
||||||
String result = details;
|
|
||||||
int offset = 0;
|
|
||||||
for (final urlMatch in urlRegex.allMatches(details)) {
|
|
||||||
bool isInsideLink = existingLinks.any((link) =>
|
|
||||||
urlMatch.start >= link.start && urlMatch.end <= link.end);
|
|
||||||
if (!isInsideLink) {
|
|
||||||
final url = urlMatch.group(0)!;
|
|
||||||
final fullUrl = url.startsWith('www.') ? 'https://$url' : url;
|
|
||||||
final replacement = '[$url]($fullUrl)';
|
|
||||||
result = result.replaceRange(
|
|
||||||
urlMatch.start + offset,
|
|
||||||
urlMatch.end + offset,
|
|
||||||
replacement,
|
|
||||||
);
|
|
||||||
offset += replacement.length - url.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
details = result;
|
|
||||||
return Html(
|
|
||||||
data: md.markdownToHtml(
|
|
||||||
details.replaceAll('\n', '<br>'),
|
|
||||||
extensionSet: md.ExtensionSet.commonMark,
|
|
||||||
),
|
|
||||||
style: {
|
|
||||||
"body": Style(margin: Margins.zero, padding: HtmlPaddings.zero, fontSize: FontSize(16), lineHeight: const LineHeight(1.5)),
|
|
||||||
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
|
|
||||||
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold),
|
|
||||||
"h1": Style(fontSize: FontSize(24)),
|
|
||||||
"h2": Style(fontSize: FontSize(20)),
|
|
||||||
"h3": Style(fontSize: FontSize(18)),
|
|
||||||
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
|
|
||||||
"li": Style(margin: Margins.only(bottom: 4), color: appTheme.black),
|
|
||||||
"strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black),
|
|
||||||
"em, i": Style(fontStyle: FontStyle.italic),
|
|
||||||
"code": Style(
|
|
||||||
backgroundColor: Colors.grey.shade100,
|
|
||||||
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
),
|
|
||||||
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
|
|
||||||
"blockquote": Style(
|
|
||||||
padding: HtmlPaddings.only(left: 16),
|
|
||||||
margin: Margins.only(left: 8, bottom: 12),
|
|
||||||
fontStyle: FontStyle.italic,
|
|
||||||
color: Colors.grey.shade700,
|
|
||||||
),
|
|
||||||
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
|
|
||||||
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
|
|
||||||
},
|
|
||||||
onLinkTap: (url, attributes, element) async {
|
|
||||||
if (url != null) {
|
|
||||||
final uri = Uri.parse(url);
|
|
||||||
if (await canLaunchUrl(uri)) {
|
|
||||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (widget.plan.details.length > 200)
|
if (widget.plan.details.length > 200)
|
||||||
@@ -312,9 +233,3 @@ class _PlanWidgetState extends State<PlanWidget> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _LinkRange {
|
|
||||||
final int start;
|
|
||||||
final int end;
|
|
||||||
_LinkRange(this.start, this.end);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -16,12 +16,10 @@ import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes
|
|||||||
import 'package:eatvio_app/generated/l10n.dart';
|
import 'package:eatvio_app/generated/l10n.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_html/flutter_html.dart';
|
import 'package:eatvio_app/core/widgets/markdown_html_widget.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'package:markdown/markdown.dart' as md;
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
|
||||||
|
|
||||||
class RecipesWidget extends StatefulWidget {
|
class RecipesWidget extends StatefulWidget {
|
||||||
final int index;
|
final int index;
|
||||||
@@ -155,88 +153,11 @@ class _RecipesWidgetState extends State<RecipesWidget> {
|
|||||||
String description = widget.recipe.description.length > 200
|
String description = widget.recipe.description.length > 200
|
||||||
? widget.recipe.description.substring(0, 200)
|
? widget.recipe.description.substring(0, 200)
|
||||||
: widget.recipe.description;
|
: widget.recipe.description;
|
||||||
// Автоматически преобразуем URL в markdown ссылки
|
|
||||||
// Сначала исправляем неправильный синтаксис markdown ссылок
|
return MarkdownHtmlWidget(
|
||||||
final wrongLinkRegex = RegExp(r'\[(https?://[^\]]+|www\.[^\]]+)\]\(([^\)]+)\)');
|
markdownText: description,
|
||||||
description = description.replaceAllMapped(wrongLinkRegex, (match) {
|
fontSize: TextStyles.textSMedium.fontSize ?? 14,
|
||||||
final urlInText = match.group(1)!;
|
textColor: appTheme.black,
|
||||||
if (!urlInText.startsWith('http://') && !urlInText.startsWith('https://') && !urlInText.startsWith('www.')) {
|
|
||||||
return match.group(0)!;
|
|
||||||
}
|
|
||||||
final fullUrl = urlInText.startsWith('www.') ? 'https://$urlInText' : urlInText;
|
|
||||||
return '[$urlInText]($fullUrl)';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Находим все существующие markdown ссылки и их позиции
|
|
||||||
final linkRegex = RegExp(r'\[([^\]]+)\]\(([^\)]+)\)');
|
|
||||||
final existingLinks = <_LinkRange>[];
|
|
||||||
for (final linkMatch in linkRegex.allMatches(description)) {
|
|
||||||
existingLinks.add(_LinkRange(linkMatch.start, linkMatch.end));
|
|
||||||
}
|
|
||||||
|
|
||||||
final urlRegex = RegExp(r'(https?://[^\s\)]+|www\.[^\s\)]+)', caseSensitive: false);
|
|
||||||
String result = description;
|
|
||||||
int offset = 0;
|
|
||||||
for (final urlMatch in urlRegex.allMatches(description)) {
|
|
||||||
bool isInsideLink = existingLinks.any((link) =>
|
|
||||||
urlMatch.start >= link.start && urlMatch.end <= link.end);
|
|
||||||
if (!isInsideLink) {
|
|
||||||
final url = urlMatch.group(0)!;
|
|
||||||
final fullUrl = url.startsWith('www.') ? 'https://$url' : url;
|
|
||||||
final replacement = '[$url]($fullUrl)';
|
|
||||||
result = result.replaceRange(
|
|
||||||
urlMatch.start + offset,
|
|
||||||
urlMatch.end + offset,
|
|
||||||
replacement,
|
|
||||||
);
|
|
||||||
offset += replacement.length - url.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
description = result;
|
|
||||||
return Html(
|
|
||||||
data: md.markdownToHtml(
|
|
||||||
description.replaceAll('\n', '<br>'),
|
|
||||||
extensionSet: md.ExtensionSet.commonMark,
|
|
||||||
),
|
|
||||||
style: {
|
|
||||||
"body": Style(
|
|
||||||
margin: Margins.zero,
|
|
||||||
padding: HtmlPaddings.zero,
|
|
||||||
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
|
|
||||||
lineHeight: const LineHeight(1.5),
|
|
||||||
),
|
|
||||||
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
|
|
||||||
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold),
|
|
||||||
"h1": Style(fontSize: FontSize(24)),
|
|
||||||
"h2": Style(fontSize: FontSize(20)),
|
|
||||||
"h3": Style(fontSize: FontSize(18)),
|
|
||||||
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
|
|
||||||
"li": Style(margin: Margins.only(bottom: 4), color: appTheme.black),
|
|
||||||
"strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black),
|
|
||||||
"em, i": Style(fontStyle: FontStyle.italic),
|
|
||||||
"code": Style(
|
|
||||||
backgroundColor: Colors.grey.shade100,
|
|
||||||
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
),
|
|
||||||
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
|
|
||||||
"blockquote": Style(
|
|
||||||
padding: HtmlPaddings.only(left: 16),
|
|
||||||
margin: Margins.only(left: 8, bottom: 12),
|
|
||||||
fontStyle: FontStyle.italic,
|
|
||||||
color: Colors.grey.shade700,
|
|
||||||
),
|
|
||||||
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
|
|
||||||
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
|
|
||||||
},
|
|
||||||
onLinkTap: (url, attributes, element) async {
|
|
||||||
if (url != null) {
|
|
||||||
final uri = Uri.parse(url);
|
|
||||||
if (await canLaunchUrl(uri)) {
|
|
||||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -330,8 +251,3 @@ class _RecipesWidgetState extends State<RecipesWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _LinkRange {
|
|
||||||
final int start;
|
|
||||||
final int end;
|
|
||||||
_LinkRange(this.start, this.end);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -21,12 +21,10 @@ import 'package:eatvio_app/features/personal_diet_suggestion/presentation/person
|
|||||||
import 'package:eatvio_app/generated/l10n.dart';
|
import 'package:eatvio_app/generated/l10n.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_html/flutter_html.dart';
|
import 'package:eatvio_app/core/widgets/markdown_html_widget.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'package:markdown/markdown.dart' as md;
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
|
||||||
|
|
||||||
class MyDietWidget extends StatefulWidget {
|
class MyDietWidget extends StatefulWidget {
|
||||||
final Diet diet;
|
final Diet diet;
|
||||||
@@ -86,86 +84,11 @@ class _MyDietWidgetState extends State<MyDietWidget> {
|
|||||||
String details = widget.diet.details.length > 200
|
String details = widget.diet.details.length > 200
|
||||||
? '${widget.diet.details.substring(0, 200)}...'
|
? '${widget.diet.details.substring(0, 200)}...'
|
||||||
: widget.diet.details;
|
: widget.diet.details;
|
||||||
// Автоматически преобразуем URL в markdown ссылки
|
|
||||||
// Сначала исправляем неправильный синтаксис markdown ссылок
|
return MarkdownHtmlWidget(
|
||||||
final wrongLinkRegex = RegExp(r'\[(https?://[^\]]+|www\.[^\]]+)\]\(([^\)]+)\)');
|
markdownText: details,
|
||||||
details = details.replaceAllMapped(wrongLinkRegex, (match) {
|
fontSize: TextStyles.textSMedium.fontSize ?? 14,
|
||||||
final urlInText = match.group(1)!;
|
textColor: appTheme.black,
|
||||||
if (!urlInText.startsWith('http://') && !urlInText.startsWith('https://') && !urlInText.startsWith('www.')) {
|
|
||||||
return match.group(0)!;
|
|
||||||
}
|
|
||||||
final fullUrl = urlInText.startsWith('www.') ? 'https://$urlInText' : urlInText;
|
|
||||||
return '[$urlInText]($fullUrl)';
|
|
||||||
});
|
|
||||||
|
|
||||||
final linkRegex = RegExp(r'\[([^\]]+)\]\(([^\)]+)\)');
|
|
||||||
final existingLinks = <_LinkRange>[];
|
|
||||||
for (final linkMatch in linkRegex.allMatches(details)) {
|
|
||||||
existingLinks.add(_LinkRange(linkMatch.start, linkMatch.end));
|
|
||||||
}
|
|
||||||
final urlRegex = RegExp(r'(https?://[^\s\)]+|www\.[^\s\)]+)', caseSensitive: false);
|
|
||||||
String result = details;
|
|
||||||
int offset = 0;
|
|
||||||
for (final urlMatch in urlRegex.allMatches(details)) {
|
|
||||||
bool isInsideLink = existingLinks.any((link) =>
|
|
||||||
urlMatch.start >= link.start && urlMatch.end <= link.end);
|
|
||||||
if (!isInsideLink) {
|
|
||||||
final url = urlMatch.group(0)!;
|
|
||||||
final fullUrl = url.startsWith('www.') ? 'https://$url' : url;
|
|
||||||
final replacement = '[$url]($fullUrl)';
|
|
||||||
result = result.replaceRange(
|
|
||||||
urlMatch.start + offset,
|
|
||||||
urlMatch.end + offset,
|
|
||||||
replacement,
|
|
||||||
);
|
|
||||||
offset += replacement.length - url.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
details = result;
|
|
||||||
return Html(
|
|
||||||
data: md.markdownToHtml(
|
|
||||||
details.replaceAll('\n', '<br>'),
|
|
||||||
extensionSet: md.ExtensionSet.commonMark,
|
|
||||||
),
|
|
||||||
style: {
|
|
||||||
"body": Style(
|
|
||||||
margin: Margins.zero,
|
|
||||||
padding: HtmlPaddings.zero,
|
|
||||||
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
|
|
||||||
lineHeight: const LineHeight(1.5),
|
|
||||||
),
|
|
||||||
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
|
|
||||||
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold),
|
|
||||||
"h1": Style(fontSize: FontSize(24)),
|
|
||||||
"h2": Style(fontSize: FontSize(20)),
|
|
||||||
"h3": Style(fontSize: FontSize(18)),
|
|
||||||
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
|
|
||||||
"li": Style(margin: Margins.only(bottom: 4), color: appTheme.black),
|
|
||||||
"strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black),
|
|
||||||
"em, i": Style(fontStyle: FontStyle.italic),
|
|
||||||
"code": Style(
|
|
||||||
backgroundColor: Colors.grey.shade100,
|
|
||||||
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
),
|
|
||||||
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
|
|
||||||
"blockquote": Style(
|
|
||||||
padding: HtmlPaddings.only(left: 16),
|
|
||||||
margin: Margins.only(left: 8, bottom: 12),
|
|
||||||
fontStyle: FontStyle.italic,
|
|
||||||
color: Colors.grey.shade700,
|
|
||||||
),
|
|
||||||
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
|
|
||||||
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
|
|
||||||
},
|
|
||||||
onLinkTap: (url, attributes, element) async {
|
|
||||||
if (url != null) {
|
|
||||||
final uri = Uri.parse(url);
|
|
||||||
if (await canLaunchUrl(uri)) {
|
|
||||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -323,8 +246,3 @@ class _MyDietWidgetState extends State<MyDietWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _LinkRange {
|
|
||||||
final int start;
|
|
||||||
final int end;
|
|
||||||
_LinkRange(this.start, this.end);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -14,12 +14,10 @@ import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.d
|
|||||||
import 'package:eatvio_app/generated/l10n.dart';
|
import 'package:eatvio_app/generated/l10n.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_html/flutter_html.dart';
|
import 'package:eatvio_app/core/widgets/markdown_html_widget.dart';
|
||||||
// Removed SVG icon usage after switching to text buttons
|
// Removed SVG icon usage after switching to text buttons
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'package:markdown/markdown.dart' as md;
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
|
||||||
|
|
||||||
class MyPlanWidget extends StatefulWidget {
|
class MyPlanWidget extends StatefulWidget {
|
||||||
final PlanModel plan;
|
final PlanModel plan;
|
||||||
@@ -78,86 +76,11 @@ class _MyPlanWidgetState extends State<MyPlanWidget> {
|
|||||||
String details = widget.plan.details.length > 200
|
String details = widget.plan.details.length > 200
|
||||||
? '${widget.plan.details.substring(0, 200)}...'
|
? '${widget.plan.details.substring(0, 200)}...'
|
||||||
: widget.plan.details;
|
: widget.plan.details;
|
||||||
// Автоматически преобразуем URL в markdown ссылки
|
|
||||||
// Сначала исправляем неправильный синтаксис markdown ссылок
|
return MarkdownHtmlWidget(
|
||||||
final wrongLinkRegex = RegExp(r'\[(https?://[^\]]+|www\.[^\]]+)\]\(([^\)]+)\)');
|
markdownText: details,
|
||||||
details = details.replaceAllMapped(wrongLinkRegex, (match) {
|
fontSize: TextStyles.textSMedium.fontSize ?? 14,
|
||||||
final urlInText = match.group(1)!;
|
textColor: appTheme.black,
|
||||||
if (!urlInText.startsWith('http://') && !urlInText.startsWith('https://') && !urlInText.startsWith('www.')) {
|
|
||||||
return match.group(0)!;
|
|
||||||
}
|
|
||||||
final fullUrl = urlInText.startsWith('www.') ? 'https://$urlInText' : urlInText;
|
|
||||||
return '[$urlInText]($fullUrl)';
|
|
||||||
});
|
|
||||||
|
|
||||||
final linkRegex = RegExp(r'\[([^\]]+)\]\(([^\)]+)\)');
|
|
||||||
final existingLinks = <_LinkRange>[];
|
|
||||||
for (final linkMatch in linkRegex.allMatches(details)) {
|
|
||||||
existingLinks.add(_LinkRange(linkMatch.start, linkMatch.end));
|
|
||||||
}
|
|
||||||
final urlRegex = RegExp(r'(https?://[^\s\)]+|www\.[^\s\)]+)', caseSensitive: false);
|
|
||||||
String result = details;
|
|
||||||
int offset = 0;
|
|
||||||
for (final urlMatch in urlRegex.allMatches(details)) {
|
|
||||||
bool isInsideLink = existingLinks.any((link) =>
|
|
||||||
urlMatch.start >= link.start && urlMatch.end <= link.end);
|
|
||||||
if (!isInsideLink) {
|
|
||||||
final url = urlMatch.group(0)!;
|
|
||||||
final fullUrl = url.startsWith('www.') ? 'https://$url' : url;
|
|
||||||
final replacement = '[$url]($fullUrl)';
|
|
||||||
result = result.replaceRange(
|
|
||||||
urlMatch.start + offset,
|
|
||||||
urlMatch.end + offset,
|
|
||||||
replacement,
|
|
||||||
);
|
|
||||||
offset += replacement.length - url.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
details = result;
|
|
||||||
return Html(
|
|
||||||
data: md.markdownToHtml(
|
|
||||||
details.replaceAll('\n', '<br>'),
|
|
||||||
extensionSet: md.ExtensionSet.commonMark,
|
|
||||||
),
|
|
||||||
style: {
|
|
||||||
"body": Style(
|
|
||||||
margin: Margins.zero,
|
|
||||||
padding: HtmlPaddings.zero,
|
|
||||||
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
|
|
||||||
lineHeight: const LineHeight(1.5),
|
|
||||||
),
|
|
||||||
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
|
|
||||||
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold),
|
|
||||||
"h1": Style(fontSize: FontSize(24)),
|
|
||||||
"h2": Style(fontSize: FontSize(20)),
|
|
||||||
"h3": Style(fontSize: FontSize(18)),
|
|
||||||
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
|
|
||||||
"li": Style(margin: Margins.only(bottom: 4), color: appTheme.black),
|
|
||||||
"strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black),
|
|
||||||
"em, i": Style(fontStyle: FontStyle.italic),
|
|
||||||
"code": Style(
|
|
||||||
backgroundColor: Colors.grey.shade100,
|
|
||||||
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
),
|
|
||||||
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
|
|
||||||
"blockquote": Style(
|
|
||||||
padding: HtmlPaddings.only(left: 16),
|
|
||||||
margin: Margins.only(left: 8, bottom: 12),
|
|
||||||
fontStyle: FontStyle.italic,
|
|
||||||
color: Colors.grey.shade700,
|
|
||||||
),
|
|
||||||
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
|
|
||||||
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
|
|
||||||
},
|
|
||||||
onLinkTap: (url, attributes, element) async {
|
|
||||||
if (url != null) {
|
|
||||||
final uri = Uri.parse(url);
|
|
||||||
if (await canLaunchUrl(uri)) {
|
|
||||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -268,8 +191,3 @@ class _MyPlanWidgetState extends State<MyPlanWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _LinkRange {
|
|
||||||
final int start;
|
|
||||||
final int end;
|
|
||||||
_LinkRange(this.start, this.end);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -24,6 +24,14 @@ class _NotificationsScreenState extends State<NotificationsScreen> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_scrollController.addListener(_onScroll);
|
_scrollController.addListener(_onScroll);
|
||||||
|
// Инициализация NotificationsCubit при первом открытии экрана
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
final cubit = context.read<NotificationsCubit>();
|
||||||
|
final state = cubit.state;
|
||||||
|
if (state is! NotificationsStateLoaded && state is! NotificationsStateError) {
|
||||||
|
cubit.getNotifications();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -153,16 +153,13 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
|
|||||||
backgroundColor: appTheme.gray100,
|
backgroundColor: appTheme.gray100,
|
||||||
resizeToAvoidBottomInset: true,
|
resizeToAvoidBottomInset: true,
|
||||||
bottomSheet: Container(
|
bottomSheet: Container(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom + 8),
|
||||||
top: 12,
|
|
||||||
bottom: 12 + MediaQuery.of(context).padding.bottom + 56, // 56 - высота bottomNavigationBar
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
|
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -13,10 +13,9 @@ import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/prof
|
|||||||
import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_state.dart';
|
import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_state.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_html/flutter_html.dart';
|
import 'package:eatvio_app/core/widgets/markdown_html_widget.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:markdown/markdown.dart' as md;
|
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|
||||||
class ProfileBlogPostWidget extends StatefulWidget {
|
class ProfileBlogPostWidget extends StatefulWidget {
|
||||||
@@ -107,37 +106,48 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
|
|||||||
if (state.blogPosts[widget.index].description.isNotEmpty)
|
if (state.blogPosts[widget.index].description.isNotEmpty)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
child: Html(
|
child: Builder(
|
||||||
data: md.markdownToHtml(
|
builder: (context) {
|
||||||
state.blogPosts[widget.index].description.length > 200
|
String description = state.blogPosts[widget.index].description;
|
||||||
? state.blogPosts[widget.index].description.substring(0, 200).replaceAll('\n', '<br>')
|
final isLong = description.length > 200;
|
||||||
: state.blogPosts[widget.index].description.replaceAll('\n', '<br>'),
|
|
||||||
),
|
if (isLong) {
|
||||||
style: {
|
// Умная обрезка: обрезаем markdown текст, не ломая синтаксис
|
||||||
"body": Style(margin: Margins.zero, padding: HtmlPaddings.zero, fontSize: FontSize(16), lineHeight: const LineHeight(1.5)),
|
int cutPoint = 200;
|
||||||
"p": Style(margin: Margins.only(bottom: 12), color: Colors.black87),
|
|
||||||
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: Colors.black, fontWeight: FontWeight.bold),
|
// Проверяем, есть ли незакрытые markdown теги
|
||||||
"h1": Style(fontSize: FontSize(24)),
|
String preview = description.substring(0, cutPoint);
|
||||||
"h2": Style(fontSize: FontSize(20)),
|
|
||||||
"h3": Style(fontSize: FontSize(18)),
|
// Считаем открывающие и закрывающие теги
|
||||||
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
|
int boldOpen = (preview.split('**').length - 1) ~/ 2;
|
||||||
"li": Style(margin: Margins.only(bottom: 4), color: Colors.black87),
|
int boldClose = preview.split('**').length - 1 - boldOpen * 2;
|
||||||
"strong, b": Style(fontWeight: FontWeight.bold, color: Colors.black),
|
bool hasUnclosedBold = boldOpen > boldClose;
|
||||||
"em, i": Style(fontStyle: FontStyle.italic),
|
|
||||||
"code": Style(
|
// Если есть незакрытый bold, ищем закрытие
|
||||||
backgroundColor: Colors.grey.shade100,
|
if (hasUnclosedBold) {
|
||||||
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
|
int searchEnd = (cutPoint + 100).clamp(0, description.length);
|
||||||
fontFamily: 'monospace',
|
int boldClosePos = description.indexOf('**', cutPoint);
|
||||||
),
|
if (boldClosePos != -1 && boldClosePos < searchEnd) {
|
||||||
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
|
cutPoint = boldClosePos + 2;
|
||||||
"blockquote": Style(
|
} else {
|
||||||
padding: HtmlPaddings.only(left: 16),
|
// Если не нашли, обрезаем до последнего пробела
|
||||||
margin: Margins.only(left: 8, bottom: 12),
|
cutPoint = description.lastIndexOf(' ', 200);
|
||||||
fontStyle: FontStyle.italic,
|
if (cutPoint == -1 || cutPoint < 150) cutPoint = 200;
|
||||||
color: Colors.grey.shade700,
|
}
|
||||||
),
|
} else {
|
||||||
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
|
// Обрезаем до последнего пробела для красоты
|
||||||
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
|
cutPoint = description.lastIndexOf(' ', cutPoint);
|
||||||
|
if (cutPoint == -1 || cutPoint < 150) cutPoint = 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
description = description.substring(0, cutPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
return MarkdownHtmlWidget(
|
||||||
|
markdownText: description,
|
||||||
|
fontSize: 16,
|
||||||
|
textColor: Colors.black87,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -99,108 +99,102 @@ class _AboutRecipeScreenState extends State<AboutRecipeScreen> {
|
|||||||
resizeToAvoidBottomInset: true,
|
resizeToAvoidBottomInset: true,
|
||||||
bottomSheet: Container(
|
bottomSheet: Container(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
top: 12,
|
bottom: MediaQuery.of(context).padding.bottom + 8,
|
||||||
bottom: 12 + MediaQuery.of(context).padding.bottom + 56, // 56 - высота bottomNavigationBar
|
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
|
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||||
child: Column(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
if (replyId != null)
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
GestureDetector(
|
||||||
children: [
|
onTap: () {
|
||||||
if (replyId != null)
|
context.read<CommentsCubit>().setReplyId(null);
|
||||||
GestureDetector(
|
setState(() {
|
||||||
onTap: () {
|
replyId = null;
|
||||||
context.read<CommentsCubit>().setReplyId(null);
|
textEditingController.clear();
|
||||||
setState(() {
|
});
|
||||||
replyId = null;
|
FocusManager.instance.primaryFocus?.unfocus();
|
||||||
textEditingController.clear();
|
},
|
||||||
});
|
child: Container(
|
||||||
FocusManager.instance.primaryFocus?.unfocus();
|
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
|
||||||
},
|
height: 32,
|
||||||
child: Container(
|
width: 32,
|
||||||
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
|
child: const Icon(Icons.close_rounded, size: 18, color: Colors.white),
|
||||||
height: 32,
|
|
||||||
width: 32,
|
|
||||||
child: const Icon(Icons.close_rounded, size: 18, color: Colors.white),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (replyId != null) const SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: const BoxConstraints(minHeight: 52),
|
|
||||||
child: TextFormField(
|
|
||||||
textCapitalization: TextCapitalization.sentences,
|
|
||||||
onChanged: (_) {
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
controller: textEditingController,
|
|
||||||
maxLines: 5,
|
|
||||||
minLines: 1,
|
|
||||||
keyboardType: TextInputType.multiline,
|
|
||||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1.2),
|
|
||||||
decoration: InputDecoration(
|
|
||||||
isDense: false,
|
|
||||||
// убираем true, чтобы padding учитывался корректно
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
|
||||||
// вертикальный паддинг увеличен
|
|
||||||
hintText: 'Комментарий',
|
|
||||||
hintStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, height: 1.2, color: Color(0xFFAEAEB2)),
|
|
||||||
filled: true,
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
|
||||||
),
|
|
||||||
enabledBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
|
||||||
),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
),
|
||||||
GestureDetector(
|
if (replyId != null) const SizedBox(width: 8),
|
||||||
onTap: () {
|
Expanded(
|
||||||
if (textEditingController.text.length > 2) {
|
child: ConstrainedBox(
|
||||||
commentsCubit.sendComment(
|
constraints: const BoxConstraints(minHeight: 52),
|
||||||
id: widget.recipeSlug,
|
child: TextFormField(
|
||||||
commentText: textEditingController.text,
|
textCapitalization: TextCapitalization.sentences,
|
||||||
parentCommentId: replyId,
|
onChanged: (_) {
|
||||||
commentSourceType: CommentSourceType.recipe,
|
setState(() {});
|
||||||
);
|
|
||||||
context.read<CommentsCubit>().setReplyId(null);
|
|
||||||
setState(() {
|
|
||||||
textEditingController.clear();
|
|
||||||
replyId = null;
|
|
||||||
});
|
|
||||||
Future.delayed(const Duration(milliseconds: 500), () {
|
|
||||||
recipeCubit.getRecipe(widget.recipeSlug, index: widget.index);
|
|
||||||
});
|
|
||||||
FocusManager.instance.primaryFocus?.unfocus();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
child: Container(
|
controller: textEditingController,
|
||||||
decoration: BoxDecoration(
|
maxLines: 5,
|
||||||
color: textEditingController.text.length > 2 ? appTheme.blueA400 : Colors.grey,
|
minLines: 1,
|
||||||
shape: BoxShape.circle,
|
keyboardType: TextInputType.multiline,
|
||||||
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1.2),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
isDense: false,
|
||||||
|
// убираем true, чтобы padding учитывался корректно
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||||
|
// вертикальный паддинг увеличен
|
||||||
|
hintText: s.comment_hint,
|
||||||
|
hintStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, height: 1.2, color: Color(0xFFAEAEB2)),
|
||||||
|
filled: true,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
||||||
),
|
),
|
||||||
height: 32,
|
|
||||||
width: 32,
|
|
||||||
child: const Icon(Icons.send_rounded, size: 18, color: Colors.white),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
if (textEditingController.text.length > 2) {
|
||||||
|
commentsCubit.sendComment(
|
||||||
|
id: widget.recipeSlug,
|
||||||
|
commentText: textEditingController.text,
|
||||||
|
parentCommentId: replyId,
|
||||||
|
commentSourceType: CommentSourceType.recipe,
|
||||||
|
);
|
||||||
|
context.read<CommentsCubit>().setReplyId(null);
|
||||||
|
setState(() {
|
||||||
|
textEditingController.clear();
|
||||||
|
replyId = null;
|
||||||
|
});
|
||||||
|
Future.delayed(const Duration(milliseconds: 500), () {
|
||||||
|
recipeCubit.getRecipe(widget.recipeSlug, index: widget.index);
|
||||||
|
});
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: textEditingController.text.length > 2 ? appTheme.blueA400 : Colors.grey,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
height: 32,
|
||||||
|
width: 32,
|
||||||
|
child: const Icon(Icons.send_rounded, size: 18, color: Colors.white),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -12,12 +12,10 @@ import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/m
|
|||||||
import 'package:eatvio_app/generated/l10n.dart';
|
import 'package:eatvio_app/generated/l10n.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_html/flutter_html.dart';
|
import 'package:eatvio_app/core/widgets/markdown_html_widget.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:markdown/markdown.dart' as md;
|
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
|
||||||
|
|
||||||
class MyRecipeWidget extends StatefulWidget {
|
class MyRecipeWidget extends StatefulWidget {
|
||||||
final MyRecipeModel recipe;
|
final MyRecipeModel recipe;
|
||||||
@@ -100,86 +98,11 @@ class _MyRecipeWidgetState extends State<MyRecipeWidget> {
|
|||||||
String description = widget.recipe.description.length > 200
|
String description = widget.recipe.description.length > 200
|
||||||
? '${widget.recipe.description.substring(0, 200)}...'
|
? '${widget.recipe.description.substring(0, 200)}...'
|
||||||
: widget.recipe.description;
|
: widget.recipe.description;
|
||||||
// Автоматически преобразуем URL в markdown ссылки
|
|
||||||
// Сначала исправляем неправильный синтаксис markdown ссылок
|
return MarkdownHtmlWidget(
|
||||||
final wrongLinkRegex = RegExp(r'\[(https?://[^\]]+|www\.[^\]]+)\]\(([^\)]+)\)');
|
markdownText: description,
|
||||||
description = description.replaceAllMapped(wrongLinkRegex, (match) {
|
fontSize: TextStyles.textSMedium.fontSize ?? 14,
|
||||||
final urlInText = match.group(1)!;
|
textColor: appTheme.black,
|
||||||
if (!urlInText.startsWith('http://') && !urlInText.startsWith('https://') && !urlInText.startsWith('www.')) {
|
|
||||||
return match.group(0)!;
|
|
||||||
}
|
|
||||||
final fullUrl = urlInText.startsWith('www.') ? 'https://$urlInText' : urlInText;
|
|
||||||
return '[$urlInText]($fullUrl)';
|
|
||||||
});
|
|
||||||
|
|
||||||
final linkRegex = RegExp(r'\[([^\]]+)\]\(([^\)]+)\)');
|
|
||||||
final existingLinks = <_LinkRange>[];
|
|
||||||
for (final linkMatch in linkRegex.allMatches(description)) {
|
|
||||||
existingLinks.add(_LinkRange(linkMatch.start, linkMatch.end));
|
|
||||||
}
|
|
||||||
final urlRegex = RegExp(r'(https?://[^\s\)]+|www\.[^\s\)]+)', caseSensitive: false);
|
|
||||||
String result = description;
|
|
||||||
int offset = 0;
|
|
||||||
for (final urlMatch in urlRegex.allMatches(description)) {
|
|
||||||
bool isInsideLink = existingLinks.any((link) =>
|
|
||||||
urlMatch.start >= link.start && urlMatch.end <= link.end);
|
|
||||||
if (!isInsideLink) {
|
|
||||||
final url = urlMatch.group(0)!;
|
|
||||||
final fullUrl = url.startsWith('www.') ? 'https://$url' : url;
|
|
||||||
final replacement = '[$url]($fullUrl)';
|
|
||||||
result = result.replaceRange(
|
|
||||||
urlMatch.start + offset,
|
|
||||||
urlMatch.end + offset,
|
|
||||||
replacement,
|
|
||||||
);
|
|
||||||
offset += replacement.length - url.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
description = result;
|
|
||||||
return Html(
|
|
||||||
data: md.markdownToHtml(
|
|
||||||
description.replaceAll('\n', '<br>'),
|
|
||||||
extensionSet: md.ExtensionSet.commonMark,
|
|
||||||
),
|
|
||||||
style: {
|
|
||||||
"body": Style(
|
|
||||||
margin: Margins.zero,
|
|
||||||
padding: HtmlPaddings.zero,
|
|
||||||
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
|
|
||||||
lineHeight: const LineHeight(1.5),
|
|
||||||
),
|
|
||||||
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
|
|
||||||
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold),
|
|
||||||
"h1": Style(fontSize: FontSize(24)),
|
|
||||||
"h2": Style(fontSize: FontSize(20)),
|
|
||||||
"h3": Style(fontSize: FontSize(18)),
|
|
||||||
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
|
|
||||||
"li": Style(margin: Margins.only(bottom: 4), color: appTheme.black),
|
|
||||||
"strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black),
|
|
||||||
"em, i": Style(fontStyle: FontStyle.italic),
|
|
||||||
"code": Style(
|
|
||||||
backgroundColor: Colors.grey.shade100,
|
|
||||||
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
),
|
|
||||||
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
|
|
||||||
"blockquote": Style(
|
|
||||||
padding: HtmlPaddings.only(left: 16),
|
|
||||||
margin: Margins.only(left: 8, bottom: 12),
|
|
||||||
fontStyle: FontStyle.italic,
|
|
||||||
color: Colors.grey.shade700,
|
|
||||||
),
|
|
||||||
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
|
|
||||||
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
|
|
||||||
},
|
|
||||||
onLinkTap: (url, attributes, element) async {
|
|
||||||
if (url != null) {
|
|
||||||
final uri = Uri.parse(url);
|
|
||||||
if (await canLaunchUrl(uri)) {
|
|
||||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -270,8 +193,3 @@ class _MyRecipeWidgetState extends State<MyRecipeWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _LinkRange {
|
|
||||||
final int start;
|
|
||||||
final int end;
|
|
||||||
_LinkRange(this.start, this.end);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:intl/message_lookup_by_library.dart';
|
import 'package:intl/message_lookup_by_library.dart';
|
||||||
import 'package:intl/src/intl_helpers.dart';
|
import 'package:intl/src/intl_helpers.dart';
|
||||||
@@ -21,8 +20,8 @@ import 'messages_ru.dart' as messages_ru;
|
|||||||
|
|
||||||
typedef Future<dynamic> LibraryLoader();
|
typedef Future<dynamic> LibraryLoader();
|
||||||
Map<String, LibraryLoader> _deferredLibraries = {
|
Map<String, LibraryLoader> _deferredLibraries = {
|
||||||
'en': () => new SynchronousFuture(null),
|
'en': () => new Future.value(null),
|
||||||
'ru': () => new SynchronousFuture(null),
|
'ru': () => new Future.value(null),
|
||||||
};
|
};
|
||||||
|
|
||||||
MessageLookupByLibrary? _findExact(String localeName) {
|
MessageLookupByLibrary? _findExact(String localeName) {
|
||||||
@@ -37,20 +36,19 @@ MessageLookupByLibrary? _findExact(String localeName) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// User programs should call this before using [localeName] for messages.
|
/// User programs should call this before using [localeName] for messages.
|
||||||
Future<bool> initializeMessages(String localeName) {
|
Future<bool> initializeMessages(String localeName) async {
|
||||||
var availableLocale = Intl.verifiedLocale(
|
var availableLocale = Intl.verifiedLocale(
|
||||||
localeName,
|
localeName,
|
||||||
(locale) => _deferredLibraries[locale] != null,
|
(locale) => _deferredLibraries[locale] != null,
|
||||||
onFailure: (_) => null,
|
onFailure: (_) => null);
|
||||||
);
|
|
||||||
if (availableLocale == null) {
|
if (availableLocale == null) {
|
||||||
return new SynchronousFuture(false);
|
return new Future.value(false);
|
||||||
}
|
}
|
||||||
var lib = _deferredLibraries[availableLocale];
|
var lib = _deferredLibraries[availableLocale];
|
||||||
lib == null ? new SynchronousFuture(false) : lib();
|
await (lib == null ? new Future.value(false) : lib());
|
||||||
initializeInternalMessageLookup(() => new CompositeMessageLookup());
|
initializeInternalMessageLookup(() => new CompositeMessageLookup());
|
||||||
messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor);
|
messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor);
|
||||||
return new SynchronousFuture(true);
|
return new Future.value(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _messagesExistFor(String locale) {
|
bool _messagesExistFor(String locale) {
|
||||||
@@ -62,11 +60,8 @@ bool _messagesExistFor(String locale) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) {
|
MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) {
|
||||||
var actualLocale = Intl.verifiedLocale(
|
var actualLocale = Intl.verifiedLocale(locale, _messagesExistFor,
|
||||||
locale,
|
onFailure: (_) => null);
|
||||||
_messagesExistFor,
|
|
||||||
onFailure: (_) => null,
|
|
||||||
);
|
|
||||||
if (actualLocale == null) return null;
|
if (actualLocale == null) return null;
|
||||||
return _findExact(actualLocale);
|
return _findExact(actualLocale);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,7 @@
|
|||||||
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
|
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
|
||||||
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
|
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
|
||||||
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
|
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
|
||||||
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
|
// ignore_for_file:unused_import, file_names
|
||||||
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
|
|
||||||
|
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:intl/message_lookup_by_library.dart';
|
import 'package:intl/message_lookup_by_library.dart';
|
||||||
@@ -21,362 +20,223 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
String get localeName => 'ru';
|
String get localeName => 'ru';
|
||||||
|
|
||||||
final messages = _notInlinedMessages(_notInlinedMessages);
|
final messages = _notInlinedMessages(_notInlinedMessages);
|
||||||
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
static _notInlinedMessages(_) => <String, Function> {
|
||||||
"Moscow": MessageLookupByLibrary.simpleMessage("Москва"),
|
"Moscow" : MessageLookupByLibrary.simpleMessage("Москва"),
|
||||||
"Russia": MessageLookupByLibrary.simpleMessage("Россия"),
|
"Russia" : MessageLookupByLibrary.simpleMessage("Россия"),
|
||||||
"about_diet": MessageLookupByLibrary.simpleMessage("Об этой диете"),
|
"about_diet" : MessageLookupByLibrary.simpleMessage("Об этой диете"),
|
||||||
"about_plan": MessageLookupByLibrary.simpleMessage("Об этом плане"),
|
"about_plan" : MessageLookupByLibrary.simpleMessage("Об этом плане"),
|
||||||
"active": MessageLookupByLibrary.simpleMessage("Активен"),
|
"active" : MessageLookupByLibrary.simpleMessage("Активен"),
|
||||||
"active_diet_state": MessageLookupByLibrary.simpleMessage("Активная"),
|
"active_diet_state" : MessageLookupByLibrary.simpleMessage("Активная"),
|
||||||
"add": MessageLookupByLibrary.simpleMessage("Добавить"),
|
"add" : MessageLookupByLibrary.simpleMessage("Добавить"),
|
||||||
"add_cover": MessageLookupByLibrary.simpleMessage("Добавьте обложку"),
|
"add_cover" : MessageLookupByLibrary.simpleMessage("Добавьте обложку"),
|
||||||
"add_measure": MessageLookupByLibrary.simpleMessage("Добавить замер"),
|
"add_measure" : MessageLookupByLibrary.simpleMessage("Добавить замер"),
|
||||||
"adding_plan": MessageLookupByLibrary.simpleMessage("Добавление плана"),
|
"adding_plan" : MessageLookupByLibrary.simpleMessage("Добавление плана"),
|
||||||
"additional_information": MessageLookupByLibrary.simpleMessage(
|
"additional_information" : MessageLookupByLibrary.simpleMessage("Дополнительная информация"),
|
||||||
"Дополнительная информация",
|
"admin" : MessageLookupByLibrary.simpleMessage("Админ"),
|
||||||
),
|
"all" : MessageLookupByLibrary.simpleMessage("Все"),
|
||||||
"admin": MessageLookupByLibrary.simpleMessage("Админ"),
|
"allergies_presence" : MessageLookupByLibrary.simpleMessage("Наличие аллергии"),
|
||||||
"all": MessageLookupByLibrary.simpleMessage("Все"),
|
"another_form_of_comment" : MessageLookupByLibrary.simpleMessage("комментария"),
|
||||||
"allergies_presence": MessageLookupByLibrary.simpleMessage(
|
"another_form_of_day" : MessageLookupByLibrary.simpleMessage("дня"),
|
||||||
"Наличие аллергии",
|
"answer" : MessageLookupByLibrary.simpleMessage("Ответить"),
|
||||||
),
|
"archived" : MessageLookupByLibrary.simpleMessage("В архиве"),
|
||||||
"another_form_of_comment": MessageLookupByLibrary.simpleMessage(
|
"authentificate" : MessageLookupByLibrary.simpleMessage("Войти"),
|
||||||
"комментария",
|
"average_pressure" : MessageLookupByLibrary.simpleMessage("Средние показатели давления:"),
|
||||||
),
|
"bodymass_chart" : MessageLookupByLibrary.simpleMessage("График ИМТ"),
|
||||||
"another_form_of_day": MessageLookupByLibrary.simpleMessage("дня"),
|
"by_default" : MessageLookupByLibrary.simpleMessage("По умолчанию"),
|
||||||
"answer": MessageLookupByLibrary.simpleMessage("Ответить"),
|
"calories" : MessageLookupByLibrary.simpleMessage("Калории"),
|
||||||
"archived": MessageLookupByLibrary.simpleMessage("В архиве"),
|
"carb_type" : MessageLookupByLibrary.simpleMessage("Мучное"),
|
||||||
"authentificate": MessageLookupByLibrary.simpleMessage("Войти"),
|
"carb_type_desc" : MessageLookupByLibrary.simpleMessage("Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы."),
|
||||||
"average_pressure": MessageLookupByLibrary.simpleMessage(
|
"carbohydrates" : MessageLookupByLibrary.simpleMessage("Углеводы"),
|
||||||
"Средние показатели давления:",
|
"chat" : MessageLookupByLibrary.simpleMessage("Чат"),
|
||||||
),
|
"chest" : MessageLookupByLibrary.simpleMessage("Грудь"),
|
||||||
"bodymass_chart": MessageLookupByLibrary.simpleMessage("График ИМТ"),
|
"chest_circumference" : MessageLookupByLibrary.simpleMessage("Обхват груди"),
|
||||||
"by_default": MessageLookupByLibrary.simpleMessage("По умолчанию"),
|
"choose_diet_duration" : MessageLookupByLibrary.simpleMessage("Выберите срок диеты"),
|
||||||
"calories": MessageLookupByLibrary.simpleMessage("Калории"),
|
"choose_plan_duration" : MessageLookupByLibrary.simpleMessage("Выберите срок плана"),
|
||||||
"carb_type": MessageLookupByLibrary.simpleMessage("Мучное"),
|
"choose_start_diet_date" : MessageLookupByLibrary.simpleMessage("Выберите дату старты диеты. Она отобразиться у Вас в дневнике"),
|
||||||
"carb_type_desc": MessageLookupByLibrary.simpleMessage(
|
"circumference_empty" : MessageLookupByLibrary.simpleMessage("Все замеры обязательны к заполнению"),
|
||||||
"Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.",
|
"circumference_is_not_a_number" : MessageLookupByLibrary.simpleMessage("Все замеры должны быть числами"),
|
||||||
),
|
"circumferences_chart" : MessageLookupByLibrary.simpleMessage("График обхватов"),
|
||||||
"carbohydrates": MessageLookupByLibrary.simpleMessage("Углеводы"),
|
"city" : MessageLookupByLibrary.simpleMessage("Город"),
|
||||||
"chat": MessageLookupByLibrary.simpleMessage("Чат"),
|
"city_of_living" : MessageLookupByLibrary.simpleMessage("Город проживания"),
|
||||||
"chest": MessageLookupByLibrary.simpleMessage("Грудь"),
|
"comfort" : MessageLookupByLibrary.simpleMessage("Комфорт"),
|
||||||
"chest_circumference": MessageLookupByLibrary.simpleMessage("Обхват груди"),
|
"comment" : MessageLookupByLibrary.simpleMessage("комментарий"),
|
||||||
"choose_diet_duration": MessageLookupByLibrary.simpleMessage(
|
"comment_hint" : MessageLookupByLibrary.simpleMessage("Комментарий"),
|
||||||
"Выберите срок диеты",
|
"comments" : MessageLookupByLibrary.simpleMessage("Комментарии"),
|
||||||
),
|
"comments_plural" : MessageLookupByLibrary.simpleMessage("комментариев"),
|
||||||
"choose_plan_duration": MessageLookupByLibrary.simpleMessage(
|
"complain" : MessageLookupByLibrary.simpleMessage("Пожаловаться"),
|
||||||
"Выберите срок плана",
|
"continueButton" : MessageLookupByLibrary.simpleMessage("Продолжить"),
|
||||||
),
|
"continue_" : MessageLookupByLibrary.simpleMessage("Продолжить"),
|
||||||
"choose_start_diet_date": MessageLookupByLibrary.simpleMessage(
|
"copied" : MessageLookupByLibrary.simpleMessage("cкопирован"),
|
||||||
"Выберите дату старты диеты. Она отобразиться у Вас в дневнике",
|
"copy_link" : MessageLookupByLibrary.simpleMessage("Скопировать ссылку"),
|
||||||
),
|
"country" : MessageLookupByLibrary.simpleMessage("Страна"),
|
||||||
"circumference_empty": MessageLookupByLibrary.simpleMessage(
|
"create_diet" : MessageLookupByLibrary.simpleMessage("Создание диеты"),
|
||||||
"Все замеры обязательны к заполнению",
|
"customer_price" : MessageLookupByLibrary.simpleMessage("Цена для покупателя"),
|
||||||
),
|
"date_of_birth" : MessageLookupByLibrary.simpleMessage("Дата рождения"),
|
||||||
"circumference_is_not_a_number": MessageLookupByLibrary.simpleMessage(
|
"day" : MessageLookupByLibrary.simpleMessage("День"),
|
||||||
"Все замеры должны быть числами",
|
"days" : MessageLookupByLibrary.simpleMessage("дней"),
|
||||||
),
|
"deleted" : MessageLookupByLibrary.simpleMessage("Удалена"),
|
||||||
"circumferences_chart": MessageLookupByLibrary.simpleMessage(
|
"diary" : MessageLookupByLibrary.simpleMessage("Дневник"),
|
||||||
"График обхватов",
|
"diet" : MessageLookupByLibrary.simpleMessage("Диета"),
|
||||||
),
|
"diet_author" : MessageLookupByLibrary.simpleMessage("Автор диеты"),
|
||||||
"city": MessageLookupByLibrary.simpleMessage("Город"),
|
"diet_plan" : MessageLookupByLibrary.simpleMessage("План диеты"),
|
||||||
"city_of_living": MessageLookupByLibrary.simpleMessage("Город проживания"),
|
"diet_state" : MessageLookupByLibrary.simpleMessage("Статус диеты"),
|
||||||
"comfort": MessageLookupByLibrary.simpleMessage("Комфорт"),
|
"diet_type" : MessageLookupByLibrary.simpleMessage("Тип диеты"),
|
||||||
"comment": MessageLookupByLibrary.simpleMessage("комментарий"),
|
"do_you_have_hypertension_tendency" : MessageLookupByLibrary.simpleMessage("Есть ли у вас гипертония?"),
|
||||||
"comments": MessageLookupByLibrary.simpleMessage("Комментарии"),
|
"do_you_have_hypotension_tendency" : MessageLookupByLibrary.simpleMessage("Есть ли у вас гипотония?"),
|
||||||
"comments_plural": MessageLookupByLibrary.simpleMessage("комментариев"),
|
"do_you_take_pills" : MessageLookupByLibrary.simpleMessage("Принимаете ли вы лекарства или бады?"),
|
||||||
"complain": MessageLookupByLibrary.simpleMessage("Пожаловаться"),
|
"down_pressure" : MessageLookupByLibrary.simpleMessage("Нижние показатели давления:"),
|
||||||
"continueButton": MessageLookupByLibrary.simpleMessage("Продолжить"),
|
"draft" : MessageLookupByLibrary.simpleMessage("Черновик"),
|
||||||
"continue_": MessageLookupByLibrary.simpleMessage("Продолжить"),
|
"drafted" : MessageLookupByLibrary.simpleMessage("Черновики"),
|
||||||
"copied": MessageLookupByLibrary.simpleMessage("cкопирован"),
|
"eating_behavior" : MessageLookupByLibrary.simpleMessage("Пищевое поведение"),
|
||||||
"copy_link": MessageLookupByLibrary.simpleMessage("Скопировать ссылку"),
|
"eating_behavior_type" : MessageLookupByLibrary.simpleMessage("Формирование пищевого поведения"),
|
||||||
"country": MessageLookupByLibrary.simpleMessage("Страна"),
|
"eating_behavior_type_desc" : MessageLookupByLibrary.simpleMessage("Формирование здорового пищевого поведения — это процесс, который включает в себя изменение привычек и отношения к еде. Он направлен на то, чтобы помочь людям научиться правильно питаться и поддерживать здоровый образ жизни.Основные принципы формирования здорового пищевого поведения: \n● Разнообразие продуктов; \n● Сбалансированный рацион; \n● Питьевой режим; \n● Ограничение вредных продуктов; \n● Регулярные приёмы пищи; \n● Умеренность;Осознанное питание; \n● Физическая активность;"),
|
||||||
"create_diet": MessageLookupByLibrary.simpleMessage("Создание диеты"),
|
"economy" : MessageLookupByLibrary.simpleMessage("Эконом"),
|
||||||
"customer_price": MessageLookupByLibrary.simpleMessage(
|
"edit" : MessageLookupByLibrary.simpleMessage("Редактировать"),
|
||||||
"Цена для покупателя",
|
"email" : MessageLookupByLibrary.simpleMessage("Электронная почта"),
|
||||||
),
|
"email_empty" : MessageLookupByLibrary.simpleMessage("Необходимо ввести адрес электронной почты"),
|
||||||
"date_of_birth": MessageLookupByLibrary.simpleMessage("Дата рождения"),
|
"email_invalid" : MessageLookupByLibrary.simpleMessage("Некорректный адрес электронной почты"),
|
||||||
"day": MessageLookupByLibrary.simpleMessage("День"),
|
"enter_additional_information" : MessageLookupByLibrary.simpleMessage("Введите любую дополнительную информацию на своё усмотрение:"),
|
||||||
"days": MessageLookupByLibrary.simpleMessage("дней"),
|
"enter_diet_description" : MessageLookupByLibrary.simpleMessage("Введите описание диеты"),
|
||||||
"deleted": MessageLookupByLibrary.simpleMessage("Удалена"),
|
"enter_diet_name" : MessageLookupByLibrary.simpleMessage("Выведите название диеты"),
|
||||||
"diary": MessageLookupByLibrary.simpleMessage("Дневник"),
|
"enter_information" : MessageLookupByLibrary.simpleMessage("Введите информацию..."),
|
||||||
"diet": MessageLookupByLibrary.simpleMessage("Диета"),
|
"exit" : MessageLookupByLibrary.simpleMessage("Выйти"),
|
||||||
"diet_author": MessageLookupByLibrary.simpleMessage("Автор диеты"),
|
"expand" : MessageLookupByLibrary.simpleMessage("Показать полностью"),
|
||||||
"diet_plan": MessageLookupByLibrary.simpleMessage("План диеты"),
|
"fats" : MessageLookupByLibrary.simpleMessage("Жиры"),
|
||||||
"diet_state": MessageLookupByLibrary.simpleMessage("Статус диеты"),
|
"favorite_foods" : MessageLookupByLibrary.simpleMessage("Любимые продукты"),
|
||||||
"diet_type": MessageLookupByLibrary.simpleMessage("Тип диеты"),
|
"favorite_unfavorite_foods" : MessageLookupByLibrary.simpleMessage("Ваши любимые/нелюбимые продукты питания?"),
|
||||||
"do_you_have_hypertension_tendency": MessageLookupByLibrary.simpleMessage(
|
"female" : MessageLookupByLibrary.simpleMessage("Женский"),
|
||||||
"Есть ли у вас гипертония?",
|
"fill_in_information" : MessageLookupByLibrary.simpleMessage("Заполните информацию:"),
|
||||||
),
|
"finish" : MessageLookupByLibrary.simpleMessage("Завершить"),
|
||||||
"do_you_have_hypotension_tendency": MessageLookupByLibrary.simpleMessage(
|
"first_name" : MessageLookupByLibrary.simpleMessage("Имя"),
|
||||||
"Есть ли у вас гипотония?",
|
"first_name_empty" : MessageLookupByLibrary.simpleMessage("Имя обязательно для заполнения"),
|
||||||
),
|
"first_name_short" : MessageLookupByLibrary.simpleMessage("В имени должен быть минимум 1 символ"),
|
||||||
"do_you_take_pills": MessageLookupByLibrary.simpleMessage(
|
"fit_type" : MessageLookupByLibrary.simpleMessage("Поддержание формы"),
|
||||||
"Принимаете ли вы лекарства или бады?",
|
"fit_type_desc" : MessageLookupByLibrary.simpleMessage("План питания для поддержания формы сбалансированный и разнообразный, чтобы обеспечить организм всеми необходимыми питательными веществами. Общие принципы: \n● Дробное питание; \n● Разнообразие продуктов; \n● Контроль порций; \n● Питьевой режим; \n● Ограничение вредных продуктов;"),
|
||||||
),
|
"fitness" : MessageLookupByLibrary.simpleMessage("Поддержка формы"),
|
||||||
"down_pressure": MessageLookupByLibrary.simpleMessage(
|
"from" : MessageLookupByLibrary.simpleMessage("от"),
|
||||||
"Нижние показатели давления:",
|
"from_a_to_z" : MessageLookupByLibrary.simpleMessage("Название: А-Я"),
|
||||||
),
|
"from_z_to_a" : MessageLookupByLibrary.simpleMessage("Название: Я-А"),
|
||||||
"draft": MessageLookupByLibrary.simpleMessage("Черновик"),
|
"gallery" : MessageLookupByLibrary.simpleMessage("Галерея"),
|
||||||
"drafted": MessageLookupByLibrary.simpleMessage("Черновики"),
|
"gr" : MessageLookupByLibrary.simpleMessage("гр"),
|
||||||
"eating_behavior": MessageLookupByLibrary.simpleMessage(
|
"grams" : MessageLookupByLibrary.simpleMessage("гр"),
|
||||||
"Пищевое поведение",
|
"height_measurements" : MessageLookupByLibrary.simpleMessage("Замер роста"),
|
||||||
),
|
"hide" : MessageLookupByLibrary.simpleMessage("Свернуть"),
|
||||||
"eating_behavior_type": MessageLookupByLibrary.simpleMessage(
|
"hip_circumference" : MessageLookupByLibrary.simpleMessage("Обхват бедер"),
|
||||||
"Формирование пищевого поведения",
|
"hips" : MessageLookupByLibrary.simpleMessage("Бедра"),
|
||||||
),
|
"in_progress" : MessageLookupByLibrary.simpleMessage("В процессе"),
|
||||||
"eating_behavior_type_desc": MessageLookupByLibrary.simpleMessage(
|
"inactive_diet_state" : MessageLookupByLibrary.simpleMessage("Черновик"),
|
||||||
"Формирование здорового пищевого поведения — это процесс, который включает в себя изменение привычек и отношения к еде. Он направлен на то, чтобы помочь людям научиться правильно питаться и поддерживать здоровый образ жизни.Основные принципы формирования здорового пищевого поведения: \n● Разнообразие продуктов; \n● Сбалансированный рацион; \n● Питьевой режим; \n● Ограничение вредных продуктов; \n● Регулярные приёмы пищи; \n● Умеренность;Осознанное питание; \n● Физическая активность;",
|
"infoAboutOrder" : MessageLookupByLibrary.simpleMessage("Информация о заказе"),
|
||||||
),
|
"initial_measurements" : MessageLookupByLibrary.simpleMessage("Первичные замеры"),
|
||||||
"economy": MessageLookupByLibrary.simpleMessage("Эконом"),
|
"label" : MessageLookupByLibrary.simpleMessage("Название"),
|
||||||
"edit": MessageLookupByLibrary.simpleMessage("Редактировать"),
|
"language" : MessageLookupByLibrary.simpleMessage("Язык"),
|
||||||
"email": MessageLookupByLibrary.simpleMessage("Электронная почта"),
|
"last_name" : MessageLookupByLibrary.simpleMessage("Фамилия"),
|
||||||
"email_empty": MessageLookupByLibrary.simpleMessage(
|
"last_name_empty" : MessageLookupByLibrary.simpleMessage("Фамилия обязательна для заполнения"),
|
||||||
"Необходимо ввести адрес электронной почты",
|
"last_name_short" : MessageLookupByLibrary.simpleMessage("В фамилии должен быть минимум 6 символов"),
|
||||||
),
|
"lbl_english" : MessageLookupByLibrary.simpleMessage("English"),
|
||||||
"email_invalid": MessageLookupByLibrary.simpleMessage(
|
"light_type" : MessageLookupByLibrary.simpleMessage("Легкая"),
|
||||||
"Некорректный адрес электронной почты",
|
"light_type_desc" : MessageLookupByLibrary.simpleMessage("С некоторыми видами диет можно употреблять алкоголь в умеренных количествах. Рекомендуется совмещать вместе с низкокалорийными закусками."),
|
||||||
),
|
"link_was_copied" : MessageLookupByLibrary.simpleMessage("Ссылка успешно скопирована"),
|
||||||
"enter_additional_information": MessageLookupByLibrary.simpleMessage(
|
"loading" : MessageLookupByLibrary.simpleMessage("Загрузка..."),
|
||||||
"Введите любую дополнительную информацию на своё усмотрение:",
|
"mail_was_copied" : MessageLookupByLibrary.simpleMessage("Почта успешно скопирована"),
|
||||||
),
|
"male" : MessageLookupByLibrary.simpleMessage("Мужской"),
|
||||||
"enter_diet_description": MessageLookupByLibrary.simpleMessage(
|
"meal" : MessageLookupByLibrary.simpleMessage("Приём пищи"),
|
||||||
"Введите описание диеты",
|
"meat" : MessageLookupByLibrary.simpleMessage("Мясо"),
|
||||||
),
|
"meat_type" : MessageLookupByLibrary.simpleMessage("С мясом"),
|
||||||
"enter_diet_name": MessageLookupByLibrary.simpleMessage(
|
"meat_type_desc" : MessageLookupByLibrary.simpleMessage("Диета с мясом полезная и сбалансированная. Включает в себя нежирное мясо: курицу, индейку, кролика или говядину. В сочетании с овощами получаете все необходимые витамины и минералы. Не забывайте о порциях и старайтесь есть медленно, прислушиваясь к своему организму."),
|
||||||
"Выведите название диеты",
|
"milliliter" : MessageLookupByLibrary.simpleMessage("мл"),
|
||||||
),
|
"minimal_customer_price" : MessageLookupByLibrary.simpleMessage("Минимальная цена для покупателя: 1200₽"),
|
||||||
"enter_information": MessageLookupByLibrary.simpleMessage(
|
"most_new" : MessageLookupByLibrary.simpleMessage("Самые новые"),
|
||||||
"Введите информацию...",
|
"most_old" : MessageLookupByLibrary.simpleMessage("Самые старые"),
|
||||||
),
|
"msg_network_err" : MessageLookupByLibrary.simpleMessage("Сетевая ошибка!"),
|
||||||
"exit": MessageLookupByLibrary.simpleMessage("Выйти"),
|
"msg_something_went_wrong" : MessageLookupByLibrary.simpleMessage("Что то пошло не так!"),
|
||||||
"expand": MessageLookupByLibrary.simpleMessage("Показать полностью"),
|
"my_blog" : MessageLookupByLibrary.simpleMessage("Мой блог"),
|
||||||
"fats": MessageLookupByLibrary.simpleMessage("Жиры"),
|
"my_diets" : MessageLookupByLibrary.simpleMessage("Мои диеты"),
|
||||||
"favorite_foods": MessageLookupByLibrary.simpleMessage("Любимые продукты"),
|
"my_plans" : MessageLookupByLibrary.simpleMessage("Мои планы"),
|
||||||
"favorite_unfavorite_foods": MessageLookupByLibrary.simpleMessage(
|
"my_settings" : MessageLookupByLibrary.simpleMessage("Мои настройки"),
|
||||||
"Ваши любимые/нелюбимые продукты питания?",
|
"name_empty" : MessageLookupByLibrary.simpleMessage("Имя пользователя обязательно для заполнения"),
|
||||||
),
|
"name_short" : MessageLookupByLibrary.simpleMessage("В имени пользователя должно быть минимум 6 символов"),
|
||||||
"female": MessageLookupByLibrary.simpleMessage("Женский"),
|
"no" : MessageLookupByLibrary.simpleMessage("Нет"),
|
||||||
"fill_in_information": MessageLookupByLibrary.simpleMessage(
|
"no_account" : MessageLookupByLibrary.simpleMessage("У меня нет аккаунта"),
|
||||||
"Заполните информацию:",
|
"notifications" : MessageLookupByLibrary.simpleMessage("Уведомления"),
|
||||||
),
|
"one_option" : MessageLookupByLibrary.simpleMessage("1 опция"),
|
||||||
"finish": MessageLookupByLibrary.simpleMessage("Завершить"),
|
"option" : MessageLookupByLibrary.simpleMessage("Опция"),
|
||||||
"first_name": MessageLookupByLibrary.simpleMessage("Имя"),
|
"option_content" : MessageLookupByLibrary.simpleMessage("Содержание опции"),
|
||||||
"first_name_empty": MessageLookupByLibrary.simpleMessage(
|
"option_content_hint_text" : MessageLookupByLibrary.simpleMessage("На связи 7 дней в неделю"),
|
||||||
"Имя обязательно для заполнения",
|
"option_desc" : MessageLookupByLibrary.simpleMessage("Описание опции"),
|
||||||
),
|
"option_desc_hint_text" : MessageLookupByLibrary.simpleMessage("Этот план питания для похудения на 7 дней сбалансированный и разнообразный"),
|
||||||
"first_name_short": MessageLookupByLibrary.simpleMessage(
|
"option_duration" : MessageLookupByLibrary.simpleMessage("Продолжительность опции"),
|
||||||
"В имени должен быть минимум 1 символ",
|
"option_personal_diet_desc" : MessageLookupByLibrary.simpleMessage("Опция подразумевает создание индивидуальной диеты"),
|
||||||
),
|
"option_title" : MessageLookupByLibrary.simpleMessage("Заголовок опции"),
|
||||||
"fit_type": MessageLookupByLibrary.simpleMessage("Поддержание формы"),
|
"order" : MessageLookupByLibrary.simpleMessage("Порядок"),
|
||||||
"fit_type_desc": MessageLookupByLibrary.simpleMessage(
|
"password" : MessageLookupByLibrary.simpleMessage("Пароль"),
|
||||||
"План питания для поддержания формы сбалансированный и разнообразный, чтобы обеспечить организм всеми необходимыми питательными веществами. Общие принципы: \n● Дробное питание; \n● Разнообразие продуктов; \n● Контроль порций; \n● Питьевой режим; \n● Ограничение вредных продуктов;",
|
"password_confirmation" : MessageLookupByLibrary.simpleMessage("Подтвердите пароль"),
|
||||||
),
|
"password_confirmation_mismatch" : MessageLookupByLibrary.simpleMessage("Пароли не совпадают"),
|
||||||
"fitness": MessageLookupByLibrary.simpleMessage("Поддержка формы"),
|
"password_empty" : MessageLookupByLibrary.simpleMessage("Пароль обязателен для заполнения"),
|
||||||
"from": MessageLookupByLibrary.simpleMessage("от"),
|
"password_short" : MessageLookupByLibrary.simpleMessage("В пароле должно быть минимум 6 символов"),
|
||||||
"from_a_to_z": MessageLookupByLibrary.simpleMessage("Название: А-Я"),
|
"pcs" : MessageLookupByLibrary.simpleMessage("шт"),
|
||||||
"from_z_to_a": MessageLookupByLibrary.simpleMessage("Название: Я-А"),
|
"personal" : MessageLookupByLibrary.simpleMessage("Персональные"),
|
||||||
"gallery": MessageLookupByLibrary.simpleMessage("Галерея"),
|
"personal_diet_state" : MessageLookupByLibrary.simpleMessage("Персональная"),
|
||||||
"gr": MessageLookupByLibrary.simpleMessage("гр"),
|
"photo" : MessageLookupByLibrary.simpleMessage("Фото"),
|
||||||
"grams": MessageLookupByLibrary.simpleMessage("гр"),
|
"place_of_living" : MessageLookupByLibrary.simpleMessage("Место проживания"),
|
||||||
"height_measurements": MessageLookupByLibrary.simpleMessage("Замер роста"),
|
"plan_author" : MessageLookupByLibrary.simpleMessage("Автор плана"),
|
||||||
"hide": MessageLookupByLibrary.simpleMessage("Свернуть"),
|
"plan_desc" : MessageLookupByLibrary.simpleMessage("Описание плана"),
|
||||||
"hip_circumference": MessageLookupByLibrary.simpleMessage("Обхват бедер"),
|
"plan_desc_example" : MessageLookupByLibrary.simpleMessage("Большое подробное, привлекательное описание плана"),
|
||||||
"hips": MessageLookupByLibrary.simpleMessage("Бедра"),
|
"plan_duration" : MessageLookupByLibrary.simpleMessage("Срок плана"),
|
||||||
"in_progress": MessageLookupByLibrary.simpleMessage("В процессе"),
|
"plan_label_desc" : MessageLookupByLibrary.simpleMessage("План для поддержания формы"),
|
||||||
"inactive_diet_state": MessageLookupByLibrary.simpleMessage("Черновик"),
|
"plans_complete" : MessageLookupByLibrary.simpleMessage("Пройденные планы"),
|
||||||
"infoAboutOrder": MessageLookupByLibrary.simpleMessage(
|
"popular" : MessageLookupByLibrary.simpleMessage("Популярные"),
|
||||||
"Информация о заказе",
|
"preferred_language" : MessageLookupByLibrary.simpleMessage("Предпочитаемый язык общения"),
|
||||||
),
|
"pressure_empty" : MessageLookupByLibrary.simpleMessage("Все показатели давления обязательны к заполнению"),
|
||||||
"initial_measurements": MessageLookupByLibrary.simpleMessage(
|
"pressure_is_not_a_number" : MessageLookupByLibrary.simpleMessage("Все показатели давления должны быть числами"),
|
||||||
"Первичные замеры",
|
"preview" : MessageLookupByLibrary.simpleMessage("Превью"),
|
||||||
),
|
"price" : MessageLookupByLibrary.simpleMessage("Цена"),
|
||||||
"label": MessageLookupByLibrary.simpleMessage("Название"),
|
"price_explanation" : MessageLookupByLibrary.simpleMessage("Разница в ценах, это комиссия Итвио"),
|
||||||
"language": MessageLookupByLibrary.simpleMessage("Язык"),
|
"profile" : MessageLookupByLibrary.simpleMessage("Профиль"),
|
||||||
"last_name": MessageLookupByLibrary.simpleMessage("Фамилия"),
|
"proteins" : MessageLookupByLibrary.simpleMessage("Белки"),
|
||||||
"last_name_empty": MessageLookupByLibrary.simpleMessage(
|
"public_diet_state" : MessageLookupByLibrary.simpleMessage("Публичная"),
|
||||||
"Фамилия обязательна для заполнения",
|
"publish" : MessageLookupByLibrary.simpleMessage("Опубликовать"),
|
||||||
),
|
"published" : MessageLookupByLibrary.simpleMessage("Опубликованные"),
|
||||||
"last_name_short": MessageLookupByLibrary.simpleMessage(
|
"removeFromActive" : MessageLookupByLibrary.simpleMessage("Снять с публикации"),
|
||||||
"В фамилии должен быть минимум 6 символов",
|
"save" : MessageLookupByLibrary.simpleMessage("Сохранить"),
|
||||||
),
|
"services" : MessageLookupByLibrary.simpleMessage("Оказание услуг"),
|
||||||
"lbl_english": MessageLookupByLibrary.simpleMessage("English"),
|
"settings" : MessageLookupByLibrary.simpleMessage("Настройки"),
|
||||||
"light_type": MessageLookupByLibrary.simpleMessage("Легкая"),
|
"share" : MessageLookupByLibrary.simpleMessage("Поделиться"),
|
||||||
"light_type_desc": MessageLookupByLibrary.simpleMessage(
|
"skip" : MessageLookupByLibrary.simpleMessage("Пропустить"),
|
||||||
"С некоторыми видами диет можно употреблять алкоголь в умеренных количествах. Рекомендуется совмещать вместе с низкокалорийными закусками.",
|
"slim" : MessageLookupByLibrary.simpleMessage("Похудение"),
|
||||||
),
|
"slim_type" : MessageLookupByLibrary.simpleMessage("Похудение"),
|
||||||
"link_was_copied": MessageLookupByLibrary.simpleMessage(
|
"slim_type_desc" : MessageLookupByLibrary.simpleMessage("План питания для похудения должен быть разработан с учётом индивидуальных особенностей организма, возраста, пола, уровня физической активности и состояния здоровья. Общие принципы: \n● Снижение калорийности; \n● Сбалансированный рацион; \n● Разнообразие продуктов; \n● Питьевой режим; \n● Ограничение вредных продуктов;"),
|
||||||
"Ссылка успешно скопирована",
|
"standard" : MessageLookupByLibrary.simpleMessage("Стандарт"),
|
||||||
),
|
"startDiet" : MessageLookupByLibrary.simpleMessage("Начать диету"),
|
||||||
"loading": MessageLookupByLibrary.simpleMessage("Загрузка..."),
|
"tell_your_schedule" : MessageLookupByLibrary.simpleMessage("Расскажите в свободной форме о том как просыпаетесь, когда едите, когда ложитесь спать:"),
|
||||||
"mail_was_copied": MessageLookupByLibrary.simpleMessage(
|
"text_was_copied" : MessageLookupByLibrary.simpleMessage("Текст успешно скопирован"),
|
||||||
"Почта успешно скопирована",
|
"three_options" : MessageLookupByLibrary.simpleMessage("3 опции"),
|
||||||
),
|
"toOffer" : MessageLookupByLibrary.simpleMessage("Предложить"),
|
||||||
"male": MessageLookupByLibrary.simpleMessage("Мужской"),
|
"to_draft" : MessageLookupByLibrary.simpleMessage("В черновик"),
|
||||||
"meal": MessageLookupByLibrary.simpleMessage("Приём пищи"),
|
"unfavorite_foods" : MessageLookupByLibrary.simpleMessage("Нелюбимые продукты"),
|
||||||
"meat": MessageLookupByLibrary.simpleMessage("Мясо"),
|
"up_pressure" : MessageLookupByLibrary.simpleMessage("Верхние показатели давления:"),
|
||||||
"meat_type": MessageLookupByLibrary.simpleMessage("С мясом"),
|
"user_authorization" : MessageLookupByLibrary.simpleMessage("Аутентификация"),
|
||||||
"meat_type_desc": MessageLookupByLibrary.simpleMessage(
|
"user_data" : MessageLookupByLibrary.simpleMessage("Данные пользователя"),
|
||||||
"Диета с мясом полезная и сбалансированная. Включает в себя нежирное мясо: курицу, индейку, кролика или говядину. В сочетании с овощами получаете все необходимые витамины и минералы. Не забывайте о порциях и старайтесь есть медленно, прислушиваясь к своему организму.",
|
"user_registration" : MessageLookupByLibrary.simpleMessage("Регистрация пользователя"),
|
||||||
),
|
"username" : MessageLookupByLibrary.simpleMessage("Имя пользователя"),
|
||||||
"milliliter": MessageLookupByLibrary.simpleMessage("мл"),
|
"vegan" : MessageLookupByLibrary.simpleMessage("Веган."),
|
||||||
"minimal_customer_price": MessageLookupByLibrary.simpleMessage(
|
"vegan_type" : MessageLookupByLibrary.simpleMessage("Вегетарианская"),
|
||||||
"Минимальная цена для покупателя: 1200₽",
|
"vegan_type_desc" : MessageLookupByLibrary.simpleMessage("Диета основана на потреблении растительной пищи и отказе от продуктов животного происхождения. Такой тип питания может быть разнообразным и вкусным, а также полезным для здоровья."),
|
||||||
),
|
"waist" : MessageLookupByLibrary.simpleMessage("Талия"),
|
||||||
"most_new": MessageLookupByLibrary.simpleMessage("Самые новые"),
|
"waist_circumference" : MessageLookupByLibrary.simpleMessage("Обхват талии"),
|
||||||
"most_old": MessageLookupByLibrary.simpleMessage("Самые старые"),
|
"weight" : MessageLookupByLibrary.simpleMessage("Вес"),
|
||||||
"msg_network_err": MessageLookupByLibrary.simpleMessage("Сетевая ошибка!"),
|
"weight_chart" : MessageLookupByLibrary.simpleMessage("График веса"),
|
||||||
"msg_something_went_wrong": MessageLookupByLibrary.simpleMessage(
|
"weight_measurements" : MessageLookupByLibrary.simpleMessage("Замер веса"),
|
||||||
"Что то пошло не так!",
|
"wellness" : MessageLookupByLibrary.simpleMessage("Улучшение самочувствия"),
|
||||||
),
|
"wellness_type" : MessageLookupByLibrary.simpleMessage("Улучшение самочувствия"),
|
||||||
"my_blog": MessageLookupByLibrary.simpleMessage("Мой блог"),
|
"wellness_type_desc" : MessageLookupByLibrary.simpleMessage("Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы."),
|
||||||
"my_diets": MessageLookupByLibrary.simpleMessage("Мои диеты"),
|
"what_is_your_name" : MessageLookupByLibrary.simpleMessage("Как вас зовут?"),
|
||||||
"my_plans": MessageLookupByLibrary.simpleMessage("Мои планы"),
|
"wrong_credentials" : MessageLookupByLibrary.simpleMessage("Неверные данные пользователя"),
|
||||||
"my_settings": MessageLookupByLibrary.simpleMessage("Мои настройки"),
|
"you_will_get" : MessageLookupByLibrary.simpleMessage("Вы получите"),
|
||||||
"name_empty": MessageLookupByLibrary.simpleMessage(
|
"your_blood_pressure" : MessageLookupByLibrary.simpleMessage("Ваше артериальное давление?"),
|
||||||
"Имя пользователя обязательно для заполнения",
|
"your_favorite_sports" : MessageLookupByLibrary.simpleMessage("Ваши спортивные увлечения?"),
|
||||||
),
|
"your_gender" : MessageLookupByLibrary.simpleMessage("Ваш пол?"),
|
||||||
"name_short": MessageLookupByLibrary.simpleMessage(
|
"your_height" : MessageLookupByLibrary.simpleMessage("Ваш рост:"),
|
||||||
"В имени пользователя должно быть минимум 6 символов",
|
"your_schedule" : MessageLookupByLibrary.simpleMessage("Ваш режим дня?"),
|
||||||
),
|
"your_weight" : MessageLookupByLibrary.simpleMessage("Ваш вес:")
|
||||||
"no": MessageLookupByLibrary.simpleMessage("Нет"),
|
|
||||||
"no_account": MessageLookupByLibrary.simpleMessage("У меня нет аккаунта"),
|
|
||||||
"notifications": MessageLookupByLibrary.simpleMessage("Уведомления"),
|
|
||||||
"one_option": MessageLookupByLibrary.simpleMessage("1 опция"),
|
|
||||||
"option": MessageLookupByLibrary.simpleMessage("Опция"),
|
|
||||||
"option_content": MessageLookupByLibrary.simpleMessage("Содержание опции"),
|
|
||||||
"option_content_hint_text": MessageLookupByLibrary.simpleMessage(
|
|
||||||
"На связи 7 дней в неделю",
|
|
||||||
),
|
|
||||||
"option_desc": MessageLookupByLibrary.simpleMessage("Описание опции"),
|
|
||||||
"option_desc_hint_text": MessageLookupByLibrary.simpleMessage(
|
|
||||||
"Этот план питания для похудения на 7 дней сбалансированный и разнообразный",
|
|
||||||
),
|
|
||||||
"option_duration": MessageLookupByLibrary.simpleMessage(
|
|
||||||
"Продолжительность опции",
|
|
||||||
),
|
|
||||||
"option_personal_diet_desc": MessageLookupByLibrary.simpleMessage(
|
|
||||||
"Опция подразумевает создание индивидуальной диеты",
|
|
||||||
),
|
|
||||||
"option_title": MessageLookupByLibrary.simpleMessage("Заголовок опции"),
|
|
||||||
"order": MessageLookupByLibrary.simpleMessage("Порядок"),
|
|
||||||
"password": MessageLookupByLibrary.simpleMessage("Пароль"),
|
|
||||||
"password_confirmation": MessageLookupByLibrary.simpleMessage(
|
|
||||||
"Подтвердите пароль",
|
|
||||||
),
|
|
||||||
"password_confirmation_mismatch": MessageLookupByLibrary.simpleMessage(
|
|
||||||
"Пароли не совпадают",
|
|
||||||
),
|
|
||||||
"password_empty": MessageLookupByLibrary.simpleMessage(
|
|
||||||
"Пароль обязателен для заполнения",
|
|
||||||
),
|
|
||||||
"password_short": MessageLookupByLibrary.simpleMessage(
|
|
||||||
"В пароле должно быть минимум 6 символов",
|
|
||||||
),
|
|
||||||
"pcs": MessageLookupByLibrary.simpleMessage("шт"),
|
|
||||||
"personal": MessageLookupByLibrary.simpleMessage("Персональные"),
|
|
||||||
"personal_diet_state": MessageLookupByLibrary.simpleMessage("Персональная"),
|
|
||||||
"photo": MessageLookupByLibrary.simpleMessage("Фото"),
|
|
||||||
"place_of_living": MessageLookupByLibrary.simpleMessage("Место проживания"),
|
|
||||||
"plan_author": MessageLookupByLibrary.simpleMessage("Автор плана"),
|
|
||||||
"plan_desc": MessageLookupByLibrary.simpleMessage("Описание плана"),
|
|
||||||
"plan_desc_example": MessageLookupByLibrary.simpleMessage(
|
|
||||||
"Большое подробное, привлекательное описание плана",
|
|
||||||
),
|
|
||||||
"plan_duration": MessageLookupByLibrary.simpleMessage("Срок плана"),
|
|
||||||
"plan_label_desc": MessageLookupByLibrary.simpleMessage(
|
|
||||||
"План для поддержания формы",
|
|
||||||
),
|
|
||||||
"plans_complete": MessageLookupByLibrary.simpleMessage("Пройденные планы"),
|
|
||||||
"popular": MessageLookupByLibrary.simpleMessage("Популярные"),
|
|
||||||
"preferred_language": MessageLookupByLibrary.simpleMessage(
|
|
||||||
"Предпочитаемый язык общения",
|
|
||||||
),
|
|
||||||
"pressure_empty": MessageLookupByLibrary.simpleMessage(
|
|
||||||
"Все показатели давления обязательны к заполнению",
|
|
||||||
),
|
|
||||||
"pressure_is_not_a_number": MessageLookupByLibrary.simpleMessage(
|
|
||||||
"Все показатели давления должны быть числами",
|
|
||||||
),
|
|
||||||
"preview": MessageLookupByLibrary.simpleMessage("Превью"),
|
|
||||||
"price": MessageLookupByLibrary.simpleMessage("Цена"),
|
|
||||||
"price_explanation": MessageLookupByLibrary.simpleMessage(
|
|
||||||
"Разница в ценах, это комиссия Итвио",
|
|
||||||
),
|
|
||||||
"profile": MessageLookupByLibrary.simpleMessage("Профиль"),
|
|
||||||
"proteins": MessageLookupByLibrary.simpleMessage("Белки"),
|
|
||||||
"public_diet_state": MessageLookupByLibrary.simpleMessage("Публичная"),
|
|
||||||
"publish": MessageLookupByLibrary.simpleMessage("Опубликовать"),
|
|
||||||
"published": MessageLookupByLibrary.simpleMessage("Опубликованные"),
|
|
||||||
"removeFromActive": MessageLookupByLibrary.simpleMessage(
|
|
||||||
"Снять с публикации",
|
|
||||||
),
|
|
||||||
"save": MessageLookupByLibrary.simpleMessage("Сохранить"),
|
|
||||||
"services": MessageLookupByLibrary.simpleMessage("Оказание услуг"),
|
|
||||||
"settings": MessageLookupByLibrary.simpleMessage("Настройки"),
|
|
||||||
"share": MessageLookupByLibrary.simpleMessage("Поделиться"),
|
|
||||||
"skip": MessageLookupByLibrary.simpleMessage("Пропустить"),
|
|
||||||
"slim": MessageLookupByLibrary.simpleMessage("Похудение"),
|
|
||||||
"slim_type": MessageLookupByLibrary.simpleMessage("Похудение"),
|
|
||||||
"slim_type_desc": MessageLookupByLibrary.simpleMessage(
|
|
||||||
"План питания для похудения должен быть разработан с учётом индивидуальных особенностей организма, возраста, пола, уровня физической активности и состояния здоровья. Общие принципы: \n● Снижение калорийности; \n● Сбалансированный рацион; \n● Разнообразие продуктов; \n● Питьевой режим; \n● Ограничение вредных продуктов;",
|
|
||||||
),
|
|
||||||
"standard": MessageLookupByLibrary.simpleMessage("Стандарт"),
|
|
||||||
"startDiet": MessageLookupByLibrary.simpleMessage("Начать диету"),
|
|
||||||
"tell_your_schedule": MessageLookupByLibrary.simpleMessage(
|
|
||||||
"Расскажите в свободной форме о том как просыпаетесь, когда едите, когда ложитесь спать:",
|
|
||||||
),
|
|
||||||
"text_was_copied": MessageLookupByLibrary.simpleMessage(
|
|
||||||
"Текст успешно скопирован",
|
|
||||||
),
|
|
||||||
"three_options": MessageLookupByLibrary.simpleMessage("3 опции"),
|
|
||||||
"toOffer": MessageLookupByLibrary.simpleMessage("Предложить"),
|
|
||||||
"to_draft": MessageLookupByLibrary.simpleMessage("В черновик"),
|
|
||||||
"unfavorite_foods": MessageLookupByLibrary.simpleMessage(
|
|
||||||
"Нелюбимые продукты",
|
|
||||||
),
|
|
||||||
"up_pressure": MessageLookupByLibrary.simpleMessage(
|
|
||||||
"Верхние показатели давления:",
|
|
||||||
),
|
|
||||||
"user_authorization": MessageLookupByLibrary.simpleMessage(
|
|
||||||
"Аутентификация",
|
|
||||||
),
|
|
||||||
"user_data": MessageLookupByLibrary.simpleMessage("Данные пользователя"),
|
|
||||||
"user_registration": MessageLookupByLibrary.simpleMessage(
|
|
||||||
"Регистрация пользователя",
|
|
||||||
),
|
|
||||||
"username": MessageLookupByLibrary.simpleMessage("Имя пользователя"),
|
|
||||||
"vegan": MessageLookupByLibrary.simpleMessage("Веган."),
|
|
||||||
"vegan_type": MessageLookupByLibrary.simpleMessage("Вегетарианская"),
|
|
||||||
"vegan_type_desc": MessageLookupByLibrary.simpleMessage(
|
|
||||||
"Диета основана на потреблении растительной пищи и отказе от продуктов животного происхождения. Такой тип питания может быть разнообразным и вкусным, а также полезным для здоровья.",
|
|
||||||
),
|
|
||||||
"waist": MessageLookupByLibrary.simpleMessage("Талия"),
|
|
||||||
"waist_circumference": MessageLookupByLibrary.simpleMessage("Обхват талии"),
|
|
||||||
"weight": MessageLookupByLibrary.simpleMessage("Вес"),
|
|
||||||
"weight_chart": MessageLookupByLibrary.simpleMessage("График веса"),
|
|
||||||
"weight_measurements": MessageLookupByLibrary.simpleMessage("Замер веса"),
|
|
||||||
"wellness": MessageLookupByLibrary.simpleMessage("Улучшение самочувствия"),
|
|
||||||
"wellness_type": MessageLookupByLibrary.simpleMessage(
|
|
||||||
"Улучшение самочувствия",
|
|
||||||
),
|
|
||||||
"wellness_type_desc": MessageLookupByLibrary.simpleMessage(
|
|
||||||
"Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.",
|
|
||||||
),
|
|
||||||
"what_is_your_name": MessageLookupByLibrary.simpleMessage("Как вас зовут?"),
|
|
||||||
"wrong_credentials": MessageLookupByLibrary.simpleMessage(
|
|
||||||
"Неверные данные пользователя",
|
|
||||||
),
|
|
||||||
"you_will_get": MessageLookupByLibrary.simpleMessage("Вы получите"),
|
|
||||||
"your_blood_pressure": MessageLookupByLibrary.simpleMessage(
|
|
||||||
"Ваше артериальное давление?",
|
|
||||||
),
|
|
||||||
"your_favorite_sports": MessageLookupByLibrary.simpleMessage(
|
|
||||||
"Ваши спортивные увлечения?",
|
|
||||||
),
|
|
||||||
"your_gender": MessageLookupByLibrary.simpleMessage("Ваш пол?"),
|
|
||||||
"your_height": MessageLookupByLibrary.simpleMessage("Ваш рост:"),
|
|
||||||
"your_schedule": MessageLookupByLibrary.simpleMessage("Ваш режим дня?"),
|
|
||||||
"your_weight": MessageLookupByLibrary.simpleMessage("Ваш вес:"),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -466,6 +466,7 @@
|
|||||||
"weAreLookingFor": "Мы собираем самое большое сообщество людей, желающих вести здоровый образ жизни!",
|
"weAreLookingFor": "Мы собираем самое большое сообщество людей, желающих вести здоровый образ жизни!",
|
||||||
"letsInvite": "Давайте пригласим близких в итвио",
|
"letsInvite": "Давайте пригласим близких в итвио",
|
||||||
"eatvioIsAHelthyHabbit": "Итвио — это полезная привычка",
|
"eatvioIsAHelthyHabbit": "Итвио — это полезная привычка",
|
||||||
"invite": "Пригласить"
|
"invite": "Пригласить",
|
||||||
|
"comment_hint": "Комментарий"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -215,5 +215,6 @@
|
|||||||
"from": "от",
|
"from": "от",
|
||||||
"user_data": "Данные пользователя",
|
"user_data": "Данные пользователя",
|
||||||
"infoAboutOrder": "Информация о заказе",
|
"infoAboutOrder": "Информация о заказе",
|
||||||
"notifications": "Уведомления"
|
"notifications": "Уведомления",
|
||||||
|
"comment_hint": "Комментарий"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
name: eatvio_app
|
name: eatvio_app
|
||||||
description: A new Flutter project.
|
description: A new Flutter project.
|
||||||
version: 1.3.11+92
|
version: 1.3.12+93
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -12,10 +12,7 @@ void main() {
|
|||||||
|
|
||||||
test('createRoutes содержит маршрут authScreen', () {
|
test('createRoutes содержит маршрут authScreen', () {
|
||||||
final routes = AuthRoutes.createRoutes();
|
final routes = AuthRoutes.createRoutes();
|
||||||
final authRoute = routes.firstWhere(
|
final authRoute = routes.firstWhere((route) => route is GoRoute && route.path == '/auth-screen', orElse: () => throw Exception('Route not found'));
|
||||||
(route) => route is GoRoute && route.path == '/auth-screen',
|
|
||||||
orElse: () => throw Exception('Route not found'),
|
|
||||||
);
|
|
||||||
expect(authRoute, isA<GoRoute>());
|
expect(authRoute, isA<GoRoute>());
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -67,10 +64,7 @@ void main() {
|
|||||||
|
|
||||||
test('createRoutes содержит маршрут verifyScreen', () {
|
test('createRoutes содержит маршрут verifyScreen', () {
|
||||||
final routes = AuthRoutes.createRoutes();
|
final routes = AuthRoutes.createRoutes();
|
||||||
final verifyRoute = routes.firstWhere(
|
final verifyRoute = routes.firstWhere((route) => route is GoRoute && route.path == '/verify-screen', orElse: () => throw Exception('Route not found'));
|
||||||
(route) => route is GoRoute && route.path == '/verify-screen',
|
|
||||||
orElse: () => throw Exception('Route not found'),
|
|
||||||
);
|
|
||||||
expect(verifyRoute, isA<GoRoute>());
|
expect(verifyRoute, isA<GoRoute>());
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -92,44 +86,25 @@ void main() {
|
|||||||
expect(registerViaSocialRoute, isA<GoRoute>());
|
expect(registerViaSocialRoute, isA<GoRoute>());
|
||||||
});
|
});
|
||||||
|
|
||||||
test('authScreen имеет redirect функцию', () {
|
|
||||||
final routes = AuthRoutes.createRoutes();
|
|
||||||
final authRoute = routes.first as GoRoute;
|
|
||||||
expect(authRoute.redirect, isNotNull);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('forgetPasswordScreen builder обрабатывает extra данные', () {
|
test('forgetPasswordScreen builder обрабатывает extra данные', () {
|
||||||
final routes = AuthRoutes.createRoutes();
|
final routes = AuthRoutes.createRoutes();
|
||||||
final authRoute = routes.first as GoRoute;
|
final authRoute = routes.first as GoRoute;
|
||||||
final forgetPasswordRoute = authRoute.routes.firstWhere(
|
final forgetPasswordRoute =
|
||||||
(route) => route is GoRoute && route.path == 'forget-password-screen',
|
authRoute.routes.firstWhere((route) => route is GoRoute && route.path == 'forget-password-screen', orElse: () => throw Exception('Route not found'))
|
||||||
orElse: () => throw Exception('Route not found'),
|
as GoRoute;
|
||||||
) as GoRoute;
|
|
||||||
expect(forgetPasswordRoute.builder, isNotNull);
|
expect(forgetPasswordRoute.builder, isNotNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('verifyScreen builder обрабатывает extra данные', () {
|
test('verifyScreen builder обрабатывает extra данные', () {
|
||||||
final routes = AuthRoutes.createRoutes();
|
final routes = AuthRoutes.createRoutes();
|
||||||
final verifyRoute = routes.firstWhere(
|
final verifyRoute = routes.firstWhere((route) => route is GoRoute && route.path == '/verify-screen') as GoRoute;
|
||||||
(route) => route is GoRoute && route.path == '/verify-screen',
|
|
||||||
) as GoRoute;
|
|
||||||
expect(verifyRoute.builder, isNotNull);
|
expect(verifyRoute.builder, isNotNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('registerViaSocialScreen builder обрабатывает extra данные', () {
|
test('registerViaSocialScreen builder обрабатывает extra данные', () {
|
||||||
final routes = AuthRoutes.createRoutes();
|
final routes = AuthRoutes.createRoutes();
|
||||||
final registerViaSocialRoute = routes.firstWhere(
|
final registerViaSocialRoute = routes.firstWhere((route) => route is GoRoute && route.path == '/register-via-social-screen') as GoRoute;
|
||||||
(route) => route is GoRoute && route.path == '/register-via-social-screen',
|
|
||||||
) as GoRoute;
|
|
||||||
expect(registerViaSocialRoute.builder, isNotNull);
|
expect(registerViaSocialRoute.builder, isNotNull);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ void main() {
|
|||||||
group('TokenStorage', () {
|
group('TokenStorage', () {
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
SharedPreferences.setMockInitialValues({});
|
SharedPreferences.setMockInitialValues({});
|
||||||
|
// Clear cached instance to ensure fresh state for each test
|
||||||
|
TokenStorage.clearCache();
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('haveTokenInStorage returns false when no token is stored', () async {
|
test('haveTokenInStorage returns false when no token is stored', () async {
|
||||||
@@ -66,6 +70,10 @@ void main() {
|
|||||||
group('SocketIdStorage', () {
|
group('SocketIdStorage', () {
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
SharedPreferences.setMockInitialValues({});
|
SharedPreferences.setMockInitialValues({});
|
||||||
|
// Clear cached instance to ensure fresh state for each test
|
||||||
|
TokenStorage.clearCache();
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('haveSocketIdInStorage returns false when no socketId is stored', () async {
|
test('haveSocketIdInStorage returns false when no socketId is stored', () async {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ void main() {
|
|||||||
const MarketingBottomSheet(),
|
const MarketingBottomSheet(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.byType(MarketingBottomSheet), findsOneWidget);
|
expect(find.byType(MarketingBottomSheet), findsOneWidget);
|
||||||
});
|
});
|
||||||
@@ -59,6 +60,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
await tester.tap(find.text('Show'));
|
await tester.tap(find.text('Show'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ void main() {
|
|||||||
|
|
||||||
testWidgets('returns SizedBox.shrink when option is null', (tester) async {
|
testWidgets('returns SizedBox.shrink when option is null', (tester) async {
|
||||||
await tester.pumpWidget(createTestWidget(const OptionWidget(option: null, duration: 7)));
|
await tester.pumpWidget(createTestWidget(const OptionWidget(option: null, duration: 7)));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.byType(OptionWidget), findsOneWidget);
|
expect(find.byType(OptionWidget), findsOneWidget);
|
||||||
expect(find.byType(SizedBox), findsWidgets);
|
expect(find.byType(SizedBox), findsWidgets);
|
||||||
@@ -33,6 +34,7 @@ void main() {
|
|||||||
const option = PacketOptions(items: ['Item 1', 'Item 2'], price: 1000.0, short: 'Short', description: 'Description', hasPersonalDiet: false);
|
const option = PacketOptions(items: ['Item 1', 'Item 2'], price: 1000.0, short: 'Short', description: 'Description', hasPersonalDiet: false);
|
||||||
|
|
||||||
await tester.pumpWidget(createTestWidget(const OptionWidget(option: option, duration: 7)));
|
await tester.pumpWidget(createTestWidget(const OptionWidget(option: option, duration: 7)));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.byType(OptionWidget), findsOneWidget);
|
expect(find.byType(OptionWidget), findsOneWidget);
|
||||||
expect(find.text('Short'), findsOneWidget);
|
expect(find.text('Short'), findsOneWidget);
|
||||||
@@ -43,6 +45,7 @@ void main() {
|
|||||||
const option = PacketOptions(items: ['Item 1'], price: 1000.0, short: 'Short', description: 'Description', hasPersonalDiet: true);
|
const option = PacketOptions(items: ['Item 1'], price: 1000.0, short: 'Short', description: 'Description', hasPersonalDiet: true);
|
||||||
|
|
||||||
await tester.pumpWidget(createTestWidget(const OptionWidget(option: option, duration: 7)));
|
await tester.pumpWidget(createTestWidget(const OptionWidget(option: option, duration: 7)));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.byType(OptionWidget), findsOneWidget);
|
expect(find.byType(OptionWidget), findsOneWidget);
|
||||||
});
|
});
|
||||||
@@ -51,6 +54,7 @@ void main() {
|
|||||||
const option = PacketOptions(items: ['Item 1'], price: 1000.0, short: 'Short', description: 'Description', hasPersonalDiet: false);
|
const option = PacketOptions(items: ['Item 1'], price: 1000.0, short: 'Short', description: 'Description', hasPersonalDiet: false);
|
||||||
|
|
||||||
await tester.pumpWidget(createTestWidget(const OptionWidget(option: option, duration: 7, disableDuration: true)));
|
await tester.pumpWidget(createTestWidget(const OptionWidget(option: option, duration: 7, disableDuration: true)));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.byType(OptionWidget), findsOneWidget);
|
expect(find.byType(OptionWidget), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
await tester.tap(find.text('Show Error'));
|
await tester.tap(find.text('Show Error'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
@@ -68,6 +69,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
await tester.tap(find.text('Show Error'));
|
await tester.tap(find.text('Show Error'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
@@ -103,6 +105,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
await tester.tap(find.text('Show Error'));
|
await tester.tap(find.text('Show Error'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
@@ -134,6 +137,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
await tester.tap(find.text('Show Error'));
|
await tester.tap(find.text('Show Error'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import 'package:eatvio_app/core/app_config.dart';
|
|||||||
import 'package:eatvio_app/core/app_initializer.dart';
|
import 'package:eatvio_app/core/app_initializer.dart';
|
||||||
import 'package:eatvio_app/core/locator_service.dart';
|
import 'package:eatvio_app/core/locator_service.dart';
|
||||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||||
|
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
||||||
|
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
|
||||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart';
|
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart';
|
||||||
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
|
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
|
||||||
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_state.dart';
|
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_state.dart';
|
||||||
@@ -20,7 +22,7 @@ import 'package:network_image_mock/network_image_mock.dart';
|
|||||||
|
|
||||||
import 'bottom_navigation_bar_widget_test.mocks.dart';
|
import 'bottom_navigation_bar_widget_test.mocks.dart';
|
||||||
|
|
||||||
@GenerateNiceMocks([MockSpec<NotificationsCubit>(), MockSpec<ChatsCubit>()])
|
@GenerateNiceMocks([MockSpec<NotificationsCubit>(), MockSpec<ChatsCubit>(), MockSpec<ProfileAvatarCubit>()])
|
||||||
void main() {
|
void main() {
|
||||||
group('BottomNavigationBarWidget smoke', () {
|
group('BottomNavigationBarWidget smoke', () {
|
||||||
setUpAll(() async {
|
setUpAll(() async {
|
||||||
@@ -86,6 +88,7 @@ void main() {
|
|||||||
mockNetworkImagesFor(() async {
|
mockNetworkImagesFor(() async {
|
||||||
final notificationsCubit = MockNotificationsCubit();
|
final notificationsCubit = MockNotificationsCubit();
|
||||||
final chatsCubit = MockChatsCubit();
|
final chatsCubit = MockChatsCubit();
|
||||||
|
final profileAvatarCubit = MockProfileAvatarCubit();
|
||||||
|
|
||||||
when(notificationsCubit.state).thenReturn(const NotificationsState());
|
when(notificationsCubit.state).thenReturn(const NotificationsState());
|
||||||
when(notificationsCubit.stream).thenAnswer((_) => const Stream.empty());
|
when(notificationsCubit.stream).thenAnswer((_) => const Stream.empty());
|
||||||
@@ -93,16 +96,20 @@ void main() {
|
|||||||
when(chatsCubit.state).thenReturn(ChatsState());
|
when(chatsCubit.state).thenReturn(ChatsState());
|
||||||
when(chatsCubit.stream).thenAnswer((_) => const Stream.empty());
|
when(chatsCubit.stream).thenAnswer((_) => const Stream.empty());
|
||||||
when(chatsCubit.close()).thenAnswer((_) async => Future<void>.value());
|
when(chatsCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||||
|
when(profileAvatarCubit.state).thenReturn(const ProfileAvatarState.loaded(profile: mockUserProfile));
|
||||||
|
when(profileAvatarCubit.stream).thenAnswer((_) => const Stream.empty());
|
||||||
|
when(profileAvatarCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MultiBlocProvider(
|
MultiBlocProvider(
|
||||||
providers: [
|
providers: [
|
||||||
BlocProvider<NotificationsCubit>.value(value: notificationsCubit),
|
BlocProvider<NotificationsCubit>.value(value: notificationsCubit),
|
||||||
BlocProvider<ChatsCubit>.value(value: chatsCubit),
|
BlocProvider<ChatsCubit>.value(value: chatsCubit),
|
||||||
|
BlocProvider<ProfileAvatarCubit>.value(value: profileAvatarCubit),
|
||||||
],
|
],
|
||||||
child: wrap(
|
child: wrap(
|
||||||
const Scaffold(
|
const Scaffold(
|
||||||
body: BottomNavigationBarWidget(profileModel: mockUserProfile, child: Text('Test Content')),
|
body: BottomNavigationBarWidget(child: Text('Test Content')),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -117,6 +124,7 @@ void main() {
|
|||||||
mockNetworkImagesFor(() async {
|
mockNetworkImagesFor(() async {
|
||||||
final notificationsCubit = MockNotificationsCubit();
|
final notificationsCubit = MockNotificationsCubit();
|
||||||
final chatsCubit = MockChatsCubit();
|
final chatsCubit = MockChatsCubit();
|
||||||
|
final profileAvatarCubit = MockProfileAvatarCubit();
|
||||||
|
|
||||||
when(notificationsCubit.state).thenReturn(const NotificationsState());
|
when(notificationsCubit.state).thenReturn(const NotificationsState());
|
||||||
when(notificationsCubit.stream).thenAnswer((_) => const Stream.empty());
|
when(notificationsCubit.stream).thenAnswer((_) => const Stream.empty());
|
||||||
@@ -124,16 +132,20 @@ void main() {
|
|||||||
when(chatsCubit.state).thenReturn(ChatsState());
|
when(chatsCubit.state).thenReturn(ChatsState());
|
||||||
when(chatsCubit.stream).thenAnswer((_) => const Stream.empty());
|
when(chatsCubit.stream).thenAnswer((_) => const Stream.empty());
|
||||||
when(chatsCubit.close()).thenAnswer((_) async => Future<void>.value());
|
when(chatsCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||||
|
when(profileAvatarCubit.state).thenReturn(const ProfileAvatarState.loaded(profile: mockConsultantProfile));
|
||||||
|
when(profileAvatarCubit.stream).thenAnswer((_) => const Stream.empty());
|
||||||
|
when(profileAvatarCubit.close()).thenAnswer((_) async => Future<void>.value());
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MultiBlocProvider(
|
MultiBlocProvider(
|
||||||
providers: [
|
providers: [
|
||||||
BlocProvider<NotificationsCubit>.value(value: notificationsCubit),
|
BlocProvider<NotificationsCubit>.value(value: notificationsCubit),
|
||||||
BlocProvider<ChatsCubit>.value(value: chatsCubit),
|
BlocProvider<ChatsCubit>.value(value: chatsCubit),
|
||||||
|
BlocProvider<ProfileAvatarCubit>.value(value: profileAvatarCubit),
|
||||||
],
|
],
|
||||||
child: wrap(
|
child: wrap(
|
||||||
const Scaffold(
|
const Scaffold(
|
||||||
body: BottomNavigationBarWidget(profileModel: mockConsultantProfile, child: Text('Test Content')),
|
body: BottomNavigationBarWidget(child: Text('Test Content')),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.byType(ChosenDietWidgetContent), findsOneWidget);
|
expect(find.byType(ChosenDietWidgetContent), findsOneWidget);
|
||||||
expect(find.text('Test Diet'), findsOneWidget);
|
expect(find.text('Test Diet'), findsOneWidget);
|
||||||
@@ -50,6 +51,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.byType(ChosenDietWidgetContent), findsOneWidget);
|
expect(find.byType(ChosenDietWidgetContent), findsOneWidget);
|
||||||
});
|
});
|
||||||
@@ -65,6 +67,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.text('My Diet'), findsOneWidget);
|
expect(find.text('My Diet'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ void main() {
|
|||||||
planName: 'Test Plan',
|
planName: 'Test Plan',
|
||||||
planDuration: 14,
|
planDuration: 14,
|
||||||
));
|
));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.byType(ChosenPlanWidgetContent), findsOneWidget);
|
expect(find.byType(ChosenPlanWidgetContent), findsOneWidget);
|
||||||
expect(find.text('Test Plan'), findsOneWidget);
|
expect(find.text('Test Plan'), findsOneWidget);
|
||||||
@@ -48,6 +49,7 @@ void main() {
|
|||||||
planName: 'My Plan',
|
planName: 'My Plan',
|
||||||
planDuration: 7,
|
planDuration: 7,
|
||||||
));
|
));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.byType(ChosenPlanWidgetContent), findsOneWidget);
|
expect(find.byType(ChosenPlanWidgetContent), findsOneWidget);
|
||||||
expect(find.textContaining('3'), findsOneWidget);
|
expect(find.textContaining('3'), findsOneWidget);
|
||||||
@@ -60,6 +62,7 @@ void main() {
|
|||||||
planName: 'Long Plan',
|
planName: 'Long Plan',
|
||||||
planDuration: 30,
|
planDuration: 30,
|
||||||
));
|
));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.byType(ChosenPlanWidgetContent), findsOneWidget);
|
expect(find.byType(ChosenPlanWidgetContent), findsOneWidget);
|
||||||
expect(find.text('Long Plan'), findsOneWidget);
|
expect(find.text('Long Plan'), findsOneWidget);
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.byType(CommentsWidget), findsOneWidget);
|
expect(find.byType(CommentsWidget), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ void main() {
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
await tester.pumpWidget(createTestWidget(const SizedBox(), mockCubit));
|
await tester.pumpWidget(createTestWidget(const SizedBox(), mockCubit));
|
||||||
|
await tester.pump();
|
||||||
await tester.tap(find.text('Show Bottom Sheet'));
|
await tester.tap(find.text('Show Bottom Sheet'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
@@ -107,6 +108,7 @@ void main() {
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
await tester.pumpWidget(createTestWidgetError(const SizedBox(), mockCubit));
|
await tester.pumpWidget(createTestWidgetError(const SizedBox(), mockCubit));
|
||||||
|
await tester.pump();
|
||||||
await tester.tap(find.text('Show Bottom Sheet'));
|
await tester.tap(find.text('Show Bottom Sheet'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ void main() {
|
|||||||
home: ChangePasswordScreen(settingsEntity: settingsEntity),
|
home: ChangePasswordScreen(settingsEntity: settingsEntity),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.byType(ChangePasswordScreen), findsOneWidget);
|
expect(find.byType(ChangePasswordScreen), findsOneWidget);
|
||||||
expect(find.text('Смена пароля'), findsOneWidget);
|
expect(find.text('Смена пароля'), findsOneWidget);
|
||||||
@@ -80,6 +81,7 @@ void main() {
|
|||||||
home: ChangePasswordScreen(settingsEntity: settingsEntity),
|
home: ChangePasswordScreen(settingsEntity: settingsEntity),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.byType(AppBar), findsOneWidget);
|
expect(find.byType(AppBar), findsOneWidget);
|
||||||
expect(find.text('Смена пароля'), findsOneWidget);
|
expect(find.text('Смена пароля'), findsOneWidget);
|
||||||
|
|||||||
Reference in New Issue
Block a user