diff --git a/lib/core/app_initializer.dart b/lib/core/app_initializer.dart index 193aa7e3..6af9865f 100644 --- a/lib/core/app_initializer.dart +++ b/lib/core/app_initializer.dart @@ -1,4 +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'; @@ -51,6 +52,7 @@ class AppInitializer { ); } catch (e) { // Разрешение не критично, продолжаем работу + log('Failed to request Firebase messaging permission: $e'); } } } diff --git a/lib/core/firebase_api.dart b/lib/core/firebase_api.dart index 887b7137..a96aa9d5 100644 --- a/lib/core/firebase_api.dart +++ b/lib/core/firebase_api.dart @@ -32,6 +32,7 @@ class FirebaseApi { ); } catch (e) { // Разрешение не критично, продолжаем + log('Failed to request Firebase messaging permission: $e'); } } token = await _firebaseMessaging.getToken().timeout( diff --git a/lib/core/routing/main_navigation_routes.dart b/lib/core/routing/main_navigation_routes.dart index d6a4978e..88a8db04 100644 --- a/lib/core/routing/main_navigation_routes.dart +++ b/lib/core/routing/main_navigation_routes.dart @@ -32,14 +32,7 @@ class MainNavigationRoutes { }); } - return BlocBuilder( - builder: (BuildContext context, ProfileAvatarState state) { - if (state is ProfileAvatarStateLoaded) { - return BottomNavigationBarWidget(profileModel: state.profile, child: child); - } - return const SizedBox.shrink(); - }, - ); + return BottomNavigationBarWidget(child: child); }, routes: [ GoRoute( diff --git a/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart b/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart index b1e846ca..f177d57a 100644 --- a/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart +++ b/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart @@ -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 { int currentBottomNavBarIndex = 1; - late final List pages; + List? pages; int tapsCount = 0; - @override - void initState() { - super.initState(); - - if (widget.profileModel.roles.contains('consultant')) { - pages = [ + List _getPages(bool isConsultant) { + if (isConsultant) { + return [ '/my-profile-screen', '/feed-screen', '/diary-screen', @@ -49,7 +45,7 @@ class _BottomNavigationBarWidgetState extends State { '/menu-screen', ]; } else { - pages = [ + return [ '/my-profile-screen', '/feed-screen', '/diary-screen', @@ -61,7 +57,8 @@ class _BottomNavigationBarWidgetState extends State { } 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 { 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().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( + 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().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( builder: (BuildContext context, ChatsState state) { final hasUnreadMessages = state.chatsWithUnreadMessagesCount > 0; @@ -206,7 +212,7 @@ class _BottomNavigationBarWidgetState extends State { 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 { ); }, ), - ), - BottomNavigationBarItem( - label: 'Уведомления', + ), + BottomNavigationBarItem( + label: 'Уведомления', icon: BlocBuilder( builder: (BuildContext context, NotificationsState state) { return badges.Badge( @@ -237,7 +243,7 @@ class _BottomNavigationBarWidgetState extends State { 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 { }, ), ), - BottomNavigationBarItem( - label: '', - icon: SvgPicture.asset( - ImageConstant.imgLogo, - height: 32, + BottomNavigationBarItem( + label: '', + icon: SvgPicture.asset( + ImageConstant.imgLogo, + height: 32, + ), + ), + ], ), - ), - ], - ), + ); + } + // Показываем child даже когда профиль еще загружается, чтобы экраны могли инициализироваться + return Scaffold( + body: widget.child, + ); + }, ); } } diff --git a/lib/features/feed/presentation/pages/feed_screen.dart b/lib/features/feed/presentation/pages/feed_screen.dart index 0c9dca9d..ebf116df 100644 --- a/lib/features/feed/presentation/pages/feed_screen.dart +++ b/lib/features/feed/presentation/pages/feed_screen.dart @@ -77,9 +77,11 @@ class _FeedScreenContentState extends State { _checkRegistrationDate(context); // Инициализация глобальных кубитов при первом открытии экрана WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; final feedCubit = context.read(); final state = feedCubit.state; - if (state is! FeedStateLoaded && state is! FeedStateLoading) { + // Загружаем данные, если они еще не загружены + if (state is! FeedStateLoaded) { feedCubit.loadFeedBlogPostList(); } }); diff --git a/test/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget_test.dart b/test/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget_test.dart index b88cc940..721af4b3 100644 --- a/test/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget_test.dart +++ b/test/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget_test.dart @@ -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(), MockSpec()]) +@GenerateNiceMocks([MockSpec(), MockSpec(), MockSpec()]) 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.value()); + when(profileAvatarCubit.state).thenReturn(const ProfileAvatarState.loaded(profile: mockUserProfile)); + when(profileAvatarCubit.stream).thenAnswer((_) => const Stream.empty()); + when(profileAvatarCubit.close()).thenAnswer((_) async => Future.value()); await tester.pumpWidget( MultiBlocProvider( providers: [ BlocProvider.value(value: notificationsCubit), BlocProvider.value(value: chatsCubit), + BlocProvider.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.value()); + when(profileAvatarCubit.state).thenReturn(const ProfileAvatarState.loaded(profile: mockConsultantProfile)); + when(profileAvatarCubit.stream).thenAnswer((_) => const Stream.empty()); + when(profileAvatarCubit.close()).thenAnswer((_) async => Future.value()); await tester.pumpWidget( MultiBlocProvider( providers: [ BlocProvider.value(value: notificationsCubit), BlocProvider.value(value: chatsCubit), + BlocProvider.value(value: profileAvatarCubit), ], child: wrap( const Scaffold( - body: BottomNavigationBarWidget(profileModel: mockConsultantProfile, child: Text('Test Content')), + body: BottomNavigationBarWidget(child: Text('Test Content')), ), ), ),