feat: diary refactor

This commit is contained in:
Alex
2024-11-27 16:23:48 +03:00
parent 421190b6c9
commit 0d19fde557
21 changed files with 1253 additions and 1229 deletions

View File

@@ -1,25 +1,12 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/widgets/loader.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/diary/presentation/widgets/calories_chat_widget.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/finish_day_button_widget.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/glossary_moderation_request_modal_widget.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/satiety_rating_widget.dart';
import 'package:eatvio_app/features/glossary/models/glossary_model.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/calendar_widget.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:flutter/cupertino.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/calories_chart_widget.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/meal_widget.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:table_calendar/table_calendar.dart';
class DiaryScreen extends StatefulWidget {
const DiaryScreen({super.key});
@@ -29,88 +16,11 @@ class DiaryScreen extends StatefulWidget {
}
class _DiaryScreenState extends State<DiaryScreen> {
final DiaryCubit diaryCubit = sl<DiaryCubit>();
CalendarFormat _calendarFormat = CalendarFormat.month;
DateTime _focusedDay = DateTime.now();
DateTime? _selectedDay = DateTime.now();
late final DateTime? today;
int currentSelectedMealNumber = 1;
GlossaryModel? selectedGlossary;
final productNameController = TextEditingController();
final quantityController = TextEditingController();
int currentSelectedPhotoIndex = 0;
String time = "${DateTime.now().hour.toString().padLeft(2, '0')}:${DateTime.now().minute.toString().padLeft(2, '0')}";
DateTime _time = DateTime.now();
@override
void initState() {
super.initState();
today = _selectedDay;
}
_showDatePicker() {
showModalBottomSheet<void>(
context: context,
useRootNavigator: true,
builder: (context) => SizedBox(
height: MediaQuery.of(context).copyWith().size.height / 3,
child: Column(
children: [
Expanded(
child: CupertinoDatePicker(
mode: CupertinoDatePickerMode.time,
use24hFormat: true,
initialDateTime: DateTime.now(),
onDateTimeChanged: (newTime) {
setState(() {
_time = newTime;
time = "${_time.hour.toString().padLeft(2, '0')}:${_time.minute.toString().padLeft(2, '0')}";
});
},
),
),
InkWell(
onTap: () {
Navigator.of(context).pop();
},
borderRadius: BorderRadius.circular(20),
child: Ink(
width: MediaQuery.of(context).size.width - 16,
height: 52,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: appTheme.blueA400,
),
child: Center(
child: Text(
'Сохранить',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return BlocProvider<DiaryCubit>(
create: (BuildContext context) => diaryCubit..getDiaryDay(_selectedDay!.toString()),
child: BlocConsumer<DiaryCubit, DiaryState>(
listener: (BuildContext context, DiaryState state) {
if (state is DiaryStateLoaded) {
setState(() {
_calendarFormat = CalendarFormat.week;
});
}
},
create: (BuildContext context) => sl<DiaryCubit>()..fetchDiary(),
child: BlocBuilder<DiaryCubit, DiaryState>(
builder: (BuildContext context, DiaryState state) {
return Scaffold(
backgroundColor: Colors.grey.shade200,
@@ -121,23 +31,10 @@ class _DiaryScreenState extends State<DiaryScreen> {
physics: const BouncingScrollPhysics(),
child: Column(
children: [
_calendar(state),
CalendarWidget(),
if (state is DiaryStateLoading) const Loader(),
if (state is DiaryStateLoaded)
_meals(
state.diary,
state.currentSelectedSatietyRating,
),
if (state is DiaryStateLoaded && _selectedDay!.compareTo(today!) != 1)
CaloriesChatWidget(
nutrition: state.nutrition,
),
if (state is DiaryStateLoaded && state.diary.closed == 0 && _selectedDay == today)
Padding(
padding: const EdgeInsets.only(top: 12),
child: FinishDayButtonWidget(diaryDay: state.diary),
),
const SizedBox(height: 32),
if (state is DiaryStateLoaded) MealWidget(),
CaloriesChartWidget(),
],
),
),
@@ -146,693 +43,4 @@ class _DiaryScreenState extends State<DiaryScreen> {
),
);
}
_timeOfMeal(String time) {
return InkWell(
onTap: () => _showDatePicker(),
borderRadius: BorderRadius.circular(100),
child: Ink(
height: 32,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(100),
color: Color(0xFFF1F2F6),
),
child: Container(
alignment: Alignment.center,
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text(
time,
style: TextStyle(
color: Color(0xFF6D6F71),
fontSize: 17,
fontWeight: FontWeight.w500,
),
),
),
),
);
}
_nextCalendarFormat() {
setState(() {
_calendarFormat = CalendarFormat.values[(_calendarFormat.index - 1) % CalendarFormat.values.length];
});
}
_arrowDown() {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onVerticalDragStart: (_) {
_nextCalendarFormat();
},
onTap: () {
_nextCalendarFormat();
},
child: Container(
width: double.infinity,
padding: const EdgeInsets.only(top: 8, bottom: 16),
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<Portion> 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,
List<int> satietyRating,
) {
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),
],
),
_selectedDay!.compareTo(today!) == 1 ? const SizedBox.shrink() : const SizedBox(height: 12),
_selectedDay!.compareTo(today!) == 1 ? const SizedBox.shrink() : _divider(context),
_selectedDay!.compareTo(today!) == 1 ? const SizedBox.shrink() : const SizedBox(height: 12),
_selectedDay!.compareTo(today!) == 1
? const SizedBox.shrink()
: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Время приема пищи',
style: TextStyle(
color: Color(0xFF323F4B),
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
_timeOfMeal(time),
],
),
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)
_selectedDay!.compareTo(today!) == 1
? const SizedBox.shrink()
: 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,
),
_selectedDay!.compareTo(today!) == 1
? const SizedBox.shrink()
: SatietyRatingWidget(
currentSelectedSatietyRating: satietyRating,
diaryId: diaryDay.id,
meal: currentSelectedMealNumber,
),
],
),
),
);
}
_showImagePicker(DiaryDayModel diaryDay) {
showCupertinoModalPopup<void>(
context: context,
builder: (context) => CupertinoActionSheet(
actions: [
CupertinoActionSheetAction(
onPressed: () async {
Navigator.of(context).pop();
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(),
);
},
child: const Row(
children: const [
Icon(Icons.photo_camera),
SizedBox(width: 8),
Text('Фото'),
],
),
),
CupertinoActionSheetAction(
onPressed: () async {
Navigator.of(context).pop();
final picker = ImagePicker();
List<XFile>? 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(),
);
},
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<DiaryPhotoModel> 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,
elevation: 0,
),
onPressed: () async {
await diaryCubit.deletePhoto(
photo: photos[currentSelectedPhotoIndex],
diaryId: diaryDay.id,
mealId: diaryDay.meals[currentSelectedMealNumber - 1].id,
date: _selectedDay.toString(),
);
setState(() {
if (currentSelectedPhotoIndex == photos.length - 1 && currentSelectedPhotoIndex != 0) {
currentSelectedPhotoIndex = photos.length - 2;
}
});
},
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 : Color(0xFF6D6F71),
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
);
}
_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),
Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: MediaQuery.of(context).size.width * 0.8 - 32,
child: 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) async {
setState(() {
selectedGlossary = newSelectedGlossary;
productNameController.text = selectedGlossary!.name;
});
},
suggestionsCallback: (query) async {
if (query.length < 1) return [];
final suggestionsList = await diaryCubit.getGlossaryListByQuery(query);
return suggestionsList;
},
builder: (context, controller, focusNode) {
return CustomTextFormField(
controller: controller,
autofocus: true,
focusNode: focusNode,
hintText: 'Введите название продукта/блюда',
);
},
emptyBuilder: (context) {
return const SizedBox.shrink();
},
errorBuilder: (context, object) => Padding(
padding: const EdgeInsets.all(8.0),
child: Text('Нет подходящих вариантов'),
),
),
),
const SizedBox(width: 4),
SizedBox(
width: MediaQuery.of(context).size.width * 0.2 - 4,
child: CustomTextFormField(
controller: quantityController,
keyboardType: TextInputType.number,
hintText: selectedGlossary == null
? '100'
: selectedGlossary!.grInPortion == 0
? '100'
: '1',
suffixText: selectedGlossary == null
? 'гр'
: 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 == null
? 'gr'
: 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 == null
? 'gr'
: selectedGlossary!.grInPortion == 0
? 'gr'
: 'pcs',
quantity: int.parse(quantityController.text),
date: _selectedDay.toString(),
);
}
Navigator.of(context).pop();
if (selectedGlossary == null) {
showModalBottomSheet(
isScrollControlled: true,
useRootNavigator: true,
context: context,
builder: (context) => Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: GlossaryModerationRequestModalWidget(
productName: productNameController.text,
diaryCubit: diaryCubit,
),
),
);
}
},
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) {
if (_selectedDay!.compareTo(today!) == 1) return const SizedBox.shrink();
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,
),
),
),
),
);
}
}