Merge pull request 'fix: splash screen stuck' (#861) from mso7nueh/eatvio_app:fix_splash_stuck into main

Reviewed-on: eatvio/eatvio_app#861
Reviewed-by: misha <misha@noreply.localhost>
This commit is contained in:
mso7nueh
2026-01-15 10:27:57 +00:00
31 changed files with 851 additions and 467 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -79,98 +79,99 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
resizeToAvoidBottomInset: true,
bottomSheet: Container(
padding: EdgeInsets.only(
top: 12,
bottom: 12 + MediaQuery.of(context).padding.bottom + 56, // 56 - высота bottomNavigationBar
bottom: MediaQuery.of(context).padding.bottom + 8,
),
decoration: BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (replyId != null)
GestureDetector(
onTap: () {
context.read<CommentsCubit>().setReplyId(null);
setState(() {
replyId = null;
textEditingController.clear();
});
FocusManager.instance.primaryFocus?.unfocus();
},
child: Container(
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
height: 32,
width: 32,
child: const Icon(Icons.close_rounded, size: 18, color: Colors.white),
),
),
if (replyId != null) const SizedBox(width: 8),
SizedBox(
width: MediaQuery.of(context).size.width - (replyId == null ? 64 : 104),
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 52),
child: TextFormField(
textCapitalization: TextCapitalization.sentences,
onChanged: (_) {
setState(() {});
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (replyId != null)
GestureDetector(
onTap: () {
context.read<CommentsCubit>().setReplyId(null);
setState(() {
replyId = null;
textEditingController.clear();
});
FocusManager.instance.primaryFocus?.unfocus();
},
controller: textEditingController,
maxLines: 5,
minLines: 1,
keyboardType: TextInputType.multiline,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1.2),
decoration: InputDecoration(
isDense: false,
// убираем true, чтобы padding учитывался корректно
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
// вертикальный паддинг увеличен
hintText: 'Комментарий',
hintStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, height: 1.2, color: Color(0xFFAEAEB2)),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
child: Container(
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
height: 32,
width: 32,
child: const Icon(Icons.close_rounded, size: 18, color: Colors.white),
),
),
if (replyId != null) const SizedBox(width: 8),
Expanded(
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 52),
child: TextFormField(
textCapitalization: TextCapitalization.sentences,
onChanged: (_) {
setState(() {});
},
controller: textEditingController,
maxLines: 5,
minLines: 1,
keyboardType: TextInputType.multiline,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1.2),
decoration: InputDecoration(
isDense: false,
// убираем true, чтобы padding учитывался корректно
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
// вертикальный паддинг увеличен
hintText: 'Комментарий',
hintStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, height: 1.2, color: Color(0xFFAEAEB2)),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
),
),
),
),
),
const SizedBox(width: 8),
GestureDetector(
onTap: () {
if (textEditingController.text.length > 2) {
commentsCubit.sendComment(
id: widget.id.toString(),
commentText: textEditingController.text,
parentCommentId: replyId,
commentSourceType: CommentSourceType.blogPost,
);
context.read<CommentsCubit>().setReplyId(null);
setState(() {
textEditingController.clear();
replyId = null;
});
FocusManager.instance.primaryFocus?.unfocus();
}
},
child: Container(
decoration: BoxDecoration(color: textEditingController.text.length > 2 ? appTheme.blueA400 : Colors.grey, shape: BoxShape.circle),
height: 32,
width: 32,
child: const Icon(Icons.send_rounded, size: 18, color: Colors.white),
const SizedBox(width: 8),
GestureDetector(
onTap: () {
if (textEditingController.text.length > 2) {
commentsCubit.sendComment(
id: widget.id.toString(),
commentText: textEditingController.text,
parentCommentId: replyId,
commentSourceType: CommentSourceType.blogPost,
);
context.read<CommentsCubit>().setReplyId(null);
setState(() {
textEditingController.clear();
replyId = null;
});
FocusManager.instance.primaryFocus?.unfocus();
}
},
child: Container(
decoration: BoxDecoration(color: textEditingController.text.length > 2 ? appTheme.blueA400 : Colors.grey, shape: BoxShape.circle),
height: 32,
width: 32,
child: const Icon(Icons.send_rounded, size: 18, color: Colors.white),
),
),
),
],
],
),
),
),
appBar: AppBar(
@@ -285,15 +286,23 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
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');
// Гарантируем, что жирный текст будет распознан
// Заменяем **текст** на <b>текст</b> вручную для надежности
final boldRegex = RegExp(r'\*\*([^*]+)\*\*');
description = description.replaceAllMapped(boldRegex, (match) {
return '<b>${match.group(1)}</b>';
});
final htmlData = md.markdownToHtml(
description,
extensionSet: md.ExtensionSet.gitHubFlavored,
);
return Html(
data: htmlData,
style: {
"body": Style(margin: Margins.zero, padding: HtmlPaddings.zero, fontSize: FontSize(16), lineHeight: const LineHeight(1.5)),
"body": Style(margin: Margins.zero, padding: HtmlPaddings.zero, fontSize: FontSize(16), lineHeight: const LineHeight(1.5), color: Colors.black87),
"p": Style(margin: Margins.only(bottom: 12), color: Colors.black87),
"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)),

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,9 @@ import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:url_launcher/url_launcher.dart';
class ExpandableText extends StatefulWidget {
final String text;
@@ -35,7 +38,9 @@ class _ExpandableTextState extends State<ExpandableText> {
void _checkTextOverflow() {
final textRenderObject = _textKey.currentContext?.findRenderObject();
if (textRenderObject is RenderBox) {
final maxLinesHeight = widget.maxLines * TextStyles.textMMediumAuto.fontSize! * 1.2;
final fontSize = TextStyles.textMMediumAuto.fontSize ?? 16;
final lineHeight = fontSize * 1.5;
final maxLinesHeight = widget.maxLines * lineHeight;
final actualHeight = textRenderObject.size.height;
if (actualHeight > maxLinesHeight && !showExpandButton) {
@@ -52,9 +57,61 @@ class _ExpandableTextState extends State<ExpandableText> {
});
}
String _processMarkdown(String text) {
// Автоматически преобразуем URL в markdown ссылки
// Сначала исправляем неправильный синтаксис markdown ссылок
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)';
});
final linkRegex = RegExp(r'\[([^\]]+)\]\(([^\)]+)\)');
final existingLinks = <_LinkRange>[];
for (final linkMatch in linkRegex.allMatches(text)) {
existingLinks.add(_LinkRange(linkMatch.start, linkMatch.end));
}
final urlRegex = RegExp(r'(https?://[^\s\)]+|www\.[^\s\)]+)', caseSensitive: false);
String result = text;
int offset = 0;
for (final urlMatch in urlRegex.allMatches(text)) {
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;
}
}
// Гарантируем, что жирный текст будет распознан
final boldRegex = RegExp(r'\*\*([^*]+)\*\*');
result = result.replaceAllMapped(boldRegex, (match) {
return '<b>${match.group(1)}</b>';
});
return result;
}
@override
Widget build(BuildContext context) {
final s = S.of(context);
final processedText = _processMarkdown(widget.text);
final htmlData = md.markdownToHtml(
processedText,
extensionSet: md.ExtensionSet.gitHubFlavored,
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -65,12 +122,56 @@ class _ExpandableTextState extends State<ExpandableText> {
curve: Curves.easeInOut,
child: Stack(
children: [
Text(
widget.text,
key: _textKey,
style: TextStyles.textMMediumAuto.copyWith(color: appTheme.black),
maxLines: isExpanded ? null : widget.maxLines,
overflow: TextOverflow.clip,
ClipRect(
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: isExpanded ? double.infinity : (widget.maxLines * (TextStyles.textMMediumAuto.fontSize ?? 16) * 1.5),
),
child: Html(
key: _textKey,
data: htmlData,
style: {
"body": Style(
margin: Margins.zero,
padding: HtmlPaddings.zero,
fontSize: FontSize(TextStyles.textMMediumAuto.fontSize ?? 16),
lineHeight: const LineHeight(1.5),
),
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
"b, strong": Style(fontWeight: FontWeight.bold, 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 (!isExpanded && showExpandButton)
Positioned.fill(
@@ -121,3 +222,9 @@ class _ExpandableTextState extends State<ExpandableText> {
);
}
}
class _LinkRange {
final int start;
final int end;
_LinkRange(this.start, this.end);
}

View File

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

View File

@@ -125,37 +125,85 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
if (state.feedBlogPostList[widget.index].description.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Html(
data: md.markdownToHtml(
(state.feedBlogPostList[widget.index].description.length > 200
? state.feedBlogPostList[widget.index].description.substring(0, 200).replaceAll('\n', '<br>')
: state.feedBlogPostList[widget.index].description.replaceAll('\n', '<br>')),
),
style: {
"body": Style(margin: Margins.zero, padding: HtmlPaddings.zero, fontSize: FontSize(16), lineHeight: const LineHeight(1.5)),
"p": Style(margin: Margins.only(bottom: 12), color: Colors.black87),
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: Colors.black, fontWeight: FontWeight.bold),
"h1": Style(fontSize: FontSize(24)),
"h2": Style(fontSize: FontSize(20)),
"h3": Style(fontSize: FontSize(18)),
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
"li": Style(margin: Margins.only(bottom: 4), color: Colors.black87),
"strong, b": Style(fontWeight: FontWeight.bold, color: Colors.black),
"em, i": Style(fontStyle: FontStyle.italic),
"code": Style(
backgroundColor: Colors.grey.shade100,
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
fontFamily: 'monospace',
),
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
"blockquote": Style(
padding: HtmlPaddings.only(left: 16),
margin: Margins.only(left: 8, bottom: 12),
fontStyle: FontStyle.italic,
color: Colors.grey.shade700,
),
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
child: Builder(
builder: (context) {
String description = state.feedBlogPostList[widget.index].description;
final isLong = description.length > 200;
if (isLong) {
// Умная обрезка: обрезаем markdown текст, не ломая синтаксис
int cutPoint = 200;
// Проверяем, есть ли незакрытые markdown теги
String preview = description.substring(0, cutPoint);
// Считаем открывающие и закрывающие теги
int boldOpen = (preview.split('**').length - 1) ~/ 2;
int boldClose = preview.split('**').length - 1 - boldOpen * 2;
bool hasUnclosedBold = boldOpen > boldClose;
// Если есть незакрытый bold, ищем закрытие
if (hasUnclosedBold) {
int searchEnd = (cutPoint + 100).clamp(0, description.length);
int boldClosePos = description.indexOf('**', cutPoint);
if (boldClosePos != -1 && boldClosePos < searchEnd) {
cutPoint = boldClosePos + 2;
} else {
// Если не нашли, обрезаем до последнего пробела
cutPoint = description.lastIndexOf(' ', 200);
if (cutPoint == -1 || cutPoint < 150) cutPoint = 200;
}
} else {
// Обрезаем до последнего пробела для красоты
cutPoint = description.lastIndexOf(' ', cutPoint);
if (cutPoint == -1 || cutPoint < 150) cutPoint = 200;
}
description = description.substring(0, cutPoint);
}
// Гарантируем, что жирный текст будет распознан
final boldRegex = RegExp(r'\*\*([^*]+)\*\*');
description = description.replaceAllMapped(boldRegex, (match) {
return '<b>${match.group(1)}</b>';
});
// Конвертируем markdown в HTML
String htmlData = md.markdownToHtml(
description,
extensionSet: md.ExtensionSet.gitHubFlavored,
);
return Html(
data: htmlData,
style: {
"body": Style(margin: Margins.zero, padding: HtmlPaddings.zero, fontSize: FontSize(16), lineHeight: const LineHeight(1.5), color: Colors.black87),
"p": Style(margin: Margins.only(bottom: 12), color: Colors.black87),
"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: 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)),
},
);
},
),
),

View File

@@ -153,10 +153,17 @@ class _DietsWidgetState extends State<DietsWidget> {
}
}
details = result;
// Гарантируем, что жирный текст будет распознан
final boldRegex = RegExp(r'\*\*([^*]+)\*\*');
details = details.replaceAllMapped(boldRegex, (match) {
return '<b>${match.group(1)}</b>';
});
return Html(
data: md.markdownToHtml(
details.replaceAll('\n', '<br>'),
extensionSet: md.ExtensionSet.commonMark,
details,
extensionSet: md.ExtensionSet.gitHubFlavored,
),
style: {
"body": Style(
@@ -166,6 +173,7 @@ class _DietsWidgetState extends State<DietsWidget> {
lineHeight: const LineHeight(1.5),
),
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
"b, strong": Style(fontWeight: FontWeight.bold, 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)),

View File

@@ -158,14 +158,22 @@ class _PlanWidgetState extends State<PlanWidget> {
}
}
details = result;
// Гарантируем, что жирный текст будет распознан
final boldRegex = RegExp(r'\*\*([^*]+)\*\*');
details = details.replaceAllMapped(boldRegex, (match) {
return '<b>${match.group(1)}</b>';
});
return Html(
data: md.markdownToHtml(
details.replaceAll('\n', '<br>'),
extensionSet: md.ExtensionSet.commonMark,
details,
extensionSet: md.ExtensionSet.gitHubFlavored,
),
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),
"b, strong": Style(fontWeight: FontWeight.bold, 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)),

View File

@@ -193,10 +193,17 @@ class _RecipesWidgetState extends State<RecipesWidget> {
}
}
description = result;
// Гарантируем, что жирный текст будет распознан
final boldRegex = RegExp(r'\*\*([^*]+)\*\*');
description = description.replaceAllMapped(boldRegex, (match) {
return '<b>${match.group(1)}</b>';
});
return Html(
data: md.markdownToHtml(
description.replaceAll('\n', '<br>'),
extensionSet: md.ExtensionSet.commonMark,
description,
extensionSet: md.ExtensionSet.gitHubFlavored,
),
style: {
"body": Style(
@@ -206,6 +213,7 @@ class _RecipesWidgetState extends State<RecipesWidget> {
lineHeight: const LineHeight(1.5),
),
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
"b, strong": Style(fontWeight: FontWeight.bold, 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)),

View File

@@ -122,10 +122,17 @@ class _MyDietWidgetState extends State<MyDietWidget> {
}
}
details = result;
// Гарантируем, что жирный текст будет распознан
final boldRegex = RegExp(r'\*\*([^*]+)\*\*');
details = details.replaceAllMapped(boldRegex, (match) {
return '<b>${match.group(1)}</b>';
});
return Html(
data: md.markdownToHtml(
details.replaceAll('\n', '<br>'),
extensionSet: md.ExtensionSet.commonMark,
details,
extensionSet: md.ExtensionSet.gitHubFlavored,
),
style: {
"body": Style(
@@ -135,6 +142,7 @@ class _MyDietWidgetState extends State<MyDietWidget> {
lineHeight: const LineHeight(1.5),
),
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
"b, strong": Style(fontWeight: FontWeight.bold, 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)),

View File

@@ -114,10 +114,17 @@ class _MyPlanWidgetState extends State<MyPlanWidget> {
}
}
details = result;
// Гарантируем, что жирный текст будет распознан
final boldRegex = RegExp(r'\*\*([^*]+)\*\*');
details = details.replaceAllMapped(boldRegex, (match) {
return '<b>${match.group(1)}</b>';
});
return Html(
data: md.markdownToHtml(
details.replaceAll('\n', '<br>'),
extensionSet: md.ExtensionSet.commonMark,
details,
extensionSet: md.ExtensionSet.gitHubFlavored,
),
style: {
"body": Style(
@@ -127,6 +134,7 @@ class _MyPlanWidgetState extends State<MyPlanWidget> {
lineHeight: const LineHeight(1.5),
),
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
"b, strong": Style(fontWeight: FontWeight.bold, 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)),

View File

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

View File

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

View File

@@ -107,37 +107,85 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
if (state.blogPosts[widget.index].description.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Html(
data: md.markdownToHtml(
state.blogPosts[widget.index].description.length > 200
? state.blogPosts[widget.index].description.substring(0, 200).replaceAll('\n', '<br>')
: state.blogPosts[widget.index].description.replaceAll('\n', '<br>'),
),
style: {
"body": Style(margin: Margins.zero, padding: HtmlPaddings.zero, fontSize: FontSize(16), lineHeight: const LineHeight(1.5)),
"p": Style(margin: Margins.only(bottom: 12), color: Colors.black87),
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: Colors.black, fontWeight: FontWeight.bold),
"h1": Style(fontSize: FontSize(24)),
"h2": Style(fontSize: FontSize(20)),
"h3": Style(fontSize: FontSize(18)),
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
"li": Style(margin: Margins.only(bottom: 4), color: Colors.black87),
"strong, b": Style(fontWeight: FontWeight.bold, color: Colors.black),
"em, i": Style(fontStyle: FontStyle.italic),
"code": Style(
backgroundColor: Colors.grey.shade100,
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
fontFamily: 'monospace',
),
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
"blockquote": Style(
padding: HtmlPaddings.only(left: 16),
margin: Margins.only(left: 8, bottom: 12),
fontStyle: FontStyle.italic,
color: Colors.grey.shade700,
),
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
child: Builder(
builder: (context) {
String description = state.blogPosts[widget.index].description;
final isLong = description.length > 200;
if (isLong) {
// Умная обрезка: обрезаем markdown текст, не ломая синтаксис
int cutPoint = 200;
// Проверяем, есть ли незакрытые markdown теги
String preview = description.substring(0, cutPoint);
// Считаем открывающие и закрывающие теги
int boldOpen = (preview.split('**').length - 1) ~/ 2;
int boldClose = preview.split('**').length - 1 - boldOpen * 2;
bool hasUnclosedBold = boldOpen > boldClose;
// Если есть незакрытый bold, ищем закрытие
if (hasUnclosedBold) {
int searchEnd = (cutPoint + 100).clamp(0, description.length);
int boldClosePos = description.indexOf('**', cutPoint);
if (boldClosePos != -1 && boldClosePos < searchEnd) {
cutPoint = boldClosePos + 2;
} else {
// Если не нашли, обрезаем до последнего пробела
cutPoint = description.lastIndexOf(' ', 200);
if (cutPoint == -1 || cutPoint < 150) cutPoint = 200;
}
} else {
// Обрезаем до последнего пробела для красоты
cutPoint = description.lastIndexOf(' ', cutPoint);
if (cutPoint == -1 || cutPoint < 150) cutPoint = 200;
}
description = description.substring(0, cutPoint);
}
// Гарантируем, что жирный текст будет распознан
final boldRegex = RegExp(r'\*\*([^*]+)\*\*');
description = description.replaceAllMapped(boldRegex, (match) {
return '<b>${match.group(1)}</b>';
});
// Конвертируем markdown в HTML
String htmlData = md.markdownToHtml(
description,
extensionSet: md.ExtensionSet.gitHubFlavored,
);
return Html(
data: htmlData,
style: {
"body": Style(margin: Margins.zero, padding: HtmlPaddings.zero, fontSize: FontSize(16), lineHeight: const LineHeight(1.5), color: Colors.black87),
"p": Style(margin: Margins.only(bottom: 12), color: Colors.black87),
"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: 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)),
},
);
},
),
),

View File

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

View File

@@ -136,10 +136,17 @@ class _MyRecipeWidgetState extends State<MyRecipeWidget> {
}
}
description = result;
// Гарантируем, что жирный текст будет распознан
final boldRegex = RegExp(r'\*\*([^*]+)\*\*');
description = description.replaceAllMapped(boldRegex, (match) {
return '<b>${match.group(1)}</b>';
});
return Html(
data: md.markdownToHtml(
description.replaceAll('\n', '<br>'),
extensionSet: md.ExtensionSet.commonMark,
description,
extensionSet: md.ExtensionSet.gitHubFlavored,
),
style: {
"body": Style(
@@ -149,6 +156,7 @@ class _MyRecipeWidgetState extends State<MyRecipeWidget> {
lineHeight: const LineHeight(1.5),
),
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
"b, strong": Style(fontWeight: FontWeight.bold, 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)),

View File

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

View File

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

View File

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

View File

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