From 9b97967d882fa83eb9b4bd0f6f78d09139a9a4d5 Mon Sep 17 00:00:00 2001 From: maxxkim Date: Wed, 3 Jul 2024 11:33:10 +0300 Subject: [PATCH] refactor(diary): Diary cubit --- lib/features/diary/models/diary_model.dart | 23 + .../diary/presentation/bloc/diary_cubit.dart | 21 + .../diary/presentation/bloc/diary_state.dart | 19 + .../presentation/pages/diary_screen.dart | 1298 +++++++++++++++++ .../diary/repositories/diary_repository.dart | 16 + .../diary/services/diary_service.dart | 13 + 6 files changed, 1390 insertions(+) create mode 100644 lib/features/diary/models/diary_model.dart create mode 100644 lib/features/diary/presentation/bloc/diary_cubit.dart create mode 100644 lib/features/diary/presentation/bloc/diary_state.dart create mode 100644 lib/features/diary/presentation/pages/diary_screen.dart create mode 100644 lib/features/diary/repositories/diary_repository.dart create mode 100644 lib/features/diary/services/diary_service.dart diff --git a/lib/features/diary/models/diary_model.dart b/lib/features/diary/models/diary_model.dart new file mode 100644 index 00000000..dbf6cee6 --- /dev/null +++ b/lib/features/diary/models/diary_model.dart @@ -0,0 +1,23 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'diary_model.freezed.dart'; +part 'diary_model.g.dart'; + +@freezed +class DiaryModel with _$DiaryModel { + const factory DiaryModel({ + required int id, + required String state, + String? image, + required String description, + String? author_name, + String? author_avatar_path, + @JsonKey(name: "youtube_url") required String youtubeUrl, + @JsonKey(name: "created_at") required DateTime createdAt, + @JsonKey(name: "updated_at") required DateTime updatedAt, + required String title, + }) = _DiaryModel; + + factory DiaryModel.fromJson(Map json) => + _$DiaryModelFromJson(json); +} diff --git a/lib/features/diary/presentation/bloc/diary_cubit.dart b/lib/features/diary/presentation/bloc/diary_cubit.dart new file mode 100644 index 00000000..3a676668 --- /dev/null +++ b/lib/features/diary/presentation/bloc/diary_cubit.dart @@ -0,0 +1,21 @@ +import 'package:eatvio_app/core/app_export.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart'; +import 'package:eatvio_app/features/diary/repositories/diary_repository.dart'; + +class DiaryCubit extends Cubit { + final DiaryRepository blogPostRepository; + + DiaryCubit({required this.blogPostRepository}) + : super(const DiaryState.loading()); + + loadDiary(String blogPostID) async { + emit(const DiaryState.loading()); + try { + final blogPost = await blogPostRepository.getDiary(blogPostID); + emit(DiaryState.loaded(blogPost: blogPost)); + } catch (e) { + emit(DiaryState.error(errorMessage: e.toString())); + print(e); + } + } +} diff --git a/lib/features/diary/presentation/bloc/diary_state.dart b/lib/features/diary/presentation/bloc/diary_state.dart new file mode 100644 index 00000000..e37b05eb --- /dev/null +++ b/lib/features/diary/presentation/bloc/diary_state.dart @@ -0,0 +1,19 @@ +import 'package:eatvio_app/features/diary/models/diary_model.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'diary_state.freezed.dart'; + +@freezed +abstract class DiaryState with _$DiaryState { + const factory DiaryState() = _DiaryState; + + const factory DiaryState.loading() = DiaryStateLoading; + + const factory DiaryState.loaded({ + required DiaryModel blogPost, + }) = DiaryStateLoaded; + + const factory DiaryState.error({ + required String errorMessage, + }) = DiaryStateError; +} diff --git a/lib/features/diary/presentation/pages/diary_screen.dart b/lib/features/diary/presentation/pages/diary_screen.dart new file mode 100644 index 00000000..d7806168 --- /dev/null +++ b/lib/features/diary/presentation/pages/diary_screen.dart @@ -0,0 +1,1298 @@ +// ignore_for_file: unused_local_variable + +import 'bloc/diary_edit_bloc.dart'; +import 'models/diary_edit_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:eatvio_app/widgets/custom_edit_text_form_field.dart'; +import 'package:eatvio_app/widgets/custom_edit_drop_down.dart'; +import 'package:eatvio_app/widgets/custom_outlined_button.dart'; +import 'package:eatvio_app/widgets/custom_scale_element.dart'; +import 'package:carousel_slider/carousel_slider.dart'; + +// ignore: must_be_immutable +class DiaryEditScreen extends StatelessWidget { + DiaryEditScreen({Key? key}) : super(key: key); + + static Widget builder(BuildContext context) { + return BlocProvider( + create: (context) => + DiaryEditBloc(DiaryEditState(diaryEditModelObj: DiaryEditModel())) + ..add(DiaryEditInitialEvent()), + child: DiaryEditScreen(), + ); + } + + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is DiaryEditInitialState) { + // Это начальное состояние, данные ещё не загружены. + return Container( + color: Colors.white, + ); + } else if (state is DiaryEditLoadedState) { + // Данные загружены, можно строить экран. + return _buildDiaryEditScreen(context, state); + } else if (state is DiaryEditErrorState) { + // Произошла ошибка при загрузке данных. + return Center(child: Text(state.errorMessage)); + } else { + // Другие состояния, если они есть. + return Container(); // Пустой контейнер для необработанных состояний. + } + }, + ); + } + + Widget _buildDiaryEditScreen( + BuildContext context, DiaryEditLoadedState state) { + return RefreshIndicator( + onRefresh: refreshPage(context), + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + FormBuilder( + child: Scaffold( + appBar: AppBar( + toolbarHeight: 0, + automaticallyImplyLeading: false, + ), + backgroundColor: Color.fromARGB(255, 240, 242, 245), + resizeToAvoidBottomInset: true, + body: SizedBox( + width: double.maxFinite, + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.only(bottom: 5.v), + child: Column( + children: [ + _buildHeaderRow(context, state), + SizedBox(height: 30.v), + _buildWakupRetire(context, state), + SizedBox(height: 30.v), + _buildMeal(context, state), + SizedBox(height: 20.v), + _buildMood(context, state), + SizedBox(height: 30.v), + _buildNutritionChart(context, state), + SizedBox(height: 30.v), + _buildComment(context, state), + ], + ), + ), + ), + ), + ], + ), + ), + ), + ), + _buildEndDay(context), + ], + ), + ); + } + + _buildEndDay(BuildContext context) { + return CustomOutlinedButton( + text: "Закончить день", + margin: EdgeInsets.only( + left: 16.h, + right: 16.h, + bottom: 16.v, + ), + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + decoration: BoxDecoration( + color: Colors.transparent, + ), + onPressed: onFinishDayPressed(context), + // Установите прозрачный фон + ); + } + + refreshPage(BuildContext context) { + return () async { + context.read().add(RefreshEvent()); + }; + } + + /// Section Widget + _buildHeaderRow(BuildContext context, DiaryEditLoadedState state) { + final day = state.diaryId; + return Container( + margin: EdgeInsets.symmetric(horizontal: 16.h, vertical: 16.h), + padding: EdgeInsets.symmetric( + horizontal: 24.h, + vertical: 12.v, + ), + 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: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.only(top: 1.v), + child: Column( + children: [ + Align( + alignment: Alignment.centerRight, + child: Text( + "$day день", + style: CustomTextStyles.titleMediumBlack, + ), + ), + SizedBox(height: 2.v), + Text( + state.diaryDate.toString(), + style: CustomTextStyles.bodyMediumBluegray800, + ), + ], + ), + ), + ], + ), + ); + } + + /// Section Widget + _buildAddProduct(BuildContext context, DiaryEditLoadedState state) { + return CustomOutlinedButton( + text: "Добавить продукт", + margin: EdgeInsets.symmetric(horizontal: 10.h), + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + onPressed: addProduct(context), + ); + } + + addProduct(BuildContext context) { + return () { + context.read().add(AddPortionEvent()); + }; + } + + /// Section Widget + _buildAddPhoto(BuildContext context, DiaryEditLoadedState state) { + bool isLoading = state.isLoading; + return GestureDetector( + onTap: addImage(context, isLoading), + child: Stack( + children: [ + if (!isLoading) + Container( + margin: EdgeInsets.symmetric(horizontal: 10.h), + padding: EdgeInsets.symmetric( + horizontal: 40.h, + vertical: 80.v, + ), + decoration: AppDecoration.fillGray.copyWith( + borderRadius: BorderRadiusStyle.roundedBorder5, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + // Показывайте прелоадер, если загрузка активна + + Padding( + padding: EdgeInsets.only( + left: 7.h, + top: 5.v, + ), + child: Text( + "Добавить фото", + selectionColor: appTheme.blueA400, + ), + ), + Spacer(), + Icon( + Icons.photo_library, + color: appTheme.blueGray300, + ), + ], + ), + ), + if (isLoading) + Container( + margin: EdgeInsets.symmetric(horizontal: 10.h), + padding: EdgeInsets.symmetric( + horizontal: 40.h, + vertical: 80.v, + ), + decoration: AppDecoration.fillGray.copyWith( + borderRadius: BorderRadiusStyle.roundedBorder5, + ), + child: Center( + child: CircularProgressIndicator( + color: appTheme + .blueGray300), // Замените на свой прелоадер при необходимости + ), + ), + ], + ), + ); + } + + addImage(BuildContext context, bool isLoading) { + if (!isLoading) { + return () async { + final picker = ImagePicker(); + List? images = await picker.pickMultiImage(); + if (images.isNotEmpty) { + for (var image in images) { + context.read().add(UploadImages(image)); + } + } + }; + } + } + + _buildMeal(BuildContext context, DiaryEditLoadedState state) { + final digitsOnly = FilteringTextInputFormatter.digitsOnly; + List _options = + state.autocompleteList!.map((dynamic item) => item as String).toList(); + //print(_options); + FocusNode focusNodeName = FocusNode(); + return Container( + margin: EdgeInsets.symmetric(horizontal: 16.h), + padding: EdgeInsets.symmetric( + horizontal: 24.h, + vertical: 12.v, + ), + 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: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 10.h), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Время приема пищи", + style: CustomTextStyles.bodyMediumBlack900, + ), + Expanded( + // Adding Expanded widget + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 10.h), + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width - 260, + ), + child: GestureDetector( + onTap: () => _selectMealTime(context, state), + child: AbsorbPointer( + child: CustomTextFormField( + hintText: state.mealTimeController + ?.format(context) + .toString(), + autofocus: false, + ), + ), + ), + ), + ), + ), + ], + ), + ), + SizedBox(height: 20.v), + Padding( + padding: EdgeInsets.symmetric(horizontal: 10.h), + child: BlocSelector( + selector: (state) => state.diaryEditModelObj, + builder: (context, diaryEditModelObj) { + return CustomDropDown( + hintText: "Прием пищи", + items: diaryEditModelObj?.mealList, + contentPadding: EdgeInsets.symmetric( + horizontal: 10.h, + vertical: 9.v, + ), + defaultValue: diaryEditModelObj?.mealList[0], + onChanged: changeMeal(context, state), + ); + }, + ), + ), + SizedBox(height: 20.v), + Padding( + padding: EdgeInsets.symmetric(horizontal: 10.h), + child: RawAutocomplete( + optionsBuilder: buildOptions(_options), + fieldViewBuilder: ( + BuildContext context, + TextEditingController productEditingController, + FocusNode focusNodeName, + VoidCallback onFieldSubmitted, + ) { + if (state.isAdded) { + print("Clear"); + } + return CustomTextFormField( + hintText: "Введите название продукта", + controller: productEditingController, + focusNode: focusNodeName, + onChanged: getAutocomplete(context, focusNodeName), + ); + }, + optionsViewBuilder: ( + BuildContext context, + AutocompleteOnSelected onSelected, + Iterable options, + ) { + return Align( + alignment: Alignment.topLeft, + child: Material( + elevation: 4.0, + child: SizedBox( + height: 200.0, + child: ListView.builder( + padding: const EdgeInsets.all(8.0), + itemCount: options.length, + itemBuilder: (BuildContext context, int index) { + final String option = options.elementAt(index); + return GestureDetector( + onTap: selectProduct(onSelected, option, state), + child: ListTile( + title: Text(option), + ), + ); + }, + ), + ), + ), + ); + }, + ), + ), + SizedBox(height: 20.v), + if (state.moderationExpanded) + Padding( + padding: EdgeInsets.symmetric(horizontal: 10.h), + child: GestureDetector( + onTap: () => openCaloriesDialog(context, state), + child: Text("Отправить на модерацию"), + ), + ), + SizedBox(height: 20.v), + Padding( + padding: EdgeInsets.symmetric(horizontal: 10.h), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CustomTextFormField( + width: 150.h, + hintText: "Кол-во", + textInputAction: TextInputAction.done, + // focusNode: focusNodeQuantity, + controller: state.productQuantityController, + keyboardType: TextInputType.number, + inputFormatters: [digitsOnly], + ), + Padding( + padding: EdgeInsets.only(left: 30.h), + child: BlocSelector( + selector: (state) => state.diaryEditModelObj, + builder: (context, diaryEditModelObj) { + return CustomDropDown( + items: diaryEditModelObj?.portionList, + defaultValue: diaryEditModelObj?.portionList?[0], + onChanged: changePortion(context, state), + ); + }, + ), + ), + ], + ), + ), + ), + SizedBox(height: 30.v), + _buildAddProduct(context, state), + SizedBox(height: 30.v), + ...state.portions! + .map((portion) => _buildProduct(context, portion)) + .toList(), + SizedBox(height: 30.v), + /*...state.photos != null + ? state.photos!.isEmpty + ? [Text('Фото пока нет')] + : state.photos! + .map((photo) => _buildPhoto(context, photo)) + .toList() + : [Text('Фото нет')],*/ + _buildCarousel(context, state), + SizedBox(height: 20.v), + _buildAddPhoto(context, state), + SizedBox(height: 30.v), + Text( + "Ваша сытость после приёма пищи", + style: CustomTextStyles.bodyMediumBlack900, + ), + SizedBox(height: 15.v), + BlocBuilder( + builder: (context, state) { + if (state is DiaryEditLoadedState) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("😭"), + ScaleElement( + color: Colors.red, + isSelected: state.selectedSatiety == 0, + onTap: changeSatiety(context, 0), + ), + ScaleElement( + color: Colors.orange, + isSelected: state.selectedSatiety == 1, + onTap: changeSatiety(context, 1), + ), + ScaleElement( + color: Colors.yellow, + isSelected: state.selectedSatiety == 2, + onTap: changeSatiety(context, 2), + ), + ScaleElement( + color: Colors.lime, + isSelected: state.selectedSatiety == 3, + onTap: changeSatiety(context, 3), + ), + ScaleElement( + color: Colors.lightGreen, + isSelected: state.selectedSatiety == 4, + onTap: changeSatiety(context, 4), + ), + ScaleElement( + color: Colors.green, + isSelected: state.selectedSatiety == 5, + onTap: changeSatiety(context, 5), + ), + Text("😊"), + // Остальные элементы шкалы + ], + ); + } else { + return Container(); // Возвращаем пустой контейнер, если состояние не является DiaryEditLoadedState + } + }, + ), + ], + ), + ); + } + + openCaloriesDialog(BuildContext context, DiaryEditLoadedState state) { + showDialog( + context: context, + builder: (BuildContext dialogContext) { + return AlertDialog( + title: Text('Введите информацию о продукте'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CustomTextFormField( + hintText: "Название", + controller: state.productNameController, + ), + SizedBox(height: 10.v), + CustomTextFormField( + hintText: "Энергетическая ценность на 100", + keyboardType: TextInputType.number, + controller: state.caloriesController, + ), + SizedBox(height: 10.v), + CustomTextFormField( + hintText: "Белки на 100г", + keyboardType: TextInputType.number, + controller: state.proteinsController, + ), + SizedBox(height: 10.v), + CustomTextFormField( + hintText: "Жиры на 100г", + keyboardType: TextInputType.number, + controller: state.fatsController, + ), + SizedBox(height: 10.v), + CustomTextFormField( + hintText: "Углеводы на 100г", + keyboardType: TextInputType.number, + controller: state.carbsController, + ), + ], + ), + actions: [ + TextButton( + onPressed: popDialog(context), + child: Text('Отмена'), + ), + TextButton( + onPressed: triggerModeration( + context, + state.productNameController!.text, + state.caloriesController!.text, + state.proteinsController!.text, + state.fatsController!.text, + state.carbsController!.text, + dialogContext, + ), + child: Text('Отправить'), + ), + ], + ); + }, + ); + } + + triggerModeration( + BuildContext context, + String name, + String energyOn100gr, + String proteinsOn100gr, + String fatsOn100gr, + String carbohydratesOn100gr, + BuildContext dialogContext, + ) { + return () { + // Call function to send moderation with the entered values + context.read().add( + SendModeration( + name: name, + energyOn100gr: energyOn100gr, + proteinsOn100gr: proteinsOn100gr, + fatsOn100gr: fatsOn100gr, + carbohydratesOn100gr: carbohydratesOn100gr, + ), + ); + Navigator.of(dialogContext).pop(); // Dismiss the dialog + }; + } + + popDialog(BuildContext dialogContext) { + return () { + Navigator.of(dialogContext) + .pop(); // Dismiss the dialog on Cancel button press + }; + } + + changeSatiety(BuildContext context, int satiety) { + return () { + context.read().add(ChangeSatiety(satiety)); + }; + } + + selectProduct(AutocompleteOnSelected onSelected, String option, + DiaryEditLoadedState state) { + state.productNameController?.text = option; + return () { + onSelected(option); + }; + } + + getAutocomplete(BuildContext context, FocusNode focusNode) { + return (value) { + /*print(state.autocompleteList + ?.map((dynamic item) => item as String) + .toList());*/ + if (value.length > 2) { + context.read().add(ProductInput(value)); + } + }; + } + + buildOptions(List _options) { + return (TextEditingValue textEditingValue) { + return _options.where((String option) { + return true; + }); + }; + } + + changeMeal(BuildContext context, DiaryEditLoadedState state) { + return (value) { + context + .read() + .add(ChangeMeal(meals: state.meals, value: value)); + }; + } + + changePortion(BuildContext context, DiaryEditLoadedState state) { + return (value) { + context.read().add(ChangePortion(value: value)); + }; + } + + _buildCarousel(BuildContext context, DiaryEditLoadedState state) { + if (state.photos!.isEmpty) { + return Text("Фото нет"); + } else { + return CarouselSlider( + options: CarouselOptions(height: 340.0), + items: state.photos!.map((photo) { + return _buildPhoto( + context, photo); // Assuming photos are stored as strings + }).toList(), + ); + } + } + + _buildProduct(BuildContext context, Map portion) { + return Container( + margin: EdgeInsets.symmetric(horizontal: 10.h), + padding: EdgeInsets.symmetric( + horizontal: 8.h, + vertical: 6.v, + ), + decoration: AppDecoration.fillGray.copyWith( + borderRadius: BorderRadiusStyle.roundedBorder5, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: EdgeInsets.only( + left: 7.h, + top: 5.v, + ), + child: Text( + portion['product_name'].length > 8 + ? portion['product_name'].substring(0, 8) + : portion['product_name'], + style: CustomTextStyles.titleMedium16, + ), + ), + Padding( + padding: EdgeInsets.only( + left: 20.h, + top: 3.v, + bottom: 3.v, + ), + child: Text( + portion['quantity'].toString() + + ' ' + + portion['portion'].toString().tr, + ), + ), + Spacer(), + GestureDetector( + onTap: removePortion(context, portion), + child: Padding( + padding: const EdgeInsets.only(right: 10), + child: Icon( + Icons.close, + color: appTheme.blueGray300, + ), + ), + ), + ], + ), + ); + } + + _buildPhoto(BuildContext context, Map photo) { + return Stack( + children: [ + Container( + margin: EdgeInsets.symmetric(horizontal: 10.h, vertical: 10.h), + decoration: AppDecoration.fillGray.copyWith( + borderRadius: BorderRadiusStyle.roundedBorder5, + ), + child: Image.network( + dotenv.env['STORAGE_URL']! + photo['path'], + ), + ), + Positioned( + top: 5, + right: 5, + child: GestureDetector( + onTap: removePhoto(context, photo), + child: Container( + padding: EdgeInsets.all(5), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.transparent, + ), + child: Icon( + Icons.close, + color: appTheme.blueGray300, + ), + ), + ), + ), + ], + ); + } + + removePortion(BuildContext context, Map portion) { + return () { + context.read().add(RemovePortionEvent(portion['id'])); + }; + } + + removePhoto(BuildContext context, Map photo) { + return () { + context + .read() + .add(RemoveImageEvent(photo['id'].toString(), photo['path'])); + }; + } + + _buildNutritionChart(BuildContext context, DiaryEditLoadedState state) { + final List recommendedValues = state + .recommendedChartValues; // Рекомендуемые значения: калории, жиры, белки, углеводы + final List consumedValues = state.consumedChartValues; // + return Container( + margin: EdgeInsets.symmetric(horizontal: 16.h), + padding: EdgeInsets.symmetric( + horizontal: 10.h, + vertical: 16.v, + ), + 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, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox(height: 3.v), + Text( + "Ваш КБЖУ", + style: theme.textTheme.titleMedium, + ), + SizedBox(height: 8.v), + Text( + "От дневной суточной нормы", + style: CustomTextStyles.bodyMediumBluegray800, + ), + SizedBox(height: 19.v), + Align( + alignment: Alignment.centerRight, + child: Padding( + padding: EdgeInsets.only( + left: 30.h, + right: 12.h, + ), + child: AspectRatio( + aspectRatio: 1.5, + child: BarChart( + BarChartData( + alignment: BarChartAlignment.spaceAround, + barTouchData: BarTouchData(enabled: false), + titlesData: FlTitlesData( + show: true, + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + //getTitlesWidget: (value, meta) => newMethod(value), + ), + ), + ), + borderData: FlBorderData(show: false), + barGroups: [ + BarChartGroupData( + x: 0, + barRods: [ + BarChartRodData( + toY: consumedValues[0] / recommendedValues[0] * 100, + color: checkConsumedValues( + [recommendedValues[0]], [consumedValues[1]]) + ? Colors.red + : Colors.blue, + ), + ], + ), + BarChartGroupData( + x: 1, + barRods: [ + BarChartRodData( + toY: consumedValues[1] / recommendedValues[1] * 100, + color: checkConsumedValues( + [recommendedValues[1]], [consumedValues[1]]) + ? Colors.red + : Colors.blue, + ), + ], + ), + BarChartGroupData( + x: 2, + barRods: [ + BarChartRodData( + toY: consumedValues[2] / recommendedValues[2] * 100, + color: checkConsumedValues( + [recommendedValues[2]], [consumedValues[2]]) + ? Colors.red + : Colors.blue, + ), + ], + ), + BarChartGroupData( + x: 3, + barRods: [ + BarChartRodData( + toY: consumedValues[3] / recommendedValues[3] * 100, + color: checkConsumedValues( + [recommendedValues[3]], [consumedValues[3]]) + ? Colors.red + : Colors.blue, + ), + ], + ), + ], + ), + ), + ), + ), + ), + SizedBox(height: 8.v), + SizedBox(height: 2.v), + Padding( + padding: EdgeInsets.only( + left: 10.h, + right: 2.h, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Калорий", + style: theme.textTheme.bodySmall, + ), + Padding( + padding: EdgeInsets.only( + left: 25.h, + bottom: 11.v, + ), + child: Text( + "Белков", + style: theme.textTheme.bodySmall, + ), + ), + Padding( + padding: EdgeInsets.only( + left: 19.h, + bottom: 11.v, + ), + child: Text( + "Жиров", + style: theme.textTheme.bodySmall, + ), + ), + Padding( + padding: EdgeInsets.only( + left: 14.h, + bottom: 11.v, + ), + child: Text( + "Углеводов", + style: theme.textTheme.bodySmall, + ), + ), + ], + ), + ), + SizedBox(height: 2.v), + Padding( + padding: EdgeInsets.only( + left: 10.h, + right: 2.h, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${consumedValues[0].toInt()} из ${recommendedValues[0].toInt()}", + style: theme.textTheme.bodySmall, + ), + Padding( + padding: EdgeInsets.only( + left: 25.h, + bottom: 11.v, + ), + child: Text( + "${consumedValues[1].toInt()} из ${recommendedValues[1].toInt()}", + style: theme.textTheme.bodySmall, + ), + ), + Padding( + padding: EdgeInsets.only( + left: 19.h, + bottom: 11.v, + ), + child: Text( + "${consumedValues[2].toInt()} из ${recommendedValues[2].toInt()}", + style: theme.textTheme.bodySmall, + ), + ), + Padding( + padding: EdgeInsets.only( + left: 14.h, + bottom: 11.v, + ), + child: Text( + "${consumedValues[3].toInt()} из ${recommendedValues[3].toInt()}", + style: theme.textTheme.bodySmall, + ), + ), + ], + ), + ), + SizedBox(height: 19.v), + if (checkConsumedValues(recommendedValues, consumedValues)) + Text( + "*Превышенная суточная норма", + style: theme.textTheme.labelLarge, + ), + ], + ), + ); + } + + /// Section Widget + _buildComment(BuildContext context, DiaryEditLoadedState state) { + return Container( + margin: EdgeInsets.symmetric(horizontal: 16.h), + padding: EdgeInsets.symmetric( + horizontal: 24.h, + vertical: 12.v, + ), + 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, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Дополнительная информация для консультанта", + style: theme.textTheme.titleMedium, + textAlign: TextAlign.center, + ), + SizedBox(height: 20.v), + Container( + width: 320.h, + child: CustomTextFormField( + autofocus: false, + hintText: "Введите информацию для консультанта", + maxLines: 7, + borderDecoration: InputBorder.none, + controller: state.commentController, + ), + ), + ], + ), + ); + } + + _buildWakupRetire(BuildContext context, DiaryEditLoadedState state) { + return Container( + margin: EdgeInsets.symmetric(horizontal: 16.h), + padding: EdgeInsets.symmetric( + horizontal: 24.h, + vertical: 12.v, + ), + 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( + children: [ + GestureDetector( + onTap: toggleWakeup(context), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + r"Подъем \ отход ко сну", + style: CustomTextStyles.bodyMediumBlack900, + ), + Icon(state.wakeupExpanded + ? Icons.arrow_drop_up + : Icons.arrow_drop_down), + ], + ), + ), + if (state.wakeupExpanded) + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Column( + children: [ + SizedBox(height: 20.v), + GestureDetector( + onTap: () => _selectWakeupTime(context, state), + child: AbsorbPointer( + child: CustomTextFormField( + hintText: + state.wakeupController?.format(context).toString(), + prefix: Icon(Icons.sunny), + autofocus: false, + ), + ), + ), + SizedBox(height: 20.v), + GestureDetector( + onTap: () => _selectRetireTime(context, state), + child: AbsorbPointer( + child: CustomTextFormField( + hintText: + state.retireController?.format(context).toString(), + prefix: Icon(Icons.bed), + autofocus: false, + ), + ), + ), + SizedBox(height: 20.v), + ], + ), + ), + ], + ), + ); + } + + toggleWakeup(BuildContext context) { + return () { + BlocProvider.of(context).add(ToggleWakeupExpandedEvent()); + }; + } + + /// Section Widget + + Function() onFinishDayPressed(BuildContext context) { + return () { + showDialog( + context: context, + builder: (BuildContext dialogContext) { + return AlertDialog( + title: Text("Подтвердите действие"), + content: Text("Вы уверены, что хотите завершить день?"), + actions: [ + TextButton( + child: Text("Отмена"), + onPressed: closeDialog(dialogContext), + ), + TextButton( + child: Text("Подтвердить"), + onPressed: sendFinishDayData(dialogContext, context), + ), + ], + ); + }, + ); + }; + } + + Function() sendFinishDayData( + BuildContext dialogContext, BuildContext context) { + return () { + closeDialog(dialogContext); + context.read().add(FinishDay()); + }; + } + + Function() closeDialog(BuildContext dialogContext) { + return () { + Navigator.of(dialogContext).pop(); + }; + } + + _buildMood(BuildContext context, DiaryEditLoadedState state) { + return Container( + margin: EdgeInsets.symmetric(horizontal: 16.h), + padding: EdgeInsets.symmetric( + horizontal: 24.h, + vertical: 12.v, + ), + 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( + children: [ + GestureDetector( + onTap: toggleMood(context), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Ваше настроение после приема пищи", + style: CustomTextStyles.bodyMediumBlack900, + ), + Icon(state.moodExpanded + ? Icons.arrow_drop_up + : Icons.arrow_drop_down), + ], + ), + ), + if (state.moodExpanded) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("😭"), + ScaleElement( + color: Colors.red, + isSelected: state.selectedMood == 0, + onTap: changeMood(context, 0), + ), + ScaleElement( + color: Colors.orange, + isSelected: state.selectedMood == 1, + onTap: changeMood(context, 1), + ), + ScaleElement( + color: Colors.yellow, + isSelected: state.selectedMood == 2, + onTap: changeMood(context, 2), + ), + ScaleElement( + color: Colors.lime, + isSelected: state.selectedMood == 3, + onTap: changeMood(context, 3), + ), + ScaleElement( + color: Colors.lightGreen, + isSelected: state.selectedMood == 4, + onTap: changeMood(context, 4), + ), + ScaleElement( + color: Colors.green, + isSelected: state.selectedMood == 5, + onTap: changeMood(context, 5), + ), + Text("😊"), + // Остальные элементы шкалы + ], + ), + ], + ), + ); + } + + changeMood(BuildContext context, int satiety) { + return () { + context.read().add(ChangeMood(satiety)); + }; + } + + toggleMood(BuildContext context) { + return () { + BlocProvider.of(context).add(ToggleMoodExpandedEvent()); + }; + } + + Future _selectWakeupTime( + BuildContext context, DiaryEditLoadedState state) async { + final TimeOfDay? pickedTime = await showTimePicker( + context: context, + initialTime: state.wakeupController ?? TimeOfDay.now(), + ); + if (pickedTime != null && pickedTime != state.wakeupController) { + state.wakeupController = pickedTime; + context.read().add(WakeupEvent(pickedTime)); + } + } + + Future _selectRetireTime( + BuildContext context, DiaryEditLoadedState state) async { + final TimeOfDay? pickedTime = await showTimePicker( + context: context, + initialTime: state.retireController ?? TimeOfDay.now(), + ); + if (pickedTime != null && pickedTime != state.retireController) { + state.retireController = pickedTime; + context.read().add(RetireEvent(pickedTime)); + } + } + + Future _selectMealTime( + BuildContext context, DiaryEditLoadedState state) async { + final TimeOfDay? pickedTime = await showTimePicker( + context: context, + initialTime: state.mealTimeController ?? TimeOfDay.now(), + ); + if (pickedTime != null && pickedTime != state.mealTimeController) { + state.mealTimeController = pickedTime; + context.read().add(ChangeMealTime(pickedTime)); + } + } +} + +bool checkConsumedValues( + List recommendedValues, List consumedValues) { + for (int i = 0; i < recommendedValues.length; i++) { + if (consumedValues[i] > recommendedValues[i]) { + // Если хотя бы одно потребленное значение превышает рекомендованное + return true; // Возвращаем false + } + } + // Если ни одно потребленное значение не превышает рекомендованное с соответствующим индексом + return false; // Возвращаем true +} diff --git a/lib/features/diary/repositories/diary_repository.dart b/lib/features/diary/repositories/diary_repository.dart new file mode 100644 index 00000000..2f5dc214 --- /dev/null +++ b/lib/features/diary/repositories/diary_repository.dart @@ -0,0 +1,16 @@ +import 'package:eatvio_app/features/diary/models/diary_model.dart'; +import 'package:eatvio_app/features/diary/services/diary_service.dart'; + +class DiaryRepository { + final DiaryService blogPostService; + + DiaryRepository({required this.blogPostService}); + + Future getDiary(String blogPostID) async { + try { + return await blogPostService.getDiary(blogPostID); + } catch (e) { + rethrow; + } + } +} diff --git a/lib/features/diary/services/diary_service.dart b/lib/features/diary/services/diary_service.dart new file mode 100644 index 00000000..181005ef --- /dev/null +++ b/lib/features/diary/services/diary_service.dart @@ -0,0 +1,13 @@ +import 'package:dio/dio.dart'; +import 'package:eatvio_app/features/diary/models/diary_model.dart'; +import 'package:retrofit/retrofit.dart'; + +part 'diary_service.g.dart'; + +@RestApi() +abstract class DiaryService { + factory DiaryService(Dio dio, {String baseUrl}) = _DiaryService; + + @GET('/feed/show/{id}') + Future getDiary(@Path() String id); +}