forked from eatvio/eatvio_app
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:
3
assets/images/img_attach.svg
Normal file
3
assets/images/img_attach.svg
Normal 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 |
3
assets/images/img_mic.svg
Normal file
3
assets/images/img_mic.svg
Normal 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 |
@@ -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 />
|
||||
|
||||
@@ -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';
|
||||
|
||||
//-------------------------------------------------------------
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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 => [];
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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) {});
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
117
lib/presentation/diets/diet_edit_screen/bloc/diet_edit_bloc.dart
Normal file
117
lib/presentation/diets/diet_edit_screen/bloc/diet_edit_bloc.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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 => [];
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
114
lib/presentation/diets/diet_edit_screen/diet_edit_screen.dart
Normal file
114
lib/presentation/diets/diet_edit_screen/diet_edit_screen.dart
Normal 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) {});
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 => [];
|
||||
}
|
||||
@@ -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}'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
154
lib/presentation/diets/diet_show_screen/diet_show_screen.dart
Normal file
154
lib/presentation/diets/diet_show_screen/diet_show_screen.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 => [];
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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) {});
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 => [];
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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) {});
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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}'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
155
lib/presentation/feed/blog_post_screen/blog_post_screen.dart
Normal file
155
lib/presentation/feed/blog_post_screen/blog_post_screen.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
70
lib/presentation/feed/feed_screen/bloc/feed_bloc.dart
Normal file
70
lib/presentation/feed/feed_screen/bloc/feed_bloc.dart
Normal 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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
23
lib/presentation/feed/feed_screen/bloc/feed_event.dart
Normal file
23
lib/presentation/feed/feed_screen/bloc/feed_event.dart
Normal 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];
|
||||
}
|
||||
49
lib/presentation/feed/feed_screen/bloc/feed_state.dart
Normal file
49
lib/presentation/feed/feed_screen/bloc/feed_state.dart
Normal 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];
|
||||
}
|
||||
183
lib/presentation/feed/feed_screen/feed_screen.dart
Normal file
183
lib/presentation/feed/feed_screen/feed_screen.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
16
lib/presentation/feed/feed_screen/models/feed_model.dart
Normal file
16
lib/presentation/feed/feed_screen/models/feed_model.dart
Normal 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
@@ -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 => [];
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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) {});
|
||||
}
|
||||
118
lib/presentation/plans/plan_edit_screen/bloc/plan_edit_bloc.dart
Normal file
118
lib/presentation/plans/plan_edit_screen/bloc/plan_edit_bloc.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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 => [];
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
114
lib/presentation/plans/plan_edit_screen/plan_edit_screen.dart
Normal file
114
lib/presentation/plans/plan_edit_screen/plan_edit_screen.dart
Normal 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) {});
|
||||
}
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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 => [];
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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}'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
153
lib/presentation/plans/plan_show_screen/plan_show_screen.dart
Normal file
153
lib/presentation/plans/plan_show_screen/plan_show_screen.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 => [];
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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) {});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 => [];
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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) {});
|
||||
}
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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 => [];
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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}'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user