Merge pull request 'Сделал экраны чата, ленты, блог постов, рецептов, диет и планов, подвязал к ним API методы' (#54) from maxxkim/eatvio_app:diary into main

Reviewed-on: eatvio/eatvio_app#54
Reviewed-by: apoloz <anatoliy.poloz@gmail.com>
This commit is contained in:
apoloz
2024-05-14 13:57:58 +00:00
98 changed files with 5589 additions and 731 deletions

View File

@@ -0,0 +1,3 @@
<svg width="31" height="30" viewBox="0 0 31 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.835 17.4827L17.3021 23.0156C14.6506 25.6671 10.3517 25.6671 7.70014 23.0156C5.04863 20.3641 5.04863 16.0651 7.70014 13.4136L15.1478 5.96595C16.9168 4.19693 19.785 4.19693 21.554 5.96595C23.323 7.73497 23.323 10.6031 21.554 12.3721L13.8717 20.0544C13.0406 20.8856 11.693 20.8856 10.8618 20.0544C10.0307 19.2233 10.0307 17.8757 10.8618 17.0446L16.6293 11.2771C16.9222 10.9842 16.9222 10.5093 16.6293 10.2164C16.3364 9.92353 15.8616 9.92353 15.5687 10.2164L9.80119 15.9839C8.38425 17.4009 8.38425 19.6982 9.80119 21.1151C11.2181 22.532 13.5154 22.532 14.9324 21.1151L22.6147 13.4328C24.9695 11.078 24.9695 7.2601 22.6147 4.90529C20.2598 2.55048 16.442 2.55048 14.0871 4.90529L6.63948 12.3529C3.40218 15.5902 3.40218 20.8389 6.63948 24.0762C9.87678 27.3135 15.1255 27.3135 18.3628 24.0762L23.8956 18.5434C24.1885 18.2505 24.1885 17.7756 23.8956 17.4827C23.6027 17.1898 23.1279 17.1898 22.835 17.4827Z" fill="#858E99"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,3 @@
<svg width="19" height="25" viewBox="0 0 19 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.5 0C12.2614 0 14.5 2.23858 14.5 5V12C14.5 14.7614 12.2614 17 9.5 17C6.73858 17 4.5 14.7614 4.5 12V5C4.5 2.23858 6.73858 0 9.5 0ZM18 11.25C18.4142 11.25 18.75 11.5858 18.75 12C18.75 16.6029 15.0002 20.3603 10.2509 20.7216L10.25 24C10.25 24.4142 9.91421 24.75 9.5 24.75C9.1203 24.75 8.80651 24.4678 8.75685 24.1018L8.75 24L8.75008 20.7216C4.00034 20.3609 0.25 16.6032 0.25 12C0.25 11.5858 0.585786 11.25 1 11.25C1.41421 11.25 1.75 11.5858 1.75 12C1.75 15.9936 5.20997 19.25 9.5 19.25C13.79 19.25 17.25 15.9936 17.25 12C17.25 11.5858 17.5858 11.25 18 11.25ZM6 5C6 3.067 7.567 1.5 9.5 1.5C11.433 1.5 13 3.067 13 5V12C13 13.933 11.433 15.5 9.5 15.5C7.567 15.5 6 13.933 6 12V5Z" fill="#858E99"/>
</svg>

After

Width:  |  Height:  |  Size: 845 B

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml version="0.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />

View File

@@ -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';
//-------------------------------------------------------------

View File

@@ -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();

View File

@@ -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<ChatEvent, ChatState> {
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<String, dynamic> && data.containsKey('chatRooms')) {
if (data is Map<String, dynamic> &&
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<ChatEvent, ChatState> {
emit(ChatErrorState('Ошибка при получении данных о чатах: $e'));
}
}
List<ChatMessageModel> fillMessages(List<dynamic> messagesJSON) {
List<ChatMessageModel> 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;
}
}

View File

@@ -14,15 +14,3 @@ class ChatInitialEvent extends ChatEvent {
@override
List<Object?> get props => [];
}
///event for dropdown selection
class ChangeDropDownEvent extends ChatEvent {
ChangeDropDownEvent({required this.value});
SelectionPopupModel value;
@override
List<Object?> get props => [
value,
];
}

View File

@@ -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<ChatMessageModel>? messages;
@override
List<Object?> get props => [
selectedDropDownValue,
chatModelObj,
];
ChatState copyWith({
SelectionPopupModel? selectedDropDownValue,
ChatModel? chatModelObj,
}) {
return ChatState(
List<Object?> get props => [chatModelObj, messages];
ChatLoadedState copyWith(
{ChatModel? chatModelObj, List<ChatMessageModel>? messages}) {
return ChatLoadedState(
chatModelObj: chatModelObj ?? this.chatModelObj,
messages: messages ?? this.messages,
);
}
}

View File

@@ -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<ChatBloc>(
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<ChatBloc, ChatState>(
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<ChatBloc, ChatState, TextEditingController?>(
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();
}
}
*/

View File

@@ -0,0 +1,36 @@
import 'package:equatable/equatable.dart';
class ChatMessageModel extends Equatable {
final int id;
final String data;
final List<dynamic> 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<dynamic>? 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<Object?> get props => [id, data, mediaData, authorId, updatedAt];
}

View File

@@ -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<Object?> get props => [];
List<Object?> get props => [chatID];
}

View File

@@ -59,7 +59,6 @@ class ChatSelectBloc extends Bloc<ChatSelectEvent, ChatSelectState> {
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
print(data['chatRooms']);
if (data is Map<String, dynamic> && data.containsKey('chatRooms')) {
if (state is ChatSelectLoadedState) {
var currentState = state as ChatSelectLoadedState;

View File

@@ -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,
)
],
),
),
)
]),
),
);
}

File diff suppressed because one or more lines are too long

View File

@@ -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<Object?> get props => [];
}
class CreateDiet extends DietCreateEvent {
@override
List<Object?> get props => [];
}

View File

@@ -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<Object?> 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<Object?> 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<Object> get props => [errorMessage];
}
class DietCreateInitialState extends DietCreateState {
DietCreateInitialState({required DietCreateModel dietModelObj})
: super(dietModelObj: dietModelObj);
}

View File

@@ -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<DietCreateBloc>(
create: (context) => DietCreateBloc(DietCreateState(
dietModelObj: DietCreateModel(dietID: dietID.toString())))
..add(DietCreateInitialEvent()),
child: DietCreateScreen());
}
Widget build(BuildContext context) {
return BlocBuilder<DietCreateBloc, DietCreateState>(
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<DietCreateBloc>().add(CreateDiet()),
child: Text('Создать'),
)
]),
)
]),
),
bottomNavigationBar: _buildBottomBar(context)));
}
}
_buildBottomBar(BuildContext context) {
return CustomBottomBar(onChanged: (BottomBarEnum type) {});
}

View File

@@ -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<Object?> get props => [dietID];
}

View File

@@ -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<DietEditEvent, DietEditState> {
DietEditBloc(DietEditState initialState) : super(initialState) {
on<DietEditInitialEvent>(_onInitialize);
on<EditDiet>(_editDiet);
}
_onInitialize(
DietEditInitialEvent event,
Emitter<DietEditState> 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<String, dynamic>) {
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<void> _editDiet(EditDiet event, Emitter<DietEditState> 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<http.Response> sendDiet(
Map<String, dynamic>? diet,
String? name,
String? details,
String? duration,
) async {
int id = diet?['id'];
String url = '${dotenv.env['SERVER_ROOT']!}/diets/update/$id';
final Map<String, dynamic> 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;
}
}

View File

@@ -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<Object?> get props => [];
}
class EditDiet extends DietEditEvent {
@override
List<Object?> get props => [];
}

View File

@@ -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<Object?> 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<String, dynamic>? diet;
TextEditingController? name;
TextEditingController? details;
TextEditingController? duration;
@override
List<Object?> get props => [dietModelObj, diet, name, details, duration];
DietEditLoadedState copyWith({
DietEditModel? dietModelObj,
Map<String, dynamic>? 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<Object> get props => [errorMessage];
}
class DietEditInitialState extends DietEditState {
DietEditInitialState({required DietEditModel dietModelObj})
: super(dietModelObj: dietModelObj);
}

View File

@@ -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<DietEditBloc>(
create: (context) => DietEditBloc(DietEditState(
dietModelObj: DietEditModel(dietID: dietID.toString())))
..add(DietEditInitialEvent()),
child: DietEditScreen());
}
Widget build(BuildContext context) {
return BlocBuilder<DietEditBloc, DietEditState>(
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<DietEditBloc>().add(EditDiet()),
child: Text('Сохранить'),
)
]),
)
]),
),
bottomNavigationBar: _buildBottomBar(context)));
}
}
_buildBottomBar(BuildContext context) {
return CustomBottomBar(onChanged: (BottomBarEnum type) {});
}

View File

@@ -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<Object?> get props => [dietID];
}

View File

@@ -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<DietSelectEvent, DietSelectState> {
DietSelectBloc(DietSelectState initialState) : super(initialState) {
on<DietSelectInitialEvent>(_onInitialize);
}
Future<void> _onInitialize(
DietSelectInitialEvent event,
Emitter<DietSelectState> 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<dynamic>) {
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'));
}
}
}

View File

@@ -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<Object?> get props => [];
}
class UploadAvatar extends DietSelectEvent {
UploadAvatar(this.image);
final XFile image;
@override
List<Object> get props => [image];
}

View File

@@ -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<Object?> 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<dynamic>? posts;
@override
List<Object?> get props => [
myProfileModelObj,
posts,
];
DietSelectLoadedState copyWith({
DietSelectModel? myProfileModelObj,
List<dynamic>? posts,
}) {
return DietSelectLoadedState(
myProfileModelObj: myProfileModelObj ?? this.myProfileModelObj,
posts: posts ?? this.posts,
);
}
}
class DietSelectErrorState extends DietSelectState {
final String errorMessage;
const DietSelectErrorState(this.errorMessage);
@override
List<Object> get props => [errorMessage];
}

View File

@@ -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<DietSelectBloc>(
create: (context) => DietSelectBloc(
DietSelectState(myProfileModelObj: DietSelectModel()))
..add(DietSelectInitialEvent()),
child: DietSelectScreen());
}
Widget build(BuildContext context) {
return BlocBuilder<DietSelectBloc, DietSelectState>(
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();
}
}

View File

@@ -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<Object?> get props => [];
}

View File

@@ -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<DietEvent, DietState> {
DietBloc(DietState initialState) : super(initialState) {
on<DietInitialEvent>(_onInitialize);
on<DeleteEntry>(_deleteEntry);
}
_onInitialize(
DietInitialEvent event,
Emitter<DietState> 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<String, dynamic>) {
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<void> _deleteEntry(
DeleteEntry event, Emitter<DietState> 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}'));
}
}
}

View File

@@ -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<Object?> get props => [];
}
class DeleteEntry extends DietEvent {
DeleteEntry(this.id);
final int id;
@override
List<Object?> get props => [id];
}

View File

@@ -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<Object?> 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<String, dynamic>? diet;
@override
List<Object?> get props => [dietModelObj, diet];
DietLoadedState copyWith(
{DietModel? dietModelObj, Map<String, dynamic>? diet}) {
return DietLoadedState(
dietModelObj: dietModelObj ?? this.dietModelObj,
diet: diet ?? this.diet,
);
}
}
class DietErrorState extends DietState {
final String errorMessage;
DietErrorState(this.errorMessage);
@override
List<Object> get props => [errorMessage];
}
class DietInitialState extends DietState {
DietInitialState({required DietModel dietModelObj})
: super(dietModelObj: dietModelObj);
}

View File

@@ -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<DietBloc>(
create: (context) => DietBloc(
DietState(dietModelObj: DietModel(dietID: dietID.toString())))
..add(DietInitialEvent()),
child: DietScreen());
}
Widget build(BuildContext context) {
return BlocBuilder<DietBloc, DietState>(
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<DietBloc>()
.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();
}
}

View File

@@ -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<Object?> get props => [dietID];
}

View File

@@ -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<BlogPostCreateEvent, BlogPostCreateState> {
BlogPostCreateBloc(BlogPostCreateState initialState) : super(initialState) {
on<BlogPostCreateInitialEvent>(_onInitialize);
on<CreateBlogPost>(_createBlogPost);
}
_onInitialize(
BlogPostCreateInitialEvent event,
Emitter<BlogPostCreateState> 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<void> _createBlogPost(
CreateBlogPost event, Emitter<BlogPostCreateState> 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<http.Response> sendBlogPost(
String? title,
String? youtube_url,
String? description,
) async {
String url = '${dotenv.env['SERVER_ROOT']!}/feed/create';
final Map<String, dynamic> 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;
}
}

View File

@@ -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<Object?> get props => [];
}
class CreateBlogPost extends BlogPostCreateEvent {
@override
List<Object?> get props => [];
}

View File

@@ -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<Object?> 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<Object?> 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<Object> get props => [errorMessage];
}
class BlogPostCreateInitialState extends BlogPostCreateState {
BlogPostCreateInitialState({required BlogPostCreateModel blog_postModelObj})
: super(blog_postModelObj: blog_postModelObj);
}

View File

@@ -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<BlogPostCreateBloc>(
create: (context) => BlogPostCreateBloc(BlogPostCreateState(
blog_postModelObj:
BlogPostCreateModel(blog_postID: blog_postID.toString())))
..add(BlogPostCreateInitialEvent()),
child: BlogPostCreateScreen());
}
Widget build(BuildContext context) {
return BlocBuilder<BlogPostCreateBloc, BlogPostCreateState>(
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<BlogPostCreateBloc>()
.add(CreateBlogPost()),
child: Text('Создать'),
)
]),
)
]),
),
bottomNavigationBar: _buildBottomBar(context)));
}
}
_buildBottomBar(BuildContext context) {
return CustomBottomBar(onChanged: (BottomBarEnum type) {});
}

View File

@@ -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<Object?> get props => [blog_postID];
}

View File

@@ -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<BlogPostEditEvent, BlogPostEditState> {
BlogPostEditBloc(BlogPostEditState initialState) : super(initialState) {
on<BlogPostEditInitialEvent>(_onInitialize);
on<EditBlogPost>(_editBlogPost);
}
_onInitialize(
BlogPostEditInitialEvent event,
Emitter<BlogPostEditState> 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<String, dynamic>) {
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<void> _editBlogPost(
EditBlogPost event, Emitter<BlogPostEditState> 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<http.Response> sendBlogPost(
Map<String, dynamic>? 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<String, dynamic> 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;
}
}

View File

@@ -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<Object?> get props => [];
}
class EditBlogPost extends BlogPostEditEvent {
@override
List<Object?> get props => [];
}

View File

@@ -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<Object?> 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<String, dynamic>? blog_post;
TextEditingController? title;
TextEditingController? description;
TextEditingController? youtube_url;
@override
List<Object?> get props =>
[blog_postModelObj, blog_post, title, description, youtube_url];
BlogPostEditLoadedState copyWith({
BlogPostEditModel? blog_postModelObj,
Map<String, dynamic>? 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<Object> get props => [errorMessage];
}
class BlogPostEditInitialState extends BlogPostEditState {
BlogPostEditInitialState({required BlogPostEditModel blog_postModelObj})
: super(blog_postModelObj: blog_postModelObj);
}

View File

@@ -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<BlogPostEditBloc>(
create: (context) => BlogPostEditBloc(BlogPostEditState(
blog_postModelObj:
BlogPostEditModel(blog_postID: blog_postID.toString())))
..add(BlogPostEditInitialEvent()),
child: BlogPostEditScreen());
}
Widget build(BuildContext context) {
return BlocBuilder<BlogPostEditBloc, BlogPostEditState>(
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<BlogPostEditBloc>().add(EditBlogPost()),
child: Text('Сохранить'),
)
]),
)
]),
),
bottomNavigationBar: _buildBottomBar(context)));
}
}
_buildBottomBar(BuildContext context) {
return CustomBottomBar(onChanged: (BottomBarEnum type) {});
}

View File

@@ -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<Object?> get props => [blog_postID];
}

View File

@@ -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<BlogPostEvent, BlogPostState> {
BlogPostBloc(BlogPostState initialState) : super(initialState) {
on<BlogPostInitialEvent>(_onInitialize);
on<DeleteEntry>(_deleteEntry);
}
_onInitialize(
BlogPostInitialEvent event,
Emitter<BlogPostState> 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<String, dynamic>) {
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<void> _deleteEntry(
DeleteEntry event, Emitter<BlogPostState> 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}'));
}
}
}

View File

@@ -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<Object?> get props => [];
}
class DeleteEntry extends BlogPostEvent {
DeleteEntry(this.id);
final int id;
@override
List<Object?> get props => [id];
}

View File

@@ -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<Object?> 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<String, dynamic>? blog_post;
@override
List<Object?> get props => [blog_postModelObj, blog_post];
BlogPostLoadedState copyWith(
{BlogPostModel? blog_postModelObj, Map<String, dynamic>? 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<Object> get props => [errorMessage];
}
class BlogPostInitialState extends BlogPostState {
BlogPostInitialState({required BlogPostModel blog_postModelObj})
: super(blog_postModelObj: blog_postModelObj);
}

View File

@@ -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<BlogPostBloc>(
create: (context) => BlogPostBloc(BlogPostState(
blog_postModelObj:
BlogPostModel(blog_postID: blog_postID.toString())))
..add(BlogPostInitialEvent()),
child: BlogPostScreen());
}
Widget build(BuildContext context) {
return BlocBuilder<BlogPostBloc, BlogPostState>(
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<BlogPostBloc>()
.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();
}
}

View File

@@ -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<Object?> get props => [blog_postID];
}

View File

@@ -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<FeedEvent, FeedState> {
FeedBloc(FeedState initialState) : super(initialState) {
on<FeedInitialEvent>(_onInitialize);
}
Future<void> _onInitialize(
FeedInitialEvent event,
Emitter<FeedState> 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<dynamic>) {
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'));
}
}
}

View File

@@ -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<Object?> get props => [];
}
class UploadAvatar extends FeedEvent {
UploadAvatar(this.image);
final XFile image;
@override
List<Object> get props => [image];
}

View File

@@ -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<Object?> 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<dynamic>? posts;
@override
List<Object?> get props => [
myProfileModelObj,
posts,
];
FeedLoadedState copyWith({
FeedModel? myProfileModelObj,
List<dynamic>? posts,
}) {
return FeedLoadedState(
myProfileModelObj: myProfileModelObj ?? this.myProfileModelObj,
posts: posts ?? this.posts,
);
}
}
class FeedErrorState extends FeedState {
final String errorMessage;
const FeedErrorState(this.errorMessage);
@override
List<Object> get props => [errorMessage];
}

View File

@@ -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<FeedBloc>(
create: (context) => FeedBloc(FeedState(myProfileModelObj: FeedModel()))
..add(FeedInitialEvent()),
child: FeedScreen());
}
Widget build(BuildContext context) {
return BlocBuilder<FeedBloc, FeedState>(
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();
}
}

View File

@@ -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<Object?> get props => [];
}

File diff suppressed because one or more lines are too long

View File

@@ -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<Object?> get props => [];
}
class CreatePlan extends PlanCreateEvent {
@override
List<Object?> get props => [];
}

View File

@@ -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<Object?> 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<Object?> 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<Object> get props => [errorMessage];
}
class PlanCreateInitialState extends PlanCreateState {
PlanCreateInitialState({required PlanCreateModel planModelObj})
: super(planModelObj: planModelObj);
}

View File

@@ -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<Object?> get props => [planID];
}

View File

@@ -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<PlanCreateBloc>(
create: (context) => PlanCreateBloc(PlanCreateState(
planModelObj: PlanCreateModel(planID: planID.toString())))
..add(PlanCreateInitialEvent()),
child: PlanCreateScreen());
}
Widget build(BuildContext context) {
return BlocBuilder<PlanCreateBloc, PlanCreateState>(
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<PlanCreateBloc>().add(CreatePlan()),
child: Text('Создать'),
)
]),
)
]),
),
bottomNavigationBar: _buildBottomBar(context)));
}
}
_buildBottomBar(BuildContext context) {
return CustomBottomBar(onChanged: (BottomBarEnum type) {});
}

View File

@@ -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<PlanEditEvent, PlanEditState> {
PlanEditBloc(PlanEditState initialState) : super(initialState) {
on<PlanEditInitialEvent>(_onInitialize);
on<EditPlan>(_editPlan);
}
_onInitialize(
PlanEditInitialEvent event,
Emitter<PlanEditState> 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<String, dynamic>) {
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<void> _editPlan(EditPlan event, Emitter<PlanEditState> 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<http.Response> sendPlan(
Map<String, dynamic>? plan,
String? name,
String? details,
String? duration,
) async {
int id = plan?['id'];
String url = '${dotenv.env['SERVER_ROOT']!}/plans/update/$id';
final Map<String, dynamic> 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;
}
}

View File

@@ -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<Object?> get props => [];
}
class EditPlan extends PlanEditEvent {
@override
List<Object?> get props => [];
}

View File

@@ -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<Object?> 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<String, dynamic>? plan;
TextEditingController? name;
TextEditingController? details;
TextEditingController? duration;
@override
List<Object?> get props => [planModelObj, plan, name, details, duration];
PlanEditLoadedState copyWith({
PlanEditModel? planModelObj,
Map<String, dynamic>? 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<Object> get props => [errorMessage];
}
class PlanEditInitialState extends PlanEditState {
PlanEditInitialState({required PlanEditModel planModelObj})
: super(planModelObj: planModelObj);
}

View File

@@ -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<Object?> get props => [planID];
}

View File

@@ -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<PlanEditBloc>(
create: (context) => PlanEditBloc(PlanEditState(
planModelObj: PlanEditModel(planID: planID.toString())))
..add(PlanEditInitialEvent()),
child: PlanEditScreen());
}
Widget build(BuildContext context) {
return BlocBuilder<PlanEditBloc, PlanEditState>(
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<PlanEditBloc>().add(EditPlan()),
child: Text('Сохранить'),
)
]),
)
]),
),
bottomNavigationBar: _buildBottomBar(context)));
}
}
_buildBottomBar(BuildContext context) {
return CustomBottomBar(onChanged: (BottomBarEnum type) {});
}

View File

@@ -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<PlanSelectEvent, PlanSelectState> {
PlanSelectBloc(PlanSelectState initialState) : super(initialState) {
on<PlanSelectInitialEvent>(_onInitialize);
}
Future<void> _onInitialize(
PlanSelectInitialEvent event,
Emitter<PlanSelectState> 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<dynamic>) {
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'));
}
}
}

View File

@@ -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<Object?> get props => [];
}
class UploadAvatar extends PlanSelectEvent {
UploadAvatar(this.image);
final XFile image;
@override
List<Object> get props => [image];
}

View File

@@ -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<Object?> 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<dynamic>? posts;
@override
List<Object?> get props => [
myProfileModelObj,
posts,
];
PlanSelectLoadedState copyWith({
PlanSelectModel? myProfileModelObj,
List<dynamic>? posts,
}) {
return PlanSelectLoadedState(
myProfileModelObj: myProfileModelObj ?? this.myProfileModelObj,
posts: posts ?? this.posts,
);
}
}
class PlanSelectErrorState extends PlanSelectState {
final String errorMessage;
const PlanSelectErrorState(this.errorMessage);
@override
List<Object> get props => [errorMessage];
}

View File

@@ -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<Object?> get props => [];
}

View File

@@ -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<PlanSelectBloc>(
create: (context) => PlanSelectBloc(
PlanSelectState(myProfileModelObj: PlanSelectModel()))
..add(PlanSelectInitialEvent()),
child: PlanSelectScreen());
}
Widget build(BuildContext context) {
return BlocBuilder<PlanSelectBloc, PlanSelectState>(
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();
}
}

View File

@@ -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<PlanEvent, PlanState> {
PlanBloc(PlanState initialState) : super(initialState) {
on<PlanInitialEvent>(_onInitialize);
on<DeleteEntry>(_deleteEntry);
}
_onInitialize(
PlanInitialEvent event,
Emitter<PlanState> 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<String, dynamic>) {
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<void> _deleteEntry(
DeleteEntry event, Emitter<PlanState> 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}'));
}
}
}

View File

@@ -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<Object?> get props => [];
}
class DeleteEntry extends PlanEvent {
DeleteEntry(this.id);
final int id;
@override
List<Object?> get props => [id];
}

View File

@@ -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<Object?> 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<String, dynamic>? plan;
@override
List<Object?> get props => [planModelObj, plan];
PlanLoadedState copyWith(
{PlanModel? planModelObj, Map<String, dynamic>? plan}) {
return PlanLoadedState(
planModelObj: planModelObj ?? this.planModelObj,
plan: plan ?? this.plan,
);
}
}
class PlanErrorState extends PlanState {
final String errorMessage;
PlanErrorState(this.errorMessage);
@override
List<Object> get props => [errorMessage];
}
class PlanInitialState extends PlanState {
PlanInitialState({required PlanModel planModelObj})
: super(planModelObj: planModelObj);
}

View File

@@ -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<Object?> get props => [planID];
}

View File

@@ -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<PlanBloc>(
create: (context) => PlanBloc(
PlanState(planModelObj: PlanModel(planID: planID.toString())))
..add(PlanInitialEvent()),
child: PlanScreen());
}
Widget build(BuildContext context) {
return BlocBuilder<PlanBloc, PlanState>(
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<PlanBloc>()
.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();
}
}

View File

@@ -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) {

View File

@@ -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<RecipeCreateEvent, RecipeCreateState> {
RecipeCreateBloc(RecipeCreateState initialState) : super(initialState) {
on<RecipeCreateInitialEvent>(_onInitialize);
on<CreateRecipe>(_createRecipe);
}
_onInitialize(
RecipeCreateInitialEvent event,
Emitter<RecipeCreateState> 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<void> _createRecipe(
CreateRecipe event, Emitter<RecipeCreateState> 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<http.Response> sendRecipe(
String? title,
String? video_url,
String? description,
) async {
String url = '${dotenv.env['SERVER_ROOT']!}/recipe/create';
final Map<String, dynamic> 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;
}
}

View File

@@ -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<Object?> get props => [];
}
class CreateRecipe extends RecipeCreateEvent {
@override
List<Object?> get props => [];
}

View File

@@ -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<Object?> 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<Object?> 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<Object> get props => [errorMessage];
}
class RecipeCreateInitialState extends RecipeCreateState {
RecipeCreateInitialState({required RecipeCreateModel recipeModelObj})
: super(recipeModelObj: recipeModelObj);
}

View File

@@ -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<Object?> get props => [recipeID];
}

View File

@@ -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<RecipeCreateBloc>(
create: (context) => RecipeCreateBloc(RecipeCreateState(
recipeModelObj: RecipeCreateModel(recipeID: recipeID.toString())))
..add(RecipeCreateInitialEvent()),
child: RecipeCreateScreen());
}
Widget build(BuildContext context) {
return BlocBuilder<RecipeCreateBloc, RecipeCreateState>(
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<RecipeCreateBloc>().add(CreateRecipe()),
child: Text('Создать'),
)
]),
)
]),
),
bottomNavigationBar: _buildBottomBar(context)));
}
}
_buildBottomBar(BuildContext context) {
return CustomBottomBar(onChanged: (BottomBarEnum type) {});
}

View File

@@ -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<RecipeEditEvent, RecipeEditState> {
RecipeEditBloc(RecipeEditState initialState) : super(initialState) {
on<RecipeEditInitialEvent>(_onInitialize);
on<EditRecipe>(_editRecipe);
}
_onInitialize(
RecipeEditInitialEvent event,
Emitter<RecipeEditState> 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<String, dynamic>) {
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<void> _editRecipe(
EditRecipe event, Emitter<RecipeEditState> 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<http.Response> sendRecipe(
Map<String, dynamic>? recipe,
String? title,
String? video_url,
String? description,
) async {
int id = recipe?['id'];
String url = '${dotenv.env['SERVER_ROOT']!}/recipe/update/$id';
final Map<String, dynamic> 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;
}
}

View File

@@ -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<Object?> get props => [];
}
class EditRecipe extends RecipeEditEvent {
@override
List<Object?> get props => [];
}

View File

@@ -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<Object?> 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<String, dynamic>? recipe;
TextEditingController? title;
TextEditingController? description;
TextEditingController? video_url;
@override
List<Object?> get props =>
[recipeModelObj, recipe, title, description, video_url];
RecipeEditLoadedState copyWith({
RecipeEditModel? recipeModelObj,
Map<String, dynamic>? 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<Object> get props => [errorMessage];
}
class RecipeEditInitialState extends RecipeEditState {
RecipeEditInitialState({required RecipeEditModel recipeModelObj})
: super(recipeModelObj: recipeModelObj);
}

View File

@@ -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<Object?> get props => [recipeID];
}

View File

@@ -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<RecipeEditBloc>(
create: (context) => RecipeEditBloc(RecipeEditState(
recipeModelObj: RecipeEditModel(recipeID: recipeID.toString())))
..add(RecipeEditInitialEvent()),
child: RecipeEditScreen());
}
Widget build(BuildContext context) {
return BlocBuilder<RecipeEditBloc, RecipeEditState>(
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<RecipeEditBloc>().add(EditRecipe()),
child: Text('Сохранить'),
)
]),
)
]),
),
bottomNavigationBar: _buildBottomBar(context)));
}
}
_buildBottomBar(BuildContext context) {
return CustomBottomBar(onChanged: (BottomBarEnum type) {});
}

View File

@@ -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<RecipeSelectEvent, RecipeSelectState> {
RecipeSelectBloc(RecipeSelectState initialState) : super(initialState) {
on<RecipeSelectInitialEvent>(_onInitialize);
}
Future<void> _onInitialize(
RecipeSelectInitialEvent event,
Emitter<RecipeSelectState> 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<dynamic>) {
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'));
}
}
}

View File

@@ -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<Object?> get props => [];
}
class UploadAvatar extends RecipeSelectEvent {
UploadAvatar(this.image);
final XFile image;
@override
List<Object> get props => [image];
}

View File

@@ -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<Object?> 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<dynamic>? posts;
@override
List<Object?> get props => [
myProfileModelObj,
posts,
];
RecipeSelectLoadedState copyWith({
RecipeSelectModel? myProfileModelObj,
List<dynamic>? posts,
}) {
return RecipeSelectLoadedState(
myProfileModelObj: myProfileModelObj ?? this.myProfileModelObj,
posts: posts ?? this.posts,
);
}
}
class RecipeSelectErrorState extends RecipeSelectState {
final String errorMessage;
const RecipeSelectErrorState(this.errorMessage);
@override
List<Object> get props => [errorMessage];
}

View File

@@ -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<Object?> get props => [];
}

View File

@@ -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<RecipeSelectBloc>(
create: (context) => RecipeSelectBloc(
RecipeSelectState(myProfileModelObj: RecipeSelectModel()))
..add(RecipeSelectInitialEvent()),
child: RecipeSelectScreen());
}
Widget build(BuildContext context) {
return BlocBuilder<RecipeSelectBloc, RecipeSelectState>(
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();
}
}

View File

@@ -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<RecipeEvent, RecipeState> {
RecipeBloc(RecipeState initialState) : super(initialState) {
on<RecipeInitialEvent>(_onInitialize);
on<DeleteEntry>(_deleteEntry);
}
_onInitialize(
RecipeInitialEvent event,
Emitter<RecipeState> 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<String, dynamic>) {
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<void> _deleteEntry(
DeleteEntry event, Emitter<RecipeState> 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}'));
}
}
}

View File

@@ -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<Object?> get props => [];
}
class DeleteEntry extends RecipeEvent {
DeleteEntry(this.id);
final int id;
@override
List<Object?> get props => [id];
}

View File

@@ -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<Object?> 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<String, dynamic>? recipe;
@override
List<Object?> get props => [recipeModelObj, recipe];
RecipeLoadedState copyWith(
{RecipeModel? recipeModelObj, Map<String, dynamic>? recipe}) {
return RecipeLoadedState(
recipeModelObj: recipeModelObj ?? this.recipeModelObj,
recipe: recipe ?? this.recipe,
);
}
}
class RecipeErrorState extends RecipeState {
final String errorMessage;
RecipeErrorState(this.errorMessage);
@override
List<Object> get props => [errorMessage];
}
class RecipeInitialState extends RecipeState {
RecipeInitialState({required RecipeModel recipeModelObj})
: super(recipeModelObj: recipeModelObj);
}

View File

@@ -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<Object?> get props => [recipeID];
}

View File

@@ -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<RecipeBloc>(
create: (context) => RecipeBloc(RecipeState(
recipeModelObj: RecipeModel(recipeID: recipeID.toString())))
..add(RecipeInitialEvent()),
child: RecipeScreen());
}
Widget build(BuildContext context) {
return BlocBuilder<RecipeBloc, RecipeState>(
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<RecipeBloc>()
.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();
}
}

View File

@@ -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<String, WidgetBuilder> 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,
};
}

View File

@@ -87,6 +87,7 @@ class CustomBottomBarState extends State<CustomBottomBar> {
Navigator.pushNamed(context, AppRoutes.myProfile);
break;
case 1:
Navigator.pushNamed(context, AppRoutes.feedScreen);
break;
case 2:
Navigator.pushNamed(context, AppRoutes.diaryEdit);

View File

@@ -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,

View File

@@ -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: