import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/features/diary/models/diary_day_model.dart'; import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart'; import 'package:eatvio_app/features/glossary/models/glossary_model.dart'; import 'package:eatvio_app/locator_service.dart'; import 'package:eatvio_app/theme/theme_helper.dart'; import 'package:eatvio_app/widgets/custom_text_form_field.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:image_picker/image_picker.dart'; import 'package:rive/rive.dart'; import 'package:table_calendar/table_calendar.dart'; class DiaryScreen extends StatefulWidget { const DiaryScreen({super.key}); @override State createState() => _DiaryScreenState(); } class _DiaryScreenState extends State { final DiaryCubit diaryCubit = sl(); CalendarFormat _calendarFormat = CalendarFormat.month; DateTime _focusedDay = DateTime.now(); DateTime? _selectedDay = DateTime.now(); int currentSelectedMealNumber = 1; GlossaryModel? selectedGlossary; final productNameController = TextEditingController(); final quantityController = TextEditingController(); int currentSelectedPhotoIndex = 0; @override Widget build(BuildContext context) { return BlocProvider( create: (BuildContext context) => diaryCubit..getDiaryDay(_selectedDay!.toString()), child: BlocConsumer( listener: (BuildContext context, DiaryState state) { if (state is DiaryStateLoaded) { setState(() { _calendarFormat = CalendarFormat.week; }); } }, builder: (BuildContext context, DiaryState state) { return Scaffold( backgroundColor: Colors.grey.shade200, appBar: AppBar( toolbarHeight: 0, ), body: SingleChildScrollView( physics: const BouncingScrollPhysics(), child: Column( children: [ _calendar(state), if (state is DiaryStateLoading) _loader(), if (state is DiaryStateLoaded) _meals(state.diary), const SizedBox(height: 32), ], ), ), ); }, ), ); } _arrowDown() { return Container( padding: const EdgeInsets.only(top: 8, bottom: 16), width: double.infinity, child: Icon( _calendarFormat == CalendarFormat.month ? Icons.keyboard_arrow_up_rounded : Icons.keyboard_arrow_down_rounded, color: Colors.grey.shade600, ), ); } _divider(BuildContext context) { return Container( height: 0.4, width: MediaQuery.of(context).size.width - 32, decoration: BoxDecoration( color: Colors.black26, borderRadius: BorderRadius.circular(16), ), ); } _products(List portions, DiaryDayModel diaryDay) { return Column( mainAxisSize: MainAxisSize.min, children: [ for (int i = 0; i < portions.length; i++) ...[ _product( diaryDay, portions[i].id, portions[i].productName, portions[i].quantity, portions[i].portion, ), i < portions.length - 1 ? const SizedBox(height: 8) : const SizedBox.shrink(), ], ], ); } _product(DiaryDayModel diaryDay, int id, String product, int quantity, String portion) { portion == 'gr' ? portion = 'г' : portion = 'шт'; return ListTile( onTap: () { setState(() { productNameController.text = product; quantityController.text = quantity.toString(); }); _modalBottomSheet(diaryDay, id); }, trailing: Icon(Icons.more_vert_rounded), title: Text(product), subtitle: Text('$quantity $portion'), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), tileColor: Color(0xFFF6FAFF), ); } _meals(DiaryDayModel diaryDay) { return Material( borderRadius: BorderRadius.circular(20), color: Colors.white, child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Прием пищи', style: TextStyle( color: Color(0xFF323F4B), fontSize: 18, fontWeight: FontWeight.w500, ), ), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _mealButton(1), _mealButton(2), _mealButton(3), _mealButton(4), _mealButton(5), ], ), const SizedBox(height: 12), _divider(context), const SizedBox(height: 12), Text( 'Время приема пищи', style: TextStyle( color: Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w500, ), ), const SizedBox(height: 12), _divider(context), const SizedBox(height: 12), _products(diaryDay.meals[currentSelectedMealNumber - 1].portions, diaryDay), const SizedBox(height: 16), _addProductButton(diaryDay), const SizedBox(height: 24), if (diaryDay.meals[currentSelectedMealNumber - 1].photos.isEmpty) GestureDetector( onTap: () => _showImagePicker(diaryDay), child: SvgPicture.asset( width: MediaQuery.of(context).size.width - 32, 'assets/no_one_image_was_added.svg', ), ), if (diaryDay.meals[currentSelectedMealNumber - 1].photos.isNotEmpty) _photoGallery( diaryDay.meals[currentSelectedMealNumber - 1].photos, diaryDay, ), ], ), ), ); } _showImagePicker(DiaryDayModel diaryDay) { showCupertinoModalPopup( context: context, builder: (context) => CupertinoActionSheet( actions: [ CupertinoActionSheetAction( onPressed: () async { final picker = ImagePicker(); XFile? image = await picker.pickImage(source: ImageSource.camera); if (image != null) diaryCubit.uploadPhotos( files: [File(image.path)], diaryId: diaryDay.id, mealId: diaryDay.meals[currentSelectedMealNumber - 1].id, date: _selectedDay.toString(), ); Navigator.of(context).pop(); }, child: const Row( children: const [ Icon(Icons.photo_camera), SizedBox(width: 8), Text('Фото'), ], ), ), CupertinoActionSheetAction( onPressed: () async { final picker = ImagePicker(); List? images = await picker.pickMultiImage(); if (images.isNotEmpty) diaryCubit.uploadPhotos( files: images.map((f) => File(f.path)).toList(), diaryId: diaryDay.id, mealId: diaryDay.meals[currentSelectedMealNumber - 1].id, date: _selectedDay.toString(), ); Navigator.of(context).pop(); }, child: const Row( children: const [ Icon(Icons.photo), SizedBox(width: 8), Text('Галерея'), ], ), ), ], cancelButton: CupertinoActionSheetAction( onPressed: () => Navigator.of(context).pop(), isDestructiveAction: true, child: const Text('Отмена'), ), ), ); } _photoGallery(List photos, DiaryDayModel diaryDay) { return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Stack( alignment: Alignment.topRight, children: [ ClipRRect( borderRadius: BorderRadius.circular(12), child: CachedNetworkImage( imageUrl: baseUrl + '/storage/' + photos[currentSelectedPhotoIndex].path, ), ), Padding( padding: const EdgeInsets.all(12), child: IconButton.filled( style: IconButton.styleFrom( backgroundColor: Colors.white, ), onPressed: () { setState(() { if (currentSelectedPhotoIndex == photos.length - 1 && currentSelectedPhotoIndex != 0) { currentSelectedPhotoIndex = photos.length - 2; } }); diaryCubit.deletePhoto( photo: photos[currentSelectedPhotoIndex], diaryId: diaryDay.id, mealId: diaryDay.meals[currentSelectedMealNumber - 1].id, date: _selectedDay.toString(), ); }, icon: Icon( Icons.close_rounded, color: Colors.grey, ), ), ), ], ), const SizedBox(height: 8), SingleChildScrollView( physics: const BouncingScrollPhysics(), scrollDirection: Axis.horizontal, child: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, children: [ for (int i = 0; i < photos.length + 1; i++) ...[ if (i == photos.length) Padding( padding: const EdgeInsets.only(left: 8), child: InkWell( onTap: () => _showImagePicker(diaryDay), borderRadius: BorderRadius.circular(8), child: Ink( height: 52, width: 52, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), color: Color(0xFFF1F2F6), ), child: Icon( Icons.add_rounded, color: appTheme.blueA400, ), ), ), ), if (i < photos.length) GestureDetector( onTap: () { setState(() { currentSelectedPhotoIndex = i; }); }, child: ClipRRect( borderRadius: BorderRadius.circular(8), child: SizedBox( height: 52, width: 52, child: CachedNetworkImage( imageUrl: baseUrl + '/storage/' + photos[i].path, fit: BoxFit.fitWidth, ), ), ), ), if (i < photos.length - 1) const SizedBox(width: 8), ], ], ), ), ], ); } _mealButton(int mealNumber) { bool isActive = currentSelectedMealNumber == mealNumber; return InkWell( onTap: () { setState(() { if (currentSelectedMealNumber == mealNumber) return; currentSelectedMealNumber = mealNumber; }); }, borderRadius: BorderRadius.circular(100), child: Ink( width: MediaQuery.of(context).size.width / 5 - (96 / 5), height: 32, decoration: BoxDecoration( borderRadius: BorderRadius.circular(100), color: isActive ? appTheme.blueA400 : Color(0xFFF1F2F6), ), child: Center( child: Text( '$mealNumber', style: TextStyle( color: isActive ? Colors.white : Colors.black, fontSize: 16, fontWeight: FontWeight.w600, ), ), ), ), ); } _loader() { return Center( child: SizedBox( height: 32, width: 32, child: RiveAnimation.asset( 'assets/rive_animations/loading.riv', ), ), ); } _calendar(DiaryState state) { return Column( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: const EdgeInsets.only(top: 16), child: TableCalendar( firstDay: DateTime.utc(2010, 10, 16), lastDay: DateTime.utc(2030, 3, 14), locale: 'ru_RU', startingDayOfWeek: StartingDayOfWeek.monday, focusedDay: _focusedDay, calendarFormat: _calendarFormat, headerStyle: HeaderStyle( titleCentered: true, formatButtonVisible: false, leftChevronVisible: false, rightChevronVisible: false, ), selectedDayPredicate: (day) { return isSameDay(_selectedDay, day); }, onDaySelected: (selectedDay, focusedDay) { if (state is! DiaryStateLoaded) return; if (!isSameDay(_selectedDay, selectedDay)) { setState(() { _selectedDay = selectedDay; _focusedDay = focusedDay; currentSelectedMealNumber = 1; }); diaryCubit.getDiaryDay(_selectedDay!.toString()); } }, onFormatChanged: (format) { if (_calendarFormat != format) { setState(() { _calendarFormat = format; }); } }, onPageChanged: (focusedDay) { _focusedDay = focusedDay; }, calendarStyle: CalendarStyle( weekendDecoration: BoxDecoration( color: Colors.white, shape: BoxShape.circle, ), defaultDecoration: BoxDecoration( color: Colors.white, shape: BoxShape.circle, ), selectedDecoration: BoxDecoration( color: appTheme.blueA400, shape: BoxShape.circle, ), todayTextStyle: TextStyle( color: Colors.black, ), todayDecoration: BoxDecoration( border: Border.all( color: appTheme.blueA400, width: 2, ), color: Colors.white, shape: BoxShape.circle, ), ), ), ), _arrowDown(), ], ); } _modalBottomSheet(DiaryDayModel diaryDay, [int? portionId]) { return showModalBottomSheet( useRootNavigator: true, context: context, shape: RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(12)), ), isScrollControlled: true, builder: (context) { return StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return Padding( padding: const EdgeInsets.all(16) + EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( 'Добавление блюда', style: TextStyle( fontSize: 20, fontWeight: FontWeight.w600, color: Color(0xFF323F4B), ), ), const SizedBox(height: 16), TypeAheadField( controller: productNameController, itemBuilder: (context, glossary) { return Padding( padding: const EdgeInsets.all(8.0), child: Text( glossary.name, style: TextStyle( height: 1, fontSize: 16, ), ), ); }, itemSeparatorBuilder: (context, index) => _divider(context), onSelected: (newSelectedGlossary) { setState(() { selectedGlossary = newSelectedGlossary; productNameController.text = selectedGlossary!.name; }); }, suggestionsCallback: (query) async { if (query.length < 1) return []; final suggestionsList = await diaryCubit.getGlossaryListByQuery(query); if (suggestionsList.length == 1) { setState(() { selectedGlossary = suggestionsList.first; }); } return suggestionsList; }, builder: (context, controller, focusNode) { return CustomTextFormField( controller: controller, autofocus: true, focusNode: focusNode, hintText: 'Введите название продукта/блюда', ); }, emptyBuilder: (context) => const SizedBox.shrink(), errorBuilder: (context, object) => Padding( padding: const EdgeInsets.all(8.0), child: Text('Нет подходящих вариантов'), ), ), if (selectedGlossary != null) Padding( padding: const EdgeInsets.only(top: 8), child: CustomTextFormField( controller: quantityController, keyboardType: TextInputType.number, hintText: selectedGlossary!.grInPortion == 0 ? '100' : '1', autofocus: true, suffixText: selectedGlossary!.grInPortion == 0 ? 'гр' : 'шт', ), ), const SizedBox(height: 16), Row( children: [ if (portionId != null) InkWell( onTap: () { diaryCubit.deleteProductFromMeal( diaryId: diaryDay.id, mealId: diaryDay.meals[currentSelectedMealNumber - 1].id, portionId: portionId, date: _selectedDay.toString(), ); Navigator.of(context).pop(); }, borderRadius: BorderRadius.circular(20), child: Ink( width: 52, height: 52, decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), color: Color(0xFFF6F6F6), ), child: Icon( Icons.delete_outline_rounded, color: Colors.red, ), ), ), if (portionId != null) const SizedBox(width: 8), InkWell( onTap: () { if (portionId == null) { diaryCubit.addProductToMeal( diaryId: diaryDay.id, mealId: diaryDay.meals[currentSelectedMealNumber - 1].id, productName: productNameController.text, portion: selectedGlossary!.grInPortion == 0 ? 'gr' : 'pcs', quantity: int.parse(quantityController.text), date: _selectedDay.toString(), ); } else { diaryCubit.updateProductInMeal( diaryId: diaryDay.id, mealId: diaryDay.meals[currentSelectedMealNumber - 1].id, portionId: portionId, productName: productNameController.text, portion: selectedGlossary!.grInPortion == 0 ? 'gr' : 'pcs', quantity: int.parse(quantityController.text), date: _selectedDay.toString(), ); } Navigator.of(context).pop(); }, borderRadius: BorderRadius.circular(20), child: Ink( width: MediaQuery.of(context).size.width - (portionId == null ? 32 : 92), height: 52, decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), color: appTheme.blueA400, ), child: Center( child: Text( portionId == null ? 'Добавить' : 'Сохранить', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white, ), ), ), ), ), ], ), ], ), ); }, ); }, ); } _addProductButton(DiaryDayModel diaryDay) { return InkWell( onTap: () { setState(() { selectedGlossary = null; productNameController.clear(); quantityController.clear(); }); _modalBottomSheet(diaryDay); }, borderRadius: BorderRadius.circular(20), child: Ink( padding: EdgeInsets.symmetric(horizontal: 16), width: MediaQuery.of(context).size.width - 32, decoration: BoxDecoration( color: appTheme.blueA400, borderRadius: BorderRadius.circular(20), ), child: Container( padding: EdgeInsets.all(16), alignment: Alignment.center, child: Text( 'Добавить', style: TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600, ), ), ), ), ); } }