From a2360a1c1926a5d34c917822f1b4f65c47c057ff Mon Sep 17 00:00:00 2001 From: maxxkim Date: Wed, 31 Jul 2024 12:21:17 +0300 Subject: [PATCH] feat(diary): Nutrition chart looks like half-donut --- .../presentation/pages/calendar_screen.dart | 41 +++-- .../diary/presentation/bloc/diary_bloc.dart | 54 ++++-- .../presentation/pages/diary_screen.dart | 161 +++++++++++++----- .../diary/repositories/diary_repository.dart | 2 - lib/main.dart | 3 +- 5 files changed, 181 insertions(+), 80 deletions(-) diff --git a/lib/features/calendar/presentation/pages/calendar_screen.dart b/lib/features/calendar/presentation/pages/calendar_screen.dart index 9dab6be2..646a3bb1 100644 --- a/lib/features/calendar/presentation/pages/calendar_screen.dart +++ b/lib/features/calendar/presentation/pages/calendar_screen.dart @@ -4,6 +4,7 @@ import 'package:eatvio_app/features/calendar/models/calendar_model.dart'; import 'package:eatvio_app/features/calendar/presentation/bloc/calendar_cubit.dart'; import 'package:eatvio_app/features/calendar/presentation/bloc/calendar_state.dart'; import 'package:eatvio_app/core/app_export.dart'; +import 'package:go_router/go_router.dart'; import 'package:table_calendar/table_calendar.dart'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; @@ -101,26 +102,34 @@ class CalendarScreen extends StatelessWidget { ], ), child: TableCalendar( + locale: 'ru_RU', firstDay: DateTime.utc(2010, 10, 16), lastDay: DateTime.utc(2030, 3, 14), focusedDay: DateTime.now(), startingDayOfWeek: StartingDayOfWeek.monday, calendarBuilders: CalendarBuilders( - selectedBuilder: (context, day, focusedDay) => _buildCellDate(day, focusedDay: focusedDay, state), - defaultBuilder: (context, day, focusedDay) => _buildCellDate(day, state), - todayBuilder: (context, day, focusedDay) => _buildCellDate(day, state), + selectedBuilder: (context, day, focusedDay) => + _buildCellDate(context, day, focusedDay: focusedDay, state), + defaultBuilder: (context, day, focusedDay) => + _buildCellDate(context, day, state), + todayBuilder: (context, day, focusedDay) => + _buildCellDate(context, day, state), ), ), ); } } -Widget _buildCellDate(DateTime day, CalendarStateLoaded state, {DateTime? focusedDay}) { +Widget _buildCellDate( + BuildContext context, DateTime day, CalendarStateLoaded state, + {DateTime? focusedDay}) { final String dayString = day.toIso8601String().split('T').first; final List calendarModels = state.calendarList; // Находим первую модель, соответствующую дате - final CalendarModel model = calendarModels.firstWhere((model) => model.date == dayString, orElse: () => CalendarModel(date: dayString, diary: {})); + final CalendarModel model = calendarModels.firstWhere( + (model) => model.date == dayString, + orElse: () => CalendarModel(date: dayString, diary: {})); Color? bgclr; // Объявляем переменную bgclr здесь @@ -138,14 +147,16 @@ Widget _buildCellDate(DateTime day, CalendarStateLoaded state, {DateTime? focuse bgclr = Colors.white; // Значение по умолчанию, если diary не является Map } - return Container( - margin: const EdgeInsets.all(5), - alignment: Alignment.center, - decoration: BoxDecoration( - shape: (day == focusedDay) ? BoxShape.circle : BoxShape.rectangle, - color: bgclr, - borderRadius: (day == focusedDay) ? null : BorderRadius.circular(4), - ), - child: Text(day.day.toString(), style: TextStyle(color: Colors.black)), - ); + return GestureDetector( + onTap: () => GoRouter.of(context).replace('/diary-screen'), + child: Container( + margin: const EdgeInsets.all(5), + alignment: Alignment.center, + decoration: BoxDecoration( + shape: (day == focusedDay) ? BoxShape.circle : BoxShape.rectangle, + color: bgclr, + borderRadius: (day == focusedDay) ? null : BorderRadius.circular(4), + ), + child: Text(day.day.toString(), style: TextStyle(color: Colors.black)), + )); } diff --git a/lib/features/diary/presentation/bloc/diary_bloc.dart b/lib/features/diary/presentation/bloc/diary_bloc.dart index 8ac0e8a2..e1b952b8 100644 --- a/lib/features/diary/presentation/bloc/diary_bloc.dart +++ b/lib/features/diary/presentation/bloc/diary_bloc.dart @@ -16,7 +16,8 @@ part 'diary_event.dart'; class DiaryBloc extends Bloc { final DiaryRepository diaryRepository; - DiaryBloc({required this.diaryRepository}) : super(const DiaryState.loading()) { + DiaryBloc({required this.diaryRepository}) + : super(const DiaryState.loading()) { on(loadDiary); on(_changeSatiety); // пользователь нажимает на шкалу сытости on(_changeMood); // пользователь нажимает на шкалу настроения @@ -53,15 +54,29 @@ class DiaryBloc extends Bloc { final DiaryModel diary = await diaryRepository.getDiary(diaryID ?? 45); final Meal currentMeal = diary.meals[0]; - List portionList = [SelectionPopupModel(title: "gr"), SelectionPopupModel(title: "pcs")]; + List portionList = [ + SelectionPopupModel(title: "gr"), + SelectionPopupModel(title: "pcs") + ]; - final Map chartData = await diaryRepository.getChartData(diaryID ?? 45); - final Map dailyNutritionIntake = chartData['dailyNutritionIntake']!; - final Map dailyNutritionRecommendations = chartData['dailyNutritionRecommendation']!; - final double ccal = dailyNutritionIntake['energy']! / dailyNutritionRecommendations['energy']! * 100; - final double proteins = dailyNutritionIntake['proteins']! / dailyNutritionRecommendations['proteins']! * 100; - final double fats = dailyNutritionIntake['fats']! / dailyNutritionRecommendations['fats']! * 100; - final double carbs = dailyNutritionIntake['carbohydrates']! / dailyNutritionRecommendations['carbohydrates']! * 100; + final Map chartData = + await diaryRepository.getChartData(diaryID ?? 45); + final Map dailyNutritionIntake = + chartData['dailyNutritionIntake']!; + final Map dailyNutritionRecommendations = + chartData['dailyNutritionRecommendation']!; + final double ccal = dailyNutritionIntake['energy']! / + dailyNutritionRecommendations['energy']! * + 50; + final double proteins = dailyNutritionIntake['proteins']! / + dailyNutritionRecommendations['proteins']! * + 50; + final double fats = dailyNutritionIntake['fats']! / + dailyNutritionRecommendations['fats']! * + 50; + final double carbs = dailyNutritionIntake['carbohydrates']! / + dailyNutritionRecommendations['carbohydrates']! * + 50; Color getColor(double value) { if (value >= 99.1) { @@ -163,9 +178,11 @@ class DiaryBloc extends Bloc { ) async { if (state is DiaryStateLoaded) { var currentState = state as DiaryStateLoaded; - var updatedState = currentState.copyWith(wakeupController: event.selectedTime); + var updatedState = + currentState.copyWith(wakeupController: event.selectedTime); final DateFormat formatter = DateFormat('HH:mm'); - final String wakeupString = formatter.format(DateTime(2022, 1, 1, event.selectedTime.hour, event.selectedTime.minute)); + final String wakeupString = formatter.format(DateTime( + 2022, 1, 1, event.selectedTime.hour, event.selectedTime.minute)); await diaryRepository.changeWakeup( currentState.diary.id, wakeupString, @@ -180,9 +197,11 @@ class DiaryBloc extends Bloc { ) async { if (state is DiaryStateLoaded) { var currentState = state as DiaryStateLoaded; - var updatedState = currentState.copyWith(retireController: event.selectedTime); + var updatedState = + currentState.copyWith(retireController: event.selectedTime); final DateFormat formatter = DateFormat('HH:mm'); - final String retireString = formatter.format(DateTime(2022, 1, 1, event.selectedTime.hour, event.selectedTime.minute)); + final String retireString = formatter.format(DateTime( + 2022, 1, 1, event.selectedTime.hour, event.selectedTime.minute)); await diaryRepository.changeRetire( currentState.diary.id, retireString, @@ -208,7 +227,8 @@ class DiaryBloc extends Bloc { ) async { if (state is DiaryStateLoaded) { final currentState = state as DiaryStateLoaded; - final Meal meal = currentState.diary.meals.firstWhere((meal) => meal.type == event.value.id); + final Meal meal = currentState.diary.meals + .firstWhere((meal) => meal.type == event.value.id); DiaryStateLoaded updatedState = currentState.copyWith( currentMeal: meal, portions: meal.portions, @@ -227,10 +247,12 @@ class DiaryBloc extends Bloc { if (state is DiaryStateLoaded) { var currentState = state as DiaryStateLoaded; - final Map> productNutritionalValues = await diaryRepository.inputProduct(event.input); + final Map> productNutritionalValues = + await diaryRepository.inputProduct(event.input); var updatedState = currentState.copyWith( product_nameController: TextEditingController(text: event.input), - autocompleteList: productNutritionalValues['product_nutritional_value'] ?? [], + autocompleteList: + productNutritionalValues['product_nutritional_value'] ?? [], ); if (event.input.length > 3) { updatedState = updatedState.copyWith(moderationExpanded: true); diff --git a/lib/features/diary/presentation/pages/diary_screen.dart b/lib/features/diary/presentation/pages/diary_screen.dart index 8be9be46..e360f35b 100644 --- a/lib/features/diary/presentation/pages/diary_screen.dart +++ b/lib/features/diary/presentation/pages/diary_screen.dart @@ -111,35 +111,80 @@ class DiaryScreen extends StatelessWidget { _buildNutritionChart(BuildContext context, DiaryStateLoaded state) { 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: SfCircularChart( - title: ChartTitle(text: 'График КБЖУ'), - legend: Legend(isVisible: true), - series: [ - RadialBarSeries( - dataSource: state.chartData, - xValueMapper: (NutritionData data, _) => data.category, - yValueMapper: (NutritionData data, _) => data.nutrition, - pointColorMapper: (NutritionData data, _) => data.color, - ) - ], - )); + 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: Column( + children: [ + Text( + "График КБЖУ", + style: theme.textTheme.titleMedium, + textAlign: TextAlign.center, + ), + Transform.rotate( + angle: -1.5708, + child: ClipRect( + child: Align( + alignment: Alignment.centerRight, + widthFactor: 0.5, + child: SfCircularChart( + legend: Legend(isVisible: false), + series: [ + RadialBarSeries( + dataSource: state.chartData, + xValueMapper: (NutritionData data, _) => data.category, + yValueMapper: (NutritionData data, _) => data.nutrition, + pointColorMapper: (NutritionData data, _) => data.color, + ) + ], + ), + ), + )), + SizedBox(height: 16.0), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Калории'), + Text('Белки'), + Text('Жиры'), + Text('Углеводы'), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + '${state.chartData![0].nutrition.toStringAsFixed(1)}'), // Калории + Text( + '${state.chartData![1].nutrition.toStringAsFixed(1)}'), // Белки + Text( + '${state.chartData![2].nutrition.toStringAsFixed(1)}'), // Жиры + Text( + '${state.chartData![3].nutrition.toStringAsFixed(1)}'), // Углеводы + ], + ), + ], + ), + ], + ), + ); } _buildEndDay(BuildContext context) { @@ -278,7 +323,9 @@ class DiaryScreen extends StatelessWidget { borderRadius: BorderRadiusStyle.roundedBorder5, ), child: Center( - child: CircularProgressIndicator(color: appTheme.blueGray300), // Замените на свой прелоадер при необходимости + child: CircularProgressIndicator( + color: appTheme + .blueGray300), // Замените на свой прелоадер при необходимости ), ), ], @@ -302,7 +349,8 @@ class DiaryScreen extends StatelessWidget { _buildMeal(BuildContext context, DiaryStateLoaded state) { final digitsOnly = FilteringTextInputFormatter.digitsOnly; - List _options = state.autocompleteList.map((Product item) => item.name).toList(); + List _options = + state.autocompleteList.map((Product item) => item.name).toList(); FocusNode focusNodeName = FocusNode(); return Container( margin: EdgeInsets.symmetric(horizontal: 16.h), @@ -346,7 +394,9 @@ class DiaryScreen extends StatelessWidget { onTap: () => _selectMealTime(context, state), child: AbsorbPointer( child: CustomTextFormField( - hintText: state.mealTimeController?.format(context).toString(), + hintText: state.mealTimeController + ?.format(context) + .toString(), autofocus: false, ), ), @@ -482,7 +532,9 @@ class DiaryScreen extends StatelessWidget { SizedBox(height: 30.v), _buildAddProduct(context, state), SizedBox(height: 30.v), - ...state.portions.map((portion) => _buildProduct(context, portion)).toList(), + ...state.portions + .map((portion) => _buildProduct(context, portion)) + .toList(), SizedBox(height: 30.v), _buildCarousel(context, state), SizedBox(height: 20.v), @@ -631,7 +683,8 @@ class DiaryScreen extends StatelessWidget { popDialog(BuildContext dialogContext) { return () { - Navigator.of(dialogContext).pop(); // Dismiss the dialog on Cancel button press + Navigator.of(dialogContext) + .pop(); // Dismiss the dialog on Cancel button press }; } @@ -641,7 +694,8 @@ class DiaryScreen extends StatelessWidget { }; } - selectProduct(AutocompleteOnSelected onSelected, String option, DiaryStateLoaded state) { + selectProduct(AutocompleteOnSelected onSelected, String option, + DiaryStateLoaded state) { state.product_nameController.text = option; return () { onSelected(option); @@ -709,7 +763,9 @@ class DiaryScreen extends StatelessWidget { top: 5.v, ), child: Text( - portion.product_name.length > 8 ? portion.product_name.substring(0, 8) : portion.product_name, + portion.product_name.length > 8 + ? portion.product_name.substring(0, 8) + : portion.product_name, style: CustomTextStyles.titleMedium16, ), ), @@ -781,7 +837,9 @@ class DiaryScreen extends StatelessWidget { removePhoto(BuildContext context, Photo photo) { return () { - context.read().add(RemoveImageEvent(photo.id.toString(), photo.path)); + context + .read() + .add(RemoveImageEvent(photo.id.toString(), photo.path)); }; } @@ -859,7 +917,9 @@ class DiaryScreen extends StatelessWidget { r"Подъем \ отход ко сну", style: CustomTextStyles.bodyMediumBlack900, ), - Icon(state.wakeupExpanded ? Icons.arrow_drop_up : Icons.arrow_drop_down), + Icon(state.wakeupExpanded + ? Icons.arrow_drop_up + : Icons.arrow_drop_down), ], ), ), @@ -873,7 +933,8 @@ class DiaryScreen extends StatelessWidget { onTap: () => _selectWakeupTime(context, state), child: AbsorbPointer( child: CustomTextFormField( - hintText: state.wakeupController.format(context).toString(), + hintText: + state.wakeupController.format(context).toString(), prefix: Icon(Icons.sunny), autofocus: false, ), @@ -884,7 +945,8 @@ class DiaryScreen extends StatelessWidget { onTap: () => _selectRetireTime(context, state), child: AbsorbPointer( child: CustomTextFormField( - hintText: state.retireController.format(context).toString(), + hintText: + state.retireController.format(context).toString(), prefix: Icon(Icons.bed), autofocus: false, ), @@ -931,7 +993,8 @@ class DiaryScreen extends StatelessWidget { }; } - Function() sendFinishDayData(BuildContext dialogContext, BuildContext context) { + Function() sendFinishDayData( + BuildContext dialogContext, BuildContext context) { return () { closeDialog(dialogContext); context.read().add(FinishDay()); @@ -974,7 +1037,9 @@ class DiaryScreen extends StatelessWidget { "Ваше настроение после приема пищи", style: CustomTextStyles.bodyMediumBlack900, ), - Icon(state.moodExpanded ? Icons.arrow_drop_up : Icons.arrow_drop_down), + Icon(state.moodExpanded + ? Icons.arrow_drop_up + : Icons.arrow_drop_down), ], ), ), @@ -1034,7 +1099,8 @@ class DiaryScreen extends StatelessWidget { }; } - Future _selectWakeupTime(BuildContext context, DiaryStateLoaded state) async { + Future _selectWakeupTime( + BuildContext context, DiaryStateLoaded state) async { final TimeOfDay? pickedTime = await showTimePicker( context: context, initialTime: state.wakeupController, @@ -1044,7 +1110,8 @@ class DiaryScreen extends StatelessWidget { } } - Future _selectRetireTime(BuildContext context, DiaryStateLoaded state) async { + Future _selectRetireTime( + BuildContext context, DiaryStateLoaded state) async { final TimeOfDay? pickedTime = await showTimePicker( context: context, initialTime: state.retireController, @@ -1054,7 +1121,8 @@ class DiaryScreen extends StatelessWidget { } } - Future _selectMealTime(BuildContext context, DiaryStateLoaded state) async { + Future _selectMealTime( + BuildContext context, DiaryStateLoaded state) async { final TimeOfDay? pickedTime = await showTimePicker( context: context, initialTime: state.mealTimeController ?? TimeOfDay.now(), @@ -1065,7 +1133,8 @@ class DiaryScreen extends StatelessWidget { } } -bool checkConsumedValues(List recommendedValues, List consumedValues) { +bool checkConsumedValues( + List recommendedValues, List consumedValues) { for (int i = 0; i < recommendedValues.length; i++) { if (consumedValues[i] > recommendedValues[i]) { // Если хотя бы одно потребленное значение превышает рекомендованное diff --git a/lib/features/diary/repositories/diary_repository.dart b/lib/features/diary/repositories/diary_repository.dart index 599c0810..04a8cad0 100644 --- a/lib/features/diary/repositories/diary_repository.dart +++ b/lib/features/diary/repositories/diary_repository.dart @@ -1,6 +1,4 @@ import 'dart:io'; -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; import 'package:eatvio_app/features/diary/models/diary_model.dart'; import 'package:eatvio_app/features/diary/models/portion.dart'; diff --git a/lib/main.dart b/lib/main.dart index 9ce1b92e..d628b940 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -18,6 +18,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:keyboard_dismisser/keyboard_dismisser.dart'; import 'package:eatvio_app/locator_service.dart' as di; +import 'package:intl/date_symbol_data_local.dart'; var globalMessengerKey = GlobalKey(); @@ -34,7 +35,7 @@ void main() async { DeviceOrientation.portraitUp, ]); await PrefUtils().init(); - runApp(MyApp()); + initializeDateFormatting().then((_) => runApp(MyApp())); } class MyApp extends StatelessWidget {