forked from eatvio/eatvio_app
353 lines
17 KiB
Dart
353 lines
17 KiB
Dart
import 'package:easy_debounce/easy_debounce.dart';
|
||
import 'package:eatvio_app/app_keys.dart';
|
||
import 'package:eatvio_app/core/const.dart';
|
||
import 'package:eatvio_app/core/cubit/marketing/marketing_cubit.dart';
|
||
import 'package:eatvio_app/core/views/cubit/views_cubit.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/core/widgets/marketing_bottom_sheet.dart';
|
||
import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.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/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';
|
||
import 'package:eatvio_app/features/feed/presentation/widgets/blog_posts.dart';
|
||
import 'package:eatvio_app/features/feed/presentation/widgets/chip_group.dart';
|
||
import 'package:eatvio_app/features/feed/presentation/widgets/consultants.dart';
|
||
import 'package:eatvio_app/features/feed/presentation/widgets/diets.dart';
|
||
import 'package:eatvio_app/features/feed/presentation/widgets/plans.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/core/locator_service.dart';
|
||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||
import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||
import 'package:go_router/go_router.dart';
|
||
import '../widgets/recipes.dart';
|
||
|
||
class FeedScreen extends StatelessWidget {
|
||
const FeedScreen({super.key, this.initialTabIndex = 0});
|
||
final int? initialTabIndex;
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return MultiBlocProvider(
|
||
providers: [
|
||
BlocProvider<RecipesCubit>(create: (context) => sl<RecipesCubit>()..loadRecipesList()),
|
||
BlocProvider<FeedProfileCubit>(create: (context) => sl<FeedProfileCubit>()..getProfile()),
|
||
BlocProvider<DietsCubit>(create: (context) => sl<DietsCubit>()..loadDiets()),
|
||
if (!hidePlans) BlocProvider<PlansCubit>(create: (context) => sl<PlansCubit>()..loadPlans()),
|
||
BlocProvider<ConsultantsCubit>(create: (context) => sl<ConsultantsCubit>()..getConsultants()),
|
||
],
|
||
child: FeedScreenContent(initialTabIndex: initialTabIndex),
|
||
);
|
||
}
|
||
}
|
||
|
||
class FeedScreenContent extends StatefulWidget {
|
||
const FeedScreenContent({super.key, required this.initialTabIndex});
|
||
final int? initialTabIndex;
|
||
@override
|
||
State<FeedScreenContent> createState() => _FeedScreenContentState();
|
||
}
|
||
|
||
class _FeedScreenContentState extends State<FeedScreenContent> {
|
||
final postsController = TextEditingController();
|
||
final recipesController = TextEditingController();
|
||
final dietsController = TextEditingController();
|
||
final plansController = TextEditingController();
|
||
final consultantsController = TextEditingController();
|
||
|
||
late final List<String> chipsLabelList;
|
||
late int _selectedChipIndex;
|
||
final blogPostsScrollController = ScrollController();
|
||
final recipesScrollController = ScrollController();
|
||
final dietsScrollController = ScrollController();
|
||
final plansScrollController = ScrollController();
|
||
final consultantsScrollController = ScrollController();
|
||
@override
|
||
void initState() {
|
||
_selectedChipIndex = widget.initialTabIndex ?? 0;
|
||
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(currentUserId: context.read<FeedProfileCubit>().state.profile?.id);
|
||
}
|
||
});
|
||
super.initState();
|
||
}
|
||
|
||
@override
|
||
void didUpdateWidget(FeedScreenContent oldWidget) {
|
||
super.didUpdateWidget(oldWidget);
|
||
final initialTabIndex = widget.initialTabIndex;
|
||
setState(() {
|
||
if (initialTabIndex != null) {
|
||
_selectedChipIndex = initialTabIndex;
|
||
}
|
||
});
|
||
}
|
||
|
||
void _onScrollConsultants() {
|
||
if (consultantsScrollController.offset >= consultantsScrollController.position.maxScrollExtent && !consultantsScrollController.position.outOfRange) {}
|
||
}
|
||
|
||
void _checkRegistrationDate(BuildContext context) {
|
||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||
final needToShowMarketingBottomSheet = await context.read<MarketingCubit>().checkRegistrationDate();
|
||
if (needToShowMarketingBottomSheet && context.mounted) {
|
||
showMarketingBottomSheet(context);
|
||
}
|
||
});
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
blogPostsScrollController.dispose();
|
||
recipesScrollController.dispose();
|
||
dietsScrollController.dispose();
|
||
plansScrollController.dispose();
|
||
consultantsScrollController.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
Future<void> _openCreateAndReloadFeed({
|
||
required Future<Object?> Function() openCreate,
|
||
required Future<void> Function() reload,
|
||
}) async {
|
||
final result = await openCreate();
|
||
if (!mounted) {
|
||
return;
|
||
}
|
||
if (shouldReloadFeedAfterCreate(result)) {
|
||
await reload();
|
||
}
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
FlutterNativeSplash.remove();
|
||
return Scaffold(
|
||
floatingActionButton: ((_selectedChipIndex == 2 && hidePlans) || (_selectedChipIndex == 3 && !hidePlans))
|
||
? FloatingActionButton.small(
|
||
heroTag: 'recipesTagsFilter',
|
||
backgroundColor: appTheme.blueA400,
|
||
shape: const CircleBorder(),
|
||
child: const Icon(Icons.filter_list_rounded, color: Colors.white),
|
||
onPressed: () async {
|
||
final recipesCubit = context.read<RecipesCubit>();
|
||
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 when !hidePlans => plansScrollController,
|
||
1 when hidePlans => consultantsScrollController,
|
||
2 when !hidePlans => consultantsScrollController,
|
||
2 when hidePlans => recipesScrollController,
|
||
3 when !hidePlans => recipesScrollController,
|
||
3 when hidePlans => dietsScrollController,
|
||
4 when !hidePlans => dietsScrollController,
|
||
_ => blogPostsScrollController,
|
||
},
|
||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||
return [
|
||
BlocBuilder<FeedProfileCubit, ProfileState>(
|
||
builder: (context, state) {
|
||
return SliverAppBar(
|
||
shape: const 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: PreferredSize(
|
||
preferredSize: Size(MediaQuery.of(context).size.width, 32),
|
||
child: ChipGroup(
|
||
selectedChipIndex: _selectedChipIndex,
|
||
chipsLabelList: chipsLabelList,
|
||
setSelectedChipIndex: (index) {
|
||
setState(() {
|
||
_selectedChipIndex = index;
|
||
});
|
||
},
|
||
),
|
||
),
|
||
title: Row(
|
||
children: [
|
||
CustomSearchBar(
|
||
searchController: switch (_selectedChipIndex) {
|
||
1 when !hidePlans => plansController,
|
||
1 when hidePlans => consultantsController,
|
||
2 when !hidePlans => consultantsController,
|
||
2 when hidePlans => recipesController,
|
||
3 when !hidePlans => recipesController,
|
||
3 when hidePlans => dietsController,
|
||
4 when !hidePlans => dietsController,
|
||
_ => postsController,
|
||
},
|
||
onChanged: (query) {
|
||
if (query.length > 2) {
|
||
EasyDebounce.debounce('searchFeed', const Duration(milliseconds: 500), () {
|
||
switch (_selectedChipIndex) {
|
||
case 0:
|
||
context.read<FeedCubit>().loadFeedBlogPostList(
|
||
query: query,
|
||
currentUserId: context.read<FeedProfileCubit>().state.profile?.id,
|
||
);
|
||
break;
|
||
case 1 when !hidePlans:
|
||
context.read<PlansCubit>().loadPlans(query: query);
|
||
break;
|
||
case 1 when hidePlans:
|
||
case 2 when !hidePlans:
|
||
context.read<ConsultantsCubit>().getConsultants();
|
||
break;
|
||
case 2 when hidePlans:
|
||
case 3 when !hidePlans:
|
||
context.read<RecipesCubit>().loadRecipesList(query: query);
|
||
break;
|
||
case 3 when hidePlans:
|
||
case 4 when !hidePlans:
|
||
context.read<DietsCubit>().loadDiets(query: query);
|
||
break;
|
||
}
|
||
});
|
||
} else {
|
||
EasyDebounce.debounce('searchFeed', const Duration(milliseconds: 500), () {
|
||
switch (_selectedChipIndex) {
|
||
case 0:
|
||
context.read<FeedCubit>().loadFeedBlogPostList(
|
||
currentUserId: context.read<FeedProfileCubit>().state.profile?.id,
|
||
);
|
||
break;
|
||
case 1 when !hidePlans:
|
||
context.read<PlansCubit>().loadPlans();
|
||
break;
|
||
case 1 when hidePlans:
|
||
case 2 when !hidePlans:
|
||
context.read<ConsultantsCubit>().getConsultants();
|
||
break;
|
||
case 2 when hidePlans:
|
||
case 3 when !hidePlans:
|
||
context.read<RecipesCubit>().loadRecipesList();
|
||
break;
|
||
case 3 when hidePlans:
|
||
case 4 when !hidePlans:
|
||
context.read<DietsCubit>().loadDiets();
|
||
break;
|
||
}
|
||
});
|
||
}
|
||
},
|
||
),
|
||
const SizedBox(width: 12),
|
||
switch (_selectedChipIndex) {
|
||
1 when !hidePlans => AddItemButton(
|
||
onTap: state.profile?.roles.contains('consultant') ?? false
|
||
? () => _openCreateAndReloadFeed(
|
||
openCreate: () => GoRouter.of(context).pushNamed(CreatePlanScreen.name),
|
||
reload: () => context.read<PlansCubit>().loadPlans(),
|
||
)
|
||
: null,
|
||
),
|
||
2 when hidePlans => AddItemButton(
|
||
onTap: state.profile?.roles.contains('consultant') ?? false
|
||
? () => _openCreateAndReloadFeed(
|
||
openCreate: () => GoRouter.of(context).pushNamed(RecipeCreateScreen.name),
|
||
reload: () => context.read<RecipesCubit>().loadRecipesList(),
|
||
)
|
||
: null,
|
||
),
|
||
3 when !hidePlans => AddItemButton(
|
||
onTap: state.profile?.roles.contains('consultant') ?? false
|
||
? () => _openCreateAndReloadFeed(
|
||
openCreate: () => GoRouter.of(context).pushNamed(RecipeCreateScreen.name),
|
||
reload: () => context.read<RecipesCubit>().loadRecipesList(),
|
||
)
|
||
: null,
|
||
),
|
||
3 when hidePlans => AddItemButton(
|
||
onTap: state.profile?.roles.contains('consultant') ?? false
|
||
? () => _openCreateAndReloadFeed(
|
||
openCreate: () => GoRouter.of(context).pushNamed(CreateDietScreen.name, extra: {'isEditing': false}),
|
||
reload: () => context.read<DietsCubit>().loadDiets(),
|
||
)
|
||
: null,
|
||
),
|
||
4 when !hidePlans => AddItemButton(
|
||
onTap: state.profile?.roles.contains('consultant') ?? false
|
||
? () => _openCreateAndReloadFeed(
|
||
openCreate: () => GoRouter.of(context).pushNamed(CreateDietScreen.name),
|
||
reload: () => context.read<DietsCubit>().loadDiets(),
|
||
)
|
||
: null,
|
||
),
|
||
_ => const AddItemButton(key: AppKeys.feedScreenBlogPostCreationButton),
|
||
},
|
||
],
|
||
),
|
||
);
|
||
},
|
||
),
|
||
];
|
||
},
|
||
body: switch (_selectedChipIndex) {
|
||
0 => BlogPosts(handleVisibility: context.read<ViewsCubit>().handleVisibility),
|
||
1 when !hidePlans => Plans(handleVisibility: context.read<ViewsCubit>().handleVisibility),
|
||
1 when hidePlans => const Consultants(),
|
||
2 when !hidePlans => const Consultants(),
|
||
2 when hidePlans => Recipes(handleVisibility: context.read<ViewsCubit>().handleVisibility),
|
||
3 when !hidePlans => Recipes(handleVisibility: context.read<ViewsCubit>().handleVisibility),
|
||
3 when hidePlans => Diets(handleVisibility: context.read<ViewsCubit>().handleVisibility),
|
||
4 when !hidePlans => Diets(handleVisibility: context.read<ViewsCubit>().handleVisibility),
|
||
_ => Center(child: Text(chipsLabelList[_selectedChipIndex])),
|
||
},
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// Create screens pop `true` (diet/plan) or `'active'` (recipe) after a successful publish.
|
||
bool shouldReloadFeedAfterCreate(Object? result) => result == true || result == 'active';
|
||
|
||
enum ViewableContentType {
|
||
plans,
|
||
posts,
|
||
diets,
|
||
recipes;
|
||
|
||
String getStringifiedVersion() => switch (this) {
|
||
ViewableContentType.plans => 'plans',
|
||
|
||
ViewableContentType.posts => 'blog_posts',
|
||
|
||
ViewableContentType.diets => 'diets',
|
||
|
||
ViewableContentType.recipes => 'recipes',
|
||
};
|
||
}
|