Files
eatvio_app/lib/features/feed/presentation/pages/feed_screen.dart

600 lines
20 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:easy_debounce/easy_debounce.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/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/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/recipes_widget.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/tags_modal_widget.dart';
import 'package:eatvio_app/features/plans/presentation/bloc/plans_cubit/plans_cubit.dart';
import 'package:eatvio_app/features/plans/presentation/bloc/plans_cubit/plans_state.dart';
import 'package:eatvio_app/locator_service.dart';
import 'package:eatvio_app/theme/theme_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:rive/rive.dart';
final bucket = PageStorageBucket();
class FeedScreen extends StatefulWidget {
const FeedScreen({super.key});
@override
State<FeedScreen> createState() => _FeedScreenState();
}
class _FeedScreenState extends State<FeedScreen> {
final FeedCubit feedCubit = sl<FeedCubit>()..loadFeedBlogPostList();
final RecipesCubit recipesCubit = sl<RecipesCubit>()..loadRecipesList();
final DietsCubit dietsCubit = sl<DietsCubit>()..loadDiets();
final PlansCubit plansCubit = sl<PlansCubit>()..loadPlans();
final searchController = TextEditingController();
final List<String> chipsLabelList = ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты'];
int _selectedChipIndex = 0;
final blogPostsScrollController = ScrollController();
final recipesScrollController = ScrollController();
final dietsScrollController = ScrollController();
final plansScrollController = ScrollController();
@override
void initState() {
blogPostsScrollController.addListener(_onScrollBlogPosts);
recipesScrollController.addListener(_onScrollRecipes);
dietsScrollController.addListener(_onScrollDiets);
plansScrollController.addListener(_onScrollPlans);
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();
}
}
@override
void dispose() {
blogPostsScrollController.dispose();
recipesScrollController.dispose();
dietsScrollController.dispose();
plansScrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return 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: _selectedChipIndex == 0
? blogPostsScrollController
: _selectedChipIndex == 1
? recipesScrollController
: dietsScrollController,
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
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: [
_searchBar(),
SizedBox(width: 12),
_addBlogPostButton(),
],
),
),
];
},
body: Builder(
builder: (BuildContext context) {
if (_selectedChipIndex == 0) {
return _buildAllBlogPosts();
} else if (_selectedChipIndex == 1) {
return _buildAllRecipes();
} else if (_selectedChipIndex == 2) {
return _buildDiets();
} else if (_selectedChipIndex == 3) {
return _buildPlans();
} else {
return Center(child: Text(chipsLabelList[_selectedChipIndex]));
}
},
),
),
);
}
_buildAllBlogPosts() {
return BlocProvider<FeedCubit>(
create: (context) => feedCubit,
child: RefreshIndicator(
onRefresh: () => feedCubit.loadFeedBlogPostList(),
child: BlocBuilder<FeedCubit, FeedState>(
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 Container(
alignment: Alignment.center,
child: SizedBox(
height: 32,
width: 32,
child: RiveAnimation.asset(
'assets/rive_animations/loading.riv',
),
),
);
}
return BlogPostWidget(feedBlogPostModel: state.feedBlogPostList[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: Center(
child: SizedBox(
height: 32,
width: 32,
child: RiveAnimation.asset(
'assets/rive_animations/loading.riv',
),
),
),
);
}
},
),
),
);
}
_buildAllRecipes() {
return BlocProvider<RecipesCubit>(
create: (context) => recipesCubit,
child: RefreshIndicator(
onRefresh: () => recipesCubit.loadRecipesList(),
child: BlocBuilder<RecipesCubit, RecipesState>(
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: Center(
child: SizedBox(
height: 32,
width: 32,
child: RiveAnimation.asset(
'assets/rive_animations/loading.riv',
),
),
),
);
}
},
),
),
);
}
_buildDiets() {
return BlocProvider<DietsCubit>(
create: (context) => dietsCubit,
child: RefreshIndicator(
onRefresh: () => dietsCubit.loadDiets(),
child: BlocBuilder<DietsCubit, DietsState>(
builder: (BuildContext context, DietsState state) {
if (state is DietsStateLoaded) {
return Stack(
children: [
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(),
),
),
],
);
} else if (state is DietsStateError) {
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: Center(
child: SizedBox(
height: 32,
width: 32,
child: RiveAnimation.asset(
'assets/rive_animations/loading.riv',
),
),
),
);
}
},
),
),
);
}
_buildPlans() {
return BlocProvider<PlansCubit>(
create: (context) => plansCubit,
child: RefreshIndicator(
onRefresh: () => plansCubit.loadPlans(),
child: BlocBuilder<PlansCubit, PlansState>(
builder: (BuildContext context, PlansState state) {
if (state is PlansStateLoaded) {
return ListView.separated(
padding: EdgeInsets.symmetric(vertical: 8),
shrinkWrap: true,
physics: BouncingScrollPhysics(),
itemBuilder: (context, index) {
return PlanWidget(planModel: state.plansModel.data[index]);
},
separatorBuilder: (context, index) {
return SizedBox(height: 8);
},
itemCount: state.plansModel.data.length,
);
} else if (state is PlansStateError) {
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: Center(
child: SizedBox(
height: 32,
width: 32,
child: RiveAnimation.asset(
'assets/rive_animations/loading.riv',
),
),
),
);
}
},
),
),
);
}
_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());
});
},
),
],
),
),
),
);
}
_addBlogPostButton() {
return InkWell(
onTap: () => GoRouter.of(context).pushNamed('blogPostCreateScreen'),
borderRadius: BorderRadius.circular(64),
child: Ink(
decoration: BoxDecoration(
color: appTheme.blueA400,
shape: BoxShape.circle,
),
child: Icon(
Icons.add_rounded,
color: appTheme.gray100,
),
width: 40,
height: 40,
),
);
}
_searchBar() {
return SizedBox(
height: 40,
width: MediaQuery.of(context).size.width - 84,
child: TextFormField(
controller: searchController,
style: TextStyle(
fontSize: 12,
),
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;
}
},
);
} else {
EasyDebounce.debounce(
'searchFeed',
Duration(milliseconds: 500),
() {
switch (_selectedChipIndex) {
case 0:
feedCubit.loadFeedBlogPostList();
break;
case 1:
recipesCubit.loadRecipesList();
break;
case 2:
dietsCubit.loadDiets(query: query);
break;
}
},
);
}
},
decoration: InputDecoration(
hintText: 'Поиск',
hintStyle: TextStyle(
fontSize: 12,
color: appTheme.gray600,
fontWeight: FontWeight.w400,
),
prefixIcon: Icon(
Icons.search,
color: appTheme.gray600,
size: 16,
),
isDense: false,
contentPadding: EdgeInsets.all(8),
fillColor: appTheme.gray100,
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
),
),
);
}
_choiceChipWidget(int index) {
return ChoiceChip(
selectedColor: appTheme.blueA400,
pressElevation: 0,
showCheckmark: false,
side: BorderSide.none,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
label: Text(chipsLabelList[index]),
labelStyle: TextStyle(
color: index == _selectedChipIndex ? Colors.white : appTheme.gray600,
fontWeight: FontWeight.w500,
fontSize: 12,
),
selected: _selectedChipIndex == index,
backgroundColor: appTheme.gray100,
onSelected: (bool selected) {
setState(() {
_selectedChipIndex = index;
searchController.clear();
FocusScope.of(context).requestFocus(new FocusNode());
});
},
);
}
}