import 'package:easy_debounce/easy_debounce.dart'; import 'package:eatvio_app/core/widgets/add_item_button.dart'; import 'package:eatvio_app/core/widgets/custom_search_bar.dart'; import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/create_plan/presentation/create_plan_screen.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/consultants_cubit/consultants_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/consultants_cubit/consultants_state.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_state.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'; import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_state.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/blog_post_widget.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/choice_chip_widget.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/consultant_widget.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/diet_filter_bottom_sheet.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/diets_widget.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/filter_badge.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/plan_widget.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/plans_filter_bottom_sheet.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/recipes_widget.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/tags_modal_widget.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_state.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; final bucket = PageStorageBucket(); class FeedScreen extends StatefulWidget { const FeedScreen({super.key}); @override State createState() => _FeedScreenState(); } class _FeedScreenState extends State { final FeedCubit feedCubit = sl(); final RecipesCubit recipesCubit = sl()..loadRecipesList(); final DietsCubit dietsCubit = sl()..loadDiets(); final PlansCubit plansCubit = sl()..loadPlans(); final ConsultantsCubit consultantsCubit = sl()..getConsultants(); final searchController = TextEditingController(); final List chipsLabelList = ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты']; int _selectedChipIndex = 0; final blogPostsScrollController = ScrollController(); final recipesScrollController = ScrollController(); final dietsScrollController = ScrollController(); final plansScrollController = ScrollController(); final consultantsScrollController = ScrollController(); @override void initState() { feedCubit.loadFeedBlogPostList(); blogPostsScrollController.addListener(_onScrollBlogPosts); recipesScrollController.addListener(_onScrollRecipes); dietsScrollController.addListener(_onScrollDiets); plansScrollController.addListener(_onScrollPlans); consultantsScrollController.addListener(_onScrollConsultants); super.initState(); } _onScrollBlogPosts() { if (blogPostsScrollController.offset >= blogPostsScrollController.position.maxScrollExtent && !blogPostsScrollController.position.outOfRange) { feedCubit.loadNextPage(); } } _onScrollRecipes() { if (recipesScrollController.offset >= recipesScrollController.position.maxScrollExtent && !recipesScrollController.position.outOfRange) { recipesCubit.loadNextPage(); } } _onScrollDiets() { if (dietsScrollController.offset >= dietsScrollController.position.maxScrollExtent && !dietsScrollController.position.outOfRange) { dietsCubit.loadNextPage(); } } _onScrollPlans() { if (plansScrollController.offset >= plansScrollController.position.maxScrollExtent && !plansScrollController.position.outOfRange) { plansCubit.loadNextPage(); } } _onScrollConsultants() { if (consultantsScrollController.offset >= consultantsScrollController.position.maxScrollExtent && !consultantsScrollController.position.outOfRange) {} } @override void dispose() { blogPostsScrollController.dispose(); recipesScrollController.dispose(); dietsScrollController.dispose(); plansScrollController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return MultiBlocProvider( providers: [ BlocProvider( create: (context) => sl()..getProfile(), ), BlocProvider( create: (context) => sl()..loadRecipesList(), ), BlocProvider( create: (context) => sl()..loadDiets(), ), BlocProvider( create: (context) => sl()..loadPlans(), ), BlocProvider( create: (context) => sl()..getConsultants(), ), ], child: Scaffold( floatingActionButton: _selectedChipIndex == 1 ? FloatingActionButton.small( heroTag: 'recipesTagsFilter', backgroundColor: appTheme.blueA400, shape: CircleBorder(), child: Icon( Icons.filter_list_rounded, color: Colors.white, ), onPressed: () async { await showModalBottomSheet( useRootNavigator: true, context: context, builder: (context) => TagsModalWidget( recipesCubit: recipesCubit, ), ); recipesCubit.loadRecipesList(); }, ) : null, appBar: AppBar( backgroundColor: Colors.white, toolbarHeight: 0, ), backgroundColor: Colors.grey.shade200, body: NestedScrollView( controller: switch (_selectedChipIndex) { 0 => blogPostsScrollController, 1 => recipesScrollController, 2 => dietsScrollController, 3 => plansScrollController, 4 => consultantsScrollController, _ => blogPostsScrollController }, headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return [ BlocBuilder( builder: (context, state) { return SliverAppBar( shape: RoundedRectangleBorder( borderRadius: BorderRadius.vertical(bottom: Radius.circular(12)), ), backgroundColor: Colors.white, foregroundColor: Colors.white, automaticallyImplyLeading: false, stretchTriggerOffset: 1, floating: true, snap: true, scrolledUnderElevation: 0, bottom: _chipGroup(), title: Row( children: [ CustomSearchBar( searchController: searchController, onChanged: (query) { if (query.length > 2) { EasyDebounce.debounce( 'searchFeed', Duration(milliseconds: 500), () { switch (_selectedChipIndex) { case 0: feedCubit.loadFeedBlogPostList(query: query); break; case 1: recipesCubit.loadRecipesList(query: query); break; case 2: dietsCubit.loadDiets(query: query); break; case 3: plansCubit.loadPlans(query: query); break; case 4: consultantsCubit.getConsultants(); break; } }, ); } else { EasyDebounce.debounce( 'searchFeed', Duration(milliseconds: 500), () { switch (_selectedChipIndex) { case 0: feedCubit.loadFeedBlogPostList(); break; case 1: recipesCubit.loadRecipesList(); break; case 2: dietsCubit.loadDiets(); break; case 3: plansCubit.loadPlans(); break; case 4: consultantsCubit.getConsultants(); break; } }, ); } }, ), SizedBox(width: 12), switch (_selectedChipIndex) { 1 => AddItemButton( onTap: () => GoRouter.of(context).pushNamed('recipeCreateScreen'), ), 2 => AddItemButton( onTap: state.profile?.role == 'consultant' ? () => GoRouter.of(context).pushNamed(CreateDietScreen.name) : null, ), 3 => AddItemButton( onTap: state.profile?.role == 'consultant' ? () => GoRouter.of(context).pushNamed(CreatePlanScreen.name) : null, ), _ => AddItemButton() }, ], ), ); }, ), ]; }, body: switch (_selectedChipIndex) { 0 => _buildAllBlogPosts(), 1 => _buildAllRecipes(), 2 => _buildDiets(), 3 => _buildPlans(), 4 => _buildConsultants(), _ => Center(child: Text(chipsLabelList[_selectedChipIndex])), }, ), ), ); } _buildAllBlogPosts() { return RefreshIndicator( onRefresh: () => feedCubit.loadFeedBlogPostList(), child: BlocBuilder( builder: (BuildContext context, FeedState state) { if (state is FeedStateLoaded) { return ListView.separated( padding: EdgeInsets.symmetric(vertical: 8), shrinkWrap: true, physics: BouncingScrollPhysics(), itemBuilder: (context, index) { if (index == state.feedBlogPostList.length) { return const Loader(); } return BlogPostWidget(index: index); }, separatorBuilder: (context, index) { return SizedBox(height: 8); }, itemCount: state.isLoading ? state.feedBlogPostList.length + 1 : state.feedBlogPostList.length, ); } else if (state is FeedStateError) { return Scaffold( backgroundColor: Colors.grey.shade200, body: Padding( padding: const EdgeInsets.all(16), child: Center( child: Text(state.errorMessage), ), ), ); } else { return Scaffold( backgroundColor: Colors.grey.shade200, body: const Loader(), ); } }, ), ); } _buildConsultants() { return RefreshIndicator( onRefresh: () => consultantsCubit.getConsultants(), child: BlocBuilder( bloc: consultantsCubit, builder: (BuildContext context, ConsultantsState state) { if (state is ConsultantsStateLoaded) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Material( color: Colors.white, borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: ListView.separated( padding: EdgeInsets.symmetric(vertical: 16), shrinkWrap: true, physics: BouncingScrollPhysics(), itemBuilder: (context, index) { return ConsultantWidget(consultant: state.consultantsList[index]); }, separatorBuilder: (context, index) { return SizedBox(height: 12); }, itemCount: state.consultantsList.length, ), ), ), ); } else if (state is ConsultantsStateError) { return Scaffold( backgroundColor: Colors.grey.shade200, body: Padding( padding: const EdgeInsets.all(16), child: Center( child: Text(state.errorMessage), ), ), ); } else { return Scaffold( backgroundColor: Colors.grey.shade200, body: const Loader(), ); } }, ), ); } _buildAllRecipes() { return RefreshIndicator( onRefresh: () => recipesCubit.loadRecipesList(), child: BlocBuilder( bloc: recipesCubit, builder: (BuildContext context, RecipesState state) { if (state is RecipesStateLoaded) { return ListView.separated( padding: EdgeInsets.symmetric(vertical: 8), shrinkWrap: true, physics: BouncingScrollPhysics(), itemBuilder: (context, index) { return RecipesWidget(recipes: state.recipesList[index]); }, separatorBuilder: (context, index) { return SizedBox(height: 8); }, itemCount: state.recipesList.length, ); } else if (state is RecipesStateError) { return Scaffold( backgroundColor: Colors.grey.shade200, body: Padding( padding: const EdgeInsets.all(16), child: Center( child: Text(state.errorMessage), ), ), ); } else { return Scaffold( backgroundColor: Colors.grey.shade200, body: const Loader(), ); } }, ), ); } _buildDiets() { return RefreshIndicator( onRefresh: () => dietsCubit.loadDiets(), child: BlocBuilder( bloc: dietsCubit, builder: (BuildContext context, DietsState state) { if (state.isLoading) { return Scaffold( backgroundColor: Colors.grey.shade200, body: const Loader(), ); } else if (state.errorMessage != null) { return Scaffold( backgroundColor: Colors.grey.shade200, body: Padding( padding: const EdgeInsets.all(16), child: Center( child: Text(state.errorMessage ?? ''), ), ), ); } else { return Stack( children: [ SizedBox.expand(), ListView.separated( padding: EdgeInsets.symmetric(vertical: 8), shrinkWrap: true, physics: BouncingScrollPhysics(), itemBuilder: (context, index) { return DietsWidget(diet: state.dietsList[index], index: index); }, separatorBuilder: (context, index) { return SizedBox(height: 8); }, itemCount: state.dietsList.length, ), Positioned( right: 0, bottom: 74, child: FilterBadge( filterBottomSheet: DietFilterBottomSheet( dietsCubit: dietsCubit, ), ), ), ], ); } }, ), ); } _buildPlans() { return RefreshIndicator( onRefresh: () => plansCubit.loadPlans(), child: BlocBuilder( bloc: plansCubit, builder: (BuildContext context, PlansState state) { if (state.isLoading) { return Scaffold( backgroundColor: Colors.grey.shade200, body: const Loader(), ); } else if (state.errorMessage != null) { return Scaffold( backgroundColor: Colors.grey.shade200, body: Padding( padding: const EdgeInsets.all(16), child: Center( child: Text(state.errorMessage ?? ''), ), ), ); } return Stack( children: [ SizedBox.expand(), ListView.separated( padding: EdgeInsets.symmetric(vertical: 8), shrinkWrap: true, physics: BouncingScrollPhysics(), itemBuilder: (context, index) { return PlanWidget( plan: state.plans[index], index: index, ); }, separatorBuilder: (context, index) { return SizedBox(height: 8); }, itemCount: state.plans.length, ), Positioned( right: 0, bottom: 74, child: FilterBadge( filterBottomSheet: PlansFilterBottomSheet( plansCubit: plansCubit, ), ), ), ], ); }, ), ); } _chipGroup() { return PreferredSize( preferredSize: Size(MediaQuery.of(context).size.width, 32), child: Align( alignment: Alignment.centerLeft, child: SingleChildScrollView( padding: EdgeInsets.symmetric(horizontal: 16), scrollDirection: Axis.horizontal, child: Row( children: [ ChoiceChipWidget( index: 0, selectedChipIndex: _selectedChipIndex, chipsLabelList: chipsLabelList, onSelected: (bool selected) { setState(() { _selectedChipIndex = 0; searchController.clear(); FocusScope.of(context).requestFocus(new FocusNode()); }); }, ), SizedBox(width: 8), ChoiceChipWidget( index: 1, selectedChipIndex: _selectedChipIndex, chipsLabelList: chipsLabelList, onSelected: (bool selected) { setState(() { _selectedChipIndex = 1; searchController.clear(); FocusScope.of(context).requestFocus(new FocusNode()); }); }, ), SizedBox(width: 8), ChoiceChipWidget( index: 2, selectedChipIndex: _selectedChipIndex, chipsLabelList: chipsLabelList, onSelected: (bool selected) { setState(() { _selectedChipIndex = 2; searchController.clear(); FocusScope.of(context).requestFocus(new FocusNode()); }); }, ), SizedBox(width: 8), ChoiceChipWidget( index: 3, selectedChipIndex: _selectedChipIndex, chipsLabelList: chipsLabelList, onSelected: (bool selected) { setState(() { _selectedChipIndex = 3; searchController.clear(); FocusScope.of(context).requestFocus(new FocusNode()); }); }, ), SizedBox(width: 8), ChoiceChipWidget( index: 4, selectedChipIndex: _selectedChipIndex, chipsLabelList: chipsLabelList, onSelected: (bool selected) { setState(() { _selectedChipIndex = 4; searchController.clear(); FocusScope.of(context).requestFocus(new FocusNode()); }); }, ), ], ), ), ), ); } }