diff --git a/assets/images/img_attach.svg b/assets/images/img_attach.svg new file mode 100644 index 00000000..9f75ca7c --- /dev/null +++ b/assets/images/img_attach.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/img_mic.svg b/assets/images/img_mic.svg new file mode 100644 index 00000000..a7c11027 --- /dev/null +++ b/assets/images/img_mic.svg @@ -0,0 +1,3 @@ + + + diff --git a/eatvio_app.iml b/eatvio_app.iml index f66303d5..ed4ab455 100644 --- a/eatvio_app.iml +++ b/eatvio_app.iml @@ -1,4 +1,4 @@ - + diff --git a/lib/core/utils/image_constant.dart b/lib/core/utils/image_constant.dart index 8534fec2..8c9c0981 100644 --- a/lib/core/utils/image_constant.dart +++ b/lib/core/utils/image_constant.dart @@ -104,6 +104,10 @@ class ImageConstant { static String imgSupport = '$imagePath/img_nav_support.svg'; + static String imgMic = '$imagePath/img_mic.svg'; + + static String imgAttach = '$imagePath/img_attach.svg'; + static String imgExit = '$imagePath/img_nav_exit.svg'; //------------------------------------------------------------- diff --git a/lib/main.dart b/lib/main.dart index 7b5326dc..3a5a94bd 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,6 +14,8 @@ void main() async { await dotenv.load(fileName: ".env"); await Hive.initFlutter(path); SystemChrome.setPreferredOrientations([ + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, DeviceOrientation.portraitUp, ]); await PrefUtils().init(); diff --git a/lib/presentation/chat/chat_screen/bloc/chat_bloc.dart b/lib/presentation/chat/chat_screen/bloc/chat_bloc.dart index 0f4a2add..52cf81df 100644 --- a/lib/presentation/chat/chat_screen/bloc/chat_bloc.dart +++ b/lib/presentation/chat/chat_screen/bloc/chat_bloc.dart @@ -6,6 +6,7 @@ import 'package:http/http.dart' as http; import 'dart:convert'; import '/core/app_export.dart'; import 'package:eatvio_app/presentation/chat/chat_screen/models/chat_model.dart'; +import 'package:eatvio_app/presentation/chat/chat_screen/models/chat_message_model.dart'; part 'chat_event.dart'; part 'chat_state.dart'; @@ -27,22 +28,26 @@ class ChatBloc extends Bloc { AppRoutes.authScreen, ); } - final int id = 1; + final String? chatID = state.chatModelObj?.chatID; final response = await http.get( - Uri.parse(dotenv.env['SERVER_ROOT']! + '/chat/$id/show'), + Uri.parse(dotenv.env['SERVER_ROOT']! + '/chat/show/$chatID'), headers: { 'Authorization': 'Bearer $token', }, ); if (response.statusCode == 200) { final data = jsonDecode(response.body); - print(data['chatRooms']); - if (data is Map && data.containsKey('chatRooms')) { + if (data is Map && + data.containsKey('chatRoom') && + data['chatRoom'].containsKey('messages')) { if (state is ChatLoadedState) { var currentState = state as ChatLoadedState; - emit(currentState.copyWith()); + emit(currentState.copyWith( + chatModelObj: currentState.chatModelObj, + messages: currentState.messages)); } else { - emit(ChatLoadedState()); + emit(ChatLoadedState( + messages: fillMessages(data['chatRoom']['messages']))); } } else { // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку @@ -57,4 +62,18 @@ class ChatBloc extends Bloc { emit(ChatErrorState('Ошибка при получении данных о чатах: $e')); } } + + List fillMessages(List messagesJSON) { + List messages = []; + for (int i = 0; i < messagesJSON.length; i++) { + messages.add(ChatMessageModel( + id: messagesJSON[i]['id'], + data: messagesJSON[i]['data'], + mediaData: messagesJSON[i]['mediaData'] ?? [], + updatedAt: messagesJSON[i]['updated_at'], + authorId: messagesJSON[i]['author_id'])); + } + + return messages; + } } diff --git a/lib/presentation/chat/chat_screen/bloc/chat_event.dart b/lib/presentation/chat/chat_screen/bloc/chat_event.dart index a3778f98..1fe6df94 100644 --- a/lib/presentation/chat/chat_screen/bloc/chat_event.dart +++ b/lib/presentation/chat/chat_screen/bloc/chat_event.dart @@ -14,15 +14,3 @@ class ChatInitialEvent extends ChatEvent { @override List get props => []; } - -///event for dropdown selection -class ChangeDropDownEvent extends ChatEvent { - ChangeDropDownEvent({required this.value}); - - SelectionPopupModel value; - - @override - List get props => [ - value, - ]; -} diff --git a/lib/presentation/chat/chat_screen/bloc/chat_state.dart b/lib/presentation/chat/chat_screen/bloc/chat_state.dart index aae133dc..6f5718d1 100644 --- a/lib/presentation/chat/chat_screen/bloc/chat_state.dart +++ b/lib/presentation/chat/chat_screen/bloc/chat_state.dart @@ -22,26 +22,18 @@ class ChatState extends Equatable { } class ChatLoadedState extends ChatState { - ChatLoadedState({ - this.selectedDropDownValue, - this.chatModelObj, - }); - - SelectionPopupModel? selectedDropDownValue; + ChatLoadedState({this.chatModelObj, this.messages}); ChatModel? chatModelObj; + List? messages; @override - List get props => [ - selectedDropDownValue, - chatModelObj, - ]; - ChatState copyWith({ - SelectionPopupModel? selectedDropDownValue, - ChatModel? chatModelObj, - }) { - return ChatState( + List get props => [chatModelObj, messages]; + ChatLoadedState copyWith( + {ChatModel? chatModelObj, List? messages}) { + return ChatLoadedState( chatModelObj: chatModelObj ?? this.chatModelObj, + messages: messages ?? this.messages, ); } } diff --git a/lib/presentation/chat/chat_screen/chat_screen.dart b/lib/presentation/chat/chat_screen/chat_screen.dart index 8b1fc731..37c3df52 100644 --- a/lib/presentation/chat/chat_screen/chat_screen.dart +++ b/lib/presentation/chat/chat_screen/chat_screen.dart @@ -1,509 +1,70 @@ -/*import 'bloc/chat_bloc.dart'; +import 'bloc/chat_bloc.dart'; import 'models/chat_model.dart'; import 'package:eatvio_app/core/app_export.dart'; import 'package:flutter/material.dart'; +import 'package:eatvio_app/widgets/app_bar/custom_app_bar.dart'; +import 'package:eatvio_app/widgets/app_bar/appbar_subtitle.dart'; +import 'package:eatvio_app/widgets/app_bar/appbar_leading_image.dart'; +import 'package:eatvio_app/widgets/app_bar/appbar_title_image.dart'; import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; +import 'package:eatvio_app/widgets/custom_text_form_field.dart'; class ChatScreen extends StatelessWidget { const ChatScreen({Key? key}) : super(key: key); static Widget builder(BuildContext context) { + final chatID = ModalRoute.of(context)!.settings.arguments; return BlocProvider( - create: (context) => ChatBloc(ChatState(chatModelObj: ChatModel())) + create: (context) => ChatBloc( + ChatState(chatModelObj: ChatModel(chatID: chatID.toString()))) ..add(ChatInitialEvent()), child: ChatScreen()); } - @override Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is ChatInitialState) { + // Это начальное состояние, данные ещё не загружены. + return Container( + color: Colors.white, + ); + } else if (state is ChatLoadedState) { + // Данные загружены, можно строить экран. + return _buildChat(context, state); + } else if (state is ChatErrorState) { + // Произошла ошибка при загрузке данных. + return Center(child: Text(state.errorMessage)); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildChat(BuildContext context, ChatLoadedState state) { mediaQueryData = MediaQuery.of(context); return Scaffold( resizeToAvoidBottomInset: false, appBar: _buildAppbar(context), body: SizedBox( width: double.maxFinite, - child: Column( - children: [ - SizedBox(height: 13.v), - Expanded( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Container( - height: 34.v, - width: 211.h, - margin: EdgeInsets.only(right: 17.h), - child: Stack( - alignment: Alignment.bottomRight, - children: [ - CustomImageView( - imagePath: ImageConstant.imgPath, - height: 34.v, - width: 211.h, - alignment: Alignment.center, - ), - Align( - alignment: Alignment.bottomRight, - child: Padding( - padding: EdgeInsets.only( - right: 9.h, - bottom: 4.v, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: EdgeInsets.only(bottom: 1.v), - child: Text( - "msg_food_is_amazing".tr, - style: theme.textTheme.bodyLarge, - ), - ), - Padding( - padding: EdgeInsets.only( - left: 7.h, - top: 7.v, - ), - child: Text( - "lbl_10_10".tr, - style: theme.textTheme.bodySmall, - ), - ), - CustomImageView( - imagePath: - ImageConstant.imgCheckmarkBlueA400, - height: 7.v, - width: 14.h, - margin: EdgeInsets.only( - left: 2.h, - top: 9.v, - bottom: 4.v, - ), - ) - ], - ), - ), - ) - ], - ), - ), - SizedBox(height: 8.v), - Padding( - padding: EdgeInsets.only( - left: 84.h, - right: 17.h, - ), - child: _buildColumncheckmark( - context, - imageOne: ImageConstant.imgRectangle, - time: "lbl_10_15".tr, - ), - ), - SizedBox(height: 8.v), - Padding( - padding: EdgeInsets.only( - left: 84.h, - right: 17.h, - ), - child: _buildColumncheckmark( - context, - imageOne: ImageConstant.imgRectangle186x254, - time: "lbl_10_15".tr, - ), - ), - SizedBox(height: 20.v), - Align( - alignment: Alignment.centerLeft, - child: Container( - margin: EdgeInsets.only( - left: 10.h, - right: 77.h, - ), - padding: EdgeInsets.symmetric( - horizontal: 11.h, - vertical: 4.v, - ), - decoration: BoxDecoration( - image: DecorationImage( - image: fs.Svg( - ImageConstant.imgGroup14, - ), - fit: BoxFit.cover, - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox(height: 3.v), - Padding( - padding: EdgeInsets.only(left: 7.h), - child: Row( - children: [ - SizedBox( - height: 33.v, - child: VerticalDivider( - width: 2.h, - thickness: 2.v, - endIndent: 1.h, - ), - ), - Padding( - padding: EdgeInsets.only(left: 8.h), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "lbl_martha_craig".tr, - style: CustomTextStyles - .bodyMediumLightblueA70001, - ), - Text( - "lbl_good_morning".tr, - style: CustomTextStyles - .bodyMediumBluegray800_1, - ) - ], - ), - ) - ], - ), - ), - SizedBox(height: 4.v), - Padding( - padding: EdgeInsets.only(left: 6.h), - child: Row( - children: [ - Text( - "msg_do_you_know_what".tr, - style: theme.textTheme.bodyLarge, - ), - Padding( - padding: EdgeInsets.only( - left: 14.h, - top: 8.v, - ), - child: Text( - "lbl_11_40".tr, - style: CustomTextStyles - .bodySmallBluegray40001, - ), - ) - ], - ), - ) - ], - ), - ), - ), - SizedBox(height: 20.v), - Container( - height: 34.v, - width: 266.h, - margin: EdgeInsets.only(right: 11.h), - child: Stack( - alignment: Alignment.topCenter, - children: [ - CustomImageView( - imagePath: ImageConstant.imgPathGray50, - height: 34.v, - width: 266.h, - alignment: Alignment.center, - ), - Align( - alignment: Alignment.topCenter, - child: Padding( - padding: EdgeInsets.only(top: 2.v), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: EdgeInsets.only(top: 4.v), - child: Text( - "msg_it_s_morning_in".tr, - style: theme.textTheme.bodyLarge, - ), - ), - CustomImageView( - imagePath: ImageConstant.img, - height: 19.v, - width: 17.h, - margin: EdgeInsets.only( - left: 10.h, - bottom: 7.v, - ), - ), - Padding( - padding: EdgeInsets.only( - left: 14.h, - top: 12.v, - ), - child: Text( - "lbl_11_43".tr, - style: theme.textTheme.bodySmall, - ), - ), - CustomImageView( - imagePath: - ImageConstant.imgCheckmarkBlueA400, - height: 7.v, - width: 14.h, - margin: EdgeInsets.only( - left: 2.h, - top: 13.v, - bottom: 5.v, - ), - ) - ], - ), - ), - ) - ], - ), - ), - SizedBox(height: 20.v), - Align( - alignment: Alignment.centerLeft, - child: Container( - width: 281.h, - margin: EdgeInsets.only( - left: 17.h, - right: 76.h, - ), - padding: EdgeInsets.symmetric( - horizontal: 11.h, - vertical: 2.v, - ), - decoration: BoxDecoration( - image: DecorationImage( - image: fs.Svg( - ImageConstant.imgGroup76, - ), - fit: BoxFit.cover, - ), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: Container( - width: 191.h, - margin: EdgeInsets.only(top: 3.v), - child: Text( - "msg_what_is_the_most".tr, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodyLarge!.copyWith( - height: 1.29, - ), - ), - ), - ), - Padding( - padding: EdgeInsets.only( - left: 38.h, - top: 34.v, - ), - child: Text( - "lbl_11_45".tr, - style: CustomTextStyles.bodySmallBluegray40001, - ), - ) - ], - ), - ), - ), - SizedBox(height: 8.v), - Align( - alignment: Alignment.centerLeft, - child: Container( - height: 34.v, - width: 178.h, - margin: EdgeInsets.only(left: 10.h), - child: Stack( - alignment: Alignment.bottomCenter, - children: [ - CustomImageView( - imagePath: ImageConstant.imgGroup14, - height: 34.v, - width: 178.h, - alignment: Alignment.center, - ), - Align( - alignment: Alignment.bottomCenter, - child: Padding( - padding: EdgeInsets.only(bottom: 3.v), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.only(bottom: 1.v), - child: Text( - "lbl_do_you_like_it".tr, - style: theme.textTheme.bodyLarge, - ), - ), - Padding( - padding: EdgeInsets.only( - left: 1.h, - top: 8.v, - ), - child: Text( - "lbl_11_45".tr, - style: CustomTextStyles - .bodySmallBluegray40001, - ), - ) - ], - ), - ), - ) - ], - ), - ), - ), - SizedBox(height: 20.v), - Container( - height: 34.v, - width: 218.h, - margin: EdgeInsets.only(right: 17.h), - child: Stack( - alignment: Alignment.bottomCenter, - children: [ - CustomImageView( - imagePath: ImageConstant.imgPath, - height: 34.v, - width: 218.h, - alignment: Alignment.center, - ), - Align( - alignment: Alignment.bottomCenter, - child: Padding( - padding: EdgeInsets.only(bottom: 4.v), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - "msg_i_think_top_two".tr, - style: theme.textTheme.bodyLarge, - ), - Padding( - padding: EdgeInsets.only( - left: 12.h, - top: 7.v, - ), - child: Text( - "lbl_11_50".tr, - style: theme.textTheme.bodySmall, - ), - ), - CustomImageView( - imagePath: - ImageConstant.imgCheckmarkBlueA400, - height: 7.v, - width: 14.h, - margin: EdgeInsets.only( - left: 2.h, - top: 9.v, - bottom: 4.v, - ), - ) - ], - ), - ), - ) - ], - ), - ), - SizedBox(height: 8.v), - Container( - width: 235.h, - margin: EdgeInsets.only( - left: 123.h, - right: 16.h, - ), - padding: EdgeInsets.symmetric( - horizontal: 9.h, - vertical: 2.v, - ), - decoration: BoxDecoration( - image: DecorationImage( - image: fs.Svg( - ImageConstant.imgGroup11, - ), - fit: BoxFit.cover, - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 20.v), - Padding( - padding: EdgeInsets.only(left: 14.h), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CustomImageView( - imagePath: ImageConstant.imgDownload, - height: 40.adaptSize, - width: 40.adaptSize, - margin: EdgeInsets.only(bottom: 2.v), - ), - Padding( - padding: EdgeInsets.only( - left: 31.h, - top: 5.v, - ), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "lbl7".tr, - style: - CustomTextStyles.bodyLargeBlueA400, - ), - Text( - "lbl_2_8_mb".tr, - style: - CustomTextStyles.bodyMediumBlueA400, - ) - ], - ), - ) - ], - ), - ), - SizedBox(height: 4.v), - _buildRowcheckmark( - context, - time: "lbl_11_51".tr, - ) - ], - ), - ), - SizedBox(height: 30.v), - _buildRowattachone(context) - ], - ), - ), - ) - ], + child: Container( + width: double.maxFinite, + decoration: AppDecoration.fillGray, + child: ListView( + children: [ + SizedBox(height: 30.v), + ], + ), ), ), - bottomNavigationBar: _buildBottomBar(context), + bottomNavigationBar: _buildTextField(context), ); } - /// Section Widget - _buildAppbar(BuildContext context) { + + /// Section Widget + _buildAppbar(BuildContext context) { return CustomAppBar( leadingWidth: 26.h, leading: AppbarLeadingImage( @@ -513,16 +74,14 @@ class ChatScreen extends StatelessWidget { top: 18.v, bottom: 25.v, ), - onTap: () { - onTapArrowleftone(context); - }, + onTap: () => onTapArrowleftone(context), ), title: Padding( padding: EdgeInsets.only(left: 12.h), child: Row( children: [ - AppbarTitleCircleimage( - imagePath: ImageConstant.imgOval40x40, + AppbarTitleImage( + imagePath: ImageConstant.imgIconMoreHorizontal, margin: EdgeInsets.only(bottom: 3.v), ), Padding( @@ -532,33 +91,29 @@ class ChatScreen extends StatelessWidget { ), child: Column( children: [ - AppbarTitle( - text: "lbl2".tr, + AppbarSubtitle( + text: "Валерий Леонтьев", ), SizedBox(height: 3.v), - AppbarSubtitle( - text: "msg_12_30".tr, - margin: EdgeInsets.only(right: 38.h), - ) + Text("Был в сети вчера", style: theme.textTheme.bodySmall!) ], ), ) ], ), ), - styleType: Style.bgFill, ); } /// Section Widget - Widget _buildRowattachone(BuildContext context) { + _buildTextField(BuildContext context) { return Container( width: double.maxFinite, padding: EdgeInsets.symmetric( horizontal: 7.h, vertical: 6.v, ), - decoration: AppDecoration.outlineGray, + decoration: AppDecoration.outlineBlueGray, child: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, @@ -579,11 +134,11 @@ class ChatScreen extends StatelessWidget { bottom: 4.v, ), child: BlocSelector( - selector: (state) => state.messageController, + selector: (state) => TextEditingController(), builder: (context, messageController) { return CustomTextFormField( controller: messageController, - hintText: "lbl_message".tr, + hintText: "Напишите сообщение...", textInputAction: TextInputAction.done, ); }, @@ -591,7 +146,7 @@ class ChatScreen extends StatelessWidget { ), ), CustomImageView( - imagePath: ImageConstant.imgMenu, + imagePath: ImageConstant.imgMic, height: 24.v, width: 18.h, margin: EdgeInsets.only( @@ -605,100 +160,13 @@ class ChatScreen extends StatelessWidget { ); } - /// Section Widget - _buildBottomBar(BuildContext context) { - return CustomBottomBar(onChanged: (BottomBarEnum type) {}); - } - /// Common widget - Widget _buildColumncheckmark( - BuildContext context, { - required String imageOne, - required String time, - }) { - return Container( - padding: EdgeInsets.all(9.h), - decoration: BoxDecoration( - image: DecorationImage( - image: fs.Svg( - ImageConstant.imgGroup11, - ), - fit: BoxFit.cover, - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - CustomImageView( - imagePath: imageOne, - height: 186.v, - width: 254.h, - radius: BorderRadius.circular( - 8.h, - ), - margin: EdgeInsets.only(right: 1.h), - ), - SizedBox(height: 10.v), - Row( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - time, - style: theme.textTheme.bodySmall!.copyWith( - color: appTheme.blueA400, - ), - ), - CustomImageView( - imagePath: ImageConstant.imgCheckmarkBlueA400, - height: 7.v, - width: 14.h, - margin: EdgeInsets.only( - left: 3.h, - bottom: 4.v, - ), - ) - ], - ) - ], - ), - ); - } - - /// Common widget - Widget _buildRowcheckmark( - BuildContext context, { - required String time, - }) { - return Row( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - time, - style: theme.textTheme.bodySmall!.copyWith( - color: appTheme.blueA400, - ), - ), - CustomImageView( - imagePath: ImageConstant.imgCheckmarkBlueA400, - height: 7.v, - width: 14.h, - margin: EdgeInsets.only( - left: 2.h, - top: 2.v, - bottom: 3.v, - ), - ) - ], - ); - } ///Handling route based on bottom click actions String getCurrentRoute(BottomBarEnum type) { switch (type) { case BottomBarEnum.tf: - return AppRoutes.k1Page; + return AppRoutes.chatScreen; default: return "/"; } @@ -710,10 +178,10 @@ class ChatScreen extends StatelessWidget { String currentRoute, ) { switch (currentRoute) { - case AppRoutes.k1Page: - return K1Page.builder(context); + case AppRoutes.chatScreen: + return ChatScreen.builder(context); default: - return DefaultWidget(); + return ChatScreen(); } } @@ -722,4 +190,3 @@ class ChatScreen extends StatelessWidget { NavigatorService.goBack(); } } -*/ \ No newline at end of file diff --git a/lib/presentation/chat/chat_screen/models/chat_message_model.dart b/lib/presentation/chat/chat_screen/models/chat_message_model.dart new file mode 100644 index 00000000..e91596f3 --- /dev/null +++ b/lib/presentation/chat/chat_screen/models/chat_message_model.dart @@ -0,0 +1,36 @@ +import 'package:equatable/equatable.dart'; + +class ChatMessageModel extends Equatable { + final int id; + final String data; + final List mediaData; + final int authorId; + final String updatedAt; + + ChatMessageModel({ + required this.id, + required this.data, + required this.mediaData, + required this.authorId, + required this.updatedAt, + }); + + ChatMessageModel copyWith({ + int? id, + String? data, + List? mediaData, + int? authorId, + String? updatedAt, + }) { + return ChatMessageModel( + id: id ?? this.id, + data: data ?? this.data, + mediaData: mediaData ?? this.mediaData, + authorId: authorId ?? this.authorId, + updatedAt: updatedAt ?? this.updatedAt, + ); + } + + @override + List get props => [id, data, mediaData, authorId, updatedAt]; +} diff --git a/lib/presentation/chat/chat_screen/models/chat_model.dart b/lib/presentation/chat/chat_screen/models/chat_model.dart index 0e74b3ee..970b9ada 100644 --- a/lib/presentation/chat/chat_screen/models/chat_model.dart +++ b/lib/presentation/chat/chat_screen/models/chat_model.dart @@ -4,13 +4,16 @@ import 'package:equatable/equatable.dart'; /// This class defines the variables used in the [chat_screen], /// and is typically used to hold data that is passed between different parts of the application. -class ChatModel extends Equatable { - ChatModel(); - ChatModel copyWith() { - return ChatModel(); +class ChatModel extends Equatable { + final String chatID; + + ChatModel({required String chatID}) : chatID = chatID; + + ChatModel copyWith(String chatID) { + return ChatModel(chatID: chatID); } @override - List get props => []; + List get props => [chatID]; } diff --git a/lib/presentation/chat/chat_select_screen/bloc/chat_select_bloc.dart b/lib/presentation/chat/chat_select_screen/bloc/chat_select_bloc.dart index 740dbab7..f543c5aa 100644 --- a/lib/presentation/chat/chat_select_screen/bloc/chat_select_bloc.dart +++ b/lib/presentation/chat/chat_select_screen/bloc/chat_select_bloc.dart @@ -59,7 +59,6 @@ class ChatSelectBloc extends Bloc { ); if (response.statusCode == 200) { final data = jsonDecode(response.body); - print(data['chatRooms']); if (data is Map && data.containsKey('chatRooms')) { if (state is ChatSelectLoadedState) { var currentState = state as ChatSelectLoadedState; diff --git a/lib/presentation/chat/chat_select_screen/widgets/chat_entry_widget.dart b/lib/presentation/chat/chat_select_screen/widgets/chat_entry_widget.dart index a26b602b..52f9de3a 100644 --- a/lib/presentation/chat/chat_select_screen/widgets/chat_entry_widget.dart +++ b/lib/presentation/chat/chat_select_screen/widgets/chat_entry_widget.dart @@ -16,29 +16,35 @@ class ChatEntryWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - padding: EdgeInsets.all(8.h), - decoration: AppDecoration.fillGray100.copyWith( - borderRadius: BorderRadiusStyle.circleBorder8, + return GestureDetector( + onTap: () => Navigator.pushNamed( + context, + AppRoutes.chatScreen, + arguments: chatEntryObj.id.toString(), ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CachedNetworkImage( - imageUrl: dotenv.env['STORAGE_URL']! + chatEntryObj.userImage!, - imageBuilder: (context, imageProvider) => CircleAvatar( - backgroundImage: imageProvider, - radius: 25, - ), - placeholder: (context, url) => CircularProgressIndicator(), - errorWidget: (context, url, error) => CircleAvatar( - child: Icon(Icons.person), - backgroundColor: Colors.white, - radius: 25, - ), - ), - /*CustomImageView( + child: Container( + padding: EdgeInsets.all(8.h), + decoration: AppDecoration.fillGray100.copyWith( + borderRadius: BorderRadiusStyle.circleBorder8, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + chatEntryObj.userImage!, + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundImage: imageProvider, + radius: 25, + ), + placeholder: (context, url) => CircularProgressIndicator(), + errorWidget: (context, url, error) => CircleAvatar( + child: Icon(Icons.person), + backgroundColor: Colors.white, + radius: 25, + ), + ), + /*CustomImageView( imagePath: dotenv.env['STORAGE_URL']! + chatEntryObj.userImage!, height: 60.adaptSize, @@ -47,62 +53,57 @@ class ChatEntryWidget extends StatelessWidget { 30.h, ), ),*/ - Expanded( - child: Padding( - padding: EdgeInsets.only( - left: 12.h, - top: 2.v, - bottom: 11.v, - ), - child: GestureDetector( - onTap: () => Navigator.pushNamed( - context, - AppRoutes.chatScreen, - arguments: chatEntryObj.id, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 227.h, - child: Row( - children: [ - Text( - chatEntryObj.username!.length <= 15 - ? chatEntryObj.username! - : chatEntryObj.username!.substring(0, 15) + '...', - style: theme.textTheme.titleMedium, - ), - Spacer(), - CustomImageView( - imagePath: ImageConstant.imageNotFound, - height: 9.v, - width: 16.h, - margin: EdgeInsets.only( - top: 4.v, - bottom: 6.v, - ), - ), - Padding( - padding: EdgeInsets.only(left: 6.h), - child: Text( - chatEntryObj.dateText!, - style: theme.textTheme.bodyMedium, - ), - ) - ], - ), + Expanded( + child: Padding( + padding: EdgeInsets.only( + left: 12.h, + top: 2.v, + bottom: 11.v, ), - SizedBox(height: 6.v), - Text( - chatEntryObj.greetingText!, - style: CustomTextStyles.bodyMediumBluegray800, - ) - ], - ), - ), - )) - ], + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 227.h, + child: Row( + children: [ + Text( + chatEntryObj.username!.length <= 15 + ? chatEntryObj.username! + : chatEntryObj.username!.substring(0, 15) + + '...', + style: theme.textTheme.titleMedium, + ), + Spacer(), + CustomImageView( + imagePath: ImageConstant.imageNotFound, + height: 9.v, + width: 16.h, + margin: EdgeInsets.only( + top: 4.v, + bottom: 6.v, + ), + ), + Padding( + padding: EdgeInsets.only(left: 6.h), + child: Text( + chatEntryObj.dateText!, + style: theme.textTheme.bodyMedium, + ), + ) + ], + ), + ), + SizedBox(height: 6.v), + Text( + chatEntryObj.greetingText!, + style: CustomTextStyles.bodyMediumBluegray800, + ) + ], + ), + ), + ) + ]), ), ); } diff --git a/lib/presentation/diets/diet_create_screen/bloc/diet_create_bloc.dart b/lib/presentation/diets/diet_create_screen/bloc/diet_create_bloc.dart new file mode 100644 index 00000000..e517c1ac --- /dev/null +++ b/lib/presentation/diets/diet_create_screen/bloc/diet_create_bloc.dart @@ -0,0 +1,93 @@ +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:hive/hive.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/diets/diet_create_screen/models/diet_create_model.dart'; +part 'diet_create_event.dart'; +part 'diet_create_state.dart'; + +/// A bloc that manages the state of a DietCreate according to the event that is dispatched to it. +class DietCreateBloc extends Bloc { + DietCreateBloc(DietCreateState initialState) : super(initialState) { + on(_onInitialize); + on(_createDiet); + } + + _onInitialize( + DietCreateInitialEvent event, + Emitter emit, + ) async { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + if (state is DietCreateLoadedState) { + var currentState = state as DietCreateLoadedState; + emit(currentState.copyWith( + dietModelObj: currentState.dietModelObj, + name: currentState.name, + details: currentState.details, + )); + } else { + emit(DietCreateLoadedState( + name: TextEditingController(), + details: TextEditingController(), + )); + } + } + + FutureOr _createDiet( + CreateDiet event, Emitter emit) async { + if (state is DietCreateLoadedState) { + var currentState = state as DietCreateLoadedState; + + var response = await sendDiet( + currentState.name?.text, + currentState.details?.text, + ); + + if (response.statusCode == 200) { + NavigatorService.pushNamed(AppRoutes.dietSelectScreen); + } else { + print( + 'Failed to create blog post. Status Code: ${response.statusCode} ${response.body}'); + } + } + } + + FutureOr sendDiet( + String? name, + String? details, + ) async { + String url = '${dotenv.env['SERVER_ROOT']!}/diets/create'; + + final Map credentials = { + 'name': name, + 'details': details, + 'duration': '14', + 'state': 'active', + 'type': '{"meat": true}', + 'daily_schedule': + '{"1": {"lunch": [{"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "milliliters", "quantity": 494}, {"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "grams", "quantity": 12}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "pcs", "quantity": 432}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 195}], "dinner": [{"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "grams", "quantity": 710}, {"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "grams", "quantity": 119}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "milliliters", "quantity": 287}], "snacks": [{"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "pcs", "quantity": 445}, {"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "grams", "quantity": 500}], "supper": [{"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "pcs", "quantity": 764}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "pcs", "quantity": 735}], "breakfast": [{"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "grams", "quantity": 127}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "pcs", "quantity": 89}]}, "2": {"lunch": [{"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "pcs", "quantity": 436}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "pcs", "quantity": 700}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "pcs", "quantity": 766}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "grams", "quantity": 194}], "dinner": [{"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "pcs", "quantity": 426}], "snacks": [{"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "grams", "quantity": 58}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "milliliters", "quantity": 152}, {"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "grams", "quantity": 925}], "supper": [{"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "pcs", "quantity": 224}], "breakfast": [{"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 918}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "grams", "quantity": 81}, {"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "pcs", "quantity": 308}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "grams", "quantity": 579}]}, "3": {"lunch": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "milliliters", "quantity": 266}], "dinner": [{"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "pcs", "quantity": 790}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "pcs", "quantity": 26}], "snacks": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "grams", "quantity": 549}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "milliliters", "quantity": 446}, {"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "grams", "quantity": 406}], "supper": [{"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "milliliters", "quantity": 92}], "breakfast": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "milliliters", "quantity": 403}, {"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "pcs", "quantity": 894}, {"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "milliliters", "quantity": 457}]}, "4": {"lunch": [{"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "milliliters", "quantity": 698}, {"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "milliliters", "quantity": 43}, {"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "grams", "quantity": 994}, {"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "pcs", "quantity": 965}], "dinner": [{"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "grams", "quantity": 42}, {"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "pcs", "quantity": 948}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "pcs", "quantity": 28}, {"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "grams", "quantity": 54}], "snacks": [{"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "grams", "quantity": 112}], "supper": [{"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "pcs", "quantity": 891}], "breakfast": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "pcs", "quantity": 538}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "pcs", "quantity": 635}, {"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "pcs", "quantity": 586}]}, "5": {"lunch": [{"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "milliliters", "quantity": 900}, {"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "milliliters", "quantity": 242}], "dinner": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "milliliters", "quantity": 375}], "snacks": [{"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 604}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "pcs", "quantity": 510}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "grams", "quantity": 887}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "grams", "quantity": 554}], "supper": [{"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "grams", "quantity": 731}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "pcs", "quantity": 574}, {"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "grams", "quantity": 274}], "breakfast": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "milliliters", "quantity": 276}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "milliliters", "quantity": 80}]}, "6": {"lunch": [{"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "milliliters", "quantity": 359}, {"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "pcs", "quantity": 358}, {"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "grams", "quantity": 639}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 861}], "dinner": [{"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "grams", "quantity": 124}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "grams", "quantity": 122}], "snacks": [{"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "grams", "quantity": 980}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 837}, {"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "grams", "quantity": 444}], "supper": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "milliliters", "quantity": 577}, {"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "milliliters", "quantity": 706}], "breakfast": [{"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "milliliters", "quantity": 839}]}, "7": {"lunch": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "grams", "quantity": 118}, {"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "pcs", "quantity": 664}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "grams", "quantity": 665}], "dinner": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "grams", "quantity": 986}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 296}], "snacks": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "grams", "quantity": 807}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "pcs", "quantity": 493}], "supper": [{"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "milliliters", "quantity": 227}, {"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "pcs", "quantity": 385}, {"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "grams", "quantity": 448}], "breakfast": [{"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "grams", "quantity": 784}]}, "8": {"lunch": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "milliliters", "quantity": 433}, {"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "milliliters", "quantity": 871}], "dinner": [{"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 332}, {"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "milliliters", "quantity": 41}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "grams", "quantity": 388}], "snacks": [{"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "pcs", "quantity": 649}], "supper": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "pcs", "quantity": 114}, {"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "milliliters", "quantity": 968}], "breakfast": [{"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "milliliters", "quantity": 180}, {"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "milliliters", "quantity": 269}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "milliliters", "quantity": 998}]}, "9": {"lunch": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "grams", "quantity": 68}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 505}, {"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "grams", "quantity": 554}], "dinner": [{"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "grams", "quantity": 579}], "snacks": [{"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "grams", "quantity": 751}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "grams", "quantity": 144}], "supper": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "grams", "quantity": 677}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "milliliters", "quantity": 263}, {"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "pcs", "quantity": 304}], "breakfast": [{"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "milliliters", "quantity": 963}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "milliliters", "quantity": 258}]}, "10": {"lunch": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "milliliters", "quantity": 167}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 306}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "grams", "quantity": 243}], "dinner": [{"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "milliliters", "quantity": 79}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "grams", "quantity": 560}], "snacks": [{"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "milliliters", "quantity": 294}, {"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "milliliters", "quantity": 145}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "milliliters", "quantity": 639}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "pcs", "quantity": 108}], "supper": [{"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "pcs", "quantity": 924}, {"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "grams", "quantity": 894}], "breakfast": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "grams", "quantity": 280}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 336}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "milliliters", "quantity": 338}]}, "11": {"lunch": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "grams", "quantity": 817}, {"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "pcs", "quantity": 968}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "milliliters", "quantity": 525}, {"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "milliliters", "quantity": 864}], "dinner": [{"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "pcs", "quantity": 457}, {"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "milliliters", "quantity": 548}], "snacks": [{"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "milliliters", "quantity": 658}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "grams", "quantity": 239}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 358}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "grams", "quantity": 406}], "supper": [{"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "grams", "quantity": 416}], "breakfast": [{"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 220}, {"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "grams", "quantity": 323}]}, "12": {"lunch": [{"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "milliliters", "quantity": 377}], "dinner": [{"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "milliliters", "quantity": 138}], "snacks": [{"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "milliliters", "quantity": 654}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 237}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "pcs", "quantity": 902}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 76}], "supper": [{"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "milliliters", "quantity": 723}, {"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "pcs", "quantity": 481}], "breakfast": [{"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "pcs", "quantity": 571}, {"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "pcs", "quantity": 515}, {"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "pcs", "quantity": 188}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "pcs", "quantity": 411}]}, "13": {"lunch": [{"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "grams", "quantity": 379}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "grams", "quantity": 231}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "pcs", "quantity": 376}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "grams", "quantity": 147}], "dinner": [{"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "pcs", "quantity": 871}, {"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "grams", "quantity": 679}, {"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "pcs", "quantity": 359}, {"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "milliliters", "quantity": 561}], "snacks": [{"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "pcs", "quantity": 414}], "supper": [{"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 827}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 649}, {"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "pcs", "quantity": 363}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "pcs", "quantity": 161}], "breakfast": [{"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 185}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "pcs", "quantity": 464}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 387}]}, "14": {"lunch": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 248}, {"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "grams", "quantity": 5}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 693}, {"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "grams", "quantity": 655}], "dinner": [{"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "pcs", "quantity": 709}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "grams", "quantity": 881}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "milliliters", "quantity": 588}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 36}], "snacks": [{"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "pcs", "quantity": 453}], "supper": [{"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "grams", "quantity": 692}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "pcs", "quantity": 186}, {"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "grams", "quantity": 721}], "breakfast": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 740}, {"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "milliliters", "quantity": 731}, {"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "milliliters", "quantity": 842}, {"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "grams", "quantity": 850}]}, "15": {"lunch": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "grams", "quantity": 124}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "pcs", "quantity": 380}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "pcs", "quantity": 211}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 689}], "dinner": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "milliliters", "quantity": 326}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "milliliters", "quantity": 629}], "snacks": [{"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "milliliters", "quantity": 201}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "grams", "quantity": 129}, {"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "milliliters", "quantity": 749}], "supper": [{"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "milliliters", "quantity": 315}], "breakfast": [{"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "grams", "quantity": 444}]}, "16": {"lunch": [{"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "grams", "quantity": 953}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "pcs", "quantity": 101}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 684}, {"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "pcs", "quantity": 506}], "dinner": [{"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "pcs", "quantity": 333}, {"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "milliliters", "quantity": 416}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "milliliters", "quantity": 796}], "snacks": [{"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "pcs", "quantity": 799}, {"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "pcs", "quantity": 799}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "pcs", "quantity": 756}], "supper": [{"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "grams", "quantity": 858}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "grams", "quantity": 619}, {"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "milliliters", "quantity": 800}], "breakfast": [{"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "pcs", "quantity": 363}, {"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "grams", "quantity": 539}]}, "17": {"lunch": [{"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "grams", "quantity": 134}], "dinner": [{"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "milliliters", "quantity": 977}], "snacks": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "pcs", "quantity": 211}], "supper": [{"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "pcs", "quantity": 209}, {"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "grams", "quantity": 227}], "breakfast": [{"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "grams", "quantity": 195}, {"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "pcs", "quantity": 917}, {"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "grams", "quantity": 706}]}, "18": {"lunch": [{"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "pcs", "quantity": 296}, {"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "milliliters", "quantity": 239}, {"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "grams", "quantity": 798}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 155}], "dinner": [{"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "grams", "quantity": 959}, {"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "pcs", "quantity": 958}], "snacks": [{"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "milliliters", "quantity": 984}], "supper": [{"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "pcs", "quantity": 134}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 432}], "breakfast": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "milliliters", "quantity": 242}]}, "19": {"lunch": [{"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 906}, {"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "milliliters", "quantity": 152}], "dinner": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "milliliters", "quantity": 610}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 605}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "grams", "quantity": 498}], "snacks": [{"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "grams", "quantity": 616}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 512}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "grams", "quantity": 146}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "pcs", "quantity": 200}], "supper": [{"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 764}, {"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "grams", "quantity": 353}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 181}], "breakfast": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 783}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "grams", "quantity": 506}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 887}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "milliliters", "quantity": 130}]}, "20": {"lunch": [{"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "grams", "quantity": 48}], "dinner": [{"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "milliliters", "quantity": 266}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "grams", "quantity": 143}], "snacks": [{"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "grams", "quantity": 55}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 174}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "grams", "quantity": 855}, {"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "milliliters", "quantity": 6}], "supper": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 296}, {"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "grams", "quantity": 878}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "pcs", "quantity": 376}], "breakfast": [{"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "milliliters", "quantity": 442}, {"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "pcs", "quantity": 528}, {"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "milliliters", "quantity": 558}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "pcs", "quantity": 885}]}, "21": {"lunch": [{"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "grams", "quantity": 504}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 652}, {"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "grams", "quantity": 996}], "dinner": [{"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "milliliters", "quantity": 200}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "pcs", "quantity": 641}, {"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "milliliters", "quantity": 87}, {"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "pcs", "quantity": 689}], "snacks": [{"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "grams", "quantity": 143}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "pcs", "quantity": 528}, {"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "grams", "quantity": 475}], "supper": [{"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "grams", "quantity": 924}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "grams", "quantity": 98}], "breakfast": [{"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "grams", "quantity": 438}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "milliliters", "quantity": 383}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "grams", "quantity": 462}]}}' + }; + + var response = await http.post( + Uri.parse(url), + headers: { + 'Content-Type': 'application/json', + }, + body: json.encode(credentials), + encoding: utf8, + ); + + return response; + } +} diff --git a/lib/presentation/diets/diet_create_screen/bloc/diet_create_event.dart b/lib/presentation/diets/diet_create_screen/bloc/diet_create_event.dart new file mode 100644 index 00000000..a346af77 --- /dev/null +++ b/lib/presentation/diets/diet_create_screen/bloc/diet_create_event.dart @@ -0,0 +1,21 @@ +// ignore_for_file: must_be_immutable + +part of 'diet_create_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///DietCreate widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class DietCreateEvent extends Equatable {} + +/// Event that is dispatched when the DietCreate widget is first created. +class DietCreateInitialEvent extends DietCreateEvent { + @override + List get props => []; +} + +class CreateDiet extends DietCreateEvent { + @override + List get props => []; +} diff --git a/lib/presentation/diets/diet_create_screen/bloc/diet_create_state.dart b/lib/presentation/diets/diet_create_screen/bloc/diet_create_state.dart new file mode 100644 index 00000000..be825abe --- /dev/null +++ b/lib/presentation/diets/diet_create_screen/bloc/diet_create_state.dart @@ -0,0 +1,65 @@ +// ignore_for_file: must_be_immutable + +part of 'diet_create_bloc.dart'; + +/// Represents the state of DietCreate in the application. +/// + +class DietCreateState extends Equatable { + DietCreateState({ + this.dietModelObj, + }); + + DietCreateModel? dietModelObj; + + @override + List get props => [dietModelObj]; + DietCreateState copyWith({DietCreateModel? dietModelObj}) { + return DietCreateState( + dietModelObj: dietModelObj ?? this.dietModelObj, + ); + } +} + +class DietCreateLoadedState extends DietCreateState { + DietCreateLoadedState({ + this.dietModelObj, + this.name, + this.details, + }); + + DietCreateModel? dietModelObj; + TextEditingController? name; + TextEditingController? details; + @override + List get props => [ + dietModelObj, + name, + details, + ]; + DietCreateLoadedState copyWith({ + DietCreateModel? dietModelObj, + TextEditingController? name, + TextEditingController? details, + }) { + return DietCreateLoadedState( + dietModelObj: dietModelObj ?? this.dietModelObj, + name: name ?? this.name, + details: details ?? this.details, + ); + } +} + +class DietCreateErrorState extends DietCreateState { + final String errorMessage; + + DietCreateErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} + +class DietCreateInitialState extends DietCreateState { + DietCreateInitialState({required DietCreateModel dietModelObj}) + : super(dietModelObj: dietModelObj); +} diff --git a/lib/presentation/diets/diet_create_screen/diet_create_screen.dart b/lib/presentation/diets/diet_create_screen/diet_create_screen.dart new file mode 100644 index 00000000..6896701c --- /dev/null +++ b/lib/presentation/diets/diet_create_screen/diet_create_screen.dart @@ -0,0 +1,97 @@ +import 'bloc/diet_create_bloc.dart'; +import 'models/diet_create_model.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:flutter/material.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; + +class DietCreateScreen extends StatelessWidget { + const DietCreateScreen({Key? key}) : super(key: key); + static Widget builder(BuildContext context) { + final dietID = ModalRoute.of(context)!.settings.arguments; + return BlocProvider( + create: (context) => DietCreateBloc(DietCreateState( + dietModelObj: DietCreateModel(dietID: dietID.toString()))) + ..add(DietCreateInitialEvent()), + child: DietCreateScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is DietCreateInitialState) { + // Это начальное состояние, данные ещё не загружены. + return Container( + color: Colors.white, + ); + } else if (state is DietCreateLoadedState) { + // Данные загружены, можно строить экран. + return _buildDietCreate(context, state); + } else if (state is DietCreateErrorState) { + // Произошла ошибка при загрузке данных. + return Center(child: Text(state.errorMessage)); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildDietCreate(BuildContext context, DietCreateLoadedState state) { + mediaQueryData = MediaQuery.of(context); + return SafeArea( + child: Scaffold( + resizeToAvoidBottomInset: false, + body: Container( + width: double.maxFinite, + decoration: AppDecoration.fillGray, + child: ListView(children: [ + SizedBox(height: 30.v), + Container( + margin: EdgeInsets.only( + left: 16.h, + right: 20.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + SizedBox(height: 16.0), + TextFormField( + controller: state.name, + decoration: InputDecoration(labelText: 'Заголовок'), + ), + SizedBox(height: 16.0), + TextFormField( + controller: state.details, + decoration: InputDecoration(labelText: 'Описание'), + ), + SizedBox(height: 16.0), + // Здесь нужно добавить виджет для загрузки картинки + ElevatedButton( + onPressed: () => + context.read().add(CreateDiet()), + child: Text('Создать'), + ) + ]), + ) + ]), + ), + bottomNavigationBar: _buildBottomBar(context))); + } +} + +_buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); +} diff --git a/lib/presentation/diets/diet_create_screen/models/diet_create_model.dart b/lib/presentation/diets/diet_create_screen/models/diet_create_model.dart new file mode 100644 index 00000000..955e8d63 --- /dev/null +++ b/lib/presentation/diets/diet_create_screen/models/diet_create_model.dart @@ -0,0 +1,19 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; + +/// This class defines the variables used in the [diet_create_screen], +/// and is typically used to hold data that is passed between different parts of the application. + +class DietCreateModel extends Equatable { + final String dietID; + + DietCreateModel({required String dietID}) : dietID = dietID; + + DietCreateModel copyWith(String dietID) { + return DietCreateModel(dietID: dietID); + } + + @override + List get props => [dietID]; +} diff --git a/lib/presentation/diets/diet_edit_screen/bloc/diet_edit_bloc.dart b/lib/presentation/diets/diet_edit_screen/bloc/diet_edit_bloc.dart new file mode 100644 index 00000000..d8167793 --- /dev/null +++ b/lib/presentation/diets/diet_edit_screen/bloc/diet_edit_bloc.dart @@ -0,0 +1,117 @@ +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:hive/hive.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/diets/diet_edit_screen/models/diet_edit_model.dart'; +part 'diet_edit_event.dart'; +part 'diet_edit_state.dart'; + +/// A bloc that manages the state of a DietEdit according to the event that is dispatched to it. +class DietEditBloc extends Bloc { + DietEditBloc(DietEditState initialState) : super(initialState) { + on(_onInitialize); + on(_editDiet); + } + + _onInitialize( + DietEditInitialEvent event, + Emitter emit, + ) async { + try { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + final String? dietID = state.dietModelObj?.dietID; + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/diets/show/$dietID'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + if (data is Map) { + if (state is DietEditLoadedState) { + var currentState = state as DietEditLoadedState; + emit(currentState.copyWith( + dietModelObj: currentState.dietModelObj, + diet: currentState.diet, + name: currentState.name, + duration: currentState.duration, + details: currentState.details)); + } else { + emit(DietEditLoadedState( + diet: data, + name: TextEditingController(text: data['name']), + details: TextEditingController(text: data['details']), + duration: TextEditingController(text: data['duration']))); + } + } else { + // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку + emit(DietEditErrorState('Некоторые необходимые данные отсутствуют.')); + } + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(DietEditErrorState('Ошибка сервера: ${response.statusCode}')); + } + } catch (e) { + // Ошибка при декодировании JSON или другая ошибка выполнения + emit(DietEditErrorState('Ошибка при получении данных о чатах: $e')); + } + } + + FutureOr _editDiet(EditDiet event, Emitter emit) async { + if (state is DietEditLoadedState) { + var currentState = state as DietEditLoadedState; + int duration = int.parse(currentState.duration!.text) * 7; + print(duration); + var response = await sendDiet(currentState.diet, currentState.name?.text, + currentState.details?.text, duration.toString()); + + if (response.statusCode == 200) { + NavigatorService.pushNamed(AppRoutes.dietScreen, + arguments: currentState.diet?['id'].toString()); + } else { + print( + 'Failed to edit blog post. Status Code: ${response.statusCode} ${response.body}'); + } + } + } + + FutureOr sendDiet( + Map? diet, + String? name, + String? details, + String? duration, + ) async { + int id = diet?['id']; + String url = '${dotenv.env['SERVER_ROOT']!}/diets/update/$id'; + + final Map credentials = { + 'id': id, + 'name': name, + 'details': details, + 'duration': '14', + }; + + var response = await http.post( + Uri.parse(url), + headers: { + 'Content-Type': 'application/json', + }, + body: json.encode(credentials), + encoding: utf8, + ); + + return response; + } +} diff --git a/lib/presentation/diets/diet_edit_screen/bloc/diet_edit_event.dart b/lib/presentation/diets/diet_edit_screen/bloc/diet_edit_event.dart new file mode 100644 index 00000000..b2184444 --- /dev/null +++ b/lib/presentation/diets/diet_edit_screen/bloc/diet_edit_event.dart @@ -0,0 +1,21 @@ +// ignore_for_file: must_be_immutable + +part of 'diet_edit_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///DietEdit widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class DietEditEvent extends Equatable {} + +/// Event that is dispatched when the DietEdit widget is first created. +class DietEditInitialEvent extends DietEditEvent { + @override + List get props => []; +} + +class EditDiet extends DietEditEvent { + @override + List get props => []; +} diff --git a/lib/presentation/diets/diet_edit_screen/bloc/diet_edit_state.dart b/lib/presentation/diets/diet_edit_screen/bloc/diet_edit_state.dart new file mode 100644 index 00000000..1c10a404 --- /dev/null +++ b/lib/presentation/diets/diet_edit_screen/bloc/diet_edit_state.dart @@ -0,0 +1,64 @@ +// ignore_for_file: must_be_immutable + +part of 'diet_edit_bloc.dart'; + +/// Represents the state of DietEdit in the application. +/// + +class DietEditState extends Equatable { + DietEditState({ + this.dietModelObj, + }); + + DietEditModel? dietModelObj; + + @override + List get props => [dietModelObj]; + DietEditState copyWith({DietEditModel? dietModelObj}) { + return DietEditState( + dietModelObj: dietModelObj ?? this.dietModelObj, + ); + } +} + +class DietEditLoadedState extends DietEditState { + DietEditLoadedState( + {this.dietModelObj, this.diet, this.name, this.details, this.duration}); + + DietEditModel? dietModelObj; + Map? diet; + TextEditingController? name; + TextEditingController? details; + TextEditingController? duration; + @override + List get props => [dietModelObj, diet, name, details, duration]; + DietEditLoadedState copyWith({ + DietEditModel? dietModelObj, + Map? diet, + TextEditingController? name, + TextEditingController? details, + TextEditingController? duration, + }) { + return DietEditLoadedState( + dietModelObj: dietModelObj ?? this.dietModelObj, + diet: diet ?? this.diet, + name: name ?? this.name, + details: details ?? this.details, + duration: duration ?? this.duration, + ); + } +} + +class DietEditErrorState extends DietEditState { + final String errorMessage; + + DietEditErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} + +class DietEditInitialState extends DietEditState { + DietEditInitialState({required DietEditModel dietModelObj}) + : super(dietModelObj: dietModelObj); +} diff --git a/lib/presentation/diets/diet_edit_screen/diet_edit_screen.dart b/lib/presentation/diets/diet_edit_screen/diet_edit_screen.dart new file mode 100644 index 00000000..75f02810 --- /dev/null +++ b/lib/presentation/diets/diet_edit_screen/diet_edit_screen.dart @@ -0,0 +1,114 @@ +import 'bloc/diet_edit_bloc.dart'; +import 'models/diet_edit_model.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:flutter/material.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +class DietEditScreen extends StatelessWidget { + const DietEditScreen({Key? key}) : super(key: key); + static Widget builder(BuildContext context) { + final dietID = ModalRoute.of(context)!.settings.arguments; + return BlocProvider( + create: (context) => DietEditBloc(DietEditState( + dietModelObj: DietEditModel(dietID: dietID.toString()))) + ..add(DietEditInitialEvent()), + child: DietEditScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is DietEditInitialState) { + // Это начальное состояние, данные ещё не загружены. + return Container( + color: Colors.white, + ); + } else if (state is DietEditLoadedState) { + // Данные загружены, можно строить экран. + return _buildDietEdit(context, state); + } else if (state is DietEditErrorState) { + // Произошла ошибка при загрузке данных. + return Center(child: Text(state.errorMessage)); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildDietEdit(BuildContext context, DietEditLoadedState state) { + mediaQueryData = MediaQuery.of(context); + return SafeArea( + child: Scaffold( + resizeToAvoidBottomInset: false, + body: Container( + width: double.maxFinite, + decoration: AppDecoration.fillGray, + child: ListView(children: [ + SizedBox(height: 30.v), + Container( + margin: EdgeInsets.only( + left: 16.h, + right: 20.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + + (state.diet?['image'] ?? ''), + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundImage: imageProvider, + radius: 25, + ), + placeholder: (context, url) => + CircularProgressIndicator(), + errorWidget: (context, url, error) => CircleAvatar( + child: Icon(Icons.person), + backgroundColor: Colors.white, + radius: 25, + ), + ), + SizedBox(height: 16.0), + TextFormField( + controller: state.name, + decoration: InputDecoration(labelText: 'Заголовок'), + ), + SizedBox(height: 16.0), + TextFormField( + controller: state.details, + decoration: InputDecoration(labelText: 'Описание'), + ), + SizedBox(height: 16.0), + // Здесь нужно добавить виджет для загрузки картинки + ElevatedButton( + onPressed: () => + context.read().add(EditDiet()), + child: Text('Сохранить'), + ) + ]), + ) + ]), + ), + bottomNavigationBar: _buildBottomBar(context))); + } +} + +_buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); +} diff --git a/lib/presentation/diets/diet_edit_screen/models/diet_edit_model.dart b/lib/presentation/diets/diet_edit_screen/models/diet_edit_model.dart new file mode 100644 index 00000000..8c02d1b1 --- /dev/null +++ b/lib/presentation/diets/diet_edit_screen/models/diet_edit_model.dart @@ -0,0 +1,19 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; + +/// This class defines the variables used in the [diet_edit_screen], +/// and is typically used to hold data that is passed between different parts of the application. + +class DietEditModel extends Equatable { + final String dietID; + + DietEditModel({required String dietID}) : dietID = dietID; + + DietEditModel copyWith(String dietID) { + return DietEditModel(dietID: dietID); + } + + @override + List get props => [dietID]; +} diff --git a/lib/presentation/diets/diet_select_screen/bloc/diet_select_bloc.dart b/lib/presentation/diets/diet_select_screen/bloc/diet_select_bloc.dart new file mode 100644 index 00000000..49def2d1 --- /dev/null +++ b/lib/presentation/diets/diet_select_screen/bloc/diet_select_bloc.dart @@ -0,0 +1,73 @@ +//import 'package:eatvio_app/presentation/auth/auth_screen/auth_screen.dart'; +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/diets/diet_select_screen/models/diet_select_model.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import 'package:hive/hive.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:image_picker/image_picker.dart'; + +part 'diet_select_event.dart'; +part 'diet_select_state.dart'; + +/// A bloc that manages the state of a DietSelect according to the event that is dispatched to it. +class DietSelectBloc extends Bloc { + DietSelectBloc(DietSelectState initialState) : super(initialState) { + on(_onInitialize); + } + + Future _onInitialize( + DietSelectInitialEvent event, + Emitter emit, + ) async { + try { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); // Если токен есть, запускаем приложение + } + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/diets/index'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + print('Got response'); + if (response.statusCode == 200) { + print('Got 200'); + final data = jsonDecode(response.body); + if (data is List) { + print('Got List'); + if (state is DietSelectLoadedState) { + print('Got loaded'); + var currentState = state as DietSelectLoadedState; + emit((currentState).copyWith( + posts: data, + myProfileModelObj: currentState.myProfileModelObj, + )); + } else { + print('Got unloaded'); + emit(DietSelectLoadedState( + posts: data, + )); + } + } else { + // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку + emit(DietSelectErrorState('Неверный формат ответа API.')); + } + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(DietSelectErrorState('Ошибка сервера: ${response.statusCode}')); + } + } catch (e) { + // Ошибка при декодировании JSON или другая ошибка выполнения + emit(DietSelectErrorState('Ошибка при получении данных ленты: $e')); + } + } +} diff --git a/lib/presentation/diets/diet_select_screen/bloc/diet_select_event.dart b/lib/presentation/diets/diet_select_screen/bloc/diet_select_event.dart new file mode 100644 index 00000000..91372953 --- /dev/null +++ b/lib/presentation/diets/diet_select_screen/bloc/diet_select_event.dart @@ -0,0 +1,23 @@ +// ignore_for_file: must_be_immutable + +part of 'diet_select_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///DietSelect widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class DietSelectEvent extends Equatable {} + +/// Event that is dispatched when the DietSelect widget is first created. +class DietSelectInitialEvent extends DietSelectEvent { + @override + List get props => []; +} + +class UploadAvatar extends DietSelectEvent { + UploadAvatar(this.image); + final XFile image; + @override + List get props => [image]; +} diff --git a/lib/presentation/diets/diet_select_screen/bloc/diet_select_state.dart b/lib/presentation/diets/diet_select_screen/bloc/diet_select_state.dart new file mode 100644 index 00000000..e7b709db --- /dev/null +++ b/lib/presentation/diets/diet_select_screen/bloc/diet_select_state.dart @@ -0,0 +1,49 @@ +// ignore_for_file: must_be_immutable + +part of 'diet_select_bloc.dart'; + +class DietSelectState extends Equatable { + const DietSelectState({this.myProfileModelObj}); + + final DietSelectModel? myProfileModelObj; + + @override + List get props => [myProfileModelObj]; +} + +class DietSelectInitialState extends DietSelectState { + DietSelectInitialState({required DietSelectModel myProfileModelObj}) + : super(myProfileModelObj: myProfileModelObj); +} + +/// Represents the state of DietSelect in the application. +class DietSelectLoadedState extends DietSelectState { + DietSelectLoadedState({this.myProfileModelObj, this.posts}) + : super(myProfileModelObj: null); + + DietSelectModel? myProfileModelObj; + List? posts; + @override + List get props => [ + myProfileModelObj, + posts, + ]; + DietSelectLoadedState copyWith({ + DietSelectModel? myProfileModelObj, + List? posts, + }) { + return DietSelectLoadedState( + myProfileModelObj: myProfileModelObj ?? this.myProfileModelObj, + posts: posts ?? this.posts, + ); + } +} + +class DietSelectErrorState extends DietSelectState { + final String errorMessage; + + const DietSelectErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} diff --git a/lib/presentation/diets/diet_select_screen/diet_select_screen.dart b/lib/presentation/diets/diet_select_screen/diet_select_screen.dart new file mode 100644 index 00000000..c44308d8 --- /dev/null +++ b/lib/presentation/diets/diet_select_screen/diet_select_screen.dart @@ -0,0 +1,184 @@ +import 'bloc/diet_select_bloc.dart'; +import 'models/diet_select_model.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:eatvio_app/widgets/app_bar/appbar_subtitle.dart'; +import 'package:eatvio_app/widgets/app_bar/appbar_leading_image.dart'; +import 'package:eatvio_app/widgets/app_bar/custom_app_bar.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter/material.dart'; + +class DietSelectScreen extends StatelessWidget { + static Widget builder(BuildContext context) { + return BlocProvider( + create: (context) => DietSelectBloc( + DietSelectState(myProfileModelObj: DietSelectModel())) + ..add(DietSelectInitialEvent()), + child: DietSelectScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is DietSelectInitialState) { + // Это начальное состояние, данные ещё не загружены. + return CircularProgressIndicator( + color: Colors.white, + backgroundColor: Colors.white, + ); + } else if (state is DietSelectLoadedState) { + // Данные загружены, можно строить экран. + return _buildSetingsScreen(context, state); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildSetingsScreen(BuildContext context, DietSelectLoadedState state) { + mediaQueryData = MediaQuery.of(context); + return SafeArea( + child: Scaffold( + backgroundColor: Colors.grey[200], + resizeToAvoidBottomInset: true, + body: SingleChildScrollView( + child: Container( + width: double.maxFinite, + decoration: BoxDecoration(color: Colors.grey[200]), + child: Padding( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox(height: 10), + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: CustomAppBar( + height: 54.v, + leadingWidth: 28.h, + leading: AppbarLeadingImage( + imagePath: ImageConstant.imgArrowLeft, + margin: EdgeInsets.only( + left: 16.h, top: 15.v, bottom: 15.v), + onTap: onTapBack(context), + ), + title: Padding( + padding: EdgeInsets.only(left: 20.h), + child: Row( + children: [ + AppbarSubtitle( + text: "Диеты", + margin: EdgeInsets.only( + left: 80.h, top: 12.v, bottom: 9.v), + ), + Spacer(), + Padding( + padding: const EdgeInsets.only(right: 20.0), + child: GestureDetector( + onTap: () => Navigator.pushNamed( + context, + AppRoutes.dietCreateScreen, + ), + child: Icon(Icons.create_outlined)), + ) + ], + ), + ), + ), + ), + SizedBox(height: 20), + _buildDietList(context, state) + ], + ), + ), + ), + ), + bottomNavigationBar: _buildBottomBar(context), + ), + ); + } + + _buildDietList(BuildContext context, DietSelectLoadedState state) { + return ListView.builder( + shrinkWrap: true, // Add this line + itemCount: state.posts?.length ?? 0, + itemBuilder: (context, index) { + final post = state.posts?[index]; + return Container( + margin: EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: ListTile( + leading: CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + post['image'], + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundImage: imageProvider, + radius: 25, + ), + placeholder: (context, url) => CircularProgressIndicator(), + errorWidget: (context, url, error) => CircleAvatar( + child: Icon(Icons.person), + backgroundColor: Colors.white, + radius: 25, + ), + ), + title: Text( + post?['name'] ?? '', + style: CustomTextStyles.titleMediumSemiBold, + ), + subtitle: Text(post?['details'] ?? ''), + onTap: () => Navigator.pushNamed( + context, + AppRoutes.dietScreen, + arguments: post?['id'].toString(), + ), + trailing: GestureDetector( + onTap: () => Navigator.pushNamed( + context, + AppRoutes.dietEditScreen, + arguments: post?['id'].toString(), + ), + child: Icon(Icons.edit)), + )); + }, + ); + } + + Function onTapBack(BuildContext context) { + return () { + onTapArrowLeft(context); + }; + } + + _buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); + } + + onTapArrowLeft(BuildContext context) { + NavigatorService.goBack(); + } +} diff --git a/lib/presentation/diets/diet_select_screen/models/diet_select_model.dart b/lib/presentation/diets/diet_select_screen/models/diet_select_model.dart new file mode 100644 index 00000000..5dcaa802 --- /dev/null +++ b/lib/presentation/diets/diet_select_screen/models/diet_select_model.dart @@ -0,0 +1,16 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; + +/// This class defines the variables used in the [k0_page], +/// and is typically used to hold data that is passed between different parts of the application. +class DietSelectModel extends Equatable { + DietSelectModel(); + + DietSelectModel copyWith() { + return DietSelectModel(); + } + + @override + List get props => []; +} diff --git a/lib/presentation/diets/diet_show_screen/bloc/diet_show_bloc.dart b/lib/presentation/diets/diet_show_screen/bloc/diet_show_bloc.dart new file mode 100644 index 00000000..a1216bd0 --- /dev/null +++ b/lib/presentation/diets/diet_show_screen/bloc/diet_show_bloc.dart @@ -0,0 +1,88 @@ +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:hive/hive.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/diets/diet_show_screen/models/diet_show_model.dart'; +part 'diet_show_event.dart'; +part 'diet_show_state.dart'; + +/// A bloc that manages the state of a Diet according to the event that is dispatched to it. +class DietBloc extends Bloc { + DietBloc(DietState initialState) : super(initialState) { + on(_onInitialize); + on(_deleteEntry); + } + + _onInitialize( + DietInitialEvent event, + Emitter emit, + ) async { + try { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + final String? dietID = state.dietModelObj?.dietID; + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/diets/show/$dietID'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + if (data is Map) { + if (state is DietLoadedState) { + var currentState = state as DietLoadedState; + emit(currentState.copyWith( + dietModelObj: currentState.dietModelObj, + diet: currentState.diet)); + } else { + emit(DietLoadedState(diet: data)); + } + } else { + // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку + emit(DietErrorState('Некоторые необходимые данные отсутствуют.')); + } + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(DietErrorState('Ошибка сервера: ${response.statusCode}')); + } + } catch (e) { + // Ошибка при декодировании JSON или другая ошибка выполнения + emit(DietErrorState('Ошибка при получении данных о чатах: $e')); + } + } + + FutureOr _deleteEntry( + DeleteEntry event, Emitter emit) async { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + final int id = event.id; + final response = await http.post( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/diets/delete/$id'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + if (response.statusCode == 200) { + NavigatorService.pushNamed(AppRoutes.dietSelectScreen); + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(DietErrorState('Ошибка: ${response.statusCode}')); + } + } +} diff --git a/lib/presentation/diets/diet_show_screen/bloc/diet_show_event.dart b/lib/presentation/diets/diet_show_screen/bloc/diet_show_event.dart new file mode 100644 index 00000000..6a131d5c --- /dev/null +++ b/lib/presentation/diets/diet_show_screen/bloc/diet_show_event.dart @@ -0,0 +1,23 @@ +// ignore_for_file: must_be_immutable + +part of 'diet_show_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///Diet widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class DietEvent extends Equatable {} + +/// Event that is dispatched when the Diet widget is first created. +class DietInitialEvent extends DietEvent { + @override + List get props => []; +} + +class DeleteEntry extends DietEvent { + DeleteEntry(this.id); + final int id; + @override + List get props => [id]; +} diff --git a/lib/presentation/diets/diet_show_screen/bloc/diet_show_state.dart b/lib/presentation/diets/diet_show_screen/bloc/diet_show_state.dart new file mode 100644 index 00000000..49913f7f --- /dev/null +++ b/lib/presentation/diets/diet_show_screen/bloc/diet_show_state.dart @@ -0,0 +1,52 @@ +// ignore_for_file: must_be_immutable + +part of 'diet_show_bloc.dart'; + +/// Represents the state of Diet in the application. +/// + +class DietState extends Equatable { + DietState({ + this.dietModelObj, + }); + + DietModel? dietModelObj; + + @override + List get props => [dietModelObj]; + DietState copyWith({DietModel? dietModelObj}) { + return DietState( + dietModelObj: dietModelObj ?? this.dietModelObj, + ); + } +} + +class DietLoadedState extends DietState { + DietLoadedState({this.dietModelObj, this.diet}); + + DietModel? dietModelObj; + Map? diet; + @override + List get props => [dietModelObj, diet]; + DietLoadedState copyWith( + {DietModel? dietModelObj, Map? diet}) { + return DietLoadedState( + dietModelObj: dietModelObj ?? this.dietModelObj, + diet: diet ?? this.diet, + ); + } +} + +class DietErrorState extends DietState { + final String errorMessage; + + DietErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} + +class DietInitialState extends DietState { + DietInitialState({required DietModel dietModelObj}) + : super(dietModelObj: dietModelObj); +} diff --git a/lib/presentation/diets/diet_show_screen/diet_show_screen.dart b/lib/presentation/diets/diet_show_screen/diet_show_screen.dart new file mode 100644 index 00000000..dcc20aea --- /dev/null +++ b/lib/presentation/diets/diet_show_screen/diet_show_screen.dart @@ -0,0 +1,154 @@ +import 'bloc/diet_show_bloc.dart'; +import 'models/diet_show_model.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:flutter/material.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +class DietScreen extends StatelessWidget { + const DietScreen({Key? key}) : super(key: key); + static Widget builder(BuildContext context) { + final dietID = ModalRoute.of(context)!.settings.arguments; + return BlocProvider( + create: (context) => DietBloc( + DietState(dietModelObj: DietModel(dietID: dietID.toString()))) + ..add(DietInitialEvent()), + child: DietScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is DietInitialState) { + // Это начальное состояние, данные ещё не загружены. + return Container( + color: Colors.white, + ); + } else if (state is DietLoadedState) { + // Данные загружены, можно строить экран. + return _buildDiet(context, state); + } else if (state is DietErrorState) { + // Произошла ошибка при загрузке данных. + return Center(child: Text(state.errorMessage)); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildDiet(BuildContext context, DietLoadedState state) { + mediaQueryData = MediaQuery.of(context); + return SafeArea( + child: Scaffold( + resizeToAvoidBottomInset: false, + body: Container( + width: double.maxFinite, + decoration: AppDecoration.fillGray, + child: ListView( + children: [ + SizedBox(height: 30.v), + Container( + margin: EdgeInsets.only( + left: 16.h, + right: 20.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + + (state.diet?['image'] ?? ''), + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundImage: imageProvider, + radius: 25, + ), + placeholder: (context, url) => + CircularProgressIndicator(), + errorWidget: (context, url, error) => CircleAvatar( + child: Icon(Icons.person), + backgroundColor: Colors.white, + radius: 25, + ), + ), + SizedBox(height: 16.0), + Text(state.diet?['name'], + style: CustomTextStyles.titleMediumBlack), + SizedBox(height: 8.0), + Text( + state.diet?['details'], + ), + SizedBox(height: 16.0), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + ElevatedButton( + onPressed: () => Navigator.pushNamed( + context, + AppRoutes.dietEditScreen, + arguments: state.diet?['id'].toString(), + ), + child: Text('Редактировать'), + ), + ElevatedButton( + onPressed: () => context + .read() + .add(DeleteEntry(state.diet?['id'])), + child: Text('Удалить'), + ), + ], + ) + ]), + ) + ], + ), + ), + bottomNavigationBar: _buildBottomBar(context))); + } + + ///Handling route based on bottom click actions + String getCurrentRoute(BottomBarEnum type) { + switch (type) { + case BottomBarEnum.tf: + return AppRoutes.dietScreen; + default: + return "/"; + } + } + + ///Handling page based on route + Widget getCurrentPage( + BuildContext context, + String currentRoute, + ) { + switch (currentRoute) { + case AppRoutes.dietScreen: + return DietScreen.builder(context); + default: + return DietScreen(); + } + } + + _buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); + } + + /// Navigates to the previous screen. + onTapArrowleftone(BuildContext context) { + NavigatorService.goBack(); + } +} diff --git a/lib/presentation/diets/diet_show_screen/models/diet_show_model.dart b/lib/presentation/diets/diet_show_screen/models/diet_show_model.dart new file mode 100644 index 00000000..803827f8 --- /dev/null +++ b/lib/presentation/diets/diet_show_screen/models/diet_show_model.dart @@ -0,0 +1,19 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; + +/// This class defines the variables used in the [diet_screen], +/// and is typically used to hold data that is passed between different parts of the application. + +class DietModel extends Equatable { + final String dietID; + + DietModel({required String dietID}) : dietID = dietID; + + DietModel copyWith(String dietID) { + return DietModel(dietID: dietID); + } + + @override + List get props => [dietID]; +} diff --git a/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_bloc.dart b/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_bloc.dart new file mode 100644 index 00000000..fee6cd51 --- /dev/null +++ b/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_bloc.dart @@ -0,0 +1,93 @@ +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:hive/hive.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/feed/blog_post_create_screen/models/blog_post_create_model.dart'; +part 'blog_post_create_event.dart'; +part 'blog_post_create_state.dart'; + +/// A bloc that manages the state of a BlogPostCreate according to the event that is dispatched to it. +class BlogPostCreateBloc + extends Bloc { + BlogPostCreateBloc(BlogPostCreateState initialState) : super(initialState) { + on(_onInitialize); + on(_createBlogPost); + } + + _onInitialize( + BlogPostCreateInitialEvent event, + Emitter emit, + ) async { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + if (state is BlogPostCreateLoadedState) { + var currentState = state as BlogPostCreateLoadedState; + emit(currentState.copyWith( + blog_postModelObj: currentState.blog_postModelObj, + title: currentState.title, + description: currentState.description, + youtube_url: currentState.youtube_url)); + } else { + emit(BlogPostCreateLoadedState( + title: TextEditingController(), + description: TextEditingController(), + youtube_url: TextEditingController())); + } + } + + FutureOr _createBlogPost( + CreateBlogPost event, Emitter emit) async { + if (state is BlogPostCreateLoadedState) { + var currentState = state as BlogPostCreateLoadedState; + + var response = await sendBlogPost( + currentState.title?.text, + currentState.youtube_url?.text, + currentState.description?.text, + ); + + if (response.statusCode == 200) { + NavigatorService.pushNamed(AppRoutes.feedScreen); + } else { + print( + 'Failed to create blog post. Status Code: ${response.statusCode} ${response.body}'); + } + } + } + + FutureOr sendBlogPost( + String? title, + String? youtube_url, + String? description, + ) async { + String url = '${dotenv.env['SERVER_ROOT']!}/feed/create'; + + final Map credentials = { + 'title': title, + 'description': description, + 'youtube_url': youtube_url, + 'status': 'active', + }; + + var response = await http.post( + Uri.parse(url), + headers: { + 'Content-Type': 'application/json', + }, + body: json.encode(credentials), + encoding: utf8, + ); + + return response; + } +} diff --git a/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_event.dart b/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_event.dart new file mode 100644 index 00000000..7631fac0 --- /dev/null +++ b/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_event.dart @@ -0,0 +1,21 @@ +// ignore_for_file: must_be_immutable + +part of 'blog_post_create_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///BlogPostCreate widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class BlogPostCreateEvent extends Equatable {} + +/// Event that is dispatched when the BlogPostCreate widget is first created. +class BlogPostCreateInitialEvent extends BlogPostCreateEvent { + @override + List get props => []; +} + +class CreateBlogPost extends BlogPostCreateEvent { + @override + List get props => []; +} diff --git a/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_state.dart b/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_state.dart new file mode 100644 index 00000000..0cc3c405 --- /dev/null +++ b/lib/presentation/feed/blog_post_create_screen/bloc/blog_post_create_state.dart @@ -0,0 +1,62 @@ +// ignore_for_file: must_be_immutable + +part of 'blog_post_create_bloc.dart'; + +/// Represents the state of BlogPostCreate in the application. +/// + +class BlogPostCreateState extends Equatable { + BlogPostCreateState({ + this.blog_postModelObj, + }); + + BlogPostCreateModel? blog_postModelObj; + + @override + List get props => [blog_postModelObj]; + BlogPostCreateState copyWith({BlogPostCreateModel? blog_postModelObj}) { + return BlogPostCreateState( + blog_postModelObj: blog_postModelObj ?? this.blog_postModelObj, + ); + } +} + +class BlogPostCreateLoadedState extends BlogPostCreateState { + BlogPostCreateLoadedState( + {this.blog_postModelObj, this.title, this.description, this.youtube_url}); + + BlogPostCreateModel? blog_postModelObj; + TextEditingController? title; + TextEditingController? description; + TextEditingController? youtube_url; + @override + List get props => + [blog_postModelObj, title, description, youtube_url]; + BlogPostCreateLoadedState copyWith({ + BlogPostCreateModel? blog_postModelObj, + TextEditingController? title, + TextEditingController? description, + TextEditingController? youtube_url, + }) { + return BlogPostCreateLoadedState( + blog_postModelObj: blog_postModelObj ?? this.blog_postModelObj, + title: title ?? this.title, + description: description ?? this.description, + youtube_url: youtube_url ?? this.youtube_url, + ); + } +} + +class BlogPostCreateErrorState extends BlogPostCreateState { + final String errorMessage; + + BlogPostCreateErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} + +class BlogPostCreateInitialState extends BlogPostCreateState { + BlogPostCreateInitialState({required BlogPostCreateModel blog_postModelObj}) + : super(blog_postModelObj: blog_postModelObj); +} diff --git a/lib/presentation/feed/blog_post_create_screen/blog_post_create_screen.dart b/lib/presentation/feed/blog_post_create_screen/blog_post_create_screen.dart new file mode 100644 index 00000000..6950af55 --- /dev/null +++ b/lib/presentation/feed/blog_post_create_screen/blog_post_create_screen.dart @@ -0,0 +1,104 @@ +import 'bloc/blog_post_create_bloc.dart'; +import 'models/blog_post_create_model.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:flutter/material.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; + +class BlogPostCreateScreen extends StatelessWidget { + const BlogPostCreateScreen({Key? key}) : super(key: key); + static Widget builder(BuildContext context) { + final blog_postID = ModalRoute.of(context)!.settings.arguments; + return BlocProvider( + create: (context) => BlogPostCreateBloc(BlogPostCreateState( + blog_postModelObj: + BlogPostCreateModel(blog_postID: blog_postID.toString()))) + ..add(BlogPostCreateInitialEvent()), + child: BlogPostCreateScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is BlogPostCreateInitialState) { + // Это начальное состояние, данные ещё не загружены. + return Container( + color: Colors.white, + ); + } else if (state is BlogPostCreateLoadedState) { + // Данные загружены, можно строить экран. + return _buildBlogPostCreate(context, state); + } else if (state is BlogPostCreateErrorState) { + // Произошла ошибка при загрузке данных. + return Center(child: Text(state.errorMessage)); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildBlogPostCreate(BuildContext context, BlogPostCreateLoadedState state) { + mediaQueryData = MediaQuery.of(context); + return SafeArea( + child: Scaffold( + resizeToAvoidBottomInset: false, + body: Container( + width: double.maxFinite, + decoration: AppDecoration.fillGray, + child: ListView(children: [ + SizedBox(height: 30.v), + Container( + margin: EdgeInsets.only( + left: 16.h, + right: 20.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + SizedBox(height: 16.0), + TextFormField( + controller: state.title, + decoration: InputDecoration(labelText: 'Заголовок'), + ), + SizedBox(height: 16.0), + TextFormField( + controller: state.description, + decoration: InputDecoration(labelText: 'Описание'), + ), + SizedBox(height: 16.0), + TextFormField( + controller: state.youtube_url, + decoration: InputDecoration(labelText: 'Ссылка'), + ), + SizedBox(height: 16.0), + // Здесь нужно добавить виджет для загрузки картинки + ElevatedButton( + onPressed: () => context + .read() + .add(CreateBlogPost()), + child: Text('Создать'), + ) + ]), + ) + ]), + ), + bottomNavigationBar: _buildBottomBar(context))); + } +} + +_buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); +} diff --git a/lib/presentation/feed/blog_post_create_screen/models/blog_post_create_model.dart b/lib/presentation/feed/blog_post_create_screen/models/blog_post_create_model.dart new file mode 100644 index 00000000..46b21c4e --- /dev/null +++ b/lib/presentation/feed/blog_post_create_screen/models/blog_post_create_model.dart @@ -0,0 +1,20 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; + +/// This class defines the variables used in the [blog_post_create_screen], +/// and is typically used to hold data that is passed between different parts of the application. + +class BlogPostCreateModel extends Equatable { + final String blog_postID; + + BlogPostCreateModel({required String blog_postID}) + : blog_postID = blog_postID; + + BlogPostCreateModel copyWith(String blog_postID) { + return BlogPostCreateModel(blog_postID: blog_postID); + } + + @override + List get props => [blog_postID]; +} diff --git a/lib/presentation/feed/blog_post_edit_screen/bloc/blog_post_edit_bloc.dart b/lib/presentation/feed/blog_post_edit_screen/bloc/blog_post_edit_bloc.dart new file mode 100644 index 00000000..f3b193e4 --- /dev/null +++ b/lib/presentation/feed/blog_post_edit_screen/bloc/blog_post_edit_bloc.dart @@ -0,0 +1,122 @@ +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:hive/hive.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/feed/blog_post_edit_screen/models/blog_post_edit_model.dart'; +part 'blog_post_edit_event.dart'; +part 'blog_post_edit_state.dart'; + +/// A bloc that manages the state of a BlogPostEdit according to the event that is dispatched to it. +class BlogPostEditBloc extends Bloc { + BlogPostEditBloc(BlogPostEditState initialState) : super(initialState) { + on(_onInitialize); + on(_editBlogPost); + } + + _onInitialize( + BlogPostEditInitialEvent event, + Emitter emit, + ) async { + try { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + final String? blog_postID = state.blog_postModelObj?.blog_postID; + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/feed/show/$blog_postID'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + if (data is Map) { + if (state is BlogPostEditLoadedState) { + var currentState = state as BlogPostEditLoadedState; + emit(currentState.copyWith( + blog_postModelObj: currentState.blog_postModelObj, + blog_post: currentState.blog_post, + title: currentState.title, + description: currentState.description, + youtube_url: currentState.youtube_url)); + } else { + emit(BlogPostEditLoadedState( + blog_post: data, + title: TextEditingController(text: data['title']), + description: TextEditingController(text: data['description']), + youtube_url: TextEditingController(text: data['youtube_url']))); + } + } else { + // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку + emit(BlogPostEditErrorState( + 'Некоторые необходимые данные отсутствуют.')); + } + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(BlogPostEditErrorState('Ошибка сервера: ${response.statusCode}')); + } + } catch (e) { + // Ошибка при декодировании JSON или другая ошибка выполнения + emit(BlogPostEditErrorState('Ошибка при получении данных о чатах: $e')); + } + } + + FutureOr _editBlogPost( + EditBlogPost event, Emitter emit) async { + if (state is BlogPostEditLoadedState) { + var currentState = state as BlogPostEditLoadedState; + + var response = await sendBlogPost( + currentState.blog_post, + currentState.title?.text, + currentState.youtube_url?.text, + currentState.description?.text, + ); + + if (response.statusCode == 200) { + NavigatorService.pushNamed(AppRoutes.blogPostScreen, + arguments: currentState.blog_post?['id'].toString()); + } else { + print( + 'Failed to edit blog post. Status Code: ${response.statusCode} ${response.body}'); + } + } + } + + FutureOr sendBlogPost( + Map? blog_post, + String? title, + String? youtube_url, + String? description, + ) async { + int id = blog_post?['id']; + String url = '${dotenv.env['SERVER_ROOT']!}/feed/update/$id'; + + final Map credentials = { + 'id': id, + 'title': title, + 'description': description, + 'youtube_url': youtube_url, + }; + + var response = await http.post( + Uri.parse(url), + headers: { + 'Content-Type': 'application/json', + }, + body: json.encode(credentials), + encoding: utf8, + ); + + return response; + } +} diff --git a/lib/presentation/feed/blog_post_edit_screen/bloc/blog_post_edit_event.dart b/lib/presentation/feed/blog_post_edit_screen/bloc/blog_post_edit_event.dart new file mode 100644 index 00000000..c8961556 --- /dev/null +++ b/lib/presentation/feed/blog_post_edit_screen/bloc/blog_post_edit_event.dart @@ -0,0 +1,21 @@ +// ignore_for_file: must_be_immutable + +part of 'blog_post_edit_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///BlogPostEdit widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class BlogPostEditEvent extends Equatable {} + +/// Event that is dispatched when the BlogPostEdit widget is first created. +class BlogPostEditInitialEvent extends BlogPostEditEvent { + @override + List get props => []; +} + +class EditBlogPost extends BlogPostEditEvent { + @override + List get props => []; +} diff --git a/lib/presentation/feed/blog_post_edit_screen/bloc/blog_post_edit_state.dart b/lib/presentation/feed/blog_post_edit_screen/bloc/blog_post_edit_state.dart new file mode 100644 index 00000000..321448ee --- /dev/null +++ b/lib/presentation/feed/blog_post_edit_screen/bloc/blog_post_edit_state.dart @@ -0,0 +1,69 @@ +// ignore_for_file: must_be_immutable + +part of 'blog_post_edit_bloc.dart'; + +/// Represents the state of BlogPostEdit in the application. +/// + +class BlogPostEditState extends Equatable { + BlogPostEditState({ + this.blog_postModelObj, + }); + + BlogPostEditModel? blog_postModelObj; + + @override + List get props => [blog_postModelObj]; + BlogPostEditState copyWith({BlogPostEditModel? blog_postModelObj}) { + return BlogPostEditState( + blog_postModelObj: blog_postModelObj ?? this.blog_postModelObj, + ); + } +} + +class BlogPostEditLoadedState extends BlogPostEditState { + BlogPostEditLoadedState( + {this.blog_postModelObj, + this.blog_post, + this.title, + this.description, + this.youtube_url}); + + BlogPostEditModel? blog_postModelObj; + Map? blog_post; + TextEditingController? title; + TextEditingController? description; + TextEditingController? youtube_url; + @override + List get props => + [blog_postModelObj, blog_post, title, description, youtube_url]; + BlogPostEditLoadedState copyWith({ + BlogPostEditModel? blog_postModelObj, + Map? blog_post, + TextEditingController? title, + TextEditingController? description, + TextEditingController? youtube_url, + }) { + return BlogPostEditLoadedState( + blog_postModelObj: blog_postModelObj ?? this.blog_postModelObj, + blog_post: blog_post ?? this.blog_post, + title: title ?? this.title, + description: description ?? this.description, + youtube_url: youtube_url ?? this.youtube_url, + ); + } +} + +class BlogPostEditErrorState extends BlogPostEditState { + final String errorMessage; + + BlogPostEditErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} + +class BlogPostEditInitialState extends BlogPostEditState { + BlogPostEditInitialState({required BlogPostEditModel blog_postModelObj}) + : super(blog_postModelObj: blog_postModelObj); +} diff --git a/lib/presentation/feed/blog_post_edit_screen/blog_post_edit_screen.dart b/lib/presentation/feed/blog_post_edit_screen/blog_post_edit_screen.dart new file mode 100644 index 00000000..1bba76d6 --- /dev/null +++ b/lib/presentation/feed/blog_post_edit_screen/blog_post_edit_screen.dart @@ -0,0 +1,120 @@ +import 'bloc/blog_post_edit_bloc.dart'; +import 'models/blog_post_edit_model.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:flutter/material.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +class BlogPostEditScreen extends StatelessWidget { + const BlogPostEditScreen({Key? key}) : super(key: key); + static Widget builder(BuildContext context) { + final blog_postID = ModalRoute.of(context)!.settings.arguments; + return BlocProvider( + create: (context) => BlogPostEditBloc(BlogPostEditState( + blog_postModelObj: + BlogPostEditModel(blog_postID: blog_postID.toString()))) + ..add(BlogPostEditInitialEvent()), + child: BlogPostEditScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is BlogPostEditInitialState) { + // Это начальное состояние, данные ещё не загружены. + return Container( + color: Colors.white, + ); + } else if (state is BlogPostEditLoadedState) { + // Данные загружены, можно строить экран. + return _buildBlogPostEdit(context, state); + } else if (state is BlogPostEditErrorState) { + // Произошла ошибка при загрузке данных. + return Center(child: Text(state.errorMessage)); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildBlogPostEdit(BuildContext context, BlogPostEditLoadedState state) { + mediaQueryData = MediaQuery.of(context); + return SafeArea( + child: Scaffold( + resizeToAvoidBottomInset: false, + body: Container( + width: double.maxFinite, + decoration: AppDecoration.fillGray, + child: ListView(children: [ + SizedBox(height: 30.v), + Container( + margin: EdgeInsets.only( + left: 16.h, + right: 20.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + + (state.blog_post?['image'] ?? ''), + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundImage: imageProvider, + radius: 25, + ), + placeholder: (context, url) => + CircularProgressIndicator(), + errorWidget: (context, url, error) => CircleAvatar( + child: Icon(Icons.person), + backgroundColor: Colors.white, + radius: 25, + ), + ), + SizedBox(height: 16.0), + TextFormField( + controller: state.title, + decoration: InputDecoration(labelText: 'Заголовок'), + ), + SizedBox(height: 16.0), + TextFormField( + controller: state.description, + decoration: InputDecoration(labelText: 'Описание'), + ), + SizedBox(height: 16.0), + TextFormField( + controller: state.youtube_url, + decoration: InputDecoration(labelText: 'Ссылка'), + ), + SizedBox(height: 16.0), + // Здесь нужно добавить виджет для загрузки картинки + ElevatedButton( + onPressed: () => + context.read().add(EditBlogPost()), + child: Text('Сохранить'), + ) + ]), + ) + ]), + ), + bottomNavigationBar: _buildBottomBar(context))); + } +} + +_buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); +} diff --git a/lib/presentation/feed/blog_post_edit_screen/models/blog_post_edit_model.dart b/lib/presentation/feed/blog_post_edit_screen/models/blog_post_edit_model.dart new file mode 100644 index 00000000..f9ef98c2 --- /dev/null +++ b/lib/presentation/feed/blog_post_edit_screen/models/blog_post_edit_model.dart @@ -0,0 +1,19 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; + +/// This class defines the variables used in the [blog_post_edit_screen], +/// and is typically used to hold data that is passed between different parts of the application. + +class BlogPostEditModel extends Equatable { + final String blog_postID; + + BlogPostEditModel({required String blog_postID}) : blog_postID = blog_postID; + + BlogPostEditModel copyWith(String blog_postID) { + return BlogPostEditModel(blog_postID: blog_postID); + } + + @override + List get props => [blog_postID]; +} diff --git a/lib/presentation/feed/blog_post_screen/bloc/blog_post_bloc.dart b/lib/presentation/feed/blog_post_screen/bloc/blog_post_bloc.dart new file mode 100644 index 00000000..74afd4d1 --- /dev/null +++ b/lib/presentation/feed/blog_post_screen/bloc/blog_post_bloc.dart @@ -0,0 +1,88 @@ +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:hive/hive.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/feed/blog_post_screen/models/blog_post_model.dart'; +part 'blog_post_event.dart'; +part 'blog_post_state.dart'; + +/// A bloc that manages the state of a BlogPost according to the event that is dispatched to it. +class BlogPostBloc extends Bloc { + BlogPostBloc(BlogPostState initialState) : super(initialState) { + on(_onInitialize); + on(_deleteEntry); + } + + _onInitialize( + BlogPostInitialEvent event, + Emitter emit, + ) async { + try { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + final String? blog_postID = state.blog_postModelObj?.blog_postID; + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/feed/show/$blog_postID'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + if (data is Map) { + if (state is BlogPostLoadedState) { + var currentState = state as BlogPostLoadedState; + emit(currentState.copyWith( + blog_postModelObj: currentState.blog_postModelObj, + blog_post: currentState.blog_post)); + } else { + emit(BlogPostLoadedState(blog_post: data)); + } + } else { + // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку + emit(BlogPostErrorState('Некоторые необходимые данные отсутствуют.')); + } + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(BlogPostErrorState('Ошибка сервера: ${response.statusCode}')); + } + } catch (e) { + // Ошибка при декодировании JSON или другая ошибка выполнения + emit(BlogPostErrorState('Ошибка при получении данных о чатах: $e')); + } + } + + FutureOr _deleteEntry( + DeleteEntry event, Emitter emit) async { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + final int id = event.id; + final response = await http.post( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/feed/delete/$id'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + if (response.statusCode == 200) { + NavigatorService.pushNamed(AppRoutes.feedScreen); + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(BlogPostErrorState('Ошибка: ${response.statusCode}')); + } + } +} diff --git a/lib/presentation/feed/blog_post_screen/bloc/blog_post_event.dart b/lib/presentation/feed/blog_post_screen/bloc/blog_post_event.dart new file mode 100644 index 00000000..de77e332 --- /dev/null +++ b/lib/presentation/feed/blog_post_screen/bloc/blog_post_event.dart @@ -0,0 +1,23 @@ +// ignore_for_file: must_be_immutable + +part of 'blog_post_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///BlogPost widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class BlogPostEvent extends Equatable {} + +/// Event that is dispatched when the BlogPost widget is first created. +class BlogPostInitialEvent extends BlogPostEvent { + @override + List get props => []; +} + +class DeleteEntry extends BlogPostEvent { + DeleteEntry(this.id); + final int id; + @override + List get props => [id]; +} diff --git a/lib/presentation/feed/blog_post_screen/bloc/blog_post_state.dart b/lib/presentation/feed/blog_post_screen/bloc/blog_post_state.dart new file mode 100644 index 00000000..033ba293 --- /dev/null +++ b/lib/presentation/feed/blog_post_screen/bloc/blog_post_state.dart @@ -0,0 +1,52 @@ +// ignore_for_file: must_be_immutable + +part of 'blog_post_bloc.dart'; + +/// Represents the state of BlogPost in the application. +/// + +class BlogPostState extends Equatable { + BlogPostState({ + this.blog_postModelObj, + }); + + BlogPostModel? blog_postModelObj; + + @override + List get props => [blog_postModelObj]; + BlogPostState copyWith({BlogPostModel? blog_postModelObj}) { + return BlogPostState( + blog_postModelObj: blog_postModelObj ?? this.blog_postModelObj, + ); + } +} + +class BlogPostLoadedState extends BlogPostState { + BlogPostLoadedState({this.blog_postModelObj, this.blog_post}); + + BlogPostModel? blog_postModelObj; + Map? blog_post; + @override + List get props => [blog_postModelObj, blog_post]; + BlogPostLoadedState copyWith( + {BlogPostModel? blog_postModelObj, Map? blog_post}) { + return BlogPostLoadedState( + blog_postModelObj: blog_postModelObj ?? this.blog_postModelObj, + blog_post: blog_post ?? this.blog_post, + ); + } +} + +class BlogPostErrorState extends BlogPostState { + final String errorMessage; + + BlogPostErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} + +class BlogPostInitialState extends BlogPostState { + BlogPostInitialState({required BlogPostModel blog_postModelObj}) + : super(blog_postModelObj: blog_postModelObj); +} diff --git a/lib/presentation/feed/blog_post_screen/blog_post_screen.dart b/lib/presentation/feed/blog_post_screen/blog_post_screen.dart new file mode 100644 index 00000000..293dfb2b --- /dev/null +++ b/lib/presentation/feed/blog_post_screen/blog_post_screen.dart @@ -0,0 +1,155 @@ +import 'bloc/blog_post_bloc.dart'; +import 'models/blog_post_model.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:flutter/material.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +class BlogPostScreen extends StatelessWidget { + const BlogPostScreen({Key? key}) : super(key: key); + static Widget builder(BuildContext context) { + final blog_postID = ModalRoute.of(context)!.settings.arguments; + return BlocProvider( + create: (context) => BlogPostBloc(BlogPostState( + blog_postModelObj: + BlogPostModel(blog_postID: blog_postID.toString()))) + ..add(BlogPostInitialEvent()), + child: BlogPostScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is BlogPostInitialState) { + // Это начальное состояние, данные ещё не загружены. + return Container( + color: Colors.white, + ); + } else if (state is BlogPostLoadedState) { + // Данные загружены, можно строить экран. + return _buildBlogPost(context, state); + } else if (state is BlogPostErrorState) { + // Произошла ошибка при загрузке данных. + return Center(child: Text(state.errorMessage)); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildBlogPost(BuildContext context, BlogPostLoadedState state) { + mediaQueryData = MediaQuery.of(context); + return SafeArea( + child: Scaffold( + resizeToAvoidBottomInset: false, + body: Container( + width: double.maxFinite, + decoration: AppDecoration.fillGray, + child: ListView( + children: [ + SizedBox(height: 30.v), + Container( + margin: EdgeInsets.only( + left: 16.h, + right: 20.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + + (state.blog_post?['image'] ?? ''), + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundImage: imageProvider, + radius: 25, + ), + placeholder: (context, url) => + CircularProgressIndicator(), + errorWidget: (context, url, error) => CircleAvatar( + child: Icon(Icons.person), + backgroundColor: Colors.white, + radius: 25, + ), + ), + SizedBox(height: 16.0), + Text(state.blog_post?['title'], + style: CustomTextStyles.titleMediumBlack), + SizedBox(height: 8.0), + Text( + state.blog_post?['description'], + ), + SizedBox(height: 16.0), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + ElevatedButton( + onPressed: () => Navigator.pushNamed( + context, + AppRoutes.blogPostEditScreen, + arguments: state.blog_post?['id'].toString(), + ), + child: Text('Редактировать'), + ), + ElevatedButton( + onPressed: () => context + .read() + .add(DeleteEntry(state.blog_post?['id'])), + child: Text('Удалить'), + ), + ], + ) + ]), + ) + ], + ), + ), + bottomNavigationBar: _buildBottomBar(context))); + } + + ///Handling route based on bottom click actions + String getCurrentRoute(BottomBarEnum type) { + switch (type) { + case BottomBarEnum.tf: + return AppRoutes.blogPostScreen; + default: + return "/"; + } + } + + ///Handling page based on route + Widget getCurrentPage( + BuildContext context, + String currentRoute, + ) { + switch (currentRoute) { + case AppRoutes.blogPostScreen: + return BlogPostScreen.builder(context); + default: + return BlogPostScreen(); + } + } + + _buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); + } + + /// Navigates to the previous screen. + onTapArrowleftone(BuildContext context) { + NavigatorService.goBack(); + } +} diff --git a/lib/presentation/feed/blog_post_screen/models/blog_post_model.dart b/lib/presentation/feed/blog_post_screen/models/blog_post_model.dart new file mode 100644 index 00000000..a005b747 --- /dev/null +++ b/lib/presentation/feed/blog_post_screen/models/blog_post_model.dart @@ -0,0 +1,19 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; + +/// This class defines the variables used in the [blog_post_screen], +/// and is typically used to hold data that is passed between different parts of the application. + +class BlogPostModel extends Equatable { + final String blog_postID; + + BlogPostModel({required String blog_postID}) : blog_postID = blog_postID; + + BlogPostModel copyWith(String blog_postID) { + return BlogPostModel(blog_postID: blog_postID); + } + + @override + List get props => [blog_postID]; +} diff --git a/lib/presentation/feed/feed_screen/bloc/feed_bloc.dart b/lib/presentation/feed/feed_screen/bloc/feed_bloc.dart new file mode 100644 index 00000000..984bbbf7 --- /dev/null +++ b/lib/presentation/feed/feed_screen/bloc/feed_bloc.dart @@ -0,0 +1,70 @@ +//import 'package:eatvio_app/presentation/auth/auth_screen/auth_screen.dart'; +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/feed/feed_screen/models/feed_model.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import 'package:hive/hive.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:image_picker/image_picker.dart'; + +part 'feed_event.dart'; +part 'feed_state.dart'; + +/// A bloc that manages the state of a Feed according to the event that is dispatched to it. +class FeedBloc extends Bloc { + FeedBloc(FeedState initialState) : super(initialState) { + on(_onInitialize); + } + + Future _onInitialize( + FeedInitialEvent event, + Emitter emit, + ) async { + try { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); // Если токен есть, запускаем приложение + } + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/feed/index'), + ); + print('Got response'); + if (response.statusCode == 200) { + print('Got 200'); + final data = jsonDecode(response.body); + if (data is List) { + print('Got List'); + if (state is FeedLoadedState) { + print('Got loaded'); + var currentState = state as FeedLoadedState; + emit((currentState).copyWith( + posts: data, + myProfileModelObj: currentState.myProfileModelObj, + )); + } else { + print('Got unloaded'); + emit(FeedLoadedState( + posts: data, + )); + } + } else { + // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку + emit(FeedErrorState('Неверный формат ответа API.')); + } + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(FeedErrorState('Ошибка сервера: ${response.statusCode}')); + } + } catch (e) { + // Ошибка при декодировании JSON или другая ошибка выполнения + emit(FeedErrorState('Ошибка при получении данных ленты: $e')); + } + } +} diff --git a/lib/presentation/feed/feed_screen/bloc/feed_event.dart b/lib/presentation/feed/feed_screen/bloc/feed_event.dart new file mode 100644 index 00000000..cbb510f5 --- /dev/null +++ b/lib/presentation/feed/feed_screen/bloc/feed_event.dart @@ -0,0 +1,23 @@ +// ignore_for_file: must_be_immutable + +part of 'feed_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///Feed widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class FeedEvent extends Equatable {} + +/// Event that is dispatched when the Feed widget is first created. +class FeedInitialEvent extends FeedEvent { + @override + List get props => []; +} + +class UploadAvatar extends FeedEvent { + UploadAvatar(this.image); + final XFile image; + @override + List get props => [image]; +} diff --git a/lib/presentation/feed/feed_screen/bloc/feed_state.dart b/lib/presentation/feed/feed_screen/bloc/feed_state.dart new file mode 100644 index 00000000..dbc57ac5 --- /dev/null +++ b/lib/presentation/feed/feed_screen/bloc/feed_state.dart @@ -0,0 +1,49 @@ +// ignore_for_file: must_be_immutable + +part of 'feed_bloc.dart'; + +class FeedState extends Equatable { + const FeedState({this.myProfileModelObj}); + + final FeedModel? myProfileModelObj; + + @override + List get props => [myProfileModelObj]; +} + +class FeedInitialState extends FeedState { + FeedInitialState({required FeedModel myProfileModelObj}) + : super(myProfileModelObj: myProfileModelObj); +} + +/// Represents the state of Feed in the application. +class FeedLoadedState extends FeedState { + FeedLoadedState({this.myProfileModelObj, this.posts}) + : super(myProfileModelObj: null); + + FeedModel? myProfileModelObj; + List? posts; + @override + List get props => [ + myProfileModelObj, + posts, + ]; + FeedLoadedState copyWith({ + FeedModel? myProfileModelObj, + List? posts, + }) { + return FeedLoadedState( + myProfileModelObj: myProfileModelObj ?? this.myProfileModelObj, + posts: posts ?? this.posts, + ); + } +} + +class FeedErrorState extends FeedState { + final String errorMessage; + + const FeedErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} diff --git a/lib/presentation/feed/feed_screen/feed_screen.dart b/lib/presentation/feed/feed_screen/feed_screen.dart new file mode 100644 index 00000000..e0712d31 --- /dev/null +++ b/lib/presentation/feed/feed_screen/feed_screen.dart @@ -0,0 +1,183 @@ +import 'bloc/feed_bloc.dart'; +import 'models/feed_model.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:eatvio_app/widgets/app_bar/appbar_subtitle.dart'; +import 'package:eatvio_app/widgets/app_bar/appbar_leading_image.dart'; +import 'package:eatvio_app/widgets/app_bar/custom_app_bar.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter/material.dart'; + +class FeedScreen extends StatelessWidget { + static Widget builder(BuildContext context) { + return BlocProvider( + create: (context) => FeedBloc(FeedState(myProfileModelObj: FeedModel())) + ..add(FeedInitialEvent()), + child: FeedScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is FeedInitialState) { + // Это начальное состояние, данные ещё не загружены. + return CircularProgressIndicator( + color: Colors.white, + backgroundColor: Colors.white, + ); + } else if (state is FeedLoadedState) { + // Данные загружены, можно строить экран. + return _buildSetingsScreen(context, state); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildSetingsScreen(BuildContext context, FeedLoadedState state) { + mediaQueryData = MediaQuery.of(context); + return SafeArea( + child: Scaffold( + backgroundColor: Colors.grey[200], + resizeToAvoidBottomInset: true, + body: SingleChildScrollView( + child: Container( + width: double.maxFinite, + decoration: BoxDecoration(color: Colors.grey[200]), + child: Padding( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox(height: 10), + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: CustomAppBar( + height: 54.v, + leadingWidth: 28.h, + leading: AppbarLeadingImage( + imagePath: ImageConstant.imgArrowLeft, + margin: EdgeInsets.only( + left: 16.h, top: 15.v, bottom: 15.v), + onTap: onTapBack(context), + ), + title: Padding( + padding: EdgeInsets.only(left: 20.h), + child: Row( + children: [ + AppbarSubtitle( + text: "Лента", + margin: EdgeInsets.only( + left: 80.h, top: 12.v, bottom: 9.v), + ), + Spacer(), + Padding( + padding: const EdgeInsets.only(right: 20.0), + child: GestureDetector( + onTap: () => Navigator.pushNamed( + context, + AppRoutes.blogPostCreateScreen, + ), + child: Icon(Icons.create_outlined)), + ) + ], + ), + ), + ), + ), + SizedBox(height: 20), + _buildBlogPostList(context, state) + ], + ), + ), + ), + ), + bottomNavigationBar: _buildBottomBar(context), + ), + ); + } + + _buildBlogPostList(BuildContext context, FeedLoadedState state) { + return ListView.builder( + shrinkWrap: true, // Add this line + itemCount: state.posts?.length ?? 0, + itemBuilder: (context, index) { + final post = state.posts?[index]; + return Container( + margin: EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: ListTile( + leading: CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + post['image'], + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundImage: imageProvider, + radius: 25, + ), + placeholder: (context, url) => CircularProgressIndicator(), + errorWidget: (context, url, error) => CircleAvatar( + child: Icon(Icons.person), + backgroundColor: Colors.white, + radius: 25, + ), + ), + title: Text( + post?['title'] ?? '', + style: CustomTextStyles.titleMediumSemiBold, + ), + subtitle: Text(post?['description'] ?? ''), + onTap: () => Navigator.pushNamed( + context, + AppRoutes.blogPostScreen, + arguments: post?['id'].toString(), + ), + trailing: GestureDetector( + onTap: () => Navigator.pushNamed( + context, + AppRoutes.blogPostEditScreen, + arguments: post?['id'].toString(), + ), + child: Icon(Icons.edit)), + )); + }, + ); + } + + Function onTapBack(BuildContext context) { + return () { + onTapArrowLeft(context); + }; + } + + _buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); + } + + onTapArrowLeft(BuildContext context) { + NavigatorService.goBack(); + } +} diff --git a/lib/presentation/feed/feed_screen/models/feed_model.dart b/lib/presentation/feed/feed_screen/models/feed_model.dart new file mode 100644 index 00000000..9e13172c --- /dev/null +++ b/lib/presentation/feed/feed_screen/models/feed_model.dart @@ -0,0 +1,16 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; + +/// This class defines the variables used in the [k0_page], +/// and is typically used to hold data that is passed between different parts of the application. +class FeedModel extends Equatable { + FeedModel(); + + FeedModel copyWith() { + return FeedModel(); + } + + @override + List get props => []; +} diff --git a/lib/presentation/plans/plan_create_screen/bloc/plan_create_bloc.dart b/lib/presentation/plans/plan_create_screen/bloc/plan_create_bloc.dart new file mode 100644 index 00000000..b06092c3 --- /dev/null +++ b/lib/presentation/plans/plan_create_screen/bloc/plan_create_bloc.dart @@ -0,0 +1,93 @@ +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:hive/hive.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/plans/plan_create_screen/models/plan_create_model.dart'; +part 'plan_create_event.dart'; +part 'plan_create_state.dart'; + +/// A bloc that manages the state of a PlanCreate according to the event that is dispatched to it. +class PlanCreateBloc extends Bloc { + PlanCreateBloc(PlanCreateState initialState) : super(initialState) { + on(_onInitialize); + on(_createPlan); + } + + _onInitialize( + PlanCreateInitialEvent event, + Emitter emit, + ) async { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + if (state is PlanCreateLoadedState) { + var currentState = state as PlanCreateLoadedState; + emit(currentState.copyWith( + planModelObj: currentState.planModelObj, + name: currentState.name, + details: currentState.details, + )); + } else { + emit(PlanCreateLoadedState( + name: TextEditingController(), + details: TextEditingController(), + )); + } + } + + FutureOr _createPlan( + CreatePlan event, Emitter emit) async { + if (state is PlanCreateLoadedState) { + var currentState = state as PlanCreateLoadedState; + + var response = await sendPlan( + currentState.name?.text, + currentState.details?.text, + ); + + if (response.statusCode == 200) { + NavigatorService.pushNamed(AppRoutes.planSelectScreen); + } else { + print( + 'Failed to create blog post. Status Code: ${response.statusCode} ${response.body}'); + } + } + } + + FutureOr sendPlan( + String? name, + String? details, + ) async { + String url = '${dotenv.env['SERVER_ROOT']!}/plans/create'; + + final Map credentials = { + 'name': name, + 'details': details, + 'duration': '14', + 'state': 'active', + 'type': '{"meat": true}', + 'daily_schedule': + '{"1": {"lunch": [{"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "milliliters", "quantity": 494}, {"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "grams", "quantity": 12}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "pcs", "quantity": 432}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 195}], "dinner": [{"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "grams", "quantity": 710}, {"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "grams", "quantity": 119}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "milliliters", "quantity": 287}], "snacks": [{"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "pcs", "quantity": 445}, {"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "grams", "quantity": 500}], "supper": [{"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "pcs", "quantity": 764}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "pcs", "quantity": 735}], "breakfast": [{"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "grams", "quantity": 127}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "pcs", "quantity": 89}]}, "2": {"lunch": [{"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "pcs", "quantity": 436}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "pcs", "quantity": 700}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "pcs", "quantity": 766}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "grams", "quantity": 194}], "dinner": [{"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "pcs", "quantity": 426}], "snacks": [{"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "grams", "quantity": 58}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "milliliters", "quantity": 152}, {"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "grams", "quantity": 925}], "supper": [{"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "pcs", "quantity": 224}], "breakfast": [{"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 918}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "grams", "quantity": 81}, {"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "pcs", "quantity": 308}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "grams", "quantity": 579}]}, "3": {"lunch": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "milliliters", "quantity": 266}], "dinner": [{"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "pcs", "quantity": 790}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "pcs", "quantity": 26}], "snacks": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "grams", "quantity": 549}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "milliliters", "quantity": 446}, {"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "grams", "quantity": 406}], "supper": [{"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "milliliters", "quantity": 92}], "breakfast": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "milliliters", "quantity": 403}, {"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "pcs", "quantity": 894}, {"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "milliliters", "quantity": 457}]}, "4": {"lunch": [{"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "milliliters", "quantity": 698}, {"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "milliliters", "quantity": 43}, {"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "grams", "quantity": 994}, {"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "pcs", "quantity": 965}], "dinner": [{"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "grams", "quantity": 42}, {"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "pcs", "quantity": 948}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "pcs", "quantity": 28}, {"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "grams", "quantity": 54}], "snacks": [{"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "grams", "quantity": 112}], "supper": [{"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "pcs", "quantity": 891}], "breakfast": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "pcs", "quantity": 538}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "pcs", "quantity": 635}, {"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "pcs", "quantity": 586}]}, "5": {"lunch": [{"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "milliliters", "quantity": 900}, {"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "milliliters", "quantity": 242}], "dinner": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "milliliters", "quantity": 375}], "snacks": [{"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 604}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "pcs", "quantity": 510}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "grams", "quantity": 887}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "grams", "quantity": 554}], "supper": [{"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "grams", "quantity": 731}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "pcs", "quantity": 574}, {"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "grams", "quantity": 274}], "breakfast": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "milliliters", "quantity": 276}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "milliliters", "quantity": 80}]}, "6": {"lunch": [{"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "milliliters", "quantity": 359}, {"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "pcs", "quantity": 358}, {"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "grams", "quantity": 639}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 861}], "dinner": [{"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "grams", "quantity": 124}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "grams", "quantity": 122}], "snacks": [{"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "grams", "quantity": 980}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 837}, {"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "grams", "quantity": 444}], "supper": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "milliliters", "quantity": 577}, {"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "milliliters", "quantity": 706}], "breakfast": [{"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "milliliters", "quantity": 839}]}, "7": {"lunch": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "grams", "quantity": 118}, {"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "pcs", "quantity": 664}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "grams", "quantity": 665}], "dinner": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "grams", "quantity": 986}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 296}], "snacks": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "grams", "quantity": 807}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "pcs", "quantity": 493}], "supper": [{"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "milliliters", "quantity": 227}, {"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "pcs", "quantity": 385}, {"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "grams", "quantity": 448}], "breakfast": [{"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "grams", "quantity": 784}]}, "8": {"lunch": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "milliliters", "quantity": 433}, {"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "milliliters", "quantity": 871}], "dinner": [{"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 332}, {"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "milliliters", "quantity": 41}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "grams", "quantity": 388}], "snacks": [{"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "pcs", "quantity": 649}], "supper": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "pcs", "quantity": 114}, {"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "milliliters", "quantity": 968}], "breakfast": [{"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "milliliters", "quantity": 180}, {"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "milliliters", "quantity": 269}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "milliliters", "quantity": 998}]}, "9": {"lunch": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "grams", "quantity": 68}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 505}, {"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "grams", "quantity": 554}], "dinner": [{"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "grams", "quantity": 579}], "snacks": [{"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "grams", "quantity": 751}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "grams", "quantity": 144}], "supper": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "grams", "quantity": 677}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "milliliters", "quantity": 263}, {"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "pcs", "quantity": 304}], "breakfast": [{"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "milliliters", "quantity": 963}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "milliliters", "quantity": 258}]}, "10": {"lunch": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "milliliters", "quantity": 167}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 306}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "grams", "quantity": 243}], "dinner": [{"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "milliliters", "quantity": 79}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "grams", "quantity": 560}], "snacks": [{"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "milliliters", "quantity": 294}, {"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "milliliters", "quantity": 145}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "milliliters", "quantity": 639}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "pcs", "quantity": 108}], "supper": [{"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "pcs", "quantity": 924}, {"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "grams", "quantity": 894}], "breakfast": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "grams", "quantity": 280}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 336}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "milliliters", "quantity": 338}]}, "11": {"lunch": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "grams", "quantity": 817}, {"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "pcs", "quantity": 968}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "milliliters", "quantity": 525}, {"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "milliliters", "quantity": 864}], "dinner": [{"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "pcs", "quantity": 457}, {"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "milliliters", "quantity": 548}], "snacks": [{"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "milliliters", "quantity": 658}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "grams", "quantity": 239}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 358}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "grams", "quantity": 406}], "supper": [{"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "grams", "quantity": 416}], "breakfast": [{"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 220}, {"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "grams", "quantity": 323}]}, "12": {"lunch": [{"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "milliliters", "quantity": 377}], "dinner": [{"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "milliliters", "quantity": 138}], "snacks": [{"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "milliliters", "quantity": 654}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 237}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "pcs", "quantity": 902}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 76}], "supper": [{"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "milliliters", "quantity": 723}, {"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "pcs", "quantity": 481}], "breakfast": [{"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "pcs", "quantity": 571}, {"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "pcs", "quantity": 515}, {"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "pcs", "quantity": 188}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "pcs", "quantity": 411}]}, "13": {"lunch": [{"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "grams", "quantity": 379}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "grams", "quantity": 231}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "pcs", "quantity": 376}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "grams", "quantity": 147}], "dinner": [{"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "pcs", "quantity": 871}, {"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "grams", "quantity": 679}, {"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "pcs", "quantity": 359}, {"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "milliliters", "quantity": 561}], "snacks": [{"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "pcs", "quantity": 414}], "supper": [{"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 827}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 649}, {"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "pcs", "quantity": 363}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "pcs", "quantity": 161}], "breakfast": [{"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 185}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "pcs", "quantity": 464}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 387}]}, "14": {"lunch": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 248}, {"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "grams", "quantity": 5}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 693}, {"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "grams", "quantity": 655}], "dinner": [{"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "pcs", "quantity": 709}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "grams", "quantity": 881}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "milliliters", "quantity": 588}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 36}], "snacks": [{"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "pcs", "quantity": 453}], "supper": [{"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "grams", "quantity": 692}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "pcs", "quantity": 186}, {"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "grams", "quantity": 721}], "breakfast": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 740}, {"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "milliliters", "quantity": 731}, {"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "milliliters", "quantity": 842}, {"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "grams", "quantity": 850}]}, "15": {"lunch": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "grams", "quantity": 124}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "pcs", "quantity": 380}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "pcs", "quantity": 211}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 689}], "dinner": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "milliliters", "quantity": 326}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "milliliters", "quantity": 629}], "snacks": [{"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "milliliters", "quantity": 201}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "grams", "quantity": 129}, {"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "milliliters", "quantity": 749}], "supper": [{"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "milliliters", "quantity": 315}], "breakfast": [{"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "grams", "quantity": 444}]}, "16": {"lunch": [{"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "grams", "quantity": 953}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "pcs", "quantity": 101}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 684}, {"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "pcs", "quantity": 506}], "dinner": [{"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "pcs", "quantity": 333}, {"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "milliliters", "quantity": 416}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "milliliters", "quantity": 796}], "snacks": [{"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "pcs", "quantity": 799}, {"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "pcs", "quantity": 799}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "pcs", "quantity": 756}], "supper": [{"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "grams", "quantity": 858}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "grams", "quantity": 619}, {"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "milliliters", "quantity": 800}], "breakfast": [{"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "pcs", "quantity": 363}, {"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "grams", "quantity": 539}]}, "17": {"lunch": [{"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "grams", "quantity": 134}], "dinner": [{"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "milliliters", "quantity": 977}], "snacks": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "pcs", "quantity": 211}], "supper": [{"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "pcs", "quantity": 209}, {"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "grams", "quantity": 227}], "breakfast": [{"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "grams", "quantity": 195}, {"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "pcs", "quantity": 917}, {"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "grams", "quantity": 706}]}, "18": {"lunch": [{"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "pcs", "quantity": 296}, {"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "milliliters", "quantity": 239}, {"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "grams", "quantity": 798}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 155}], "dinner": [{"name": "Little Hamburger", "uniq": "Little Hamburger-1709115623", "portion": "grams", "quantity": 959}, {"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "pcs", "quantity": 958}], "snacks": [{"name": "Pasta", "uniq": "Pasta-1709115623", "portion": "milliliters", "quantity": 984}], "supper": [{"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "pcs", "quantity": 134}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 432}], "breakfast": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "milliliters", "quantity": 242}]}, "19": {"lunch": [{"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 906}, {"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "milliliters", "quantity": 152}], "dinner": [{"name": "Cheeseburger", "uniq": "Cheeseburger-1709115623", "portion": "milliliters", "quantity": 610}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 605}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "grams", "quantity": 498}], "snacks": [{"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "grams", "quantity": 616}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 512}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "grams", "quantity": 146}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "pcs", "quantity": 200}], "supper": [{"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 764}, {"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "grams", "quantity": 353}, {"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 181}], "breakfast": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 783}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "grams", "quantity": 506}, {"name": "Bacon Cheeseburger", "uniq": "Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 887}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "milliliters", "quantity": 130}]}, "20": {"lunch": [{"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "grams", "quantity": 48}], "dinner": [{"name": "Bacon Dog", "uniq": "Bacon Dog-1709115623", "portion": "milliliters", "quantity": 266}, {"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "grams", "quantity": 143}], "snacks": [{"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "grams", "quantity": 55}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "grams", "quantity": 174}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "grams", "quantity": 855}, {"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "milliliters", "quantity": 6}], "supper": [{"name": "Grilled Cheese", "uniq": "Grilled Cheese-1709115623", "portion": "pcs", "quantity": 296}, {"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "grams", "quantity": 878}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "pcs", "quantity": 376}], "breakfast": [{"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "milliliters", "quantity": 442}, {"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "pcs", "quantity": 528}, {"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "milliliters", "quantity": 558}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "pcs", "quantity": 885}]}, "21": {"lunch": [{"name": "Little Bacon Burger", "uniq": "Little Bacon Burger-1709115623", "portion": "grams", "quantity": 504}, {"name": "Little Bacon Cheeseburger", "uniq": "Little Bacon Cheeseburger-1709115623", "portion": "milliliters", "quantity": 652}, {"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "grams", "quantity": 996}], "dinner": [{"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "milliliters", "quantity": 200}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "pcs", "quantity": 641}, {"name": "Cheese Veggie Sandwich", "uniq": "Cheese Veggie Sandwich-1709115623", "portion": "milliliters", "quantity": 87}, {"name": "Cheese Dog", "uniq": "Cheese Dog-1709115623", "portion": "pcs", "quantity": 689}], "snacks": [{"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "grams", "quantity": 143}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "pcs", "quantity": 528}, {"name": "Veggie Sandwich", "uniq": "Veggie Sandwich-1709115623", "portion": "grams", "quantity": 475}], "supper": [{"name": "Little Cheeseburger", "uniq": "Little Cheeseburger-1709115623", "portion": "grams", "quantity": 924}, {"name": "Hamburger", "uniq": "Hamburger-1709115623", "portion": "grams", "quantity": 98}], "breakfast": [{"name": "Bacon Cheese Dog", "uniq": "Bacon Cheese Dog-1709115623", "portion": "grams", "quantity": 438}, {"name": "Cheese Pizza", "uniq": "Cheese Pizza-1709115623", "portion": "milliliters", "quantity": 383}, {"name": "Bacon Burger", "uniq": "Bacon Burger-1709115623", "portion": "grams", "quantity": 462}]}}' + }; + + var response = await http.post( + Uri.parse(url), + headers: { + 'Content-Type': 'application/json', + }, + body: json.encode(credentials), + encoding: utf8, + ); + + return response; + } +} diff --git a/lib/presentation/plans/plan_create_screen/bloc/plan_create_event.dart b/lib/presentation/plans/plan_create_screen/bloc/plan_create_event.dart new file mode 100644 index 00000000..0bcfb740 --- /dev/null +++ b/lib/presentation/plans/plan_create_screen/bloc/plan_create_event.dart @@ -0,0 +1,21 @@ +// ignore_for_file: must_be_immutable + +part of 'plan_create_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///PlanCreate widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class PlanCreateEvent extends Equatable {} + +/// Event that is dispatched when the PlanCreate widget is first created. +class PlanCreateInitialEvent extends PlanCreateEvent { + @override + List get props => []; +} + +class CreatePlan extends PlanCreateEvent { + @override + List get props => []; +} diff --git a/lib/presentation/plans/plan_create_screen/bloc/plan_create_state.dart b/lib/presentation/plans/plan_create_screen/bloc/plan_create_state.dart new file mode 100644 index 00000000..93e4aaf2 --- /dev/null +++ b/lib/presentation/plans/plan_create_screen/bloc/plan_create_state.dart @@ -0,0 +1,65 @@ +// ignore_for_file: must_be_immutable + +part of 'plan_create_bloc.dart'; + +/// Represents the state of PlanCreate in the application. +/// + +class PlanCreateState extends Equatable { + PlanCreateState({ + this.planModelObj, + }); + + PlanCreateModel? planModelObj; + + @override + List get props => [planModelObj]; + PlanCreateState copyWith({PlanCreateModel? planModelObj}) { + return PlanCreateState( + planModelObj: planModelObj ?? this.planModelObj, + ); + } +} + +class PlanCreateLoadedState extends PlanCreateState { + PlanCreateLoadedState({ + this.planModelObj, + this.name, + this.details, + }); + + PlanCreateModel? planModelObj; + TextEditingController? name; + TextEditingController? details; + @override + List get props => [ + planModelObj, + name, + details, + ]; + PlanCreateLoadedState copyWith({ + PlanCreateModel? planModelObj, + TextEditingController? name, + TextEditingController? details, + }) { + return PlanCreateLoadedState( + planModelObj: planModelObj ?? this.planModelObj, + name: name ?? this.name, + details: details ?? this.details, + ); + } +} + +class PlanCreateErrorState extends PlanCreateState { + final String errorMessage; + + PlanCreateErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} + +class PlanCreateInitialState extends PlanCreateState { + PlanCreateInitialState({required PlanCreateModel planModelObj}) + : super(planModelObj: planModelObj); +} diff --git a/lib/presentation/plans/plan_create_screen/models/plan_create_model.dart b/lib/presentation/plans/plan_create_screen/models/plan_create_model.dart new file mode 100644 index 00000000..b105414c --- /dev/null +++ b/lib/presentation/plans/plan_create_screen/models/plan_create_model.dart @@ -0,0 +1,19 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; + +/// This class defines the variables used in the [plan_create_screen], +/// and is typically used to hold data that is passed between different parts of the application. + +class PlanCreateModel extends Equatable { + final String planID; + + PlanCreateModel({required String planID}) : planID = planID; + + PlanCreateModel copyWith(String planID) { + return PlanCreateModel(planID: planID); + } + + @override + List get props => [planID]; +} diff --git a/lib/presentation/plans/plan_create_screen/plan_create_screen.dart b/lib/presentation/plans/plan_create_screen/plan_create_screen.dart new file mode 100644 index 00000000..af04abc7 --- /dev/null +++ b/lib/presentation/plans/plan_create_screen/plan_create_screen.dart @@ -0,0 +1,97 @@ +import 'bloc/plan_create_bloc.dart'; +import 'models/plan_create_model.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:flutter/material.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; + +class PlanCreateScreen extends StatelessWidget { + const PlanCreateScreen({Key? key}) : super(key: key); + static Widget builder(BuildContext context) { + final planID = ModalRoute.of(context)!.settings.arguments; + return BlocProvider( + create: (context) => PlanCreateBloc(PlanCreateState( + planModelObj: PlanCreateModel(planID: planID.toString()))) + ..add(PlanCreateInitialEvent()), + child: PlanCreateScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is PlanCreateInitialState) { + // Это начальное состояние, данные ещё не загружены. + return Container( + color: Colors.white, + ); + } else if (state is PlanCreateLoadedState) { + // Данные загружены, можно строить экран. + return _buildPlanCreate(context, state); + } else if (state is PlanCreateErrorState) { + // Произошла ошибка при загрузке данных. + return Center(child: Text(state.errorMessage)); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildPlanCreate(BuildContext context, PlanCreateLoadedState state) { + mediaQueryData = MediaQuery.of(context); + return SafeArea( + child: Scaffold( + resizeToAvoidBottomInset: false, + body: Container( + width: double.maxFinite, + decoration: AppDecoration.fillGray, + child: ListView(children: [ + SizedBox(height: 30.v), + Container( + margin: EdgeInsets.only( + left: 16.h, + right: 20.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + SizedBox(height: 16.0), + TextFormField( + controller: state.name, + decoration: InputDecoration(labelText: 'Заголовок'), + ), + SizedBox(height: 16.0), + TextFormField( + controller: state.details, + decoration: InputDecoration(labelText: 'Описание'), + ), + SizedBox(height: 16.0), + // Здесь нужно добавить виджет для загрузки картинки + ElevatedButton( + onPressed: () => + context.read().add(CreatePlan()), + child: Text('Создать'), + ) + ]), + ) + ]), + ), + bottomNavigationBar: _buildBottomBar(context))); + } +} + +_buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); +} diff --git a/lib/presentation/plans/plan_edit_screen/bloc/plan_edit_bloc.dart b/lib/presentation/plans/plan_edit_screen/bloc/plan_edit_bloc.dart new file mode 100644 index 00000000..e8fa9eae --- /dev/null +++ b/lib/presentation/plans/plan_edit_screen/bloc/plan_edit_bloc.dart @@ -0,0 +1,118 @@ +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:hive/hive.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/plans/plan_edit_screen/models/plan_edit_model.dart'; +part 'plan_edit_event.dart'; +part 'plan_edit_state.dart'; + +/// A bloc that manages the state of a PlanEdit according to the event that is dispatched to it. +class PlanEditBloc extends Bloc { + PlanEditBloc(PlanEditState initialState) : super(initialState) { + on(_onInitialize); + on(_editPlan); + } + + _onInitialize( + PlanEditInitialEvent event, + Emitter emit, + ) async { + try { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + final String? planID = state.planModelObj?.planID; + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/plans/show/$planID'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + if (data is Map) { + if (state is PlanEditLoadedState) { + var currentState = state as PlanEditLoadedState; + emit(currentState.copyWith( + planModelObj: currentState.planModelObj, + plan: currentState.plan, + name: currentState.name, + duration: currentState.duration, + details: currentState.details)); + } else { + emit(PlanEditLoadedState( + plan: data, + name: TextEditingController(text: data['name']), + details: TextEditingController(text: data['details']), + duration: + TextEditingController(text: data['duration'].toString()))); + } + } else { + // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку + emit(PlanEditErrorState('Некоторые необходимые данные отсутствуют.')); + } + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(PlanEditErrorState('Ошибка сервера: ${response.statusCode}')); + } + } catch (e) { + // Ошибка при декодировании JSON или другая ошибка выполнения + emit(PlanEditErrorState('Ошибка при получении данных о чатах: $e')); + } + } + + FutureOr _editPlan(EditPlan event, Emitter emit) async { + if (state is PlanEditLoadedState) { + var currentState = state as PlanEditLoadedState; + int duration = int.parse(currentState.duration!.text) * 7; + print(duration); + var response = await sendPlan(currentState.plan, currentState.name?.text, + currentState.details?.text, duration.toString()); + + if (response.statusCode == 200) { + NavigatorService.pushNamed(AppRoutes.planScreen, + arguments: currentState.plan?['id'].toString()); + } else { + print( + 'Failed to edit blog post. Status Code: ${response.statusCode} ${response.body}'); + } + } + } + + FutureOr sendPlan( + Map? plan, + String? name, + String? details, + String? duration, + ) async { + int id = plan?['id']; + String url = '${dotenv.env['SERVER_ROOT']!}/plans/update/$id'; + + final Map credentials = { + 'id': id, + 'name': name, + 'details': details, + 'duration': '14', + }; + + var response = await http.post( + Uri.parse(url), + headers: { + 'Content-Type': 'application/json', + }, + body: json.encode(credentials), + encoding: utf8, + ); + + return response; + } +} diff --git a/lib/presentation/plans/plan_edit_screen/bloc/plan_edit_event.dart b/lib/presentation/plans/plan_edit_screen/bloc/plan_edit_event.dart new file mode 100644 index 00000000..2596b8aa --- /dev/null +++ b/lib/presentation/plans/plan_edit_screen/bloc/plan_edit_event.dart @@ -0,0 +1,21 @@ +// ignore_for_file: must_be_immutable + +part of 'plan_edit_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///PlanEdit widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class PlanEditEvent extends Equatable {} + +/// Event that is dispatched when the PlanEdit widget is first created. +class PlanEditInitialEvent extends PlanEditEvent { + @override + List get props => []; +} + +class EditPlan extends PlanEditEvent { + @override + List get props => []; +} diff --git a/lib/presentation/plans/plan_edit_screen/bloc/plan_edit_state.dart b/lib/presentation/plans/plan_edit_screen/bloc/plan_edit_state.dart new file mode 100644 index 00000000..427bf0df --- /dev/null +++ b/lib/presentation/plans/plan_edit_screen/bloc/plan_edit_state.dart @@ -0,0 +1,64 @@ +// ignore_for_file: must_be_immutable + +part of 'plan_edit_bloc.dart'; + +/// Represents the state of PlanEdit in the application. +/// + +class PlanEditState extends Equatable { + PlanEditState({ + this.planModelObj, + }); + + PlanEditModel? planModelObj; + + @override + List get props => [planModelObj]; + PlanEditState copyWith({PlanEditModel? planModelObj}) { + return PlanEditState( + planModelObj: planModelObj ?? this.planModelObj, + ); + } +} + +class PlanEditLoadedState extends PlanEditState { + PlanEditLoadedState( + {this.planModelObj, this.plan, this.name, this.details, this.duration}); + + PlanEditModel? planModelObj; + Map? plan; + TextEditingController? name; + TextEditingController? details; + TextEditingController? duration; + @override + List get props => [planModelObj, plan, name, details, duration]; + PlanEditLoadedState copyWith({ + PlanEditModel? planModelObj, + Map? plan, + TextEditingController? name, + TextEditingController? details, + TextEditingController? duration, + }) { + return PlanEditLoadedState( + planModelObj: planModelObj ?? this.planModelObj, + plan: plan ?? this.plan, + name: name ?? this.name, + details: details ?? this.details, + duration: duration ?? this.duration, + ); + } +} + +class PlanEditErrorState extends PlanEditState { + final String errorMessage; + + PlanEditErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} + +class PlanEditInitialState extends PlanEditState { + PlanEditInitialState({required PlanEditModel planModelObj}) + : super(planModelObj: planModelObj); +} diff --git a/lib/presentation/plans/plan_edit_screen/models/plan_edit_model.dart b/lib/presentation/plans/plan_edit_screen/models/plan_edit_model.dart new file mode 100644 index 00000000..22c3d277 --- /dev/null +++ b/lib/presentation/plans/plan_edit_screen/models/plan_edit_model.dart @@ -0,0 +1,19 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; + +/// This class defines the variables used in the [plan_edit_screen], +/// and is typically used to hold data that is passed between different parts of the application. + +class PlanEditModel extends Equatable { + final String planID; + + PlanEditModel({required String planID}) : planID = planID; + + PlanEditModel copyWith(String planID) { + return PlanEditModel(planID: planID); + } + + @override + List get props => [planID]; +} diff --git a/lib/presentation/plans/plan_edit_screen/plan_edit_screen.dart b/lib/presentation/plans/plan_edit_screen/plan_edit_screen.dart new file mode 100644 index 00000000..71ac12b7 --- /dev/null +++ b/lib/presentation/plans/plan_edit_screen/plan_edit_screen.dart @@ -0,0 +1,114 @@ +import 'bloc/plan_edit_bloc.dart'; +import 'models/plan_edit_model.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:flutter/material.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +class PlanEditScreen extends StatelessWidget { + const PlanEditScreen({Key? key}) : super(key: key); + static Widget builder(BuildContext context) { + final planID = ModalRoute.of(context)!.settings.arguments; + return BlocProvider( + create: (context) => PlanEditBloc(PlanEditState( + planModelObj: PlanEditModel(planID: planID.toString()))) + ..add(PlanEditInitialEvent()), + child: PlanEditScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is PlanEditInitialState) { + // Это начальное состояние, данные ещё не загружены. + return Container( + color: Colors.white, + ); + } else if (state is PlanEditLoadedState) { + // Данные загружены, можно строить экран. + return _buildPlanEdit(context, state); + } else if (state is PlanEditErrorState) { + // Произошла ошибка при загрузке данных. + return Center(child: Text(state.errorMessage)); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildPlanEdit(BuildContext context, PlanEditLoadedState state) { + mediaQueryData = MediaQuery.of(context); + return SafeArea( + child: Scaffold( + resizeToAvoidBottomInset: false, + body: Container( + width: double.maxFinite, + decoration: AppDecoration.fillGray, + child: ListView(children: [ + SizedBox(height: 30.v), + Container( + margin: EdgeInsets.only( + left: 16.h, + right: 20.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + + (state.plan?['image'] ?? ''), + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundImage: imageProvider, + radius: 25, + ), + placeholder: (context, url) => + CircularProgressIndicator(), + errorWidget: (context, url, error) => CircleAvatar( + child: Icon(Icons.person), + backgroundColor: Colors.white, + radius: 25, + ), + ), + SizedBox(height: 16.0), + TextFormField( + controller: state.name, + decoration: InputDecoration(labelText: 'Заголовок'), + ), + SizedBox(height: 16.0), + TextFormField( + controller: state.details, + decoration: InputDecoration(labelText: 'Описание'), + ), + SizedBox(height: 16.0), + // Здесь нужно добавить виджет для загрузки картинки + ElevatedButton( + onPressed: () => + context.read().add(EditPlan()), + child: Text('Сохранить'), + ) + ]), + ) + ]), + ), + bottomNavigationBar: _buildBottomBar(context))); + } +} + +_buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); +} diff --git a/lib/presentation/plans/plan_select_screen/bloc/plan_select_bloc.dart b/lib/presentation/plans/plan_select_screen/bloc/plan_select_bloc.dart new file mode 100644 index 00000000..40d5f6fd --- /dev/null +++ b/lib/presentation/plans/plan_select_screen/bloc/plan_select_bloc.dart @@ -0,0 +1,70 @@ +//import 'package:eatvio_app/presentation/auth/auth_screen/auth_screen.dart'; +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/plans/plan_select_screen/models/plan_select_model.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import 'package:hive/hive.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:image_picker/image_picker.dart'; + +part 'plan_select_event.dart'; +part 'plan_select_state.dart'; + +/// A bloc that manages the state of a PlanSelect according to the event that is dispatched to it. +class PlanSelectBloc extends Bloc { + PlanSelectBloc(PlanSelectState initialState) : super(initialState) { + on(_onInitialize); + } + + Future _onInitialize( + PlanSelectInitialEvent event, + Emitter emit, + ) async { + try { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); // Если токен есть, запускаем приложение + } + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/plans/index'), + ); + print('Got response'); + if (response.statusCode == 200) { + print('Got 200'); + final data = jsonDecode(response.body); + if (data is List) { + print('Got List'); + if (state is PlanSelectLoadedState) { + print('Got loaded'); + var currentState = state as PlanSelectLoadedState; + emit((currentState).copyWith( + posts: data, + myProfileModelObj: currentState.myProfileModelObj, + )); + } else { + print('Got unloaded'); + emit(PlanSelectLoadedState( + posts: data, + )); + } + } else { + // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку + emit(PlanSelectErrorState('Неверный формат ответа API.')); + } + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(PlanSelectErrorState('Ошибка сервера: ${response.statusCode}')); + } + } catch (e) { + // Ошибка при декодировании JSON или другая ошибка выполнения + emit(PlanSelectErrorState('Ошибка при получении данных ленты: $e')); + } + } +} diff --git a/lib/presentation/plans/plan_select_screen/bloc/plan_select_event.dart b/lib/presentation/plans/plan_select_screen/bloc/plan_select_event.dart new file mode 100644 index 00000000..5cb4b41b --- /dev/null +++ b/lib/presentation/plans/plan_select_screen/bloc/plan_select_event.dart @@ -0,0 +1,23 @@ +// ignore_for_file: must_be_immutable + +part of 'plan_select_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///PlanSelect widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class PlanSelectEvent extends Equatable {} + +/// Event that is dispatched when the PlanSelect widget is first created. +class PlanSelectInitialEvent extends PlanSelectEvent { + @override + List get props => []; +} + +class UploadAvatar extends PlanSelectEvent { + UploadAvatar(this.image); + final XFile image; + @override + List get props => [image]; +} diff --git a/lib/presentation/plans/plan_select_screen/bloc/plan_select_state.dart b/lib/presentation/plans/plan_select_screen/bloc/plan_select_state.dart new file mode 100644 index 00000000..5ab68c28 --- /dev/null +++ b/lib/presentation/plans/plan_select_screen/bloc/plan_select_state.dart @@ -0,0 +1,49 @@ +// ignore_for_file: must_be_immutable + +part of 'plan_select_bloc.dart'; + +class PlanSelectState extends Equatable { + const PlanSelectState({this.myProfileModelObj}); + + final PlanSelectModel? myProfileModelObj; + + @override + List get props => [myProfileModelObj]; +} + +class PlanSelectInitialState extends PlanSelectState { + PlanSelectInitialState({required PlanSelectModel myProfileModelObj}) + : super(myProfileModelObj: myProfileModelObj); +} + +/// Represents the state of PlanSelect in the application. +class PlanSelectLoadedState extends PlanSelectState { + PlanSelectLoadedState({this.myProfileModelObj, this.posts}) + : super(myProfileModelObj: null); + + PlanSelectModel? myProfileModelObj; + List? posts; + @override + List get props => [ + myProfileModelObj, + posts, + ]; + PlanSelectLoadedState copyWith({ + PlanSelectModel? myProfileModelObj, + List? posts, + }) { + return PlanSelectLoadedState( + myProfileModelObj: myProfileModelObj ?? this.myProfileModelObj, + posts: posts ?? this.posts, + ); + } +} + +class PlanSelectErrorState extends PlanSelectState { + final String errorMessage; + + const PlanSelectErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} diff --git a/lib/presentation/plans/plan_select_screen/models/plan_select_model.dart b/lib/presentation/plans/plan_select_screen/models/plan_select_model.dart new file mode 100644 index 00000000..238869f6 --- /dev/null +++ b/lib/presentation/plans/plan_select_screen/models/plan_select_model.dart @@ -0,0 +1,16 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; + +/// This class defines the variables used in the [k0_page], +/// and is typically used to hold data that is passed between different parts of the application. +class PlanSelectModel extends Equatable { + PlanSelectModel(); + + PlanSelectModel copyWith() { + return PlanSelectModel(); + } + + @override + List get props => []; +} diff --git a/lib/presentation/plans/plan_select_screen/plan_select_screen.dart b/lib/presentation/plans/plan_select_screen/plan_select_screen.dart new file mode 100644 index 00000000..7f3486a2 --- /dev/null +++ b/lib/presentation/plans/plan_select_screen/plan_select_screen.dart @@ -0,0 +1,184 @@ +import 'bloc/plan_select_bloc.dart'; +import 'models/plan_select_model.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:eatvio_app/widgets/app_bar/appbar_subtitle.dart'; +import 'package:eatvio_app/widgets/app_bar/appbar_leading_image.dart'; +import 'package:eatvio_app/widgets/app_bar/custom_app_bar.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter/material.dart'; + +class PlanSelectScreen extends StatelessWidget { + static Widget builder(BuildContext context) { + return BlocProvider( + create: (context) => PlanSelectBloc( + PlanSelectState(myProfileModelObj: PlanSelectModel())) + ..add(PlanSelectInitialEvent()), + child: PlanSelectScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is PlanSelectInitialState) { + // Это начальное состояние, данные ещё не загружены. + return CircularProgressIndicator( + color: Colors.white, + backgroundColor: Colors.white, + ); + } else if (state is PlanSelectLoadedState) { + // Данные загружены, можно строить экран. + return _buildSetingsScreen(context, state); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildSetingsScreen(BuildContext context, PlanSelectLoadedState state) { + mediaQueryData = MediaQuery.of(context); + return SafeArea( + child: Scaffold( + backgroundColor: Colors.grey[200], + resizeToAvoidBottomInset: true, + body: SingleChildScrollView( + child: Container( + width: double.maxFinite, + decoration: BoxDecoration(color: Colors.grey[200]), + child: Padding( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox(height: 10), + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: CustomAppBar( + height: 54.v, + leadingWidth: 28.h, + leading: AppbarLeadingImage( + imagePath: ImageConstant.imgArrowLeft, + margin: EdgeInsets.only( + left: 16.h, top: 15.v, bottom: 15.v), + onTap: onTapBack(context), + ), + title: Padding( + padding: EdgeInsets.only(left: 20.h), + child: Row( + children: [ + AppbarSubtitle( + text: "Планы", + margin: EdgeInsets.only( + left: 80.h, top: 12.v, bottom: 9.v), + ), + Spacer(), + Padding( + padding: const EdgeInsets.only(right: 20.0), + child: GestureDetector( + onTap: () => Navigator.pushNamed( + context, + AppRoutes.planCreateScreen, + ), + child: Icon(Icons.create_outlined)), + ) + ], + ), + ), + ), + ), + SizedBox(height: 20), + _buildPlanList(context, state) + ], + ), + ), + ), + ), + bottomNavigationBar: _buildBottomBar(context), + ), + ); + } + + _buildPlanList(BuildContext context, PlanSelectLoadedState state) { + return ListView.builder( + shrinkWrap: true, // Add this line + itemCount: state.posts?.length ?? 0, + itemBuilder: (context, index) { + final post = state.posts?[index]; + return Container( + margin: EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: ListTile( + leading: CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + post['image'], + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundImage: imageProvider, + radius: 25, + ), + placeholder: (context, url) => CircularProgressIndicator(), + errorWidget: (context, url, error) => CircleAvatar( + child: Icon(Icons.person), + backgroundColor: Colors.white, + radius: 25, + ), + ), + title: Text( + post?['name'] ?? '', + style: CustomTextStyles.titleMediumSemiBold, + ), + subtitle: Text(post?['details'] ?? ''), + onTap: () => Navigator.pushNamed( + context, + AppRoutes.planScreen, + arguments: post?['id'].toString(), + ), + trailing: GestureDetector( + onTap: () => Navigator.pushNamed( + context, + AppRoutes.planEditScreen, + arguments: post?['id'].toString(), + ), + child: Icon(Icons.edit)), + )); + }, + ); + } + + Function onTapBack(BuildContext context) { + return () { + onTapArrowLeft(context); + }; + } + + _buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); + } + + onTapArrowLeft(BuildContext context) { + NavigatorService.goBack(); + } +} diff --git a/lib/presentation/plans/plan_show_screen/bloc/plan_show_bloc.dart b/lib/presentation/plans/plan_show_screen/bloc/plan_show_bloc.dart new file mode 100644 index 00000000..616b521a --- /dev/null +++ b/lib/presentation/plans/plan_show_screen/bloc/plan_show_bloc.dart @@ -0,0 +1,88 @@ +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:hive/hive.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/plans/plan_show_screen/models/plan_show_model.dart'; +part 'plan_show_event.dart'; +part 'plan_show_state.dart'; + +/// A bloc that manages the state of a Plan according to the event that is dispatched to it. +class PlanBloc extends Bloc { + PlanBloc(PlanState initialState) : super(initialState) { + on(_onInitialize); + on(_deleteEntry); + } + + _onInitialize( + PlanInitialEvent event, + Emitter emit, + ) async { + try { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + final String? planID = state.planModelObj?.planID; + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/plans/show/$planID'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + if (data is Map) { + if (state is PlanLoadedState) { + var currentState = state as PlanLoadedState; + emit(currentState.copyWith( + planModelObj: currentState.planModelObj, + plan: currentState.plan)); + } else { + emit(PlanLoadedState(plan: data)); + } + } else { + // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку + emit(PlanErrorState('Некоторые необходимые данные отсутствуют.')); + } + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(PlanErrorState('Ошибка сервера: ${response.statusCode}')); + } + } catch (e) { + // Ошибка при декодировании JSON или другая ошибка выполнения + emit(PlanErrorState('Ошибка при получении данных о чатах: $e')); + } + } + + FutureOr _deleteEntry( + DeleteEntry event, Emitter emit) async { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + final int id = event.id; + final response = await http.post( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/plans/delete/$id'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + if (response.statusCode == 200) { + NavigatorService.pushNamed(AppRoutes.planSelectScreen); + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(PlanErrorState('Ошибка: ${response.statusCode}')); + } + } +} diff --git a/lib/presentation/plans/plan_show_screen/bloc/plan_show_event.dart b/lib/presentation/plans/plan_show_screen/bloc/plan_show_event.dart new file mode 100644 index 00000000..fc83e024 --- /dev/null +++ b/lib/presentation/plans/plan_show_screen/bloc/plan_show_event.dart @@ -0,0 +1,23 @@ +// ignore_for_file: must_be_immutable + +part of 'plan_show_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///Plan widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class PlanEvent extends Equatable {} + +/// Event that is dispatched when the Plan widget is first created. +class PlanInitialEvent extends PlanEvent { + @override + List get props => []; +} + +class DeleteEntry extends PlanEvent { + DeleteEntry(this.id); + final int id; + @override + List get props => [id]; +} diff --git a/lib/presentation/plans/plan_show_screen/bloc/plan_show_state.dart b/lib/presentation/plans/plan_show_screen/bloc/plan_show_state.dart new file mode 100644 index 00000000..cefb6cf6 --- /dev/null +++ b/lib/presentation/plans/plan_show_screen/bloc/plan_show_state.dart @@ -0,0 +1,52 @@ +// ignore_for_file: must_be_immutable + +part of 'plan_show_bloc.dart'; + +/// Represents the state of Plan in the application. +/// + +class PlanState extends Equatable { + PlanState({ + this.planModelObj, + }); + + PlanModel? planModelObj; + + @override + List get props => [planModelObj]; + PlanState copyWith({PlanModel? planModelObj}) { + return PlanState( + planModelObj: planModelObj ?? this.planModelObj, + ); + } +} + +class PlanLoadedState extends PlanState { + PlanLoadedState({this.planModelObj, this.plan}); + + PlanModel? planModelObj; + Map? plan; + @override + List get props => [planModelObj, plan]; + PlanLoadedState copyWith( + {PlanModel? planModelObj, Map? plan}) { + return PlanLoadedState( + planModelObj: planModelObj ?? this.planModelObj, + plan: plan ?? this.plan, + ); + } +} + +class PlanErrorState extends PlanState { + final String errorMessage; + + PlanErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} + +class PlanInitialState extends PlanState { + PlanInitialState({required PlanModel planModelObj}) + : super(planModelObj: planModelObj); +} diff --git a/lib/presentation/plans/plan_show_screen/models/plan_show_model.dart b/lib/presentation/plans/plan_show_screen/models/plan_show_model.dart new file mode 100644 index 00000000..6a6c37c2 --- /dev/null +++ b/lib/presentation/plans/plan_show_screen/models/plan_show_model.dart @@ -0,0 +1,19 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; + +/// This class defines the variables used in the [plan_screen], +/// and is typically used to hold data that is passed between different parts of the application. + +class PlanModel extends Equatable { + final String planID; + + PlanModel({required String planID}) : planID = planID; + + PlanModel copyWith(String planID) { + return PlanModel(planID: planID); + } + + @override + List get props => [planID]; +} diff --git a/lib/presentation/plans/plan_show_screen/plan_show_screen.dart b/lib/presentation/plans/plan_show_screen/plan_show_screen.dart new file mode 100644 index 00000000..d34e1a43 --- /dev/null +++ b/lib/presentation/plans/plan_show_screen/plan_show_screen.dart @@ -0,0 +1,153 @@ +import 'bloc/plan_show_bloc.dart'; +import 'models/plan_show_model.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:flutter/material.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +class PlanScreen extends StatelessWidget { + const PlanScreen({Key? key}) : super(key: key); + static Widget builder(BuildContext context) { + final planID = ModalRoute.of(context)!.settings.arguments; + return BlocProvider( + create: (context) => PlanBloc( + PlanState(planModelObj: PlanModel(planID: planID.toString()))) + ..add(PlanInitialEvent()), + child: PlanScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is PlanInitialState) { + // Это начальное состояние, данные ещё не загружены. + return Container( + color: Colors.white, + ); + } else if (state is PlanLoadedState) { + // Данные загружены, можно строить экран. + return _buildPlan(context, state); + } else if (state is PlanErrorState) { + // Произошла ошибка при загрузке данных. + return Center(child: Text(state.errorMessage)); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildPlan(BuildContext context, PlanLoadedState state) { + mediaQueryData = MediaQuery.of(context); + return SafeArea( + child: Scaffold( + resizeToAvoidBottomInset: false, + body: Container( + width: double.maxFinite, + decoration: AppDecoration.fillGray, + child: ListView( + children: [ + SizedBox(height: 30.v), + Container( + margin: EdgeInsets.only( + left: 16.h, + right: 20.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + + (state.plan?['image'] ?? ''), + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundImage: imageProvider, + radius: 25, + ), + placeholder: (context, url) => + CircularProgressIndicator(), + errorWidget: (context, url, error) => CircleAvatar( + child: Icon(Icons.person), + backgroundColor: Colors.white, + radius: 25, + ), + ), + SizedBox(height: 16.0), + Text(state.plan?['name'], + style: CustomTextStyles.titleMediumBlack), + SizedBox(height: 8.0), + Text( + state.plan?['details'], + ), + SizedBox(height: 16.0), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + ElevatedButton( + onPressed: () { + // Действие для редактирования + }, + child: Text('Редактировать'), + ), + ElevatedButton( + onPressed: () => context + .read() + .add(DeleteEntry(state.plan?['id'])), + child: Text('Удалить'), + ), + ], + ) + ]), + ) + ], + ), + ), + bottomNavigationBar: _buildBottomBar(context))); + } + + + ///Handling route based on bottom click actions + String getCurrentRoute(BottomBarEnum type) { + switch (type) { + case BottomBarEnum.tf: + return AppRoutes.planScreen; + default: + return "/"; + } + } + + ///Handling page based on route + Widget getCurrentPage( + BuildContext context, + String currentRoute, + ) { + switch (currentRoute) { + case AppRoutes.planScreen: + return PlanScreen.builder(context); + default: + return PlanScreen(); + } + } + + _buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); + } + + /// Navigates to the previous screen. + onTapArrowleftone(BuildContext context) { + NavigatorService.goBack(); + } +} diff --git a/lib/presentation/profile/my_profile_screen/my_profile_screen.dart b/lib/presentation/profile/my_profile_screen/my_profile_screen.dart index ad417337..5d1f9bf3 100644 --- a/lib/presentation/profile/my_profile_screen/my_profile_screen.dart +++ b/lib/presentation/profile/my_profile_screen/my_profile_screen.dart @@ -140,12 +140,13 @@ class MyProfileScreen extends StatelessWidget { /// Section Widget _buildInfo(BuildContext context, MyProfileLoadedState state) { - return Column(children: [ - GestureDetector( - onTap: addImage(context), + return Column( + children: [ + GestureDetector( + onTap: () => addImage(context), child: CachedNetworkImage( - imageUrl: - dotenv.env['STORAGE_URL']! + state.user?['profile_photo_path'], + imageUrl: dotenv.env['STORAGE_URL']! + + (state.user?['profile_photo_path'] ?? ''), imageBuilder: (context, imageProvider) => CircleAvatar( backgroundImage: imageProvider, radius: 60, @@ -153,14 +154,16 @@ class MyProfileScreen extends StatelessWidget { placeholder: (context, url) => CircularProgressIndicator(), errorWidget: (context, url, error) => CircleAvatar( child: Icon(Icons.person), - backgroundColor: Colors.grey[200], + backgroundColor: Colors.white, radius: 60, ), - )), - AppbarSubtitle(text: state.user?['name']), - SizedBox(height: 20.v), - Text(state.userDetails?['additional_info']), - ]); + ), + ), + AppbarSubtitle(text: state.user?['name'] ?? ''), + SizedBox(height: 20.v), + Text(state.userDetails?['additional_info'] ?? ''), + ], + ); } addImage(BuildContext context) { diff --git a/lib/presentation/recipe/recipe_create_screen/bloc/recipe_create_bloc.dart b/lib/presentation/recipe/recipe_create_screen/bloc/recipe_create_bloc.dart new file mode 100644 index 00000000..d438c245 --- /dev/null +++ b/lib/presentation/recipe/recipe_create_screen/bloc/recipe_create_bloc.dart @@ -0,0 +1,92 @@ +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:hive/hive.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/recipe/recipe_create_screen/models/recipe_create_model.dart'; +part 'recipe_create_event.dart'; +part 'recipe_create_state.dart'; + +/// A bloc that manages the state of a RecipeCreate according to the event that is dispatched to it. +class RecipeCreateBloc extends Bloc { + RecipeCreateBloc(RecipeCreateState initialState) : super(initialState) { + on(_onInitialize); + on(_createRecipe); + } + + _onInitialize( + RecipeCreateInitialEvent event, + Emitter emit, + ) async { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + if (state is RecipeCreateLoadedState) { + var currentState = state as RecipeCreateLoadedState; + emit(currentState.copyWith( + recipeModelObj: currentState.recipeModelObj, + title: currentState.title, + description: currentState.description, + video_url: currentState.video_url)); + } else { + emit(RecipeCreateLoadedState( + title: TextEditingController(), + description: TextEditingController(), + video_url: TextEditingController())); + } + } + + FutureOr _createRecipe( + CreateRecipe event, Emitter emit) async { + if (state is RecipeCreateLoadedState) { + var currentState = state as RecipeCreateLoadedState; + + var response = await sendRecipe( + currentState.title?.text, + currentState.video_url?.text, + currentState.description?.text, + ); + + if (response.statusCode == 200) { + NavigatorService.pushNamed(AppRoutes.recipeSelectScreen); + } else { + print( + 'Failed to create blog post. Status Code: ${response.statusCode} ${response.body}'); + } + } + } + + FutureOr sendRecipe( + String? title, + String? video_url, + String? description, + ) async { + String url = '${dotenv.env['SERVER_ROOT']!}/recipe/create'; + + final Map credentials = { + 'title': title, + 'description': description, + 'video_url': video_url, + 'status': 'active', + }; + + var response = await http.post( + Uri.parse(url), + headers: { + 'Content-Type': 'application/json', + }, + body: json.encode(credentials), + encoding: utf8, + ); + + return response; + } +} diff --git a/lib/presentation/recipe/recipe_create_screen/bloc/recipe_create_event.dart b/lib/presentation/recipe/recipe_create_screen/bloc/recipe_create_event.dart new file mode 100644 index 00000000..fcea8078 --- /dev/null +++ b/lib/presentation/recipe/recipe_create_screen/bloc/recipe_create_event.dart @@ -0,0 +1,21 @@ +// ignore_for_file: must_be_immutable + +part of 'recipe_create_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///RecipeCreate widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class RecipeCreateEvent extends Equatable {} + +/// Event that is dispatched when the RecipeCreate widget is first created. +class RecipeCreateInitialEvent extends RecipeCreateEvent { + @override + List get props => []; +} + +class CreateRecipe extends RecipeCreateEvent { + @override + List get props => []; +} diff --git a/lib/presentation/recipe/recipe_create_screen/bloc/recipe_create_state.dart b/lib/presentation/recipe/recipe_create_screen/bloc/recipe_create_state.dart new file mode 100644 index 00000000..cf5b12e3 --- /dev/null +++ b/lib/presentation/recipe/recipe_create_screen/bloc/recipe_create_state.dart @@ -0,0 +1,61 @@ +// ignore_for_file: must_be_immutable + +part of 'recipe_create_bloc.dart'; + +/// Represents the state of RecipeCreate in the application. +/// + +class RecipeCreateState extends Equatable { + RecipeCreateState({ + this.recipeModelObj, + }); + + RecipeCreateModel? recipeModelObj; + + @override + List get props => [recipeModelObj]; + RecipeCreateState copyWith({RecipeCreateModel? recipeModelObj}) { + return RecipeCreateState( + recipeModelObj: recipeModelObj ?? this.recipeModelObj, + ); + } +} + +class RecipeCreateLoadedState extends RecipeCreateState { + RecipeCreateLoadedState( + {this.recipeModelObj, this.title, this.description, this.video_url}); + + RecipeCreateModel? recipeModelObj; + TextEditingController? title; + TextEditingController? description; + TextEditingController? video_url; + @override + List get props => [recipeModelObj, title, description, video_url]; + RecipeCreateLoadedState copyWith({ + RecipeCreateModel? recipeModelObj, + TextEditingController? title, + TextEditingController? description, + TextEditingController? video_url, + }) { + return RecipeCreateLoadedState( + recipeModelObj: recipeModelObj ?? this.recipeModelObj, + title: title ?? this.title, + description: description ?? this.description, + video_url: video_url ?? this.video_url, + ); + } +} + +class RecipeCreateErrorState extends RecipeCreateState { + final String errorMessage; + + RecipeCreateErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} + +class RecipeCreateInitialState extends RecipeCreateState { + RecipeCreateInitialState({required RecipeCreateModel recipeModelObj}) + : super(recipeModelObj: recipeModelObj); +} diff --git a/lib/presentation/recipe/recipe_create_screen/models/recipe_create_model.dart b/lib/presentation/recipe/recipe_create_screen/models/recipe_create_model.dart new file mode 100644 index 00000000..1883aa86 --- /dev/null +++ b/lib/presentation/recipe/recipe_create_screen/models/recipe_create_model.dart @@ -0,0 +1,19 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; + +/// This class defines the variables used in the [recipe_create_screen], +/// and is typically used to hold data that is passed between different parts of the application. + +class RecipeCreateModel extends Equatable { + final String recipeID; + + RecipeCreateModel({required String recipeID}) : recipeID = recipeID; + + RecipeCreateModel copyWith(String recipeID) { + return RecipeCreateModel(recipeID: recipeID); + } + + @override + List get props => [recipeID]; +} diff --git a/lib/presentation/recipe/recipe_create_screen/recipe_create_screen.dart b/lib/presentation/recipe/recipe_create_screen/recipe_create_screen.dart new file mode 100644 index 00000000..82d2bd9a --- /dev/null +++ b/lib/presentation/recipe/recipe_create_screen/recipe_create_screen.dart @@ -0,0 +1,102 @@ +import 'bloc/recipe_create_bloc.dart'; +import 'models/recipe_create_model.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:flutter/material.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; + +class RecipeCreateScreen extends StatelessWidget { + const RecipeCreateScreen({Key? key}) : super(key: key); + static Widget builder(BuildContext context) { + final recipeID = ModalRoute.of(context)!.settings.arguments; + return BlocProvider( + create: (context) => RecipeCreateBloc(RecipeCreateState( + recipeModelObj: RecipeCreateModel(recipeID: recipeID.toString()))) + ..add(RecipeCreateInitialEvent()), + child: RecipeCreateScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is RecipeCreateInitialState) { + // Это начальное состояние, данные ещё не загружены. + return Container( + color: Colors.white, + ); + } else if (state is RecipeCreateLoadedState) { + // Данные загружены, можно строить экран. + return _buildRecipeCreate(context, state); + } else if (state is RecipeCreateErrorState) { + // Произошла ошибка при загрузке данных. + return Center(child: Text(state.errorMessage)); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildRecipeCreate(BuildContext context, RecipeCreateLoadedState state) { + mediaQueryData = MediaQuery.of(context); + return SafeArea( + child: Scaffold( + resizeToAvoidBottomInset: false, + body: Container( + width: double.maxFinite, + decoration: AppDecoration.fillGray, + child: ListView(children: [ + SizedBox(height: 30.v), + Container( + margin: EdgeInsets.only( + left: 16.h, + right: 20.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + SizedBox(height: 16.0), + TextFormField( + controller: state.title, + decoration: InputDecoration(labelText: 'Заголовок'), + ), + SizedBox(height: 16.0), + TextFormField( + controller: state.description, + decoration: InputDecoration(labelText: 'Описание'), + ), + SizedBox(height: 16.0), + TextFormField( + controller: state.video_url, + decoration: InputDecoration(labelText: 'Ссылка'), + ), + SizedBox(height: 16.0), + // Здесь нужно добавить виджет для загрузки картинки + ElevatedButton( + onPressed: () => + context.read().add(CreateRecipe()), + child: Text('Создать'), + ) + ]), + ) + ]), + ), + bottomNavigationBar: _buildBottomBar(context))); + } +} + +_buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); +} diff --git a/lib/presentation/recipe/recipe_edit_screen/bloc/recipe_edit_bloc.dart b/lib/presentation/recipe/recipe_edit_screen/bloc/recipe_edit_bloc.dart new file mode 100644 index 00000000..8cafde30 --- /dev/null +++ b/lib/presentation/recipe/recipe_edit_screen/bloc/recipe_edit_bloc.dart @@ -0,0 +1,126 @@ +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:hive/hive.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/recipe/recipe_edit_screen/models/recipe_edit_model.dart'; +part 'recipe_edit_event.dart'; +part 'recipe_edit_state.dart'; + +/// A bloc that manages the state of a RecipeEdit according to the event that is dispatched to it. +class RecipeEditBloc extends Bloc { + RecipeEditBloc(RecipeEditState initialState) : super(initialState) { + on(_onInitialize); + on(_editRecipe); + } + + _onInitialize( + RecipeEditInitialEvent event, + Emitter emit, + ) async { + try { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + final String? recipeID = state.recipeModelObj?.recipeID; + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/recipe/show/$recipeID'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + if (data is Map) { + if (state is RecipeEditLoadedState) { + var currentState = state as RecipeEditLoadedState; + emit(currentState.copyWith( + recipeModelObj: currentState.recipeModelObj, + recipe: currentState.recipe, + title: currentState.title, + description: currentState.description, + video_url: currentState.video_url)); + } else { + emit(RecipeEditLoadedState( + recipe: data, + title: TextEditingController(text: data['title']), + description: TextEditingController(text: data['description']), + video_url: TextEditingController(text: data['video_url']))); + } + } else { + // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку + emit(RecipeEditErrorState( + 'Некоторые необходимые данные отсутствуют.')); + } + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(RecipeEditErrorState('Ошибка сервера: ${response.statusCode}')); + } + } catch (e) { + // Ошибка при декодировании JSON или другая ошибка выполнения + emit(RecipeEditErrorState('Ошибка при получении данных о чатах: $e')); + } + } + + FutureOr _editRecipe( + EditRecipe event, Emitter emit) async { + if (state is RecipeEditLoadedState) { + var currentState = state as RecipeEditLoadedState; + + var response = await sendRecipe( + currentState.recipe, + currentState.title?.text, + currentState.video_url?.text, + currentState.description?.text, + ); + + if (response.statusCode == 200) { + NavigatorService.pushNamed(AppRoutes.recipeScreen, + arguments: currentState.recipe?['id'].toString()); + } else { + print( + 'Failed to edit blog post. Status Code: ${response.statusCode} ${response.body}'); + } + } + } + + FutureOr sendRecipe( + Map? recipe, + String? title, + String? video_url, + String? description, + ) async { + int id = recipe?['id']; + String url = '${dotenv.env['SERVER_ROOT']!}/recipe/update/$id'; + + final Map credentials = { + 'id': id, + 'title': title, + 'description': description, + 'video_url': video_url, + 'steps': + '[{"image": "steps/bT91V52zY2hIkjZezUxO4jW4njfhDfCe2PcMxciZ.jpg", "description": "Excepturi fuga sed quaerat impedit facere recusandae. Nihil exercitationem architecto accusantium ut. Qui consequuntur rerum quam asperiores repellendus molestias."}, {"image": "steps/7vaLlotrw65NXbqaD0dK4OTLVyxMgmB9wL3xBzg3.jpg", "description": "Sit hic sint ullam qui. Odit nihil possimus quo id itaque nisi non."}, {"image": "steps/et7cKUsnQkegcRAE72TS0mfAf2jLJXW93QDAGfjz.jpg", "description": "Ab dolores qui voluptatem sed quasi vero. Culpa minus optio in voluptatibus aut corrupti. Dolor expedita minus aut nesciunt quos quibusdam rerum."}, {"image": "steps/260401J6r71r9kuUt694VcsuMMQKTlQMrlIwfaKo.jpg", "description": "Eos dolores corrupti cupiditate aut. Nam nesciunt eveniet eos nam vitae ut. Quibusdam ab commodi ut natus. Nisi earum porro quidem et eligendi iste doloribus."}, {"image": "steps/Ol10BkOFeZ9EG4BAShgB1wnVwtCimoUMcSNDh2EB.jpg", "description": "Consectetur optio porro nobis voluptas quibusdam. Consequatur dolor dolorem laboriosam magnam iste atque."}, {"image": "steps/8PanRq2aWWdXG1xCYkRyJ0M3Z39y6jMPfnHCqlVi.jpg", "description": "A optio porro eligendi ullam. Sed numquam ullam quasi aperiam inventore corrupti enim. Alias id et consequatur perferendis. Ullam consequatur incidunt nesciunt ut et aut."}, {"image": "steps/19U2sYSGDkprS3L8SIFT3xz59OwQJ5xs0A8etZjr.jpg", "description": "In doloribus vel voluptas dolorum. Temporibus iusto est repellat quo sed. Perspiciatis enim sit beatae in dolor ut. Quis ad qui eligendi nisi. Est suscipit dolore ratione dolores suscipit molestiae."}, {"image": "steps/CRxJtjgQw84kG3teJVtl9GqwWUQ1WKpvOeBbwf9a.jpg", "description": "Aut quia maiores et aut. Quidem ab voluptates blanditiis. Est quam quia repellat exercitationem rerum et iste mollitia. Et sed dolores tempore nam et similique qui."}, {"image": "steps/NoG9rHtvHNLrYI2LujURT3DMQCmEe68UbpiXco2y.jpg", "description": "Nam et dolorum delectus. Labore fuga asperiores nam cumque sit. Eum hic officiis ea asperiores qui. Soluta totam enim quasi explicabo sed aut."}]', + 'ingredients': + '[{"name": "Cheeseburger", "unit": "pcs", "portion": 336}, {"name": "Little Cheeseburger", "unit": "gr", "portion": 623}, {"name": "Bacon Burger", "unit": "ml", "portion": 713}, {"name": "Little Bacon Burger", "unit": "pcs", "portion": 160}, {"name": "Hamburger", "unit": "tablespoon", "portion": 390}, {"name": "Bacon Cheese Dog", "unit": "teespons", "portion": 295}, {"name": "Hamburger", "unit": "pcs", "portion": 813}, {"name": "Cheeseburger", "unit": "gr", "portion": 951}]' + }; + + var response = await http.post( + Uri.parse(url), + headers: { + 'Content-Type': 'application/json', + }, + body: json.encode(credentials), + encoding: utf8, + ); + + return response; + } +} diff --git a/lib/presentation/recipe/recipe_edit_screen/bloc/recipe_edit_event.dart b/lib/presentation/recipe/recipe_edit_screen/bloc/recipe_edit_event.dart new file mode 100644 index 00000000..bcbe219c --- /dev/null +++ b/lib/presentation/recipe/recipe_edit_screen/bloc/recipe_edit_event.dart @@ -0,0 +1,21 @@ +// ignore_for_file: must_be_immutable + +part of 'recipe_edit_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///RecipeEdit widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class RecipeEditEvent extends Equatable {} + +/// Event that is dispatched when the RecipeEdit widget is first created. +class RecipeEditInitialEvent extends RecipeEditEvent { + @override + List get props => []; +} + +class EditRecipe extends RecipeEditEvent { + @override + List get props => []; +} diff --git a/lib/presentation/recipe/recipe_edit_screen/bloc/recipe_edit_state.dart b/lib/presentation/recipe/recipe_edit_screen/bloc/recipe_edit_state.dart new file mode 100644 index 00000000..ec001270 --- /dev/null +++ b/lib/presentation/recipe/recipe_edit_screen/bloc/recipe_edit_state.dart @@ -0,0 +1,69 @@ +// ignore_for_file: must_be_immutable + +part of 'recipe_edit_bloc.dart'; + +/// Represents the state of RecipeEdit in the application. +/// + +class RecipeEditState extends Equatable { + RecipeEditState({ + this.recipeModelObj, + }); + + RecipeEditModel? recipeModelObj; + + @override + List get props => [recipeModelObj]; + RecipeEditState copyWith({RecipeEditModel? recipeModelObj}) { + return RecipeEditState( + recipeModelObj: recipeModelObj ?? this.recipeModelObj, + ); + } +} + +class RecipeEditLoadedState extends RecipeEditState { + RecipeEditLoadedState( + {this.recipeModelObj, + this.recipe, + this.title, + this.description, + this.video_url}); + + RecipeEditModel? recipeModelObj; + Map? recipe; + TextEditingController? title; + TextEditingController? description; + TextEditingController? video_url; + @override + List get props => + [recipeModelObj, recipe, title, description, video_url]; + RecipeEditLoadedState copyWith({ + RecipeEditModel? recipeModelObj, + Map? recipe, + TextEditingController? title, + TextEditingController? description, + TextEditingController? video_url, + }) { + return RecipeEditLoadedState( + recipeModelObj: recipeModelObj ?? this.recipeModelObj, + recipe: recipe ?? this.recipe, + title: title ?? this.title, + description: description ?? this.description, + video_url: video_url ?? this.video_url, + ); + } +} + +class RecipeEditErrorState extends RecipeEditState { + final String errorMessage; + + RecipeEditErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} + +class RecipeEditInitialState extends RecipeEditState { + RecipeEditInitialState({required RecipeEditModel recipeModelObj}) + : super(recipeModelObj: recipeModelObj); +} diff --git a/lib/presentation/recipe/recipe_edit_screen/models/recipe_edit_model.dart b/lib/presentation/recipe/recipe_edit_screen/models/recipe_edit_model.dart new file mode 100644 index 00000000..72176186 --- /dev/null +++ b/lib/presentation/recipe/recipe_edit_screen/models/recipe_edit_model.dart @@ -0,0 +1,19 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; + +/// This class defines the variables used in the [recipe_edit_screen], +/// and is typically used to hold data that is passed between different parts of the application. + +class RecipeEditModel extends Equatable { + final String recipeID; + + RecipeEditModel({required String recipeID}) : recipeID = recipeID; + + RecipeEditModel copyWith(String recipeID) { + return RecipeEditModel(recipeID: recipeID); + } + + @override + List get props => [recipeID]; +} diff --git a/lib/presentation/recipe/recipe_edit_screen/recipe_edit_screen.dart b/lib/presentation/recipe/recipe_edit_screen/recipe_edit_screen.dart new file mode 100644 index 00000000..31a723a9 --- /dev/null +++ b/lib/presentation/recipe/recipe_edit_screen/recipe_edit_screen.dart @@ -0,0 +1,119 @@ +import 'bloc/recipe_edit_bloc.dart'; +import 'models/recipe_edit_model.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:flutter/material.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +class RecipeEditScreen extends StatelessWidget { + const RecipeEditScreen({Key? key}) : super(key: key); + static Widget builder(BuildContext context) { + final recipeID = ModalRoute.of(context)!.settings.arguments; + return BlocProvider( + create: (context) => RecipeEditBloc(RecipeEditState( + recipeModelObj: RecipeEditModel(recipeID: recipeID.toString()))) + ..add(RecipeEditInitialEvent()), + child: RecipeEditScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is RecipeEditInitialState) { + // Это начальное состояние, данные ещё не загружены. + return Container( + color: Colors.white, + ); + } else if (state is RecipeEditLoadedState) { + // Данные загружены, можно строить экран. + return _buildRecipeEdit(context, state); + } else if (state is RecipeEditErrorState) { + // Произошла ошибка при загрузке данных. + return Center(child: Text(state.errorMessage)); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildRecipeEdit(BuildContext context, RecipeEditLoadedState state) { + mediaQueryData = MediaQuery.of(context); + return SafeArea( + child: Scaffold( + resizeToAvoidBottomInset: false, + body: Container( + width: double.maxFinite, + decoration: AppDecoration.fillGray, + child: ListView(children: [ + SizedBox(height: 30.v), + Container( + margin: EdgeInsets.only( + left: 16.h, + right: 20.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + + (state.recipe?['image'] ?? ''), + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundImage: imageProvider, + radius: 25, + ), + placeholder: (context, url) => + CircularProgressIndicator(), + errorWidget: (context, url, error) => CircleAvatar( + child: Icon(Icons.person), + backgroundColor: Colors.white, + radius: 25, + ), + ), + SizedBox(height: 16.0), + TextFormField( + controller: state.title, + decoration: InputDecoration(labelText: 'Заголовок'), + ), + SizedBox(height: 16.0), + TextFormField( + controller: state.description, + decoration: InputDecoration(labelText: 'Описание'), + ), + SizedBox(height: 16.0), + TextFormField( + controller: state.video_url, + decoration: InputDecoration(labelText: 'Ссылка'), + ), + SizedBox(height: 16.0), + // Здесь нужно добавить виджет для загрузки картинки + ElevatedButton( + onPressed: () => + context.read().add(EditRecipe()), + child: Text('Сохранить'), + ) + ]), + ) + ]), + ), + bottomNavigationBar: _buildBottomBar(context))); + } +} + +_buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); +} diff --git a/lib/presentation/recipe/recipe_select_screen/bloc/recipe_select_bloc.dart b/lib/presentation/recipe/recipe_select_screen/bloc/recipe_select_bloc.dart new file mode 100644 index 00000000..fc9a96f4 --- /dev/null +++ b/lib/presentation/recipe/recipe_select_screen/bloc/recipe_select_bloc.dart @@ -0,0 +1,70 @@ +//import 'package:eatvio_app/presentation/auth/auth_screen/auth_screen.dart'; +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/recipe/recipe_select_screen/models/recipe_select_model.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import 'package:hive/hive.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:image_picker/image_picker.dart'; + +part 'recipe_select_event.dart'; +part 'recipe_select_state.dart'; + +/// A bloc that manages the state of a RecipeSelect according to the event that is dispatched to it. +class RecipeSelectBloc extends Bloc { + RecipeSelectBloc(RecipeSelectState initialState) : super(initialState) { + on(_onInitialize); + } + + Future _onInitialize( + RecipeSelectInitialEvent event, + Emitter emit, + ) async { + try { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); // Если токен есть, запускаем приложение + } + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/recipe/index'), + ); + print('Got response'); + if (response.statusCode == 200) { + print('Got 200'); + final data = jsonDecode(response.body); + if (data is List) { + print('Got List'); + if (state is RecipeSelectLoadedState) { + print('Got loaded'); + var currentState = state as RecipeSelectLoadedState; + emit((currentState).copyWith( + posts: data, + myProfileModelObj: currentState.myProfileModelObj, + )); + } else { + print('Got unloaded'); + emit(RecipeSelectLoadedState( + posts: data, + )); + } + } else { + // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку + emit(RecipeSelectErrorState('Неверный формат ответа API.')); + } + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(RecipeSelectErrorState('Ошибка сервера: ${response.statusCode}')); + } + } catch (e) { + // Ошибка при декодировании JSON или другая ошибка выполнения + emit(RecipeSelectErrorState('Ошибка при получении данных ленты: $e')); + } + } +} diff --git a/lib/presentation/recipe/recipe_select_screen/bloc/recipe_select_event.dart b/lib/presentation/recipe/recipe_select_screen/bloc/recipe_select_event.dart new file mode 100644 index 00000000..460c79c8 --- /dev/null +++ b/lib/presentation/recipe/recipe_select_screen/bloc/recipe_select_event.dart @@ -0,0 +1,23 @@ +// ignore_for_file: must_be_immutable + +part of 'recipe_select_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///RecipeSelect widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class RecipeSelectEvent extends Equatable {} + +/// Event that is dispatched when the RecipeSelect widget is first created. +class RecipeSelectInitialEvent extends RecipeSelectEvent { + @override + List get props => []; +} + +class UploadAvatar extends RecipeSelectEvent { + UploadAvatar(this.image); + final XFile image; + @override + List get props => [image]; +} diff --git a/lib/presentation/recipe/recipe_select_screen/bloc/recipe_select_state.dart b/lib/presentation/recipe/recipe_select_screen/bloc/recipe_select_state.dart new file mode 100644 index 00000000..feb87b2e --- /dev/null +++ b/lib/presentation/recipe/recipe_select_screen/bloc/recipe_select_state.dart @@ -0,0 +1,49 @@ +// ignore_for_file: must_be_immutable + +part of 'recipe_select_bloc.dart'; + +class RecipeSelectState extends Equatable { + const RecipeSelectState({this.myProfileModelObj}); + + final RecipeSelectModel? myProfileModelObj; + + @override + List get props => [myProfileModelObj]; +} + +class RecipeSelectInitialState extends RecipeSelectState { + RecipeSelectInitialState({required RecipeSelectModel myProfileModelObj}) + : super(myProfileModelObj: myProfileModelObj); +} + +/// Represents the state of RecipeSelect in the application. +class RecipeSelectLoadedState extends RecipeSelectState { + RecipeSelectLoadedState({this.myProfileModelObj, this.posts}) + : super(myProfileModelObj: null); + + RecipeSelectModel? myProfileModelObj; + List? posts; + @override + List get props => [ + myProfileModelObj, + posts, + ]; + RecipeSelectLoadedState copyWith({ + RecipeSelectModel? myProfileModelObj, + List? posts, + }) { + return RecipeSelectLoadedState( + myProfileModelObj: myProfileModelObj ?? this.myProfileModelObj, + posts: posts ?? this.posts, + ); + } +} + +class RecipeSelectErrorState extends RecipeSelectState { + final String errorMessage; + + const RecipeSelectErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} diff --git a/lib/presentation/recipe/recipe_select_screen/models/recipe_select_model.dart b/lib/presentation/recipe/recipe_select_screen/models/recipe_select_model.dart new file mode 100644 index 00000000..1e9a1d2d --- /dev/null +++ b/lib/presentation/recipe/recipe_select_screen/models/recipe_select_model.dart @@ -0,0 +1,16 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; + +/// This class defines the variables used in the [k0_page], +/// and is typically used to hold data that is passed between different parts of the application. +class RecipeSelectModel extends Equatable { + RecipeSelectModel(); + + RecipeSelectModel copyWith() { + return RecipeSelectModel(); + } + + @override + List get props => []; +} diff --git a/lib/presentation/recipe/recipe_select_screen/recipe_select_screen.dart b/lib/presentation/recipe/recipe_select_screen/recipe_select_screen.dart new file mode 100644 index 00000000..0623c053 --- /dev/null +++ b/lib/presentation/recipe/recipe_select_screen/recipe_select_screen.dart @@ -0,0 +1,184 @@ +import 'bloc/recipe_select_bloc.dart'; +import 'models/recipe_select_model.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:eatvio_app/widgets/app_bar/appbar_subtitle.dart'; +import 'package:eatvio_app/widgets/app_bar/appbar_leading_image.dart'; +import 'package:eatvio_app/widgets/app_bar/custom_app_bar.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter/material.dart'; + +class RecipeSelectScreen extends StatelessWidget { + static Widget builder(BuildContext context) { + return BlocProvider( + create: (context) => RecipeSelectBloc( + RecipeSelectState(myProfileModelObj: RecipeSelectModel())) + ..add(RecipeSelectInitialEvent()), + child: RecipeSelectScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is RecipeSelectInitialState) { + // Это начальное состояние, данные ещё не загружены. + return CircularProgressIndicator( + color: Colors.white, + backgroundColor: Colors.white, + ); + } else if (state is RecipeSelectLoadedState) { + // Данные загружены, можно строить экран. + return _buildRecipeScreen(context, state); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildRecipeScreen(BuildContext context, RecipeSelectLoadedState state) { + mediaQueryData = MediaQuery.of(context); + return SafeArea( + child: Scaffold( + backgroundColor: Colors.grey[200], + resizeToAvoidBottomInset: true, + body: SingleChildScrollView( + child: Container( + width: double.maxFinite, + decoration: BoxDecoration(color: Colors.grey[200]), + child: Padding( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox(height: 10), + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: CustomAppBar( + height: 54.v, + leadingWidth: 28.h, + leading: AppbarLeadingImage( + imagePath: ImageConstant.imgArrowLeft, + margin: EdgeInsets.only( + left: 16.h, top: 15.v, bottom: 15.v), + onTap: onTapBack(context), + ), + title: Padding( + padding: EdgeInsets.only(left: 20.h), + child: Row( + children: [ + AppbarSubtitle( + text: "Рецепты", + margin: EdgeInsets.only( + left: 80.h, top: 12.v, bottom: 9.v), + ), + Spacer(), + Padding( + padding: const EdgeInsets.only(right: 20.0), + child: GestureDetector( + onTap: () => Navigator.pushNamed( + context, + AppRoutes.recipeCreateScreen, + ), + child: Icon(Icons.create_outlined)), + ) + ], + ), + ), + ), + ), + SizedBox(height: 20), + _buildRecipeList(context, state), + ], + ), + ), + ), + ), + bottomNavigationBar: _buildBottomBar(context), + ), + ); + } + + _buildRecipeList(BuildContext context, RecipeSelectLoadedState state) { + return ListView.builder( + shrinkWrap: true, // Add this line + itemCount: state.posts?.length ?? 0, + itemBuilder: (context, index) { + final post = state.posts?[index]; + return Container( + margin: EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: ListTile( + leading: CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + post['image'], + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundImage: imageProvider, + radius: 25, + ), + placeholder: (context, url) => CircularProgressIndicator(), + errorWidget: (context, url, error) => CircleAvatar( + child: Icon(Icons.person), + backgroundColor: Colors.white, + radius: 25, + ), + ), + title: Text( + post?['title'] ?? '', + style: CustomTextStyles.titleMediumSemiBold, + ), + subtitle: Text(post?['description'] ?? ''), + onTap: () => Navigator.pushNamed( + context, + AppRoutes.recipeScreen, + arguments: post?['id'].toString(), + ), + trailing: GestureDetector( + onTap: () => Navigator.pushNamed( + context, + AppRoutes.recipeEditScreen, + arguments: post?['id'].toString(), + ), + child: Icon(Icons.edit)), + )); + }, + ); + } + + Function onTapBack(BuildContext context) { + return () { + onTapArrowLeft(context); + }; + } + + _buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); + } + + onTapArrowLeft(BuildContext context) { + NavigatorService.goBack(); + } +} diff --git a/lib/presentation/recipe/recipe_show_screen/bloc/recipe_show_bloc.dart b/lib/presentation/recipe/recipe_show_screen/bloc/recipe_show_bloc.dart new file mode 100644 index 00000000..a65348ed --- /dev/null +++ b/lib/presentation/recipe/recipe_show_screen/bloc/recipe_show_bloc.dart @@ -0,0 +1,88 @@ +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:hive/hive.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import '/core/app_export.dart'; +import 'package:eatvio_app/presentation/recipe/recipe_show_screen/models/recipe_show_model.dart'; +part 'recipe_show_event.dart'; +part 'recipe_show_state.dart'; + +/// A bloc that manages the state of a Recipe according to the event that is dispatched to it. +class RecipeBloc extends Bloc { + RecipeBloc(RecipeState initialState) : super(initialState) { + on(_onInitialize); + on(_deleteEntry); + } + + _onInitialize( + RecipeInitialEvent event, + Emitter emit, + ) async { + try { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + final String? recipeID = state.recipeModelObj?.recipeID; + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/recipe/show/$recipeID'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + if (data is Map) { + if (state is RecipeLoadedState) { + var currentState = state as RecipeLoadedState; + emit(currentState.copyWith( + recipeModelObj: currentState.recipeModelObj, + recipe: currentState.recipe)); + } else { + emit(RecipeLoadedState(recipe: data)); + } + } else { + // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку + emit(RecipeErrorState('Некоторые необходимые данные отсутствуют.')); + } + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(RecipeErrorState('Ошибка сервера: ${response.statusCode}')); + } + } catch (e) { + // Ошибка при декодировании JSON или другая ошибка выполнения + emit(RecipeErrorState('Ошибка при получении данных о чатах: $e')); + } + } + + FutureOr _deleteEntry( + DeleteEntry event, Emitter emit) async { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + final int id = event.id; + final response = await http.post( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/recipe/delete/$id'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + if (response.statusCode == 200) { + NavigatorService.pushNamed(AppRoutes.recipeSelectScreen); + } else { + // Статус ответа не равен 200, обрабатываем ошибку + emit(RecipeErrorState('Ошибка: ${response.statusCode}')); + } + } +} diff --git a/lib/presentation/recipe/recipe_show_screen/bloc/recipe_show_event.dart b/lib/presentation/recipe/recipe_show_screen/bloc/recipe_show_event.dart new file mode 100644 index 00000000..6257e893 --- /dev/null +++ b/lib/presentation/recipe/recipe_show_screen/bloc/recipe_show_event.dart @@ -0,0 +1,23 @@ +// ignore_for_file: must_be_immutable + +part of 'recipe_show_bloc.dart'; + +/// Abstract class for all events that can be dispatched from the +///Recipe widget. +/// +/// Events must be immutable and implement the [Equatable] interface. +@immutable +abstract class RecipeEvent extends Equatable {} + +/// Event that is dispatched when the Recipe widget is first created. +class RecipeInitialEvent extends RecipeEvent { + @override + List get props => []; +} + +class DeleteEntry extends RecipeEvent { + DeleteEntry(this.id); + final int id; + @override + List get props => [id]; +} diff --git a/lib/presentation/recipe/recipe_show_screen/bloc/recipe_show_state.dart b/lib/presentation/recipe/recipe_show_screen/bloc/recipe_show_state.dart new file mode 100644 index 00000000..e8c1807f --- /dev/null +++ b/lib/presentation/recipe/recipe_show_screen/bloc/recipe_show_state.dart @@ -0,0 +1,52 @@ +// ignore_for_file: must_be_immutable + +part of 'recipe_show_bloc.dart'; + +/// Represents the state of Recipe in the application. +/// + +class RecipeState extends Equatable { + RecipeState({ + this.recipeModelObj, + }); + + RecipeModel? recipeModelObj; + + @override + List get props => [recipeModelObj]; + RecipeState copyWith({RecipeModel? recipeModelObj}) { + return RecipeState( + recipeModelObj: recipeModelObj ?? this.recipeModelObj, + ); + } +} + +class RecipeLoadedState extends RecipeState { + RecipeLoadedState({this.recipeModelObj, this.recipe}); + + RecipeModel? recipeModelObj; + Map? recipe; + @override + List get props => [recipeModelObj, recipe]; + RecipeLoadedState copyWith( + {RecipeModel? recipeModelObj, Map? recipe}) { + return RecipeLoadedState( + recipeModelObj: recipeModelObj ?? this.recipeModelObj, + recipe: recipe ?? this.recipe, + ); + } +} + +class RecipeErrorState extends RecipeState { + final String errorMessage; + + RecipeErrorState(this.errorMessage); + + @override + List get props => [errorMessage]; +} + +class RecipeInitialState extends RecipeState { + RecipeInitialState({required RecipeModel recipeModelObj}) + : super(recipeModelObj: recipeModelObj); +} diff --git a/lib/presentation/recipe/recipe_show_screen/models/recipe_show_model.dart b/lib/presentation/recipe/recipe_show_screen/models/recipe_show_model.dart new file mode 100644 index 00000000..1786388e --- /dev/null +++ b/lib/presentation/recipe/recipe_show_screen/models/recipe_show_model.dart @@ -0,0 +1,19 @@ +// ignore_for_file: must_be_immutable + +import 'package:equatable/equatable.dart'; + +/// This class defines the variables used in the [recipe_screen], +/// and is typically used to hold data that is passed between different parts of the application. + +class RecipeModel extends Equatable { + final String recipeID; + + RecipeModel({required String recipeID}) : recipeID = recipeID; + + RecipeModel copyWith(String recipeID) { + return RecipeModel(recipeID: recipeID); + } + + @override + List get props => [recipeID]; +} diff --git a/lib/presentation/recipe/recipe_show_screen/recipe_show_screen.dart b/lib/presentation/recipe/recipe_show_screen/recipe_show_screen.dart new file mode 100644 index 00000000..ee757764 --- /dev/null +++ b/lib/presentation/recipe/recipe_show_screen/recipe_show_screen.dart @@ -0,0 +1,152 @@ +import 'bloc/recipe_show_bloc.dart'; +import 'models/recipe_show_model.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:flutter/material.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +class RecipeScreen extends StatelessWidget { + const RecipeScreen({Key? key}) : super(key: key); + static Widget builder(BuildContext context) { + final recipeID = ModalRoute.of(context)!.settings.arguments; + return BlocProvider( + create: (context) => RecipeBloc(RecipeState( + recipeModelObj: RecipeModel(recipeID: recipeID.toString()))) + ..add(RecipeInitialEvent()), + child: RecipeScreen()); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is RecipeInitialState) { + // Это начальное состояние, данные ещё не загружены. + return Container( + color: Colors.white, + ); + } else if (state is RecipeLoadedState) { + // Данные загружены, можно строить экран. + return _buildRecipe(context, state); + } else if (state is RecipeErrorState) { + // Произошла ошибка при загрузке данных. + return Center(child: Text(state.errorMessage)); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + _buildRecipe(BuildContext context, RecipeLoadedState state) { + mediaQueryData = MediaQuery.of(context); + return SafeArea( + child: Scaffold( + resizeToAvoidBottomInset: false, + body: Container( + width: double.maxFinite, + decoration: AppDecoration.fillGray, + child: ListView( + children: [ + SizedBox(height: 30.v), + Container( + margin: EdgeInsets.only( + left: 16.h, + right: 20.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + + (state.recipe?['image'] ?? ''), + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundImage: imageProvider, + radius: 25, + ), + placeholder: (context, url) => + CircularProgressIndicator(), + errorWidget: (context, url, error) => CircleAvatar( + child: Icon(Icons.person), + backgroundColor: Colors.white, + radius: 25, + ), + ), + SizedBox(height: 16.0), + Text(state.recipe?['title'], + style: CustomTextStyles.titleMediumBlack), + SizedBox(height: 8.0), + Text( + state.recipe?['description'], + ), + SizedBox(height: 16.0), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + ElevatedButton( + onPressed: () => Navigator.pushNamed( + context, AppRoutes.recipeEditScreen, + arguments: state.recipe?['id'].toString()), + child: Text('Редактировать'), + ), + ElevatedButton( + onPressed: () => context + .read() + .add(DeleteEntry(state.recipe?['id'])), + child: Text('Удалить'), + ), + ], + ) + ]), + ) + ], + ), + ), + bottomNavigationBar: _buildBottomBar(context))); + } + + ///Handling route based on bottom click actions + String getCurrentRoute(BottomBarEnum type) { + switch (type) { + case BottomBarEnum.tf: + return AppRoutes.recipeScreen; + default: + return "/"; + } + } + + ///Handling page based on route + Widget getCurrentPage( + BuildContext context, + String currentRoute, + ) { + switch (currentRoute) { + case AppRoutes.recipeScreen: + return RecipeScreen.builder(context); + default: + return RecipeScreen(); + } + } + + _buildBottomBar(BuildContext context) { + return CustomBottomBar(onChanged: (BottomBarEnum type) {}); + } + + /// Navigates to the previous screen. + onTapArrowleftone(BuildContext context) { + NavigatorService.goBack(); + } +} diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index 6f988361..7163f5e5 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -1,3 +1,4 @@ +import 'package:eatvio_app/presentation/diets/diet_select_screen/diet_select_screen.dart'; import 'package:flutter/material.dart'; import 'package:eatvio_app/presentation/registration/date_of_birth_screen/date_of_birth_screen.dart'; import 'package:eatvio_app/presentation/registration/city_of_living_screen/city_of_living_screen.dart'; @@ -20,8 +21,23 @@ import 'package:eatvio_app/presentation/auth/auth_screen/auth_screen.dart'; import 'package:eatvio_app/presentation/profile/my_profile_screen/my_profile_screen.dart'; import 'package:eatvio_app/presentation/profile/my_profile_screen/my_settings_screen.dart'; import 'package:eatvio_app/presentation/diary/diary_edit_screen/diary_edit_screen.dart'; -//import 'package:eatvio_app/presentation/chat/chat_screen/chat_screen.dart'; +import 'package:eatvio_app/presentation/chat/chat_screen/chat_screen.dart'; import 'package:eatvio_app/presentation/chat/chat_select_screen/chat_select_screen.dart'; +import 'package:eatvio_app/presentation/feed/feed_screen/feed_screen.dart'; +import 'package:eatvio_app/presentation/recipe/recipe_select_screen/recipe_select_screen.dart'; +import 'package:eatvio_app/presentation/plans/plan_select_screen/plan_select_screen.dart'; +import 'package:eatvio_app/presentation/plans/plan_edit_screen/plan_edit_screen.dart'; +import 'package:eatvio_app/presentation/plans/plan_create_screen/plan_create_screen.dart'; +import 'package:eatvio_app/presentation/diets/diet_show_screen/diet_show_screen.dart'; +import 'package:eatvio_app/presentation/diets/diet_edit_screen/diet_edit_screen.dart'; +import 'package:eatvio_app/presentation/diets/diet_create_screen/diet_create_screen.dart'; +import 'package:eatvio_app/presentation/plans/plan_show_screen/plan_show_screen.dart'; +import 'package:eatvio_app/presentation/recipe/recipe_show_screen/recipe_show_screen.dart'; +import 'package:eatvio_app/presentation/recipe/recipe_create_screen/recipe_create_screen.dart'; +import 'package:eatvio_app/presentation/recipe/recipe_edit_screen/recipe_edit_screen.dart'; +import 'package:eatvio_app/presentation/feed/blog_post_screen/blog_post_screen.dart'; +import 'package:eatvio_app/presentation/feed/blog_post_edit_screen/blog_post_edit_screen.dart'; +import 'package:eatvio_app/presentation/feed/blog_post_create_screen/blog_post_create_screen.dart'; class AppRoutes { // REGISTRATION @@ -78,6 +94,38 @@ class AppRoutes { static const String chatSelectScreen = '/chatSelect'; + static const String feedScreen = '/feed'; + + static const String blogPostScreen = '/blogPost'; + + static const String blogPostEditScreen = '/blogPostEdit'; + + static const String blogPostCreateScreen = '/blogPostCreate'; + + static const String recipeSelectScreen = '/recipeSelect'; + + static const String recipeScreen = '/recipe'; + + static const String recipeEditScreen = '/recipeEdit'; + + static const String recipeCreateScreen = '/recipeCreate'; + + static const String dietSelectScreen = '/dietSelect'; + + static const String dietScreen = '/diet'; + + static const String dietEditScreen = '/dietEdit'; + + static const String dietCreateScreen = '/dietCreate'; + + static const String planSelectScreen = '/planSelect'; + + static const String planScreen = '/plan'; + + static const String planEditScreen = '/planEdit'; + + static const String planCreateScreen = '/planCreate'; + static Map get routes => { dateOfBirthScreen: DateOfBirthScreen.builder, cityOfLivingScreen: CityOfLivingScreen.builder, @@ -101,7 +149,23 @@ class AppRoutes { myProfile: MyProfileScreen.builder, mySettings: MySettingsScreen.builder, diaryEdit: DiaryEditScreen.builder, - //chatScreen: ChatScreen.builder, + chatScreen: ChatScreen.builder, chatSelectScreen: ChatSelectScreen.builder, + recipeSelectScreen: RecipeSelectScreen.builder, + recipeScreen: RecipeScreen.builder, + recipeEditScreen: RecipeEditScreen.builder, + recipeCreateScreen: RecipeCreateScreen.builder, + dietSelectScreen: DietSelectScreen.builder, + dietScreen: DietScreen.builder, + dietEditScreen: DietEditScreen.builder, + dietCreateScreen: DietCreateScreen.builder, + planSelectScreen: PlanSelectScreen.builder, + planScreen: PlanScreen.builder, + planEditScreen: PlanEditScreen.builder, + planCreateScreen: PlanCreateScreen.builder, + feedScreen: FeedScreen.builder, + blogPostScreen: BlogPostScreen.builder, + blogPostEditScreen: BlogPostEditScreen.builder, + blogPostCreateScreen: BlogPostCreateScreen.builder, }; } diff --git a/lib/widgets/custom_bottom_bar.dart b/lib/widgets/custom_bottom_bar.dart index 6968a085..d6e47d6d 100644 --- a/lib/widgets/custom_bottom_bar.dart +++ b/lib/widgets/custom_bottom_bar.dart @@ -87,6 +87,7 @@ class CustomBottomBarState extends State { Navigator.pushNamed(context, AppRoutes.myProfile); break; case 1: + Navigator.pushNamed(context, AppRoutes.feedScreen); break; case 2: Navigator.pushNamed(context, AppRoutes.diaryEdit); diff --git a/lib/widgets/custom_navigation_menu.dart b/lib/widgets/custom_navigation_menu.dart index fbe6cbce..674dd0f2 100644 --- a/lib/widgets/custom_navigation_menu.dart +++ b/lib/widgets/custom_navigation_menu.dart @@ -21,23 +21,23 @@ class CustomNavigationMenu extends StatelessWidget { MenuItem( icon: ImageConstant.imgRecipes, text: 'Мои рецепты', - onTap: () { - // Действие при нажатии на "Мои рецепты" - }, + onTap: () => + Navigator.pushNamed(context, AppRoutes.recipeSelectScreen), + // Действие при нажатии на "Мои рецепты" ), MenuItem( icon: ImageConstant.imgDiets, text: 'Мои диеты', - onTap: () { - // Действие при нажатии на "Мои диеты" - }, + onTap: () => + Navigator.pushNamed(context, AppRoutes.dietSelectScreen), + // Действие при нажатии на "Мои рецепты" ), MenuItem( icon: ImageConstant.imgPlans, text: 'Мои планы', - onTap: () { - // Действие при нажатии на "Мои планы" - }, + onTap: () => + Navigator.pushNamed(context, AppRoutes.planSelectScreen), + // Действие при нажатии на "Мои рецепты" ), MenuItem( icon: ImageConstant.imgFinance, diff --git a/pubspec.yaml b/pubspec.yaml index 14690c6d..8f1a6f45 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.0.0+1 +version: 0.0.0+1 environment: sdk: '>=2.12.0 <3.0.0' dependencies: