diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 196d2aa1..0b639038 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -24,7 +24,8 @@ jobs: - name: Install Android SDK run: | apt-get update - https://dl.google.com/android/repository/commandlinetools-linux-7583922_latest.zip -O commandlinetools.zip + apt-get install -y openjdk-11-jdk wget + wget https://dl.google.com/android/repository/commandlinetools-linux-7583922_latest.zip -O commandlinetools.zip unzip commandlinetools.zip mkdir -p $HOME/Android/Sdk/cmdline-tools/latest mv cmdline-tools/* $HOME/Android/Sdk/cmdline-tools/latest/ diff --git a/android/.idea/caches/deviceStreaming.xml b/android/.idea/caches/deviceStreaming.xml new file mode 100644 index 00000000..5a2f8669 --- /dev/null +++ b/android/.idea/caches/deviceStreaming.xml @@ -0,0 +1,329 @@ + + + + + + \ No newline at end of file diff --git a/android/.idea/gradle.xml b/android/.idea/gradle.xml new file mode 100644 index 00000000..efe09167 --- /dev/null +++ b/android/.idea/gradle.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/android/.idea/migrations.xml b/android/.idea/migrations.xml new file mode 100644 index 00000000..f8051a6f --- /dev/null +++ b/android/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/android/.idea/misc.xml b/android/.idea/misc.xml new file mode 100644 index 00000000..48725873 --- /dev/null +++ b/android/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/android/.idea/modules.xml b/android/.idea/modules.xml new file mode 100644 index 00000000..bf609e00 --- /dev/null +++ b/android/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/android/.idea/modules/android.iml b/android/.idea/modules/android.iml new file mode 100644 index 00000000..2af55b99 --- /dev/null +++ b/android/.idea/modules/android.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/android/.idea/runConfigurations.xml b/android/.idea/runConfigurations.xml new file mode 100644 index 00000000..16660f1d --- /dev/null +++ b/android/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/android/.idea/vcs.xml b/android/.idea/vcs.xml new file mode 100644 index 00000000..6c0b8635 --- /dev/null +++ b/android/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android/.idea/workspace.xml b/android/.idea/workspace.xml new file mode 100644 index 00000000..df82a64e --- /dev/null +++ b/android/.idea/workspace.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + 1731295514828 + + + + \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index e5218dce..26791c12 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -7,7 +7,7 @@ plugins { android { namespace = "com.eatvioapp.app" compileSdk = flutter.compileSdkVersion - ndkVersion = "28.0.12433566" + ndkVersion = flutter.ndkVersion compileOptions { sourceCompatibility JavaVersion.VERSION_21 diff --git a/android/settings.gradle b/android/settings.gradle index b5e1b3f5..42a51c65 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -18,8 +18,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.3.2" apply false - id "org.jetbrains.kotlin.android" version "2.0.20" apply false + id "com.android.application" version "8.7.2" apply false + id "org.jetbrains.kotlin.android" version "2.0.21" apply false } include ":app" diff --git a/assets/icons/satiety_emoji_left.svg b/assets/icons/satiety_emoji_left.svg new file mode 100644 index 00000000..e2a21bda --- /dev/null +++ b/assets/icons/satiety_emoji_left.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/satiety_emoji_right.svg b/assets/icons/satiety_emoji_right.svg new file mode 100644 index 00000000..6791ee88 --- /dev/null +++ b/assets/icons/satiety_emoji_right.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/lib/features/diary/models/nutrition_model.dart b/lib/features/diary/models/nutrition_model.dart new file mode 100644 index 00000000..73103842 --- /dev/null +++ b/lib/features/diary/models/nutrition_model.dart @@ -0,0 +1,27 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'nutrition_model.freezed.dart'; + +part 'nutrition_model.g.dart'; + +@freezed +class NutritionDataModel with _$NutritionDataModel { + const factory NutritionDataModel({ + required NutritionModel dailyNutritionIntake, + required NutritionModel dailyNutritionRecommendation, + }) = _NutritionDataModel; + + factory NutritionDataModel.fromJson(Map json) => _$NutritionDataModelFromJson(json); +} + +@freezed +class NutritionModel with _$NutritionModel { + const factory NutritionModel({ + required double energy, + required double proteins, + required double fats, + required double carbohydrates, + }) = _NutritionModel; + + factory NutritionModel.fromJson(Map json) => _$NutritionModelFromJson(json); +} diff --git a/lib/features/diary/models/nutrition_model.freezed.dart b/lib/features/diary/models/nutrition_model.freezed.dart new file mode 100644 index 00000000..46da50b5 --- /dev/null +++ b/lib/features/diary/models/nutrition_model.freezed.dart @@ -0,0 +1,437 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'nutrition_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +NutritionDataModel _$NutritionDataModelFromJson(Map json) { + return _NutritionDataModel.fromJson(json); +} + +/// @nodoc +mixin _$NutritionDataModel { + NutritionModel get dailyNutritionIntake => throw _privateConstructorUsedError; + NutritionModel get dailyNutritionRecommendation => + throw _privateConstructorUsedError; + + /// Serializes this NutritionDataModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of NutritionDataModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $NutritionDataModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $NutritionDataModelCopyWith<$Res> { + factory $NutritionDataModelCopyWith( + NutritionDataModel value, $Res Function(NutritionDataModel) then) = + _$NutritionDataModelCopyWithImpl<$Res, NutritionDataModel>; + @useResult + $Res call( + {NutritionModel dailyNutritionIntake, + NutritionModel dailyNutritionRecommendation}); + + $NutritionModelCopyWith<$Res> get dailyNutritionIntake; + $NutritionModelCopyWith<$Res> get dailyNutritionRecommendation; +} + +/// @nodoc +class _$NutritionDataModelCopyWithImpl<$Res, $Val extends NutritionDataModel> + implements $NutritionDataModelCopyWith<$Res> { + _$NutritionDataModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of NutritionDataModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? dailyNutritionIntake = null, + Object? dailyNutritionRecommendation = null, + }) { + return _then(_value.copyWith( + dailyNutritionIntake: null == dailyNutritionIntake + ? _value.dailyNutritionIntake + : dailyNutritionIntake // ignore: cast_nullable_to_non_nullable + as NutritionModel, + dailyNutritionRecommendation: null == dailyNutritionRecommendation + ? _value.dailyNutritionRecommendation + : dailyNutritionRecommendation // ignore: cast_nullable_to_non_nullable + as NutritionModel, + ) as $Val); + } + + /// Create a copy of NutritionDataModel + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $NutritionModelCopyWith<$Res> get dailyNutritionIntake { + return $NutritionModelCopyWith<$Res>(_value.dailyNutritionIntake, (value) { + return _then(_value.copyWith(dailyNutritionIntake: value) as $Val); + }); + } + + /// Create a copy of NutritionDataModel + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $NutritionModelCopyWith<$Res> get dailyNutritionRecommendation { + return $NutritionModelCopyWith<$Res>(_value.dailyNutritionRecommendation, + (value) { + return _then( + _value.copyWith(dailyNutritionRecommendation: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$NutritionDataModelImplCopyWith<$Res> + implements $NutritionDataModelCopyWith<$Res> { + factory _$$NutritionDataModelImplCopyWith(_$NutritionDataModelImpl value, + $Res Function(_$NutritionDataModelImpl) then) = + __$$NutritionDataModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {NutritionModel dailyNutritionIntake, + NutritionModel dailyNutritionRecommendation}); + + @override + $NutritionModelCopyWith<$Res> get dailyNutritionIntake; + @override + $NutritionModelCopyWith<$Res> get dailyNutritionRecommendation; +} + +/// @nodoc +class __$$NutritionDataModelImplCopyWithImpl<$Res> + extends _$NutritionDataModelCopyWithImpl<$Res, _$NutritionDataModelImpl> + implements _$$NutritionDataModelImplCopyWith<$Res> { + __$$NutritionDataModelImplCopyWithImpl(_$NutritionDataModelImpl _value, + $Res Function(_$NutritionDataModelImpl) _then) + : super(_value, _then); + + /// Create a copy of NutritionDataModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? dailyNutritionIntake = null, + Object? dailyNutritionRecommendation = null, + }) { + return _then(_$NutritionDataModelImpl( + dailyNutritionIntake: null == dailyNutritionIntake + ? _value.dailyNutritionIntake + : dailyNutritionIntake // ignore: cast_nullable_to_non_nullable + as NutritionModel, + dailyNutritionRecommendation: null == dailyNutritionRecommendation + ? _value.dailyNutritionRecommendation + : dailyNutritionRecommendation // ignore: cast_nullable_to_non_nullable + as NutritionModel, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$NutritionDataModelImpl implements _NutritionDataModel { + const _$NutritionDataModelImpl( + {required this.dailyNutritionIntake, + required this.dailyNutritionRecommendation}); + + factory _$NutritionDataModelImpl.fromJson(Map json) => + _$$NutritionDataModelImplFromJson(json); + + @override + final NutritionModel dailyNutritionIntake; + @override + final NutritionModel dailyNutritionRecommendation; + + @override + String toString() { + return 'NutritionDataModel(dailyNutritionIntake: $dailyNutritionIntake, dailyNutritionRecommendation: $dailyNutritionRecommendation)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$NutritionDataModelImpl && + (identical(other.dailyNutritionIntake, dailyNutritionIntake) || + other.dailyNutritionIntake == dailyNutritionIntake) && + (identical(other.dailyNutritionRecommendation, + dailyNutritionRecommendation) || + other.dailyNutritionRecommendation == + dailyNutritionRecommendation)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, dailyNutritionIntake, dailyNutritionRecommendation); + + /// Create a copy of NutritionDataModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$NutritionDataModelImplCopyWith<_$NutritionDataModelImpl> get copyWith => + __$$NutritionDataModelImplCopyWithImpl<_$NutritionDataModelImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$NutritionDataModelImplToJson( + this, + ); + } +} + +abstract class _NutritionDataModel implements NutritionDataModel { + const factory _NutritionDataModel( + {required final NutritionModel dailyNutritionIntake, + required final NutritionModel dailyNutritionRecommendation}) = + _$NutritionDataModelImpl; + + factory _NutritionDataModel.fromJson(Map json) = + _$NutritionDataModelImpl.fromJson; + + @override + NutritionModel get dailyNutritionIntake; + @override + NutritionModel get dailyNutritionRecommendation; + + /// Create a copy of NutritionDataModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$NutritionDataModelImplCopyWith<_$NutritionDataModelImpl> get copyWith => + throw _privateConstructorUsedError; +} + +NutritionModel _$NutritionModelFromJson(Map json) { + return _NutritionModel.fromJson(json); +} + +/// @nodoc +mixin _$NutritionModel { + double get energy => throw _privateConstructorUsedError; + double get proteins => throw _privateConstructorUsedError; + double get fats => throw _privateConstructorUsedError; + double get carbohydrates => throw _privateConstructorUsedError; + + /// Serializes this NutritionModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of NutritionModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $NutritionModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $NutritionModelCopyWith<$Res> { + factory $NutritionModelCopyWith( + NutritionModel value, $Res Function(NutritionModel) then) = + _$NutritionModelCopyWithImpl<$Res, NutritionModel>; + @useResult + $Res call( + {double energy, double proteins, double fats, double carbohydrates}); +} + +/// @nodoc +class _$NutritionModelCopyWithImpl<$Res, $Val extends NutritionModel> + implements $NutritionModelCopyWith<$Res> { + _$NutritionModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of NutritionModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? energy = null, + Object? proteins = null, + Object? fats = null, + Object? carbohydrates = null, + }) { + return _then(_value.copyWith( + energy: null == energy + ? _value.energy + : energy // ignore: cast_nullable_to_non_nullable + as double, + proteins: null == proteins + ? _value.proteins + : proteins // ignore: cast_nullable_to_non_nullable + as double, + fats: null == fats + ? _value.fats + : fats // ignore: cast_nullable_to_non_nullable + as double, + carbohydrates: null == carbohydrates + ? _value.carbohydrates + : carbohydrates // ignore: cast_nullable_to_non_nullable + as double, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$NutritionModelImplCopyWith<$Res> + implements $NutritionModelCopyWith<$Res> { + factory _$$NutritionModelImplCopyWith(_$NutritionModelImpl value, + $Res Function(_$NutritionModelImpl) then) = + __$$NutritionModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {double energy, double proteins, double fats, double carbohydrates}); +} + +/// @nodoc +class __$$NutritionModelImplCopyWithImpl<$Res> + extends _$NutritionModelCopyWithImpl<$Res, _$NutritionModelImpl> + implements _$$NutritionModelImplCopyWith<$Res> { + __$$NutritionModelImplCopyWithImpl( + _$NutritionModelImpl _value, $Res Function(_$NutritionModelImpl) _then) + : super(_value, _then); + + /// Create a copy of NutritionModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? energy = null, + Object? proteins = null, + Object? fats = null, + Object? carbohydrates = null, + }) { + return _then(_$NutritionModelImpl( + energy: null == energy + ? _value.energy + : energy // ignore: cast_nullable_to_non_nullable + as double, + proteins: null == proteins + ? _value.proteins + : proteins // ignore: cast_nullable_to_non_nullable + as double, + fats: null == fats + ? _value.fats + : fats // ignore: cast_nullable_to_non_nullable + as double, + carbohydrates: null == carbohydrates + ? _value.carbohydrates + : carbohydrates // ignore: cast_nullable_to_non_nullable + as double, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$NutritionModelImpl implements _NutritionModel { + const _$NutritionModelImpl( + {required this.energy, + required this.proteins, + required this.fats, + required this.carbohydrates}); + + factory _$NutritionModelImpl.fromJson(Map json) => + _$$NutritionModelImplFromJson(json); + + @override + final double energy; + @override + final double proteins; + @override + final double fats; + @override + final double carbohydrates; + + @override + String toString() { + return 'NutritionModel(energy: $energy, proteins: $proteins, fats: $fats, carbohydrates: $carbohydrates)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$NutritionModelImpl && + (identical(other.energy, energy) || other.energy == energy) && + (identical(other.proteins, proteins) || + other.proteins == proteins) && + (identical(other.fats, fats) || other.fats == fats) && + (identical(other.carbohydrates, carbohydrates) || + other.carbohydrates == carbohydrates)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => + Object.hash(runtimeType, energy, proteins, fats, carbohydrates); + + /// Create a copy of NutritionModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$NutritionModelImplCopyWith<_$NutritionModelImpl> get copyWith => + __$$NutritionModelImplCopyWithImpl<_$NutritionModelImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$NutritionModelImplToJson( + this, + ); + } +} + +abstract class _NutritionModel implements NutritionModel { + const factory _NutritionModel( + {required final double energy, + required final double proteins, + required final double fats, + required final double carbohydrates}) = _$NutritionModelImpl; + + factory _NutritionModel.fromJson(Map json) = + _$NutritionModelImpl.fromJson; + + @override + double get energy; + @override + double get proteins; + @override + double get fats; + @override + double get carbohydrates; + + /// Create a copy of NutritionModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$NutritionModelImplCopyWith<_$NutritionModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/features/diary/models/nutrition_model.g.dart b/lib/features/diary/models/nutrition_model.g.dart new file mode 100644 index 00000000..6ecd533c --- /dev/null +++ b/lib/features/diary/models/nutrition_model.g.dart @@ -0,0 +1,40 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'nutrition_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$NutritionDataModelImpl _$$NutritionDataModelImplFromJson( + Map json) => + _$NutritionDataModelImpl( + dailyNutritionIntake: NutritionModel.fromJson( + json['dailyNutritionIntake'] as Map), + dailyNutritionRecommendation: NutritionModel.fromJson( + json['dailyNutritionRecommendation'] as Map), + ); + +Map _$$NutritionDataModelImplToJson( + _$NutritionDataModelImpl instance) => + { + 'dailyNutritionIntake': instance.dailyNutritionIntake, + 'dailyNutritionRecommendation': instance.dailyNutritionRecommendation, + }; + +_$NutritionModelImpl _$$NutritionModelImplFromJson(Map json) => + _$NutritionModelImpl( + energy: (json['energy'] as num).toDouble(), + proteins: (json['proteins'] as num).toDouble(), + fats: (json['fats'] as num).toDouble(), + carbohydrates: (json['carbohydrates'] as num).toDouble(), + ); + +Map _$$NutritionModelImplToJson( + _$NutritionModelImpl instance) => + { + 'energy': instance.energy, + 'proteins': instance.proteins, + 'fats': instance.fats, + 'carbohydrates': instance.carbohydrates, + }; diff --git a/lib/features/diary/presentation/bloc/diary_cubit.dart b/lib/features/diary/presentation/bloc/diary_cubit.dart index d7f31da6..16df5d4f 100644 --- a/lib/features/diary/presentation/bloc/diary_cubit.dart +++ b/lib/features/diary/presentation/bloc/diary_cubit.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:eatvio_app/features/diary/models/diary_day_model.dart'; +import 'package:eatvio_app/features/diary/models/nutrition_model.dart'; import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart'; import 'package:eatvio_app/features/diary/repositories/diary_repository.dart'; import 'package:eatvio_app/features/glossary/models/glossary_model.dart'; @@ -10,6 +11,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; class DiaryCubit extends Cubit { final DiaryRepository diaryRepository; final GlossaryRepository glossaryRepository; + double currentSelectedSatietyRating = 3; DiaryCubit({ required this.diaryRepository, @@ -19,13 +21,23 @@ class DiaryCubit extends Cubit { getDiaryDay(String date, [bool needLoading = true]) async { if (needLoading) emit(const DiaryState.loading()); try { - emit(DiaryState.loaded(diary: await diaryRepository.getDiaryByDate(date))); + emit( + DiaryState.loaded( + diary: await diaryRepository.getDiaryByDate(date), + nutrition: await getNutrition(await diaryRepository.getDiaryIndexByDate(date)), + currentSelectedSatietyRating: currentSelectedSatietyRating, + ), + ); } catch (e) { emit(DiaryState.error(errorMessage: e.toString())); throw e; } } + Future getNutrition(int index) async { + return (await diaryRepository.getNutritionData(index)); + } + addProductToMeal({ required int diaryId, required int mealId, @@ -109,4 +121,9 @@ class DiaryCubit extends Cubit { ); await getDiaryDay(date, false); } + + updateCurrentSelectedSatietyRating(double value) { + currentSelectedSatietyRating = value; + emit((state as DiaryStateLoaded).copyWith(currentSelectedSatietyRating: currentSelectedSatietyRating)); + } } diff --git a/lib/features/diary/presentation/bloc/diary_state.dart b/lib/features/diary/presentation/bloc/diary_state.dart index c9b58a8d..001c9d16 100644 --- a/lib/features/diary/presentation/bloc/diary_state.dart +++ b/lib/features/diary/presentation/bloc/diary_state.dart @@ -1,4 +1,5 @@ import 'package:eatvio_app/features/diary/models/diary_day_model.dart'; +import 'package:eatvio_app/features/diary/models/nutrition_model.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'diary_state.freezed.dart'; @@ -15,5 +16,7 @@ class DiaryState with _$DiaryState { const factory DiaryState.loaded({ required DiaryDayModel diary, + required NutritionDataModel nutrition, + required double currentSelectedSatietyRating, }) = DiaryStateLoaded; } diff --git a/lib/features/diary/presentation/bloc/diary_state.freezed.dart b/lib/features/diary/presentation/bloc/diary_state.freezed.dart index 513d217d..58fa1471 100644 --- a/lib/features/diary/presentation/bloc/diary_state.freezed.dart +++ b/lib/features/diary/presentation/bloc/diary_state.freezed.dart @@ -21,7 +21,9 @@ mixin _$DiaryState { TResult Function() $default, { required TResult Function() loading, required TResult Function(String errorMessage) error, - required TResult Function(DiaryDayModel diary) loaded, + required TResult Function(DiaryDayModel diary, NutritionDataModel nutrition, + double currentSelectedSatietyRating) + loaded, }) => throw _privateConstructorUsedError; @optionalTypeArgs @@ -29,7 +31,9 @@ mixin _$DiaryState { TResult? Function()? $default, { TResult? Function()? loading, TResult? Function(String errorMessage)? error, - TResult? Function(DiaryDayModel diary)? loaded, + TResult? Function(DiaryDayModel diary, NutritionDataModel nutrition, + double currentSelectedSatietyRating)? + loaded, }) => throw _privateConstructorUsedError; @optionalTypeArgs @@ -37,7 +41,9 @@ mixin _$DiaryState { TResult Function()? $default, { TResult Function()? loading, TResult Function(String errorMessage)? error, - TResult Function(DiaryDayModel diary)? loaded, + TResult Function(DiaryDayModel diary, NutritionDataModel nutrition, + double currentSelectedSatietyRating)? + loaded, required TResult orElse(), }) => throw _privateConstructorUsedError; @@ -133,7 +139,9 @@ class _$DiaryStateImpl implements _DiaryState { TResult Function() $default, { required TResult Function() loading, required TResult Function(String errorMessage) error, - required TResult Function(DiaryDayModel diary) loaded, + required TResult Function(DiaryDayModel diary, NutritionDataModel nutrition, + double currentSelectedSatietyRating) + loaded, }) { return $default(); } @@ -144,7 +152,9 @@ class _$DiaryStateImpl implements _DiaryState { TResult? Function()? $default, { TResult? Function()? loading, TResult? Function(String errorMessage)? error, - TResult? Function(DiaryDayModel diary)? loaded, + TResult? Function(DiaryDayModel diary, NutritionDataModel nutrition, + double currentSelectedSatietyRating)? + loaded, }) { return $default?.call(); } @@ -155,7 +165,9 @@ class _$DiaryStateImpl implements _DiaryState { TResult Function()? $default, { TResult Function()? loading, TResult Function(String errorMessage)? error, - TResult Function(DiaryDayModel diary)? loaded, + TResult Function(DiaryDayModel diary, NutritionDataModel nutrition, + double currentSelectedSatietyRating)? + loaded, required TResult orElse(), }) { if ($default != null) { @@ -250,7 +262,9 @@ class _$DiaryStateLoadingImpl implements DiaryStateLoading { TResult Function() $default, { required TResult Function() loading, required TResult Function(String errorMessage) error, - required TResult Function(DiaryDayModel diary) loaded, + required TResult Function(DiaryDayModel diary, NutritionDataModel nutrition, + double currentSelectedSatietyRating) + loaded, }) { return loading(); } @@ -261,7 +275,9 @@ class _$DiaryStateLoadingImpl implements DiaryStateLoading { TResult? Function()? $default, { TResult? Function()? loading, TResult? Function(String errorMessage)? error, - TResult? Function(DiaryDayModel diary)? loaded, + TResult? Function(DiaryDayModel diary, NutritionDataModel nutrition, + double currentSelectedSatietyRating)? + loaded, }) { return loading?.call(); } @@ -272,7 +288,9 @@ class _$DiaryStateLoadingImpl implements DiaryStateLoading { TResult Function()? $default, { TResult Function()? loading, TResult Function(String errorMessage)? error, - TResult Function(DiaryDayModel diary)? loaded, + TResult Function(DiaryDayModel diary, NutritionDataModel nutrition, + double currentSelectedSatietyRating)? + loaded, required TResult orElse(), }) { if (loading != null) { @@ -396,7 +414,9 @@ class _$DiaryStateErrorImpl implements DiaryStateError { TResult Function() $default, { required TResult Function() loading, required TResult Function(String errorMessage) error, - required TResult Function(DiaryDayModel diary) loaded, + required TResult Function(DiaryDayModel diary, NutritionDataModel nutrition, + double currentSelectedSatietyRating) + loaded, }) { return error(errorMessage); } @@ -407,7 +427,9 @@ class _$DiaryStateErrorImpl implements DiaryStateError { TResult? Function()? $default, { TResult? Function()? loading, TResult? Function(String errorMessage)? error, - TResult? Function(DiaryDayModel diary)? loaded, + TResult? Function(DiaryDayModel diary, NutritionDataModel nutrition, + double currentSelectedSatietyRating)? + loaded, }) { return error?.call(errorMessage); } @@ -418,7 +440,9 @@ class _$DiaryStateErrorImpl implements DiaryStateError { TResult Function()? $default, { TResult Function()? loading, TResult Function(String errorMessage)? error, - TResult Function(DiaryDayModel diary)? loaded, + TResult Function(DiaryDayModel diary, NutritionDataModel nutrition, + double currentSelectedSatietyRating)? + loaded, required TResult orElse(), }) { if (error != null) { @@ -484,9 +508,13 @@ abstract class _$$DiaryStateLoadedImplCopyWith<$Res> { $Res Function(_$DiaryStateLoadedImpl) then) = __$$DiaryStateLoadedImplCopyWithImpl<$Res>; @useResult - $Res call({DiaryDayModel diary}); + $Res call( + {DiaryDayModel diary, + NutritionDataModel nutrition, + double currentSelectedSatietyRating}); $DiaryDayModelCopyWith<$Res> get diary; + $NutritionDataModelCopyWith<$Res> get nutrition; } /// @nodoc @@ -503,12 +531,22 @@ class __$$DiaryStateLoadedImplCopyWithImpl<$Res> @override $Res call({ Object? diary = null, + Object? nutrition = null, + Object? currentSelectedSatietyRating = null, }) { return _then(_$DiaryStateLoadedImpl( diary: null == diary ? _value.diary : diary // ignore: cast_nullable_to_non_nullable as DiaryDayModel, + nutrition: null == nutrition + ? _value.nutrition + : nutrition // ignore: cast_nullable_to_non_nullable + as NutritionDataModel, + currentSelectedSatietyRating: null == currentSelectedSatietyRating + ? _value.currentSelectedSatietyRating + : currentSelectedSatietyRating // ignore: cast_nullable_to_non_nullable + as double, )); } @@ -521,19 +559,36 @@ class __$$DiaryStateLoadedImplCopyWithImpl<$Res> return _then(_value.copyWith(diary: value)); }); } + + /// Create a copy of DiaryState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $NutritionDataModelCopyWith<$Res> get nutrition { + return $NutritionDataModelCopyWith<$Res>(_value.nutrition, (value) { + return _then(_value.copyWith(nutrition: value)); + }); + } } /// @nodoc class _$DiaryStateLoadedImpl implements DiaryStateLoaded { - const _$DiaryStateLoadedImpl({required this.diary}); + const _$DiaryStateLoadedImpl( + {required this.diary, + required this.nutrition, + required this.currentSelectedSatietyRating}); @override final DiaryDayModel diary; + @override + final NutritionDataModel nutrition; + @override + final double currentSelectedSatietyRating; @override String toString() { - return 'DiaryState.loaded(diary: $diary)'; + return 'DiaryState.loaded(diary: $diary, nutrition: $nutrition, currentSelectedSatietyRating: $currentSelectedSatietyRating)'; } @override @@ -541,11 +596,18 @@ class _$DiaryStateLoadedImpl implements DiaryStateLoaded { return identical(this, other) || (other.runtimeType == runtimeType && other is _$DiaryStateLoadedImpl && - (identical(other.diary, diary) || other.diary == diary)); + (identical(other.diary, diary) || other.diary == diary) && + (identical(other.nutrition, nutrition) || + other.nutrition == nutrition) && + (identical(other.currentSelectedSatietyRating, + currentSelectedSatietyRating) || + other.currentSelectedSatietyRating == + currentSelectedSatietyRating)); } @override - int get hashCode => Object.hash(runtimeType, diary); + int get hashCode => + Object.hash(runtimeType, diary, nutrition, currentSelectedSatietyRating); /// Create a copy of DiaryState /// with the given fields replaced by the non-null parameter values. @@ -562,9 +624,11 @@ class _$DiaryStateLoadedImpl implements DiaryStateLoaded { TResult Function() $default, { required TResult Function() loading, required TResult Function(String errorMessage) error, - required TResult Function(DiaryDayModel diary) loaded, + required TResult Function(DiaryDayModel diary, NutritionDataModel nutrition, + double currentSelectedSatietyRating) + loaded, }) { - return loaded(diary); + return loaded(diary, nutrition, currentSelectedSatietyRating); } @override @@ -573,9 +637,11 @@ class _$DiaryStateLoadedImpl implements DiaryStateLoaded { TResult? Function()? $default, { TResult? Function()? loading, TResult? Function(String errorMessage)? error, - TResult? Function(DiaryDayModel diary)? loaded, + TResult? Function(DiaryDayModel diary, NutritionDataModel nutrition, + double currentSelectedSatietyRating)? + loaded, }) { - return loaded?.call(diary); + return loaded?.call(diary, nutrition, currentSelectedSatietyRating); } @override @@ -584,11 +650,13 @@ class _$DiaryStateLoadedImpl implements DiaryStateLoaded { TResult Function()? $default, { TResult Function()? loading, TResult Function(String errorMessage)? error, - TResult Function(DiaryDayModel diary)? loaded, + TResult Function(DiaryDayModel diary, NutritionDataModel nutrition, + double currentSelectedSatietyRating)? + loaded, required TResult orElse(), }) { if (loaded != null) { - return loaded(diary); + return loaded(diary, nutrition, currentSelectedSatietyRating); } return orElse(); } @@ -632,10 +700,15 @@ class _$DiaryStateLoadedImpl implements DiaryStateLoaded { } abstract class DiaryStateLoaded implements DiaryState { - const factory DiaryStateLoaded({required final DiaryDayModel diary}) = + const factory DiaryStateLoaded( + {required final DiaryDayModel diary, + required final NutritionDataModel nutrition, + required final double currentSelectedSatietyRating}) = _$DiaryStateLoadedImpl; DiaryDayModel get diary; + NutritionDataModel get nutrition; + double get currentSelectedSatietyRating; /// Create a copy of DiaryState /// with the given fields replaced by the non-null parameter values. diff --git a/lib/features/diary/presentation/pages/diary_screen.dart b/lib/features/diary/presentation/pages/diary_screen.dart index bb9e530a..79f9ac2f 100644 --- a/lib/features/diary/presentation/pages/diary_screen.dart +++ b/lib/features/diary/presentation/pages/diary_screen.dart @@ -2,8 +2,11 @@ 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/models/nutrition_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/diary/presentation/widgets/satiety_rating_widget.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'; @@ -15,6 +18,7 @@ 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:syncfusion_flutter_gauges/gauges.dart'; import 'package:table_calendar/table_calendar.dart'; class DiaryScreen extends StatefulWidget { @@ -34,6 +38,65 @@ class _DiaryScreenState extends State { 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(); + + _showDatePicker() { + showModalBottomSheet( + 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(); + showModalBottomSheet( + useRootNavigator: true, + useSafeArea: false, + context: context, + builder: (context) => GlossaryModerationRequestModalWidget(), + ); + }, + 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) { @@ -59,7 +122,12 @@ class _DiaryScreenState extends State { children: [ _calendar(state), if (state is DiaryStateLoading) _loader(), - if (state is DiaryStateLoaded) _meals(state.diary), + if (state is DiaryStateLoaded) + _meals( + state.diary, + state.currentSelectedSatietyRating, + ), + if (state is DiaryStateLoaded) _chart(state.nutrition), const SizedBox(height: 32), ], ), @@ -70,13 +138,47 @@ class _DiaryScreenState extends State { ); } + _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, + ), + ), + ), + ), + ); + } + _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, + return GestureDetector( + behavior: HitTestBehavior.translucent, + onVerticalDragStart: (_) { + setState(() { + _calendarFormat = CalendarFormat.values[(_calendarFormat.index - 1) % CalendarFormat.values.length]; + }); + }, + 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, + ), ), ); } @@ -130,7 +232,10 @@ class _DiaryScreenState extends State { ); } - _meals(DiaryDayModel diaryDay) { + _meals( + DiaryDayModel diaryDay, + double currentSelectedSatietyRating, + ) { return Material( borderRadius: BorderRadius.circular(20), color: Colors.white, @@ -161,13 +266,19 @@ class _DiaryScreenState extends State { const SizedBox(height: 12), _divider(context), const SizedBox(height: 12), - Text( - 'Время приема пищи', - style: TextStyle( - color: Color(0xFF323F4B), - fontSize: 16, - fontWeight: FontWeight.w500, - ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Время приема пищи', + style: TextStyle( + color: Color(0xFF323F4B), + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + _timeOfMeal(time), + ], ), const SizedBox(height: 12), _divider(context), @@ -189,12 +300,254 @@ class _DiaryScreenState extends State { diaryDay.meals[currentSelectedMealNumber - 1].photos, diaryDay, ), + SatietyRatingWidget( + currentSelectedSatietyRating: currentSelectedSatietyRating, + ), ], ), ), ); } + _chart(NutritionDataModel nutrition) { + final List chartData = [ + ChartData( + nutrition.dailyNutritionIntake.carbohydrates, + nutrition.dailyNutritionRecommendation.carbohydrates, + ), + ChartData( + nutrition.dailyNutritionIntake.fats, + nutrition.dailyNutritionRecommendation.fats, + ), + ChartData( + nutrition.dailyNutritionIntake.proteins, + nutrition.dailyNutritionRecommendation.proteins, + ), + ChartData( + nutrition.dailyNutritionIntake.energy, + nutrition.dailyNutritionRecommendation.energy, + ), + ]; + + 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( + startAngle: 180, + endAngle: 0, + minimum: 0, + maximum: 100, + showLabels: false, + showTicks: false, + radiusFactor: 0.7, + showAxisLine: false, + pointers: [ + RangePointer( + value: 100, + width: 0.16, + animationDuration: 2000, + color: Color(0xFFF6FAFF), + sizeUnit: GaugeSizeUnit.factor, + enableAnimation: true, + cornerStyle: CornerStyle.bothCurve, + ), + RangePointer( + value: (chartData[0].intake / chartData[0].recommendation) * 100, + width: 0.16, + color: chartData[0].intake < chartData[0].recommendation ? 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: (chartData[1].intake / chartData[1].recommendation) * 100, + width: 0.16, + color: chartData[1].intake < chartData[1].recommendation ? 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: (chartData[2].intake / chartData[2].recommendation) * 100, + width: 0.16, + color: chartData[2].intake < chartData[2].recommendation ? 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: (chartData[3].intake / chartData[3].recommendation) * 100, + width: 0.16, + color: chartData[3].intake < chartData[3].recommendation ? appTheme.blueA400 : Colors.red, + sizeUnit: GaugeSizeUnit.factor, + enableAnimation: true, + cornerStyle: CornerStyle.bothCurve, + pointerOffset: -0.54, + ), + ], + annotations: [ + 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('Калории', + '${nutrition.dailyNutritionIntake.energy.toStringAsFixed(0)} из ${nutrition.dailyNutritionRecommendation.energy.toStringAsFixed(0)} ккал'), + const SizedBox(height: 8), + _chartData('Белки', + '${nutrition.dailyNutritionIntake.proteins.toStringAsFixed(0)} из ${nutrition.dailyNutritionRecommendation.proteins.toStringAsFixed(0)} грамм'), + const SizedBox(height: 8), + _chartData('Жиры', + '${nutrition.dailyNutritionIntake.fats.toStringAsFixed(0)} из ${nutrition.dailyNutritionRecommendation.fats.toStringAsFixed(0)} грамм'), + const SizedBox(height: 8), + _chartData('Углеводы', + '${nutrition.dailyNutritionIntake.carbohydrates.toStringAsFixed(0)} из ${nutrition.dailyNutritionRecommendation.carbohydrates.toStringAsFixed(0)} грамм'), + ], + ), + ], + ), + ], + ), + ); + } + + _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), + ), + ), + ], + ); + } + _showImagePicker(DiaryDayModel diaryDay) { showCupertinoModalPopup( context: context, @@ -271,19 +624,20 @@ class _DiaryScreenState extends State { child: IconButton.filled( style: IconButton.styleFrom( backgroundColor: Colors.white, + elevation: 0, ), - onPressed: () { - setState(() { - if (currentSelectedPhotoIndex == photos.length - 1 && currentSelectedPhotoIndex != 0) { - currentSelectedPhotoIndex = photos.length - 2; - } - }); - diaryCubit.deletePhoto( + 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, @@ -372,7 +726,7 @@ class _DiaryScreenState extends State { child: Text( '$mealNumber', style: TextStyle( - color: isActive ? Colors.white : Colors.black, + color: isActive ? Colors.white : Color(0xFF6D6F71), fontSize: 16, fontWeight: FontWeight.w600, ), @@ -669,3 +1023,13 @@ class _DiaryScreenState extends State { ); } } + +class ChartData { + final double intake; + final double recommendation; + + ChartData( + this.intake, + this.recommendation, + ); +} diff --git a/lib/features/diary/presentation/widgets/glossary_moderation_request_modal_widget.dart b/lib/features/diary/presentation/widgets/glossary_moderation_request_modal_widget.dart new file mode 100644 index 00000000..c51a4cb7 --- /dev/null +++ b/lib/features/diary/presentation/widgets/glossary_moderation_request_modal_widget.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class GlossaryModerationRequestModalWidget extends StatefulWidget { + const GlossaryModerationRequestModalWidget({super.key}); + + @override + State createState() => _GlossaryModerationRequestModalWidgetState(); +} + +class _GlossaryModerationRequestModalWidgetState extends State { + @override + Widget build(BuildContext context) { + return Dialog( + child: Column(), + ); + } +} diff --git a/lib/features/diary/presentation/widgets/satiety_rating_widget.dart b/lib/features/diary/presentation/widgets/satiety_rating_widget.dart new file mode 100644 index 00000000..69af3f22 --- /dev/null +++ b/lib/features/diary/presentation/widgets/satiety_rating_widget.dart @@ -0,0 +1,179 @@ +import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.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 double currentSelectedSatietyRating; + + const SatietyRatingWidget({ + super.key, + required this.currentSelectedSatietyRating, + }); + + @override + State createState() => _SatietyRatingWidgetState(); +} + +class _SatietyRatingWidgetState extends State { + @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: 3, + 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 == 1 ? EdgeInsets.symmetric(horizontal: 2) : null, + decoration: BoxDecoration( + color: Color(0xFFC82222), + border: widget.currentSelectedSatietyRating == 1 + ? Border.all( + width: 2, + color: Color(0xFFAA3333), + strokeAlign: BorderSide.strokeAlignOutside, + ) + : null, + borderRadius: widget.currentSelectedSatietyRating == 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 == 2 ? EdgeInsets.symmetric(horizontal: 2) : null, + decoration: BoxDecoration( + color: Color(0xFFFF3E3E), + border: widget.currentSelectedSatietyRating == 2 + ? Border.all( + width: 2, + color: Color(0xFF550000), + strokeAlign: BorderSide.strokeAlignOutside, + ) + : null, + borderRadius: widget.currentSelectedSatietyRating == 2 ? BorderRadius.circular(2) : null, + ), + ); + case 2: + return Container( + width: MediaQuery.of(context).size.width / 6, + height: 24, + margin: widget.currentSelectedSatietyRating == 3 ? EdgeInsets.symmetric(horizontal: 2) : null, + decoration: BoxDecoration( + color: Color(0xFFF8C926), + border: widget.currentSelectedSatietyRating == 3 + ? Border.all( + width: 2, + color: Color(0xFF8C5D00), + strokeAlign: BorderSide.strokeAlignOutside, + ) + : null, + borderRadius: widget.currentSelectedSatietyRating == 3 ? BorderRadius.circular(2) : null, + ), + ); + case 3: + return Container( + width: MediaQuery.of(context).size.width / 6, + height: 24, + margin: widget.currentSelectedSatietyRating == 4 ? EdgeInsets.symmetric(horizontal: 2) : null, + decoration: BoxDecoration( + color: Color(0xFFFFF3BF), + border: widget.currentSelectedSatietyRating == 4 + ? Border.all( + width: 2, + color: Color(0xFFB5911B), + strokeAlign: BorderSide.strokeAlignOutside, + ) + : null, + borderRadius: widget.currentSelectedSatietyRating == 4 ? BorderRadius.circular(2) : null, + ), + ); + case 4: + return Container( + width: MediaQuery.of(context).size.width / 6, + height: 24, + margin: widget.currentSelectedSatietyRating == 5 ? EdgeInsets.symmetric(horizontal: 2) : null, + decoration: BoxDecoration( + color: Color(0xFF62E833), + border: widget.currentSelectedSatietyRating == 5 + ? Border.all( + width: 2, + color: Color(0xFF276918), + strokeAlign: BorderSide.strokeAlignOutside, + ) + : null, + borderRadius: widget.currentSelectedSatietyRating == 5 ? BorderRadius.circular(2) : null, + ), + ); + default: + return Container( + width: MediaQuery.of(context).size.width / 6, + height: 24, + margin: widget.currentSelectedSatietyRating == 6 ? EdgeInsets.symmetric(horizontal: 2) : null, + decoration: BoxDecoration( + color: Color(0xFF4DCE20), + border: widget.currentSelectedSatietyRating == 6 + ? Border.all( + width: 2, + color: Color(0xFF184D12), + strokeAlign: BorderSide.strokeAlignOutside, + ) + : null, + borderRadius: widget.currentSelectedSatietyRating == 6 + ? BorderRadius.horizontal( + left: Radius.circular(2), + right: Radius.circular(8), + ) + : BorderRadius.horizontal( + right: Radius.circular(8), + ), + ), + ); + } + }, + onRatingUpdate: (value) => context.read().updateCurrentSelectedSatietyRating(value), + ), + SvgPicture.asset( + 'assets/icons/satiety_emoji_right.svg', + height: 32, + width: 32, + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/features/diary/repositories/diary_repository.dart b/lib/features/diary/repositories/diary_repository.dart index b9b8243c..abb05a33 100644 --- a/lib/features/diary/repositories/diary_repository.dart +++ b/lib/features/diary/repositories/diary_repository.dart @@ -1,5 +1,6 @@ import 'dart:io'; import 'package:eatvio_app/features/diary/models/diary_day_model.dart'; +import 'package:eatvio_app/features/diary/models/nutrition_model.dart'; import 'package:eatvio_app/features/diary/services/diary_service.dart'; class DiaryRepository { @@ -11,15 +12,19 @@ class DiaryRepository { Future getDiaryByDate(String date) async { return (await diaryService.getDiaryDay( - index: await _getDiaryIndexByDate(date), + index: await getDiaryIndexByDate(date), )) .diary; } - Future _getDiaryIndexByDate(String date) async { + Future getDiaryIndexByDate(String date) async { return (await diaryService.getDiaryIndexByDate(date: date)).id; } + Future getNutritionData(int index) async { + return await diaryService.getNutrition(index: index); + } + Future addPortionToMeal({ required int diaryId, required int mealId, diff --git a/lib/features/diary/services/diary_service.dart b/lib/features/diary/services/diary_service.dart index 16990b1b..bf7f1763 100644 --- a/lib/features/diary/services/diary_service.dart +++ b/lib/features/diary/services/diary_service.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:eatvio_app/features/diary/models/diary_day_model.dart'; import 'package:eatvio_app/features/diary/models/diary_index_model.dart'; +import 'package:eatvio_app/features/diary/models/nutrition_model.dart'; import 'package:retrofit/retrofit.dart'; part 'diary_service.g.dart'; @@ -20,6 +21,11 @@ abstract class DiaryService { @Path('diary_id') required int index, }); + @GET('/api/diary/{diary_id}/dailyNutritionData') + Future getNutrition({ + @Path('diary_id') required int index, + }); + @POST('/api/diary/{diary_id}/meal/{meal_id}/photo/upload') @MultiPart() Future uploadPhotoOfMeal({ diff --git a/lib/features/diary/services/diary_service.g.dart b/lib/features/diary/services/diary_service.g.dart index 74de16af..1672b50a 100644 --- a/lib/features/diary/services/diary_service.g.dart +++ b/lib/features/diary/services/diary_service.g.dart @@ -87,6 +87,39 @@ class _DiaryService implements DiaryService { return _value; } + @override + Future getNutrition({required int index}) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType(Options( + method: 'GET', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/diary/${index}/dailyNutritionData', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + ))); + final _result = await _dio.fetch>(_options); + late NutritionDataModel _value; + try { + _value = NutritionDataModel.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + return _value; + } + @override Future uploadPhotoOfMeal({ required int diaryId, diff --git a/lib/features/diets/presentation/widgets/type_badges.dart b/lib/features/diets/presentation/widgets/type_badges.dart index 204d8c30..b72c85d4 100644 --- a/lib/features/diets/presentation/widgets/type_badges.dart +++ b/lib/features/diets/presentation/widgets/type_badges.dart @@ -8,6 +8,7 @@ import 'package:flutter_svg/svg.dart'; class TypeBadges extends StatefulWidget { const TypeBadges({super.key, required this.type}); + final DietType type; @override @@ -16,6 +17,7 @@ class TypeBadges extends StatefulWidget { class _TypeBadgesState extends State { final List overlaysVisibilityState = [false, false, false, false]; + @override Widget build(BuildContext context) { final s = S.of(context); @@ -117,11 +119,18 @@ class _TypeBadgesState extends State { } class _InfoOverlayEntry extends StatelessWidget { - const _InfoOverlayEntry({required this.title, required this.description, required this.assetPath, required this.onClose}); + const _InfoOverlayEntry({ + required this.title, + required this.description, + required this.assetPath, + required this.onClose, + }); + final String title; final String description; final assetPath; final void Function() onClose; + @override Widget build(BuildContext context) { return MainBackgroundBouble( diff --git a/lib/main.dart b/lib/main.dart index 4bb339cd..23b031a9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,7 +12,7 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_portal/flutter_portal.dart'; import 'package:keyboard_dismisser/keyboard_dismisser.dart'; -Future main() async { +void main() async { WidgetsFlutterBinding.ensureInitialized(); await di.init(); runApp(MyApp()); diff --git a/pubspec.yaml b/pubspec.yaml index 5ace4bf8..de9c3ac0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,6 +40,7 @@ dependencies: google_fonts: ^6.2.1 location: ^7.0.1 syncfusion_flutter_charts: ^27.1.55 + syncfusion_flutter_gauges: ^27.1.58 table_calendar: ^3.1.2 flutter_keyboard_visibility: ^6.0.0 share_plus: ^10.1.0 @@ -52,6 +53,7 @@ dependencies: lottie: ^3.1.3 flutter_portal: ^1.1.4 fluttertoast: ^8.2.8 + flutter_rating_bar: ^4.0.1 dev_dependencies: build_runner: ^2.4.13