Merge remote-tracking branch 'upstream/main'

This commit is contained in:
Username1548
2024-12-02 15:59:15 +07:00
94 changed files with 2097 additions and 1589 deletions

View File

@@ -10,27 +10,43 @@ import 'package:eatvio_app/features/glossary/repositories/glossary_repository.da
import 'package:flutter_bloc/flutter_bloc.dart';
class DiaryCubit extends Cubit<DiaryState> {
late final DateTime today;
late DateTime selectedDay;
late int selectedMeal;
final DiaryRepository diaryRepository;
final GlossaryRepository glossaryRepository;
DiaryCubit({
required this.diaryRepository,
required this.glossaryRepository,
}) : super(const DiaryState.loading());
}) : super(const DiaryState.loading()) {
today = DateTime.now();
selectedDay = today;
}
getDiaryDay(String date, [bool needLoading = true]) async {
if (needLoading) emit(const DiaryState.loading());
setSelectedDay(DateTime day) async {
selectedDay = day;
await fetchDiary(true);
}
setSelectedMeal(int meal) {
selectedMeal = meal;
emit((state as DiaryStateLoaded).copyWith(selectedMeal: selectedMeal));
}
fetchDiary([bool needLoading = false]) async {
if (needLoading) {
emit(const DiaryState.loading());
selectedMeal = 1;
}
try {
final diaryDay = await diaryRepository.getDiaryByDate(date);
final List<int> satiety = [];
for (int i = 0; i < diaryDay.meals.length; i++) {
satiety.add(diaryDay.meals[i].satiety);
}
final diaryDay = await diaryRepository.getDiaryByDate('$selectedDay');
emit(
DiaryState.loaded(
diary: diaryDay,
nutrition: await getNutrition((await diaryRepository.getDiaryByDate(date)).id),
currentSelectedSatietyRating: satiety,
nutrition: await getNutrition((await diaryRepository.getDiaryByDate('$selectedDay')).id),
selectedDay: selectedDay,
selectedMeal: selectedMeal,
),
);
} catch (e) {
@@ -40,16 +56,18 @@ class DiaryCubit extends Cubit<DiaryState> {
}
finishDiaryDay({
required String date,
required int diaryId,
required String wakeUp,
required String retire,
required String comment,
}) async {
await diaryRepository.finishDiaryDay(
diaryId: diaryId,
wakeUp: '',
retire: '',
comment: '',
wakeUp: wakeUp,
retire: retire,
comment: comment,
);
await getDiaryDay(date, false);
await fetchDiary();
}
Future<NutritionDataModel> getNutrition(int index) async {
@@ -62,7 +80,7 @@ class DiaryCubit extends Cubit<DiaryState> {
required String productName,
required String portion,
required int quantity,
required String date,
int? productNutritionalValueId,
}) async {
await diaryRepository.addPortionToMeal(
diaryId: diaryId,
@@ -70,8 +88,9 @@ class DiaryCubit extends Cubit<DiaryState> {
productName: productName,
portion: portion,
quantity: quantity,
productNutritionalValueId: productNutritionalValueId,
);
await getDiaryDay(date, false);
await fetchDiary();
}
updateProductInMeal({
@@ -81,7 +100,7 @@ class DiaryCubit extends Cubit<DiaryState> {
required String productName,
required String portion,
required int quantity,
required String date,
int? productNutritionalValueId,
}) async {
await diaryRepository.updateProductInMeal(
diaryId: diaryId,
@@ -90,22 +109,22 @@ class DiaryCubit extends Cubit<DiaryState> {
productName: productName,
portion: portion,
quantity: quantity,
productNutritionalValueId: productNutritionalValueId,
);
await getDiaryDay(date, false);
await fetchDiary();
}
deleteProductFromMeal({
required int diaryId,
required int mealId,
required int portionId,
required String date,
}) async {
await diaryRepository.deleteProductFromMeal(
diaryId: diaryId,
mealId: mealId,
portionId: portionId,
);
await getDiaryDay(date, false);
await fetchDiary();
}
Future<List<GlossaryModel>> getGlossaryListByQuery(String query) async {
@@ -115,40 +134,25 @@ class DiaryCubit extends Cubit<DiaryState> {
uploadPhotos({
required List<File> files,
required int diaryId,
required int mealId,
required String date,
}) async {
await diaryRepository.uploadPhotos(
files: await ImageCompressUtility.compressFilesList(files),
diaryId: diaryId,
mealId: mealId,
mealId: selectedMeal,
);
await getDiaryDay(date, false);
await fetchDiary();
}
deletePhoto({
required DiaryPhotoModel photo,
required int diaryId,
required int mealId,
required String date,
}) async {
await diaryRepository.deletePhoto(
photo: photo,
diaryId: diaryId,
mealId: mealId,
mealId: selectedMeal,
);
await getDiaryDay(date, false);
}
updateCurrentSelectedSatietyRating({
required int diaryId,
required int meal,
required int value,
}) {
List<int> satiety = [...(state as DiaryStateLoaded).currentSelectedSatietyRating];
satiety[meal] = value;
diaryRepository.fillSatiety(diaryId: diaryId, meal: meal, satiety: value.toInt() - 1);
emit((state as DiaryStateLoaded).copyWith(currentSelectedSatietyRating: satiety));
await fetchDiary();
}
Future<GlossaryModerationModel> sendProductToModeration({
@@ -166,4 +170,27 @@ class DiaryCubit extends Cubit<DiaryState> {
carbohydrates: carbohydrates,
);
}
fillTime({
required int diaryId,
required String time,
}) async {
await diaryRepository.fillTime(
diaryId: diaryId,
mealId: selectedMeal,
time: time,
);
await fetchDiary();
}
fillSatiety({
required int diaryId,
required int satiety,
}) async {
await diaryRepository.fillSatiety(
diaryId: diaryId,
mealId: selectedMeal,
satiety: satiety,
);
}
}

View File

@@ -17,6 +17,7 @@ class DiaryState with _$DiaryState {
const factory DiaryState.loaded({
required DiaryDayModel diary,
required NutritionDataModel nutrition,
required List<int> currentSelectedSatietyRating,
required DateTime selectedDay,
required int selectedMeal,
}) = DiaryStateLoaded;
}

View File

@@ -22,7 +22,7 @@ mixin _$DiaryState {
required TResult Function() loading,
required TResult Function(String errorMessage) error,
required TResult Function(DiaryDayModel diary, NutritionDataModel nutrition,
List<int> currentSelectedSatietyRating)
DateTime selectedDay, int selectedMeal)
loaded,
}) =>
throw _privateConstructorUsedError;
@@ -32,7 +32,7 @@ mixin _$DiaryState {
TResult? Function()? loading,
TResult? Function(String errorMessage)? error,
TResult? Function(DiaryDayModel diary, NutritionDataModel nutrition,
List<int> currentSelectedSatietyRating)?
DateTime selectedDay, int selectedMeal)?
loaded,
}) =>
throw _privateConstructorUsedError;
@@ -42,7 +42,7 @@ mixin _$DiaryState {
TResult Function()? loading,
TResult Function(String errorMessage)? error,
TResult Function(DiaryDayModel diary, NutritionDataModel nutrition,
List<int> currentSelectedSatietyRating)?
DateTime selectedDay, int selectedMeal)?
loaded,
required TResult orElse(),
}) =>
@@ -140,7 +140,7 @@ class _$DiaryStateImpl implements _DiaryState {
required TResult Function() loading,
required TResult Function(String errorMessage) error,
required TResult Function(DiaryDayModel diary, NutritionDataModel nutrition,
List<int> currentSelectedSatietyRating)
DateTime selectedDay, int selectedMeal)
loaded,
}) {
return $default();
@@ -153,7 +153,7 @@ class _$DiaryStateImpl implements _DiaryState {
TResult? Function()? loading,
TResult? Function(String errorMessage)? error,
TResult? Function(DiaryDayModel diary, NutritionDataModel nutrition,
List<int> currentSelectedSatietyRating)?
DateTime selectedDay, int selectedMeal)?
loaded,
}) {
return $default?.call();
@@ -166,7 +166,7 @@ class _$DiaryStateImpl implements _DiaryState {
TResult Function()? loading,
TResult Function(String errorMessage)? error,
TResult Function(DiaryDayModel diary, NutritionDataModel nutrition,
List<int> currentSelectedSatietyRating)?
DateTime selectedDay, int selectedMeal)?
loaded,
required TResult orElse(),
}) {
@@ -263,7 +263,7 @@ class _$DiaryStateLoadingImpl implements DiaryStateLoading {
required TResult Function() loading,
required TResult Function(String errorMessage) error,
required TResult Function(DiaryDayModel diary, NutritionDataModel nutrition,
List<int> currentSelectedSatietyRating)
DateTime selectedDay, int selectedMeal)
loaded,
}) {
return loading();
@@ -276,7 +276,7 @@ class _$DiaryStateLoadingImpl implements DiaryStateLoading {
TResult? Function()? loading,
TResult? Function(String errorMessage)? error,
TResult? Function(DiaryDayModel diary, NutritionDataModel nutrition,
List<int> currentSelectedSatietyRating)?
DateTime selectedDay, int selectedMeal)?
loaded,
}) {
return loading?.call();
@@ -289,7 +289,7 @@ class _$DiaryStateLoadingImpl implements DiaryStateLoading {
TResult Function()? loading,
TResult Function(String errorMessage)? error,
TResult Function(DiaryDayModel diary, NutritionDataModel nutrition,
List<int> currentSelectedSatietyRating)?
DateTime selectedDay, int selectedMeal)?
loaded,
required TResult orElse(),
}) {
@@ -415,7 +415,7 @@ class _$DiaryStateErrorImpl implements DiaryStateError {
required TResult Function() loading,
required TResult Function(String errorMessage) error,
required TResult Function(DiaryDayModel diary, NutritionDataModel nutrition,
List<int> currentSelectedSatietyRating)
DateTime selectedDay, int selectedMeal)
loaded,
}) {
return error(errorMessage);
@@ -428,7 +428,7 @@ class _$DiaryStateErrorImpl implements DiaryStateError {
TResult? Function()? loading,
TResult? Function(String errorMessage)? error,
TResult? Function(DiaryDayModel diary, NutritionDataModel nutrition,
List<int> currentSelectedSatietyRating)?
DateTime selectedDay, int selectedMeal)?
loaded,
}) {
return error?.call(errorMessage);
@@ -441,7 +441,7 @@ class _$DiaryStateErrorImpl implements DiaryStateError {
TResult Function()? loading,
TResult Function(String errorMessage)? error,
TResult Function(DiaryDayModel diary, NutritionDataModel nutrition,
List<int> currentSelectedSatietyRating)?
DateTime selectedDay, int selectedMeal)?
loaded,
required TResult orElse(),
}) {
@@ -511,7 +511,8 @@ abstract class _$$DiaryStateLoadedImplCopyWith<$Res> {
$Res call(
{DiaryDayModel diary,
NutritionDataModel nutrition,
List<int> currentSelectedSatietyRating});
DateTime selectedDay,
int selectedMeal});
$DiaryDayModelCopyWith<$Res> get diary;
$NutritionDataModelCopyWith<$Res> get nutrition;
@@ -532,7 +533,8 @@ class __$$DiaryStateLoadedImplCopyWithImpl<$Res>
$Res call({
Object? diary = null,
Object? nutrition = null,
Object? currentSelectedSatietyRating = null,
Object? selectedDay = null,
Object? selectedMeal = null,
}) {
return _then(_$DiaryStateLoadedImpl(
diary: null == diary
@@ -543,10 +545,14 @@ class __$$DiaryStateLoadedImplCopyWithImpl<$Res>
? _value.nutrition
: nutrition // ignore: cast_nullable_to_non_nullable
as NutritionDataModel,
currentSelectedSatietyRating: null == currentSelectedSatietyRating
? _value._currentSelectedSatietyRating
: currentSelectedSatietyRating // ignore: cast_nullable_to_non_nullable
as List<int>,
selectedDay: null == selectedDay
? _value.selectedDay
: selectedDay // ignore: cast_nullable_to_non_nullable
as DateTime,
selectedMeal: null == selectedMeal
? _value.selectedMeal
: selectedMeal // ignore: cast_nullable_to_non_nullable
as int,
));
}
@@ -577,25 +583,21 @@ class _$DiaryStateLoadedImpl implements DiaryStateLoaded {
const _$DiaryStateLoadedImpl(
{required this.diary,
required this.nutrition,
required final List<int> currentSelectedSatietyRating})
: _currentSelectedSatietyRating = currentSelectedSatietyRating;
required this.selectedDay,
required this.selectedMeal});
@override
final DiaryDayModel diary;
@override
final NutritionDataModel nutrition;
final List<int> _currentSelectedSatietyRating;
@override
List<int> get currentSelectedSatietyRating {
if (_currentSelectedSatietyRating is EqualUnmodifiableListView)
return _currentSelectedSatietyRating;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_currentSelectedSatietyRating);
}
final DateTime selectedDay;
@override
final int selectedMeal;
@override
String toString() {
return 'DiaryState.loaded(diary: $diary, nutrition: $nutrition, currentSelectedSatietyRating: $currentSelectedSatietyRating)';
return 'DiaryState.loaded(diary: $diary, nutrition: $nutrition, selectedDay: $selectedDay, selectedMeal: $selectedMeal)';
}
@override
@@ -606,14 +608,15 @@ class _$DiaryStateLoadedImpl implements DiaryStateLoaded {
(identical(other.diary, diary) || other.diary == diary) &&
(identical(other.nutrition, nutrition) ||
other.nutrition == nutrition) &&
const DeepCollectionEquality().equals(
other._currentSelectedSatietyRating,
_currentSelectedSatietyRating));
(identical(other.selectedDay, selectedDay) ||
other.selectedDay == selectedDay) &&
(identical(other.selectedMeal, selectedMeal) ||
other.selectedMeal == selectedMeal));
}
@override
int get hashCode => Object.hash(runtimeType, diary, nutrition,
const DeepCollectionEquality().hash(_currentSelectedSatietyRating));
int get hashCode =>
Object.hash(runtimeType, diary, nutrition, selectedDay, selectedMeal);
/// Create a copy of DiaryState
/// with the given fields replaced by the non-null parameter values.
@@ -631,10 +634,10 @@ class _$DiaryStateLoadedImpl implements DiaryStateLoaded {
required TResult Function() loading,
required TResult Function(String errorMessage) error,
required TResult Function(DiaryDayModel diary, NutritionDataModel nutrition,
List<int> currentSelectedSatietyRating)
DateTime selectedDay, int selectedMeal)
loaded,
}) {
return loaded(diary, nutrition, currentSelectedSatietyRating);
return loaded(diary, nutrition, selectedDay, selectedMeal);
}
@override
@@ -644,10 +647,10 @@ class _$DiaryStateLoadedImpl implements DiaryStateLoaded {
TResult? Function()? loading,
TResult? Function(String errorMessage)? error,
TResult? Function(DiaryDayModel diary, NutritionDataModel nutrition,
List<int> currentSelectedSatietyRating)?
DateTime selectedDay, int selectedMeal)?
loaded,
}) {
return loaded?.call(diary, nutrition, currentSelectedSatietyRating);
return loaded?.call(diary, nutrition, selectedDay, selectedMeal);
}
@override
@@ -657,12 +660,12 @@ class _$DiaryStateLoadedImpl implements DiaryStateLoaded {
TResult Function()? loading,
TResult Function(String errorMessage)? error,
TResult Function(DiaryDayModel diary, NutritionDataModel nutrition,
List<int> currentSelectedSatietyRating)?
DateTime selectedDay, int selectedMeal)?
loaded,
required TResult orElse(),
}) {
if (loaded != null) {
return loaded(diary, nutrition, currentSelectedSatietyRating);
return loaded(diary, nutrition, selectedDay, selectedMeal);
}
return orElse();
}
@@ -707,14 +710,15 @@ class _$DiaryStateLoadedImpl implements DiaryStateLoaded {
abstract class DiaryStateLoaded implements DiaryState {
const factory DiaryStateLoaded(
{required final DiaryDayModel diary,
required final NutritionDataModel nutrition,
required final List<int> currentSelectedSatietyRating}) =
_$DiaryStateLoadedImpl;
{required final DiaryDayModel diary,
required final NutritionDataModel nutrition,
required final DateTime selectedDay,
required final int selectedMeal}) = _$DiaryStateLoadedImpl;
DiaryDayModel get diary;
NutritionDataModel get nutrition;
List<int> get currentSelectedSatietyRating;
DateTime get selectedDay;
int get selectedMeal;
/// Create a copy of DiaryState
/// with the given fields replaced by the non-null parameter values.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,73 @@
import 'package:eatvio_app/core/theme/theme_helper.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/add_product_modal.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class AddProductButtonWidget extends StatefulWidget {
const AddProductButtonWidget({super.key});
@override
State<AddProductButtonWidget> createState() => _AddProductButtonWidgetState();
}
class _AddProductButtonWidgetState extends State<AddProductButtonWidget> {
late final DiaryCubit diaryCubit;
@override
void initState() {
super.initState();
diaryCubit = context.read<DiaryCubit>();
}
addProductInMeal(DiaryStateLoaded state, [int? portionId]) {
return showModalBottomSheet(
useRootNavigator: true,
context: context,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
),
isScrollControlled: true,
builder: (context) => AddProductModal(
state: state,
diaryCubit: diaryCubit,
),
);
}
@override
Widget build(BuildContext context) {
return BlocBuilder<DiaryCubit, DiaryState>(
builder: (context, state) {
if (state is DiaryStateLoaded) {
return InkWell(
onTap: () => addProductInMeal(state),
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,
),
),
),
),
);
}
return const SizedBox.shrink();
},
);
}
}

View File

@@ -0,0 +1,263 @@
import 'package:easy_debounce/easy_debounce.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/custom_divider.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.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/glossary_moderation_request_modal_widget.dart';
import 'package:eatvio_app/features/glossary/models/glossary_model.dart';
import 'package:flutter/material.dart';
class AddProductModal extends StatefulWidget {
final Portion? portion;
final DiaryStateLoaded state;
final DiaryCubit diaryCubit;
const AddProductModal({
super.key,
this.portion,
required this.state,
required this.diaryCubit,
});
@override
State<AddProductModal> createState() => _AddProductModalState();
}
class _AddProductModalState extends State<AddProductModal> {
final productNameController = TextEditingController();
final quantityController = TextEditingController();
GlossaryModel? selectedGlossary;
List<GlossaryModel> glossariesList = [];
@override
void initState() {
super.initState();
if (widget.portion != null) {
productNameController.text = widget.portion!.productName;
quantityController.text = '${widget.portion!.quantity}';
selectedGlossary = GlossaryModel(
id: widget.portion!.id,
name: widget.portion!.productName,
grInPortion: widget.portion!.portion == 'gr' ? 0 : 1,
);
}
}
@override
Widget build(BuildContext 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: CustomTextFormField(
controller: productNameController,
autofocus: true,
hintText: 'Введите название продукта/блюда',
onChanged: (query) {
EasyDebounce.debounce(
'client-typing',
Duration(milliseconds: 250),
() async {
glossariesList = await widget.diaryCubit.getGlossaryListByQuery(query);
setState(() {});
},
);
},
),
),
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
? 'гр'
: 'шт',
),
),
],
),
glossariesList.isEmpty ? const SizedBox(height: 16) : const SizedBox.shrink(),
Container(
constraints: BoxConstraints(
maxHeight: 256,
),
child: ListView.separated(
physics: const BouncingScrollPhysics(),
shrinkWrap: true,
itemBuilder: (context, i) {
return ListTile(
onTap: () async {
setState(() {
selectedGlossary = glossariesList[i];
productNameController.text = selectedGlossary!.name;
});
glossariesList = await widget.diaryCubit.getGlossaryListByQuery(glossariesList[i].name);
setState(() {});
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
title: Text(
glossariesList[i].name,
style: TextStyle(
height: 1,
),
),
subtitle: glossariesList[i].grInPortion == 0
? null
: Text(
'${glossariesList[i].grInPortion} г',
style: TextStyle(
height: 1,
color: Colors.grey.shade600,
),
),
trailing: Icon(
Icons.keyboard_arrow_right_rounded,
color: Colors.grey.shade400,
),
contentPadding: EdgeInsets.only(left: 12),
);
},
itemCount: glossariesList.length,
separatorBuilder: (BuildContext context, int index) {
return CustomDivider();
},
),
),
Row(
children: [
if (widget.portion != null)
InkWell(
onTap: () {
widget.diaryCubit.deleteProductFromMeal(
diaryId: widget.state.diary.id,
mealId: widget.state.selectedMeal,
portionId: widget.portion!.id,
);
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 (widget.portion != null) const SizedBox(width: 8),
InkWell(
onTap: () {
if (widget.portion == null) {
widget.diaryCubit.addProductToMeal(
diaryId: widget.state.diary.id,
mealId: widget.state.selectedMeal,
productName: productNameController.text,
portion: selectedGlossary == null
? 'gr'
: selectedGlossary!.grInPortion == 0
? 'gr'
: 'pcs',
quantity: int.parse(quantityController.text),
productNutritionalValueId: selectedGlossary?.id,
);
} else {
widget.diaryCubit.updateProductInMeal(
diaryId: widget.state.diary.id,
mealId: widget.state.selectedMeal,
portionId: widget.portion!.id,
productName: productNameController.text,
portion: selectedGlossary == null
? 'gr'
: selectedGlossary!.grInPortion == 0
? 'gr'
: 'pcs',
quantity: int.parse(quantityController.text),
productNutritionalValueId: selectedGlossary?.id,
);
}
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: widget.diaryCubit,
),
),
);
}
},
borderRadius: BorderRadius.circular(20),
child: Ink(
width: MediaQuery.of(context).size.width - (widget.portion == null ? 32 : 92),
height: 52,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: appTheme.blueA400,
),
child: Center(
child: Text(
widget.portion == null ? 'Добавить' : 'Сохранить',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
),
],
),
],
),
);
},
);
}
}

View File

@@ -0,0 +1,130 @@
import 'package:eatvio_app/core/theme/theme_helper.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:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:table_calendar/table_calendar.dart';
class CalendarWidget extends StatefulWidget {
const CalendarWidget({super.key});
@override
State<CalendarWidget> createState() => _CalendarWidgetState();
}
class _CalendarWidgetState extends State<CalendarWidget> {
CalendarFormat _calendarFormat = CalendarFormat.week;
DateTime _focusedDay = DateTime.now();
DateTime _selectedDay = DateTime.now();
@override
Widget build(BuildContext context) {
return BlocConsumer<DiaryCubit, DiaryState>(
listener: (BuildContext context, DiaryState state) {
if (state is DiaryStateLoaded) {
setState(() {
_selectedDay = state.selectedDay;
});
}
},
builder: (BuildContext context, DiaryState state) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(top: 16),
child: TableCalendar(
focusedDay: _focusedDay,
firstDay: DateTime.utc(2020, 10, 16),
lastDay: DateTime.utc(2030, 3, 14),
locale: 'ru_RU',
startingDayOfWeek: StartingDayOfWeek.monday,
calendarFormat: _calendarFormat,
headerStyle: HeaderStyle(
titleCentered: true,
formatButtonVisible: false,
leftChevronVisible: false,
rightChevronVisible: false,
),
onDaySelected: (selectedDay, focusedDay) {
if (!isSameDay(_selectedDay, selectedDay)) {
setState(() {
_selectedDay = selectedDay;
_focusedDay = focusedDay;
});
context.read<DiaryCubit>().setSelectedDay(_selectedDay);
}
},
selectedDayPredicate: (day) {
return isSameDay(_selectedDay, day);
},
onPageChanged: (focusedDay) {
_focusedDay = focusedDay;
},
onFormatChanged: (format) {
if (_calendarFormat != format) {
setState(() {
_calendarFormat = format;
});
}
},
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(),
],
);
},
);
}
_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,
),
),
);
}
}

View File

@@ -0,0 +1,246 @@
import 'package:eatvio_app/core/theme/theme_helper.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:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncfusion_flutter_gauges/gauges.dart';
class CaloriesChartWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<DiaryCubit, DiaryState>(
builder: (BuildContext context, DiaryState state) {
if (state is DiaryStateLoaded) {
return Container(
padding: const EdgeInsets.all(16),
margin: const EdgeInsets.only(top: 12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: Colors.white,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'Ваш КБЖУ от дневной\nсуточной нормы',
textAlign: TextAlign.center,
style: TextStyle(
color: Color(0xFF323F4B),
fontWeight: FontWeight.w500,
fontSize: 18,
height: 1,
),
),
const SizedBox(height: 48),
Stack(
alignment: Alignment.bottomCenter,
children: [
SfRadialGauge(
axes: <RadialAxis>[
RadialAxis(
startAngle: 180,
endAngle: 0,
minimum: 0,
maximum: 100,
showLabels: false,
showTicks: false,
radiusFactor: 0.7,
showAxisLine: false,
pointers: <GaugePointer>[
RangePointer(
value: 100,
width: 0.16,
animationDuration: 2000,
color: Color(0xFFF6FAFF),
sizeUnit: GaugeSizeUnit.factor,
enableAnimation: true,
cornerStyle: CornerStyle.bothCurve,
),
RangePointer(
value: (state.nutrition.dailyNutritionIntake.carbohydrates / state.nutrition.dailyNutritionRecommendation.carbohydrates) * 100,
width: 0.16,
color: state.nutrition.dailyNutritionIntake.carbohydrates < state.nutrition.dailyNutritionRecommendation.carbohydrates ? appTheme.blueA400 : Colors.red,
sizeUnit: GaugeSizeUnit.factor,
enableAnimation: true,
cornerStyle: CornerStyle.bothCurve,
),
RangePointer(
value: 100,
width: 0.16,
animationDuration: 2000,
color: Color(0xFFF6FAFF),
sizeUnit: GaugeSizeUnit.factor,
enableAnimation: true,
cornerStyle: CornerStyle.bothCurve,
pointerOffset: -0.18,
),
RangePointer(
value: (state.nutrition.dailyNutritionIntake.fats / state.nutrition.dailyNutritionRecommendation.fats) * 100,
width: 0.16,
color: state.nutrition.dailyNutritionIntake.fats < state.nutrition.dailyNutritionRecommendation.fats ? appTheme.blueA400 : Colors.red,
sizeUnit: GaugeSizeUnit.factor,
enableAnimation: true,
cornerStyle: CornerStyle.bothCurve,
pointerOffset: -0.18,
),
RangePointer(
value: 100,
width: 0.16,
animationDuration: 2000,
color: Color(0xFFF6FAFF),
sizeUnit: GaugeSizeUnit.factor,
enableAnimation: true,
cornerStyle: CornerStyle.bothCurve,
pointerOffset: -0.36,
),
RangePointer(
value: (state.nutrition.dailyNutritionIntake.proteins / state.nutrition.dailyNutritionRecommendation.proteins) * 100,
width: 0.16,
color: state.nutrition.dailyNutritionIntake.proteins < state.nutrition.dailyNutritionRecommendation.proteins ? appTheme.blueA400 : Colors.red,
sizeUnit: GaugeSizeUnit.factor,
enableAnimation: true,
cornerStyle: CornerStyle.bothCurve,
pointerOffset: -0.36,
),
RangePointer(
value: 100,
width: 0.16,
animationDuration: 2000,
color: Color(0xFFF6FAFF),
sizeUnit: GaugeSizeUnit.factor,
enableAnimation: true,
cornerStyle: CornerStyle.bothCurve,
pointerOffset: -0.54,
),
RangePointer(
value: (state.nutrition.dailyNutritionIntake.energy / state.nutrition.dailyNutritionRecommendation.energy) * 100,
width: 0.16,
color: state.nutrition.dailyNutritionIntake.energy < state.nutrition.dailyNutritionRecommendation.energy ? appTheme.blueA400 : Colors.red,
sizeUnit: GaugeSizeUnit.factor,
enableAnimation: true,
cornerStyle: CornerStyle.bothCurve,
pointerOffset: -0.54,
),
],
annotations: <GaugeAnnotation>[
GaugeAnnotation(
widget: Transform.rotate(
angle: 30.1,
child: Text(
'Углеводы',
style: TextStyle(
color: Colors.white,
fontSize: 12,
),
),
),
angle: 198,
positionFactor: 0.92,
),
GaugeAnnotation(
widget: Transform.rotate(
angle: 30,
child: Text(
'Жиры',
style: TextStyle(
color: Colors.white,
fontSize: 12,
),
),
),
angle: 190,
positionFactor: 1.1,
),
GaugeAnnotation(
widget: Transform.rotate(
angle: 30,
child: Text(
'Белки',
style: TextStyle(
color: Colors.white,
fontSize: 12,
),
),
),
angle: 190,
positionFactor: 1.27,
),
GaugeAnnotation(
widget: Transform.rotate(
angle: 30,
child: Text(
'Калории',
style: TextStyle(
color: Colors.white,
fontSize: 12,
),
),
),
angle: 192,
positionFactor: 1.46,
),
],
),
],
),
Column(
children: [
_chartData(
'Калории',
'${state.nutrition.dailyNutritionIntake.energy.toStringAsFixed(0)} из ${state.nutrition.dailyNutritionRecommendation.energy.toStringAsFixed(0)} ккал',
),
const SizedBox(height: 8),
_chartData(
'Белки',
'${state.nutrition.dailyNutritionIntake.proteins.toStringAsFixed(0)} из ${state.nutrition.dailyNutritionRecommendation.proteins.toStringAsFixed(
0,
)} грамм',
),
const SizedBox(height: 8),
_chartData(
'Жиры',
'${state.nutrition.dailyNutritionIntake.fats.toStringAsFixed(0)} из ${state.nutrition.dailyNutritionRecommendation.fats.toStringAsFixed(0)} грамм',
),
const SizedBox(height: 8),
_chartData(
'Углеводы',
'${state.nutrition.dailyNutritionIntake.carbohydrates.toStringAsFixed(0)} из ${state.nutrition.dailyNutritionRecommendation.carbohydrates.toStringAsFixed(0)} грамм',
),
],
),
],
),
],
),
);
}
return const SizedBox.shrink();
},
);
}
_chartData(String title, String value) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
color: Color(0xFF323F4B),
),
),
Text(
value,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
color: Color(0xFF323F4B),
),
),
],
);
}
}

View File

@@ -1,50 +1,50 @@
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/core/theme/theme_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class FinishDayButtonWidget extends StatefulWidget {
final DiaryDayModel diaryDay;
const FinishDayButtonWidget({
super.key,
required this.diaryDay,
});
@override
State<FinishDayButtonWidget> createState() => _FinishDayButtonWidgetState();
}
class _FinishDayButtonWidgetState extends State<FinishDayButtonWidget> {
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () => context.read<DiaryCubit>().finishDiaryDay(
diaryId: widget.diaryDay.id,
date: widget.diaryDay.date,
),
borderRadius: BorderRadius.circular(20),
child: Ink(
padding: EdgeInsets.symmetric(horizontal: 8),
width: MediaQuery.of(context).size.width - 16,
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,
),
),
),
),
);
}
}
// 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/core/theme/theme_helper.dart';
// import 'package:flutter/material.dart';
// import 'package:flutter_bloc/flutter_bloc.dart';
//
// class FinishDayButtonWidget extends StatefulWidget {
// final DiaryDayModel diaryDay;
//
// const FinishDayButtonWidget({
// super.key,
// required this.diaryDay,
// });
//
// @override
// State<FinishDayButtonWidget> createState() => _FinishDayButtonWidgetState();
// }
//
// class _FinishDayButtonWidgetState extends State<FinishDayButtonWidget> {
// @override
// Widget build(BuildContext context) {
// return InkWell(
// onTap: () => context.read<DiaryCubit>().finishDiaryDay(
// diaryId: widget.diaryDay.id,
// date: widget.diaryDay.date,
// ),
// borderRadius: BorderRadius.circular(20),
// child: Ink(
// padding: EdgeInsets.symmetric(horizontal: 8),
// width: MediaQuery.of(context).size.width - 16,
// 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,
// ),
// ),
// ),
// ),
// );
// }
// }

View File

@@ -227,7 +227,7 @@ class _GlossaryModerationRequestModalWidgetState extends State<GlossaryModeratio
child: CustomTextFormField(
hintText: 'Калории',
controller: energyController,
keyboardType: TextInputType.number,
keyboardType: TextInputType.numberWithOptions(),
),
),
const SizedBox(width: 8),
@@ -235,7 +235,7 @@ class _GlossaryModerationRequestModalWidgetState extends State<GlossaryModeratio
child: CustomTextFormField(
hintText: 'Белки',
controller: proteinsController,
keyboardType: TextInputType.number,
keyboardType: TextInputType.numberWithOptions(),
),
),
],
@@ -248,7 +248,7 @@ class _GlossaryModerationRequestModalWidgetState extends State<GlossaryModeratio
child: CustomTextFormField(
hintText: 'Жиры',
controller: fatsController,
keyboardType: TextInputType.number,
keyboardType: TextInputType.numberWithOptions(),
),
),
const SizedBox(width: 8),
@@ -256,7 +256,7 @@ class _GlossaryModerationRequestModalWidgetState extends State<GlossaryModeratio
child: CustomTextFormField(
hintText: 'Углеводы',
controller: carbohydratesController,
keyboardType: TextInputType.number,
keyboardType: TextInputType.numberWithOptions(),
),
),
],

View File

@@ -0,0 +1,54 @@
import 'package:eatvio_app/core/theme/theme_helper.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:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MealButtonWidget extends StatefulWidget {
final int mealNumber;
const MealButtonWidget({
super.key,
required this.mealNumber,
});
@override
State<MealButtonWidget> createState() => _MealButtonWidgetState();
}
class _MealButtonWidgetState extends State<MealButtonWidget> {
@override
Widget build(BuildContext context) {
return BlocBuilder<DiaryCubit, DiaryState>(
builder: (context, state) {
if (state is DiaryStateLoaded) {
return InkWell(
onTap: () {
context.read<DiaryCubit>().setSelectedMeal(widget.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: state.selectedMeal == widget.mealNumber ? appTheme.blueA400 : Color(0xFFF1F2F6),
),
child: Center(
child: Text(
'${widget.mealNumber}',
style: TextStyle(
color: state.selectedMeal == widget.mealNumber ? Colors.white : Color(0xFF6D6F71),
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
);
}
return const SizedBox.shrink();
},
);
}
}

View File

@@ -0,0 +1,61 @@
import 'package:eatvio_app/features/diary/presentation/widgets/add_product_button_widget.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/meal_button_widget.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/photo_gallery_widget.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/products_list_widget.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/satiety_rating_widget.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/time_of_meal_picker_widget.dart';
import 'package:flutter/material.dart';
class MealWidget extends StatefulWidget {
const MealWidget({super.key});
@override
State<MealWidget> createState() => _MealWidgetState();
}
class _MealWidgetState extends State<MealWidget> {
@override
Widget build(BuildContext context) {
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: [
MealButtonWidget(mealNumber: 1),
MealButtonWidget(mealNumber: 2),
MealButtonWidget(mealNumber: 3),
MealButtonWidget(mealNumber: 4),
MealButtonWidget(mealNumber: 5),
],
),
const SizedBox(height: 12),
TimeOfMealPickerWidget(),
const SizedBox(height: 12),
ProductsListWidget(),
const SizedBox(height: 12),
AddProductButtonWidget(),
const SizedBox(height: 12),
PhotoGalleryWidget(),
const SizedBox(height: 12),
SatietyRatingWidget(),
],
),
),
);
}
}

View File

@@ -0,0 +1,200 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/theme/theme_helper.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:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:image_picker/image_picker.dart';
class PhotoGalleryWidget extends StatefulWidget {
const PhotoGalleryWidget({super.key});
@override
State<PhotoGalleryWidget> createState() => _PhotoGalleryWidgetState();
}
class _PhotoGalleryWidgetState extends State<PhotoGalleryWidget> {
late final DiaryCubit diaryCubit;
int currentSelectedPhotoIndex = 0;
@override
void initState() {
super.initState();
diaryCubit = context.read<DiaryCubit>();
}
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,
);
},
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,
);
},
child: const Row(
children: const [
Icon(Icons.photo),
SizedBox(width: 8),
Text('Галерея'),
],
),
),
],
cancelButton: CupertinoActionSheetAction(
onPressed: () => Navigator.of(context).pop(),
isDestructiveAction: true,
child: const Text('Отмена'),
),
),
);
}
@override
Widget build(BuildContext context) {
return BlocBuilder<DiaryCubit, DiaryState>(
builder: (context, state) {
if (state is DiaryStateLoaded) {
int mealIndex = state.selectedMeal - 1;
if (state.diary.meals[mealIndex].photos.isEmpty) {
return GestureDetector(
onTap: () => showImagePicker(state.diary),
child: SvgPicture.asset(
width: MediaQuery.of(context).size.width - 32,
'assets/no_one_image_was_added.svg',
),
);
} else {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
alignment: Alignment.topRight,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: CachedNetworkImage(
imageUrl: baseUrl + '/storage/' + state.diary.meals[mealIndex].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: state.diary.meals[mealIndex].photos[currentSelectedPhotoIndex],
diaryId: state.diary.id,
);
setState(() {
if (currentSelectedPhotoIndex == state.diary.meals[mealIndex].photos.length - 1 && currentSelectedPhotoIndex != 0) {
currentSelectedPhotoIndex = state.diary.meals[mealIndex].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 < state.diary.meals[mealIndex].photos.length + 1; i++) ...[
if (i == state.diary.meals[mealIndex].photos.length)
Padding(
padding: const EdgeInsets.only(left: 8),
child: InkWell(
onTap: () => showImagePicker(state.diary),
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 < state.diary.meals[mealIndex].photos.length)
GestureDetector(
onTap: () {
setState(() {
currentSelectedPhotoIndex = i;
});
},
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: SizedBox(
height: 52,
width: 52,
child: CachedNetworkImage(
imageUrl: baseUrl + '/storage/' + state.diary.meals[mealIndex].photos[i].path,
fit: BoxFit.fitWidth,
),
),
),
),
if (i < state.diary.meals[mealIndex].photos.length - 1) const SizedBox(width: 8),
],
],
),
),
],
);
}
}
return const SizedBox.shrink();
},
);
}
}

View File

@@ -0,0 +1,67 @@
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/add_product_modal.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class ProductWidget extends StatefulWidget {
final Portion portion;
const ProductWidget({
super.key,
required this.portion,
});
@override
State<ProductWidget> createState() => _ProductWidgetState();
}
class _ProductWidgetState extends State<ProductWidget> {
late final DiaryCubit diaryCubit;
late String portionName;
@override
void initState() {
super.initState();
diaryCubit = context.read<DiaryCubit>();
portionName = widget.portion == 'gr' ? 'гр' : 'шт';
}
updateProductInMeal(DiaryStateLoaded state, Portion portion) {
return showModalBottomSheet(
useRootNavigator: true,
context: context,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
),
isScrollControlled: true,
builder: (context) => AddProductModal(
state: state,
portion: portion,
diaryCubit: diaryCubit,
),
);
}
@override
Widget build(BuildContext context) {
return BlocBuilder<DiaryCubit, DiaryState>(
builder: (context, state) {
if (state is DiaryStateLoaded) {
return ListTile(
onTap: () => updateProductInMeal(state, widget.portion),
trailing: Icon(Icons.more_vert_rounded),
title: Text(widget.portion.productName),
subtitle: Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
tileColor: Color(0xFFF6FAFF),
);
}
return const SizedBox.shrink();
},
);
}
}

View File

@@ -0,0 +1,35 @@
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/product_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class ProductsListWidget extends StatefulWidget {
const ProductsListWidget({super.key});
@override
State<ProductsListWidget> createState() => _ProductsListWidgetState();
}
class _ProductsListWidgetState extends State<ProductsListWidget> {
@override
Widget build(BuildContext context) {
return BlocBuilder<DiaryCubit, DiaryState>(
builder: (BuildContext context, DiaryState state) {
if (state is DiaryStateLoaded) {
int mealIndex = state.selectedMeal - 1;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
for (int i = 0; i < state.diary.meals[mealIndex].portions.length; i++) ...[
ProductWidget(portion: state.diary.meals[mealIndex].portions[i]),
i < state.diary.meals[mealIndex].portions.length - 1 ? const SizedBox(height: 8) : const SizedBox.shrink(),
],
],
);
}
return const SizedBox.shrink();
},
);
}
}

View File

@@ -1,187 +1,199 @@
import 'package:easy_debounce/easy_debounce.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:flutter/cupertino.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
import 'package:flutter_svg/flutter_svg.dart';
class SatietyRatingWidget extends StatefulWidget {
final List<int> currentSelectedSatietyRating;
final int diaryId;
final int meal;
const SatietyRatingWidget({
super.key,
required this.currentSelectedSatietyRating,
required this.diaryId,
required this.meal,
});
@override
State<SatietyRatingWidget> createState() => _SatietyRatingWidgetState();
}
class _SatietyRatingWidgetState extends State<SatietyRatingWidget> {
late double currentSelectedRating;
bool isFirstFetch = true;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Ваша сытость после приёма пищи',
style: TextStyle(
fontSize: 16,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SvgPicture.asset(
'assets/icons/satiety_emoji_left.svg',
height: 32,
width: 32,
),
RatingBar.builder(
initialRating: widget.currentSelectedSatietyRating[widget.meal - 1].toDouble() + 1,
itemCount: 6,
itemSize: MediaQuery.of(context).size.width / 6 - (124 / 6),
updateOnDrag: true,
itemBuilder: (context, index) {
switch (index) {
case 0:
return Container(
width: MediaQuery.of(context).size.width / 6,
height: 24,
margin: widget.currentSelectedSatietyRating[widget.meal - 1].toDouble() + 1 == 1 ? EdgeInsets.symmetric(horizontal: 2) : null,
decoration: BoxDecoration(
color: Color(0xFFC82222),
border: widget.currentSelectedSatietyRating[widget.meal - 1].toDouble() + 1 == 1
? Border.all(
width: 2,
color: Color(0xFFAA3333),
strokeAlign: BorderSide.strokeAlignOutside,
)
: null,
borderRadius: widget.currentSelectedSatietyRating[widget.meal - 1].toDouble() + 1 == 1
? BorderRadius.horizontal(
left: Radius.circular(8),
right: Radius.circular(2),
)
: BorderRadius.horizontal(
left: Radius.circular(8),
),
),
);
case 1:
return Container(
width: MediaQuery.of(context).size.width / 6,
height: 24,
margin: widget.currentSelectedSatietyRating[widget.meal - 1].toDouble() + 1 == 2 ? EdgeInsets.symmetric(horizontal: 2) : null,
decoration: BoxDecoration(
color: Color(0xFFFF3E3E),
border: widget.currentSelectedSatietyRating[widget.meal - 1].toDouble() + 1 == 2
? Border.all(
width: 2,
color: Color(0xFF550000),
strokeAlign: BorderSide.strokeAlignOutside,
)
: null,
borderRadius: widget.currentSelectedSatietyRating[widget.meal - 1].toDouble() + 1 == 2 ? BorderRadius.circular(2) : null,
),
);
case 2:
return Container(
width: MediaQuery.of(context).size.width / 6,
height: 24,
margin: widget.currentSelectedSatietyRating[widget.meal - 1].toDouble() + 1 == 3 ? EdgeInsets.symmetric(horizontal: 2) : null,
decoration: BoxDecoration(
color: Color(0xFFF8C926),
border: widget.currentSelectedSatietyRating[widget.meal - 1].toDouble() + 1 == 3
? Border.all(
width: 2,
color: Color(0xFF8C5D00),
strokeAlign: BorderSide.strokeAlignOutside,
)
: null,
borderRadius: widget.currentSelectedSatietyRating[widget.meal - 1].toDouble() + 1 == 3 ? BorderRadius.circular(2) : null,
),
);
case 3:
return Container(
width: MediaQuery.of(context).size.width / 6,
height: 24,
margin: widget.currentSelectedSatietyRating[widget.meal - 1].toDouble() + 1 == 4 ? EdgeInsets.symmetric(horizontal: 2) : null,
decoration: BoxDecoration(
color: Color(0xFFFFF3BF),
border: widget.currentSelectedSatietyRating[widget.meal - 1].toDouble() + 1 == 4
? Border.all(
width: 2,
color: Color(0xFFB5911B),
strokeAlign: BorderSide.strokeAlignOutside,
)
: null,
borderRadius: widget.currentSelectedSatietyRating[widget.meal - 1].toDouble() + 1 == 4 ? BorderRadius.circular(2) : null,
),
);
case 4:
return Container(
width: MediaQuery.of(context).size.width / 6,
height: 24,
margin: widget.currentSelectedSatietyRating[widget.meal - 1].toDouble() + 1 == 5 ? EdgeInsets.symmetric(horizontal: 2) : null,
decoration: BoxDecoration(
color: Color(0xFF62E833),
border: widget.currentSelectedSatietyRating[widget.meal - 1].toDouble() + 1 == 5
? Border.all(
width: 2,
color: Color(0xFF276918),
strokeAlign: BorderSide.strokeAlignOutside,
)
: null,
borderRadius: widget.currentSelectedSatietyRating[widget.meal - 1].toDouble() + 1 == 5 ? BorderRadius.circular(2) : null,
),
);
default:
return Container(
width: MediaQuery.of(context).size.width / 6,
height: 24,
margin: widget.currentSelectedSatietyRating[widget.meal - 1].toDouble() + 1 == 6 ? EdgeInsets.symmetric(horizontal: 2) : null,
decoration: BoxDecoration(
color: Color(0xFF4DCE20),
border: widget.currentSelectedSatietyRating[widget.meal - 1].toDouble() + 1 == 6
? Border.all(
width: 2,
color: Color(0xFF184D12),
strokeAlign: BorderSide.strokeAlignOutside,
)
: null,
borderRadius: widget.currentSelectedSatietyRating[widget.meal - 1].toDouble() + 1 == 6
? BorderRadius.horizontal(
left: Radius.circular(2),
right: Radius.circular(8),
)
: BorderRadius.horizontal(
right: Radius.circular(8),
),
),
);
}
},
onRatingUpdate: (value) => context.read<DiaryCubit>().updateCurrentSelectedSatietyRating(
diaryId: widget.diaryId,
meal: widget.meal,
value: value.toInt(),
return BlocBuilder<DiaryCubit, DiaryState>(
builder: (BuildContext context, DiaryState state) {
if (state is DiaryStateLoaded) {
if (isFirstFetch) {
currentSelectedRating = state.diary.meals[state.selectedMeal - 1].satiety.toDouble();
isFirstFetch = false;
}
return Padding(
padding: const EdgeInsets.only(top: 24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Ваша сытость после приёма пищи',
style: TextStyle(
fontSize: 16,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SvgPicture.asset(
'assets/icons/satiety_emoji_left.svg',
height: 32,
width: 32,
),
),
SvgPicture.asset(
'assets/icons/satiety_emoji_right.svg',
height: 32,
width: 32,
),
],
),
],
),
RatingBar.builder(
initialRating: state.diary.meals[state.selectedMeal - 1].satiety.toDouble(),
itemCount: 6,
itemSize: MediaQuery.of(context).size.width / 6 - (124 / 6),
updateOnDrag: true,
itemBuilder: (context, index) {
switch (index) {
case 0:
return Container(
width: MediaQuery.of(context).size.width / 6,
height: 24,
margin: currentSelectedRating == 1 ? EdgeInsets.symmetric(horizontal: 2) : null,
decoration: BoxDecoration(
color: Color(0xFFC82222),
border: currentSelectedRating == 1
? Border.all(
width: 2,
color: Color(0xFFAA3333),
strokeAlign: BorderSide.strokeAlignOutside,
)
: null,
borderRadius: currentSelectedRating == 1
? BorderRadius.horizontal(
left: Radius.circular(8),
right: Radius.circular(2),
)
: BorderRadius.horizontal(
left: Radius.circular(8),
),
),
);
case 1:
return Container(
width: MediaQuery.of(context).size.width / 6,
height: 24,
margin: currentSelectedRating == 2 ? EdgeInsets.symmetric(horizontal: 2) : null,
decoration: BoxDecoration(
color: Color(0xFFFF3E3E),
border: currentSelectedRating == 2
? Border.all(
width: 2,
color: Color(0xFF550000),
strokeAlign: BorderSide.strokeAlignOutside,
)
: null,
borderRadius: currentSelectedRating == 2 ? BorderRadius.circular(2) : null,
),
);
case 2:
return Container(
width: MediaQuery.of(context).size.width / 6,
height: 24,
margin: currentSelectedRating == 3 ? EdgeInsets.symmetric(horizontal: 2) : null,
decoration: BoxDecoration(
color: Color(0xFFF8C926),
border: currentSelectedRating == 3
? Border.all(
width: 2,
color: Color(0xFF8C5D00),
strokeAlign: BorderSide.strokeAlignOutside,
)
: null,
borderRadius: currentSelectedRating == 3 ? BorderRadius.circular(2) : null,
),
);
case 3:
return Container(
width: MediaQuery.of(context).size.width / 6,
height: 24,
margin: currentSelectedRating == 4 ? EdgeInsets.symmetric(horizontal: 2) : null,
decoration: BoxDecoration(
color: Color(0xFFFFF3BF),
border: currentSelectedRating == 4
? Border.all(
width: 2,
color: Color(0xFFB5911B),
strokeAlign: BorderSide.strokeAlignOutside,
)
: null,
borderRadius: currentSelectedRating == 4 ? BorderRadius.circular(2) : null,
),
);
case 4:
return Container(
width: MediaQuery.of(context).size.width / 6,
height: 24,
margin: currentSelectedRating == 5 ? EdgeInsets.symmetric(horizontal: 2) : null,
decoration: BoxDecoration(
color: Color(0xFF62E833),
border: currentSelectedRating == 5
? Border.all(
width: 2,
color: Color(0xFF276918),
strokeAlign: BorderSide.strokeAlignOutside,
)
: null,
borderRadius: currentSelectedRating == 5 ? BorderRadius.circular(2) : null,
),
);
default:
return Container(
width: MediaQuery.of(context).size.width / 6,
height: 24,
margin: currentSelectedRating == 6 ? EdgeInsets.symmetric(horizontal: 2) : null,
decoration: BoxDecoration(
color: Color(0xFF4DCE20),
border: currentSelectedRating == 6
? Border.all(
width: 2,
color: Color(0xFF184D12),
strokeAlign: BorderSide.strokeAlignOutside,
)
: null,
borderRadius: currentSelectedRating == 6
? BorderRadius.horizontal(
left: Radius.circular(2),
right: Radius.circular(8),
)
: BorderRadius.horizontal(
right: Radius.circular(8),
),
),
);
}
},
onRatingUpdate: (value) {
setState(() {
currentSelectedRating = value;
});
EasyDebounce.debounce(
'change-satiety',
Duration(milliseconds: 250),
() {
context.read<DiaryCubit>().fillSatiety(diaryId: state.diary.id, satiety: currentSelectedRating.toInt());
},
);
},
),
SvgPicture.asset(
'assets/icons/satiety_emoji_right.svg',
height: 32,
width: 32,
),
],
),
],
),
);
}
return const SizedBox.shrink();
},
);
}
}

View File

@@ -0,0 +1,133 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/custom_divider.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:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class TimeOfMealPickerWidget extends StatefulWidget {
const TimeOfMealPickerWidget({super.key});
@override
State<TimeOfMealPickerWidget> createState() => _TimeOfMealPickerWidgetState();
}
class _TimeOfMealPickerWidgetState extends State<TimeOfMealPickerWidget> {
late final DiaryCubit diaryCubit;
DateTime value = DateTime.now();
@override
void initState() {
super.initState();
diaryCubit = context.read<DiaryCubit>();
}
showTimePicker(int diaryId) {
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) {
value = newTime;
},
),
),
InkWell(
onTap: () {
Navigator.of(context).pop();
diaryCubit.fillTime(
diaryId: diaryId,
time: '${value.hour.toString().padLeft(2, '0')}:${value.minute.toString().padLeft(2, '0')}:00',
);
},
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 BlocBuilder<DiaryCubit, DiaryState>(
builder: (context, state) {
if (state is DiaryStateLoaded) {
return Column(
children: [
CustomDivider(),
Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Время приема пищи',
style: TextStyle(
color: Color(0xFF323F4B),
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
InkWell(
onTap: () => showTimePicker(state.diary.id),
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(
state.diary.meals[state.selectedMeal - 1].time.substring(0, state.diary.meals[state.selectedMeal - 1].time.length - 3),
style: TextStyle(
color: Color(0xFF6D6F71),
fontSize: 17,
fontWeight: FontWeight.w500,
),
),
),
),
),
],
),
),
CustomDivider(),
],
);
}
return const SizedBox.shrink();
},
);
}
}