diff --git a/assets/icons/add_step_photo.svg b/assets/icons/add_step_photo.svg
new file mode 100644
index 00000000..2967b460
--- /dev/null
+++ b/assets/icons/add_step_photo.svg
@@ -0,0 +1,10 @@
+
diff --git a/assets/icons/upload_photo_of_portion.svg b/assets/icons/upload_photo_of_portion.svg
new file mode 100644
index 00000000..3149160f
--- /dev/null
+++ b/assets/icons/upload_photo_of_portion.svg
@@ -0,0 +1,10 @@
+
diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart
index 899804f1..7a90c01c 100644
--- a/lib/core/app_router.dart
+++ b/lib/core/app_router.dart
@@ -29,7 +29,6 @@ import 'package:eatvio_app/features/profile/presentation/pages/profile_screen.da
import 'package:eatvio_app/features/profile/presentation/pages/subs_screen.dart';
import 'package:eatvio_app/features/recipes/presentation/pages/about_recipes_screen.dart';
import 'package:eatvio_app/features/recipes/presentation/pages/recipe_create_screen.dart';
-import 'package:eatvio_app/features/recipes/presentation/pages/recipe_edit_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/registration_screen.dart';
import 'package:eatvio_app/features/settings/entities/settings_entity.dart';
import 'package:eatvio_app/features/settings/presentation/pages/change_email_screen.dart';
@@ -133,13 +132,6 @@ class AppRouter {
name: 'recipeCreateScreen',
builder: (context, state) => RecipeCreateScreen(),
),
- GoRoute(
- path: '/recipe-edit-screen',
- name: 'recipeEditScreen',
- builder: (context, state) => RecipeEditScreen(
- recipeID: state.extra as String,
- ),
- ),
GoRoute(
path: '/profile-screen',
name: 'profileScreen',
diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart
index 7bc58da5..95ca1af9 100644
--- a/lib/core/locator_service.dart
+++ b/lib/core/locator_service.dart
@@ -46,7 +46,7 @@ import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/prof
import 'package:eatvio_app/features/profile/presentation/bloc/sub_cubit/sub_cubit.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:eatvio_app/features/profile/services/profile_service.dart';
-import 'package:eatvio_app/features/recipes/presentation/bloc/recipe_create_cubit.dart';
+import 'package:eatvio_app/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart';
import 'package:eatvio_app/features/recipes/reporitories/recipe_repository.dart';
import 'package:eatvio_app/features/recipes/services/recipe_service.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart';
@@ -75,7 +75,6 @@ Future init() async {
sl.registerFactory(() => CalendarCubit(calendarRepository: sl()));
sl.registerFactory(() => ProfileCubit(profileRepository: sl(), chatRepository: sl()));
sl.registerFactory(() => profilePlans.PlansCubit(plansRepository: sl()));
- sl.registerFactory(() => RecipeCreateCubit(recipeRepository: sl()));
sl.registerFactory(() => DietsCubit(feedRepository: sl(), dietsRepository: sl()));
sl.registerFactory(() => CommentsCubit(feedRepository: sl()));
sl.registerFactory(() => ChatSelectCubit(chatRepository: sl()));
@@ -90,6 +89,7 @@ Future init() async {
sl.registerFactory(() => CreatePlanCubit());
sl.registerFactory(() => FeedProfileCubit(profileRepository: sl()));
sl.registerFactory(() => AboutPlanCubit(planRepository: sl()));
+ sl.registerFactory(() => CreateRecipeCubit());
// Repository
sl.registerLazySingleton(() => ChatRepository(chatService: sl()));
diff --git a/lib/core/widgets/custom_drop_down.dart b/lib/core/widgets/custom_drop_down.dart
index 887f4597..10edb929 100644
--- a/lib/core/widgets/custom_drop_down.dart
+++ b/lib/core/widgets/custom_drop_down.dart
@@ -64,11 +64,6 @@ class CustomDropDown extends StatelessWidget {
value: value,
isDense: true,
isExpanded: true,
- style: TextStyle(
- fontSize: 16,
- color: Colors.black,
- fontWeight: FontWeight.w400,
- ),
items: items?.map((SelectionPopupModel item) {
return DropdownMenuItem(
value: item,
diff --git a/lib/core/widgets/custom_text_form_field.dart b/lib/core/widgets/custom_text_form_field.dart
index b9588f68..85b9c7ce 100644
--- a/lib/core/widgets/custom_text_form_field.dart
+++ b/lib/core/widgets/custom_text_form_field.dart
@@ -38,9 +38,11 @@ class CustomTextFormField extends StatelessWidget {
this.expands = false,
this.textAlignVertical,
this.counter = const SizedBox.shrink(),
+ this.enabled = true,
super.key,
this.hasError = false,
});
+
final Widget? counter;
final Alignment? alignment;
final double? width;
@@ -76,11 +78,14 @@ class CustomTextFormField extends StatelessWidget {
final bool expands;
final TextAlignVertical? textAlignVertical;
final bool hasError;
+ final bool enabled;
+
@override
Widget build(BuildContext context) {
return Align(
alignment: alignment ?? Alignment.center,
child: TextFormField(
+ enabled: enabled,
textAlignVertical: textAlignVertical,
expands: expands,
minLines: minLines,
@@ -111,6 +116,7 @@ class CustomTextFormField extends StatelessWidget {
prefixIcon: prefix,
prefixIconConstraints: prefixConstraints,
suffixIcon: suffix,
+ floatingLabelBehavior: FloatingLabelBehavior.always,
suffixStyle: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
diff --git a/lib/features/diary/presentation/pages/diary_screen.dart b/lib/features/diary/presentation/pages/diary_screen.dart
index c6c8e291..4fe82667 100644
--- a/lib/features/diary/presentation/pages/diary_screen.dart
+++ b/lib/features/diary/presentation/pages/diary_screen.dart
@@ -34,8 +34,9 @@ class _DiaryScreenState extends State {
children: [
CalendarWidget(),
if (state is DiaryStateLoading) const Loader(),
- if (state is DiaryStateLoaded) MealWidget(),
CaloriesChartWidget(),
+ const SizedBox(height: 12),
+ if (state is DiaryStateLoaded) MealWidget(),
],
),
),
diff --git a/lib/features/diary/presentation/widgets/calories_chart_widget.dart b/lib/features/diary/presentation/widgets/calories_chart_widget.dart
index d59d3b24..b5a6c625 100644
--- a/lib/features/diary/presentation/widgets/calories_chart_widget.dart
+++ b/lib/features/diary/presentation/widgets/calories_chart_widget.dart
@@ -13,7 +13,6 @@ class CaloriesChartWidget extends StatelessWidget {
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,
diff --git a/lib/features/feed/presentation/pages/feed_screen.dart b/lib/features/feed/presentation/pages/feed_screen.dart
index 0459426e..24ec166f 100644
--- a/lib/features/feed/presentation/pages/feed_screen.dart
+++ b/lib/features/feed/presentation/pages/feed_screen.dart
@@ -152,6 +152,9 @@ class _FeedScreenState extends State {
_searchBar(),
SizedBox(width: 12),
switch (_selectedChipIndex) {
+ 1 => _addItemButton(
+ onTap: () => GoRouter.of(context).pushNamed('recipeCreateScreen'),
+ ),
2 => _addItemButton(
onTap: state.profile?.role == 'consultant' ? () => GoRouter.of(context).pushNamed(CreateDietScreen.name) : null,
),
diff --git a/lib/features/recipes/entities/recipe_create_entity.dart b/lib/features/recipes/entities/recipe_create_entity.dart
index e69de29b..43a44a55 100644
--- a/lib/features/recipes/entities/recipe_create_entity.dart
+++ b/lib/features/recipes/entities/recipe_create_entity.dart
@@ -0,0 +1,50 @@
+import 'dart:io';
+
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part 'recipe_create_entity.freezed.dart';
+
+enum TypeOfRecipe {
+ first_meals,
+ second_meals,
+ salads,
+ snacks,
+ sweets,
+ drinks,
+ other,
+}
+
+@freezed
+class RecipeCreateEntity with _$RecipeCreateEntity {
+ const factory RecipeCreateEntity({
+ String? title,
+ TypeOfRecipe? typeOfRecipe,
+ int? countOfPortions,
+ double? energyInOnePortion,
+ String? totalCookTime, // hh:mm
+ String? activeCookTime, // hh:mm
+ String? description,
+ File? image,
+ String? videoUrl,
+ required List ingredients,
+ required List steps,
+ required List tags, // tags for search
+ }) = _RecipeCreateEntity;
+}
+
+@freezed
+class IngredientEntity with _$IngredientEntity {
+ const factory IngredientEntity({
+ required String name,
+ required String unit,
+ required String portion,
+ }) = _IngredientEntity;
+}
+
+@freezed
+class StepEntity with _$StepEntity {
+ const factory StepEntity({
+ String? description,
+ File? image,
+ }) = _StepEntity;
+}
diff --git a/lib/features/recipes/entities/recipe_create_entity.freezed.dart b/lib/features/recipes/entities/recipe_create_entity.freezed.dart
new file mode 100644
index 00000000..e42db8f4
--- /dev/null
+++ b/lib/features/recipes/entities/recipe_create_entity.freezed.dart
@@ -0,0 +1,722 @@
+// 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 'recipe_create_entity.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');
+
+/// @nodoc
+mixin _$RecipeCreateEntity {
+ String? get title => throw _privateConstructorUsedError;
+ TypeOfRecipe? get typeOfRecipe => throw _privateConstructorUsedError;
+ int? get countOfPortions => throw _privateConstructorUsedError;
+ double? get energyInOnePortion => throw _privateConstructorUsedError;
+ String? get totalCookTime => throw _privateConstructorUsedError; // hh:mm
+ String? get activeCookTime => throw _privateConstructorUsedError; // hh:mm
+ String? get description => throw _privateConstructorUsedError;
+ File? get image => throw _privateConstructorUsedError;
+ String? get videoUrl => throw _privateConstructorUsedError;
+ List get ingredients => throw _privateConstructorUsedError;
+ List get steps => throw _privateConstructorUsedError;
+ List get tags => throw _privateConstructorUsedError;
+
+ /// Create a copy of RecipeCreateEntity
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ $RecipeCreateEntityCopyWith get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $RecipeCreateEntityCopyWith<$Res> {
+ factory $RecipeCreateEntityCopyWith(
+ RecipeCreateEntity value, $Res Function(RecipeCreateEntity) then) =
+ _$RecipeCreateEntityCopyWithImpl<$Res, RecipeCreateEntity>;
+ @useResult
+ $Res call(
+ {String? title,
+ TypeOfRecipe? typeOfRecipe,
+ int? countOfPortions,
+ double? energyInOnePortion,
+ String? totalCookTime,
+ String? activeCookTime,
+ String? description,
+ File? image,
+ String? videoUrl,
+ List ingredients,
+ List steps,
+ List tags});
+}
+
+/// @nodoc
+class _$RecipeCreateEntityCopyWithImpl<$Res, $Val extends RecipeCreateEntity>
+ implements $RecipeCreateEntityCopyWith<$Res> {
+ _$RecipeCreateEntityCopyWithImpl(this._value, this._then);
+
+ // ignore: unused_field
+ final $Val _value;
+ // ignore: unused_field
+ final $Res Function($Val) _then;
+
+ /// Create a copy of RecipeCreateEntity
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? title = freezed,
+ Object? typeOfRecipe = freezed,
+ Object? countOfPortions = freezed,
+ Object? energyInOnePortion = freezed,
+ Object? totalCookTime = freezed,
+ Object? activeCookTime = freezed,
+ Object? description = freezed,
+ Object? image = freezed,
+ Object? videoUrl = freezed,
+ Object? ingredients = null,
+ Object? steps = null,
+ Object? tags = null,
+ }) {
+ return _then(_value.copyWith(
+ title: freezed == title
+ ? _value.title
+ : title // ignore: cast_nullable_to_non_nullable
+ as String?,
+ typeOfRecipe: freezed == typeOfRecipe
+ ? _value.typeOfRecipe
+ : typeOfRecipe // ignore: cast_nullable_to_non_nullable
+ as TypeOfRecipe?,
+ countOfPortions: freezed == countOfPortions
+ ? _value.countOfPortions
+ : countOfPortions // ignore: cast_nullable_to_non_nullable
+ as int?,
+ energyInOnePortion: freezed == energyInOnePortion
+ ? _value.energyInOnePortion
+ : energyInOnePortion // ignore: cast_nullable_to_non_nullable
+ as double?,
+ totalCookTime: freezed == totalCookTime
+ ? _value.totalCookTime
+ : totalCookTime // ignore: cast_nullable_to_non_nullable
+ as String?,
+ activeCookTime: freezed == activeCookTime
+ ? _value.activeCookTime
+ : activeCookTime // ignore: cast_nullable_to_non_nullable
+ as String?,
+ description: freezed == description
+ ? _value.description
+ : description // ignore: cast_nullable_to_non_nullable
+ as String?,
+ image: freezed == image
+ ? _value.image
+ : image // ignore: cast_nullable_to_non_nullable
+ as File?,
+ videoUrl: freezed == videoUrl
+ ? _value.videoUrl
+ : videoUrl // ignore: cast_nullable_to_non_nullable
+ as String?,
+ ingredients: null == ingredients
+ ? _value.ingredients
+ : ingredients // ignore: cast_nullable_to_non_nullable
+ as List,
+ steps: null == steps
+ ? _value.steps
+ : steps // ignore: cast_nullable_to_non_nullable
+ as List,
+ tags: null == tags
+ ? _value.tags
+ : tags // ignore: cast_nullable_to_non_nullable
+ as List,
+ ) as $Val);
+ }
+}
+
+/// @nodoc
+abstract class _$$RecipeCreateEntityImplCopyWith<$Res>
+ implements $RecipeCreateEntityCopyWith<$Res> {
+ factory _$$RecipeCreateEntityImplCopyWith(_$RecipeCreateEntityImpl value,
+ $Res Function(_$RecipeCreateEntityImpl) then) =
+ __$$RecipeCreateEntityImplCopyWithImpl<$Res>;
+ @override
+ @useResult
+ $Res call(
+ {String? title,
+ TypeOfRecipe? typeOfRecipe,
+ int? countOfPortions,
+ double? energyInOnePortion,
+ String? totalCookTime,
+ String? activeCookTime,
+ String? description,
+ File? image,
+ String? videoUrl,
+ List ingredients,
+ List steps,
+ List tags});
+}
+
+/// @nodoc
+class __$$RecipeCreateEntityImplCopyWithImpl<$Res>
+ extends _$RecipeCreateEntityCopyWithImpl<$Res, _$RecipeCreateEntityImpl>
+ implements _$$RecipeCreateEntityImplCopyWith<$Res> {
+ __$$RecipeCreateEntityImplCopyWithImpl(_$RecipeCreateEntityImpl _value,
+ $Res Function(_$RecipeCreateEntityImpl) _then)
+ : super(_value, _then);
+
+ /// Create a copy of RecipeCreateEntity
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? title = freezed,
+ Object? typeOfRecipe = freezed,
+ Object? countOfPortions = freezed,
+ Object? energyInOnePortion = freezed,
+ Object? totalCookTime = freezed,
+ Object? activeCookTime = freezed,
+ Object? description = freezed,
+ Object? image = freezed,
+ Object? videoUrl = freezed,
+ Object? ingredients = null,
+ Object? steps = null,
+ Object? tags = null,
+ }) {
+ return _then(_$RecipeCreateEntityImpl(
+ title: freezed == title
+ ? _value.title
+ : title // ignore: cast_nullable_to_non_nullable
+ as String?,
+ typeOfRecipe: freezed == typeOfRecipe
+ ? _value.typeOfRecipe
+ : typeOfRecipe // ignore: cast_nullable_to_non_nullable
+ as TypeOfRecipe?,
+ countOfPortions: freezed == countOfPortions
+ ? _value.countOfPortions
+ : countOfPortions // ignore: cast_nullable_to_non_nullable
+ as int?,
+ energyInOnePortion: freezed == energyInOnePortion
+ ? _value.energyInOnePortion
+ : energyInOnePortion // ignore: cast_nullable_to_non_nullable
+ as double?,
+ totalCookTime: freezed == totalCookTime
+ ? _value.totalCookTime
+ : totalCookTime // ignore: cast_nullable_to_non_nullable
+ as String?,
+ activeCookTime: freezed == activeCookTime
+ ? _value.activeCookTime
+ : activeCookTime // ignore: cast_nullable_to_non_nullable
+ as String?,
+ description: freezed == description
+ ? _value.description
+ : description // ignore: cast_nullable_to_non_nullable
+ as String?,
+ image: freezed == image
+ ? _value.image
+ : image // ignore: cast_nullable_to_non_nullable
+ as File?,
+ videoUrl: freezed == videoUrl
+ ? _value.videoUrl
+ : videoUrl // ignore: cast_nullable_to_non_nullable
+ as String?,
+ ingredients: null == ingredients
+ ? _value._ingredients
+ : ingredients // ignore: cast_nullable_to_non_nullable
+ as List,
+ steps: null == steps
+ ? _value._steps
+ : steps // ignore: cast_nullable_to_non_nullable
+ as List,
+ tags: null == tags
+ ? _value._tags
+ : tags // ignore: cast_nullable_to_non_nullable
+ as List,
+ ));
+ }
+}
+
+/// @nodoc
+
+class _$RecipeCreateEntityImpl implements _RecipeCreateEntity {
+ const _$RecipeCreateEntityImpl(
+ {this.title,
+ this.typeOfRecipe,
+ this.countOfPortions,
+ this.energyInOnePortion,
+ this.totalCookTime,
+ this.activeCookTime,
+ this.description,
+ this.image,
+ this.videoUrl,
+ required final List ingredients,
+ required final List steps,
+ required final List tags})
+ : _ingredients = ingredients,
+ _steps = steps,
+ _tags = tags;
+
+ @override
+ final String? title;
+ @override
+ final TypeOfRecipe? typeOfRecipe;
+ @override
+ final int? countOfPortions;
+ @override
+ final double? energyInOnePortion;
+ @override
+ final String? totalCookTime;
+// hh:mm
+ @override
+ final String? activeCookTime;
+// hh:mm
+ @override
+ final String? description;
+ @override
+ final File? image;
+ @override
+ final String? videoUrl;
+ final List _ingredients;
+ @override
+ List get ingredients {
+ if (_ingredients is EqualUnmodifiableListView) return _ingredients;
+ // ignore: implicit_dynamic_type
+ return EqualUnmodifiableListView(_ingredients);
+ }
+
+ final List _steps;
+ @override
+ List get steps {
+ if (_steps is EqualUnmodifiableListView) return _steps;
+ // ignore: implicit_dynamic_type
+ return EqualUnmodifiableListView(_steps);
+ }
+
+ final List _tags;
+ @override
+ List get tags {
+ if (_tags is EqualUnmodifiableListView) return _tags;
+ // ignore: implicit_dynamic_type
+ return EqualUnmodifiableListView(_tags);
+ }
+
+ @override
+ String toString() {
+ return 'RecipeCreateEntity(title: $title, typeOfRecipe: $typeOfRecipe, countOfPortions: $countOfPortions, energyInOnePortion: $energyInOnePortion, totalCookTime: $totalCookTime, activeCookTime: $activeCookTime, description: $description, image: $image, videoUrl: $videoUrl, ingredients: $ingredients, steps: $steps, tags: $tags)';
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ (other.runtimeType == runtimeType &&
+ other is _$RecipeCreateEntityImpl &&
+ (identical(other.title, title) || other.title == title) &&
+ (identical(other.typeOfRecipe, typeOfRecipe) ||
+ other.typeOfRecipe == typeOfRecipe) &&
+ (identical(other.countOfPortions, countOfPortions) ||
+ other.countOfPortions == countOfPortions) &&
+ (identical(other.energyInOnePortion, energyInOnePortion) ||
+ other.energyInOnePortion == energyInOnePortion) &&
+ (identical(other.totalCookTime, totalCookTime) ||
+ other.totalCookTime == totalCookTime) &&
+ (identical(other.activeCookTime, activeCookTime) ||
+ other.activeCookTime == activeCookTime) &&
+ (identical(other.description, description) ||
+ other.description == description) &&
+ (identical(other.image, image) || other.image == image) &&
+ (identical(other.videoUrl, videoUrl) ||
+ other.videoUrl == videoUrl) &&
+ const DeepCollectionEquality()
+ .equals(other._ingredients, _ingredients) &&
+ const DeepCollectionEquality().equals(other._steps, _steps) &&
+ const DeepCollectionEquality().equals(other._tags, _tags));
+ }
+
+ @override
+ int get hashCode => Object.hash(
+ runtimeType,
+ title,
+ typeOfRecipe,
+ countOfPortions,
+ energyInOnePortion,
+ totalCookTime,
+ activeCookTime,
+ description,
+ image,
+ videoUrl,
+ const DeepCollectionEquality().hash(_ingredients),
+ const DeepCollectionEquality().hash(_steps),
+ const DeepCollectionEquality().hash(_tags));
+
+ /// Create a copy of RecipeCreateEntity
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ @pragma('vm:prefer-inline')
+ _$$RecipeCreateEntityImplCopyWith<_$RecipeCreateEntityImpl> get copyWith =>
+ __$$RecipeCreateEntityImplCopyWithImpl<_$RecipeCreateEntityImpl>(
+ this, _$identity);
+}
+
+abstract class _RecipeCreateEntity implements RecipeCreateEntity {
+ const factory _RecipeCreateEntity(
+ {final String? title,
+ final TypeOfRecipe? typeOfRecipe,
+ final int? countOfPortions,
+ final double? energyInOnePortion,
+ final String? totalCookTime,
+ final String? activeCookTime,
+ final String? description,
+ final File? image,
+ final String? videoUrl,
+ required final List ingredients,
+ required final List steps,
+ required final List tags}) = _$RecipeCreateEntityImpl;
+
+ @override
+ String? get title;
+ @override
+ TypeOfRecipe? get typeOfRecipe;
+ @override
+ int? get countOfPortions;
+ @override
+ double? get energyInOnePortion;
+ @override
+ String? get totalCookTime; // hh:mm
+ @override
+ String? get activeCookTime; // hh:mm
+ @override
+ String? get description;
+ @override
+ File? get image;
+ @override
+ String? get videoUrl;
+ @override
+ List get ingredients;
+ @override
+ List get steps;
+ @override
+ List get tags;
+
+ /// Create a copy of RecipeCreateEntity
+ /// with the given fields replaced by the non-null parameter values.
+ @override
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ _$$RecipeCreateEntityImplCopyWith<_$RecipeCreateEntityImpl> get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+mixin _$IngredientEntity {
+ String get name => throw _privateConstructorUsedError;
+ String get unit => throw _privateConstructorUsedError;
+ String get portion => throw _privateConstructorUsedError;
+
+ /// Create a copy of IngredientEntity
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ $IngredientEntityCopyWith get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $IngredientEntityCopyWith<$Res> {
+ factory $IngredientEntityCopyWith(
+ IngredientEntity value, $Res Function(IngredientEntity) then) =
+ _$IngredientEntityCopyWithImpl<$Res, IngredientEntity>;
+ @useResult
+ $Res call({String name, String unit, String portion});
+}
+
+/// @nodoc
+class _$IngredientEntityCopyWithImpl<$Res, $Val extends IngredientEntity>
+ implements $IngredientEntityCopyWith<$Res> {
+ _$IngredientEntityCopyWithImpl(this._value, this._then);
+
+ // ignore: unused_field
+ final $Val _value;
+ // ignore: unused_field
+ final $Res Function($Val) _then;
+
+ /// Create a copy of IngredientEntity
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? name = null,
+ Object? unit = null,
+ Object? portion = null,
+ }) {
+ return _then(_value.copyWith(
+ name: null == name
+ ? _value.name
+ : name // ignore: cast_nullable_to_non_nullable
+ as String,
+ unit: null == unit
+ ? _value.unit
+ : unit // ignore: cast_nullable_to_non_nullable
+ as String,
+ portion: null == portion
+ ? _value.portion
+ : portion // ignore: cast_nullable_to_non_nullable
+ as String,
+ ) as $Val);
+ }
+}
+
+/// @nodoc
+abstract class _$$IngredientEntityImplCopyWith<$Res>
+ implements $IngredientEntityCopyWith<$Res> {
+ factory _$$IngredientEntityImplCopyWith(_$IngredientEntityImpl value,
+ $Res Function(_$IngredientEntityImpl) then) =
+ __$$IngredientEntityImplCopyWithImpl<$Res>;
+ @override
+ @useResult
+ $Res call({String name, String unit, String portion});
+}
+
+/// @nodoc
+class __$$IngredientEntityImplCopyWithImpl<$Res>
+ extends _$IngredientEntityCopyWithImpl<$Res, _$IngredientEntityImpl>
+ implements _$$IngredientEntityImplCopyWith<$Res> {
+ __$$IngredientEntityImplCopyWithImpl(_$IngredientEntityImpl _value,
+ $Res Function(_$IngredientEntityImpl) _then)
+ : super(_value, _then);
+
+ /// Create a copy of IngredientEntity
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? name = null,
+ Object? unit = null,
+ Object? portion = null,
+ }) {
+ return _then(_$IngredientEntityImpl(
+ name: null == name
+ ? _value.name
+ : name // ignore: cast_nullable_to_non_nullable
+ as String,
+ unit: null == unit
+ ? _value.unit
+ : unit // ignore: cast_nullable_to_non_nullable
+ as String,
+ portion: null == portion
+ ? _value.portion
+ : portion // ignore: cast_nullable_to_non_nullable
+ as String,
+ ));
+ }
+}
+
+/// @nodoc
+
+class _$IngredientEntityImpl implements _IngredientEntity {
+ const _$IngredientEntityImpl(
+ {required this.name, required this.unit, required this.portion});
+
+ @override
+ final String name;
+ @override
+ final String unit;
+ @override
+ final String portion;
+
+ @override
+ String toString() {
+ return 'IngredientEntity(name: $name, unit: $unit, portion: $portion)';
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ (other.runtimeType == runtimeType &&
+ other is _$IngredientEntityImpl &&
+ (identical(other.name, name) || other.name == name) &&
+ (identical(other.unit, unit) || other.unit == unit) &&
+ (identical(other.portion, portion) || other.portion == portion));
+ }
+
+ @override
+ int get hashCode => Object.hash(runtimeType, name, unit, portion);
+
+ /// Create a copy of IngredientEntity
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ @pragma('vm:prefer-inline')
+ _$$IngredientEntityImplCopyWith<_$IngredientEntityImpl> get copyWith =>
+ __$$IngredientEntityImplCopyWithImpl<_$IngredientEntityImpl>(
+ this, _$identity);
+}
+
+abstract class _IngredientEntity implements IngredientEntity {
+ const factory _IngredientEntity(
+ {required final String name,
+ required final String unit,
+ required final String portion}) = _$IngredientEntityImpl;
+
+ @override
+ String get name;
+ @override
+ String get unit;
+ @override
+ String get portion;
+
+ /// Create a copy of IngredientEntity
+ /// with the given fields replaced by the non-null parameter values.
+ @override
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ _$$IngredientEntityImplCopyWith<_$IngredientEntityImpl> get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+mixin _$StepEntity {
+ String? get description => throw _privateConstructorUsedError;
+ File? get image => throw _privateConstructorUsedError;
+
+ /// Create a copy of StepEntity
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ $StepEntityCopyWith get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $StepEntityCopyWith<$Res> {
+ factory $StepEntityCopyWith(
+ StepEntity value, $Res Function(StepEntity) then) =
+ _$StepEntityCopyWithImpl<$Res, StepEntity>;
+ @useResult
+ $Res call({String? description, File? image});
+}
+
+/// @nodoc
+class _$StepEntityCopyWithImpl<$Res, $Val extends StepEntity>
+ implements $StepEntityCopyWith<$Res> {
+ _$StepEntityCopyWithImpl(this._value, this._then);
+
+ // ignore: unused_field
+ final $Val _value;
+ // ignore: unused_field
+ final $Res Function($Val) _then;
+
+ /// Create a copy of StepEntity
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? description = freezed,
+ Object? image = freezed,
+ }) {
+ return _then(_value.copyWith(
+ description: freezed == description
+ ? _value.description
+ : description // ignore: cast_nullable_to_non_nullable
+ as String?,
+ image: freezed == image
+ ? _value.image
+ : image // ignore: cast_nullable_to_non_nullable
+ as File?,
+ ) as $Val);
+ }
+}
+
+/// @nodoc
+abstract class _$$StepEntityImplCopyWith<$Res>
+ implements $StepEntityCopyWith<$Res> {
+ factory _$$StepEntityImplCopyWith(
+ _$StepEntityImpl value, $Res Function(_$StepEntityImpl) then) =
+ __$$StepEntityImplCopyWithImpl<$Res>;
+ @override
+ @useResult
+ $Res call({String? description, File? image});
+}
+
+/// @nodoc
+class __$$StepEntityImplCopyWithImpl<$Res>
+ extends _$StepEntityCopyWithImpl<$Res, _$StepEntityImpl>
+ implements _$$StepEntityImplCopyWith<$Res> {
+ __$$StepEntityImplCopyWithImpl(
+ _$StepEntityImpl _value, $Res Function(_$StepEntityImpl) _then)
+ : super(_value, _then);
+
+ /// Create a copy of StepEntity
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? description = freezed,
+ Object? image = freezed,
+ }) {
+ return _then(_$StepEntityImpl(
+ description: freezed == description
+ ? _value.description
+ : description // ignore: cast_nullable_to_non_nullable
+ as String?,
+ image: freezed == image
+ ? _value.image
+ : image // ignore: cast_nullable_to_non_nullable
+ as File?,
+ ));
+ }
+}
+
+/// @nodoc
+
+class _$StepEntityImpl implements _StepEntity {
+ const _$StepEntityImpl({this.description, this.image});
+
+ @override
+ final String? description;
+ @override
+ final File? image;
+
+ @override
+ String toString() {
+ return 'StepEntity(description: $description, image: $image)';
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ (other.runtimeType == runtimeType &&
+ other is _$StepEntityImpl &&
+ (identical(other.description, description) ||
+ other.description == description) &&
+ (identical(other.image, image) || other.image == image));
+ }
+
+ @override
+ int get hashCode => Object.hash(runtimeType, description, image);
+
+ /// Create a copy of StepEntity
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ @pragma('vm:prefer-inline')
+ _$$StepEntityImplCopyWith<_$StepEntityImpl> get copyWith =>
+ __$$StepEntityImplCopyWithImpl<_$StepEntityImpl>(this, _$identity);
+}
+
+abstract class _StepEntity implements StepEntity {
+ const factory _StepEntity({final String? description, final File? image}) =
+ _$StepEntityImpl;
+
+ @override
+ String? get description;
+ @override
+ File? get image;
+
+ /// Create a copy of StepEntity
+ /// with the given fields replaced by the non-null parameter values.
+ @override
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ _$$StepEntityImplCopyWith<_$StepEntityImpl> get copyWith =>
+ throw _privateConstructorUsedError;
+}
diff --git a/lib/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart b/lib/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart
new file mode 100644
index 00000000..03253f26
--- /dev/null
+++ b/lib/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart
@@ -0,0 +1,219 @@
+import 'dart:io';
+
+import 'package:eatvio_app/features/recipes/entities/recipe_create_entity.dart';
+import 'package:eatvio_app/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_state.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class CreateRecipeCubit extends Cubit {
+ CreateRecipeCubit()
+ : super(
+ CreateRecipeState(
+ RecipeCreateEntity(
+ ingredients: [],
+ steps: [],
+ tags: [],
+ ),
+ ),
+ );
+
+ addStep() {
+ emit(
+ CreateRecipeState(
+ state.entity.copyWith(
+ steps: [
+ ...state.entity.steps,
+ StepEntity(),
+ ],
+ ),
+ ),
+ );
+ }
+
+ removeStep(StepEntity step) {
+ List steps = [...state.entity.steps];
+ steps.remove(step);
+
+ emit(
+ CreateRecipeState(state.entity.copyWith(steps: steps)),
+ );
+ }
+
+ addIngredient(IngredientEntity entity) {
+ emit(
+ CreateRecipeState(
+ state.entity.copyWith(
+ ingredients: [
+ ...state.entity.ingredients,
+ entity,
+ ],
+ ),
+ ),
+ );
+ }
+
+ removeIngredient(int index) {
+ List ingredients = [...state.entity.ingredients];
+ ingredients.removeAt(index);
+
+ emit(
+ CreateRecipeState(
+ state.entity.copyWith(
+ ingredients: ingredients,
+ ),
+ ),
+ );
+ }
+
+ replaceIngredientPosition(int oldIndex, int newIndex) {
+ List ingredients = [...state.entity.ingredients];
+ final IngredientEntity item = ingredients.removeAt(oldIndex);
+ ingredients.insert(newIndex, item);
+ emit(
+ CreateRecipeState(
+ state.entity.copyWith(
+ ingredients: ingredients,
+ ),
+ ),
+ );
+ }
+
+ replaceTitle(String title) {
+ emit(
+ CreateRecipeState(
+ state.entity.copyWith(
+ title: title,
+ ),
+ ),
+ );
+ }
+
+ replaceDescription(String description) {
+ emit(
+ CreateRecipeState(
+ state.entity.copyWith(
+ description: description,
+ ),
+ ),
+ );
+ }
+
+ changeType(TypeOfRecipe typeOfRecipe) {
+ emit(
+ CreateRecipeState(
+ state.entity.copyWith(
+ typeOfRecipe: typeOfRecipe,
+ ),
+ ),
+ );
+ }
+
+ addTag(String tag) {
+ emit(
+ CreateRecipeState(
+ state.entity.copyWith(
+ tags: [
+ ...state.entity.tags,
+ tag,
+ ],
+ ),
+ ),
+ );
+ }
+
+ removeTag(String tag) {
+ List tags = [...state.entity.tags];
+ tags.remove(tag);
+
+ emit(
+ CreateRecipeState(
+ state.entity.copyWith(
+ tags: tags,
+ ),
+ ),
+ );
+ }
+
+ replacePortionsCount(int count) {
+ emit(
+ CreateRecipeState(
+ state.entity.copyWith(
+ countOfPortions: count,
+ ),
+ ),
+ );
+ }
+
+ replaceEnegryInPortion(double energy) {
+ emit(
+ CreateRecipeState(
+ state.entity.copyWith(
+ energyInOnePortion: energy,
+ ),
+ ),
+ );
+ }
+
+ replaceTotalTime(String time) {
+ emit(
+ CreateRecipeState(
+ state.entity.copyWith(
+ totalCookTime: time,
+ ),
+ ),
+ );
+ }
+
+ replaceActiveTime(String time) {
+ emit(
+ CreateRecipeState(
+ state.entity.copyWith(
+ activeCookTime: time,
+ ),
+ ),
+ );
+ }
+
+ replaceVideoUrl(String videoUrl) {
+ emit(
+ CreateRecipeState(
+ state.entity.copyWith(
+ videoUrl: videoUrl,
+ ),
+ ),
+ );
+ }
+
+ replaceImage(File? image) {
+ emit(
+ CreateRecipeState(
+ state.entity.copyWith(
+ image: image,
+ ),
+ ),
+ );
+ }
+
+ replaceDescriptionInStep(String description, int index) {
+ List steps = [...state.entity.steps];
+ steps[index] = steps[index].copyWith(description: description);
+ emit(
+ CreateRecipeState(
+ state.entity.copyWith(
+ steps: steps,
+ ),
+ ),
+ );
+ }
+
+ replaceImageInStep(File? image, int index) {
+ List steps = [...state.entity.steps];
+ steps[index] = steps[index].copyWith(image: image);
+ emit(
+ CreateRecipeState(
+ state.entity.copyWith(
+ steps: steps,
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_state.dart b/lib/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_state.dart
new file mode 100644
index 00000000..19adabf8
--- /dev/null
+++ b/lib/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_state.dart
@@ -0,0 +1,11 @@
+import 'package:eatvio_app/features/recipes/entities/recipe_create_entity.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part 'create_recipe_state.freezed.dart';
+
+@freezed
+class CreateRecipeState with _$CreateRecipeState {
+ const factory CreateRecipeState(
+ RecipeCreateEntity entity,
+ ) = _CreateRecipeState;
+}
diff --git a/lib/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_state.freezed.dart b/lib/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_state.freezed.dart
new file mode 100644
index 00000000..b1661b76
--- /dev/null
+++ b/lib/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_state.freezed.dart
@@ -0,0 +1,160 @@
+// 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 'create_recipe_state.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');
+
+/// @nodoc
+mixin _$CreateRecipeState {
+ RecipeCreateEntity get entity => throw _privateConstructorUsedError;
+
+ /// Create a copy of CreateRecipeState
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ $CreateRecipeStateCopyWith get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $CreateRecipeStateCopyWith<$Res> {
+ factory $CreateRecipeStateCopyWith(
+ CreateRecipeState value, $Res Function(CreateRecipeState) then) =
+ _$CreateRecipeStateCopyWithImpl<$Res, CreateRecipeState>;
+ @useResult
+ $Res call({RecipeCreateEntity entity});
+
+ $RecipeCreateEntityCopyWith<$Res> get entity;
+}
+
+/// @nodoc
+class _$CreateRecipeStateCopyWithImpl<$Res, $Val extends CreateRecipeState>
+ implements $CreateRecipeStateCopyWith<$Res> {
+ _$CreateRecipeStateCopyWithImpl(this._value, this._then);
+
+ // ignore: unused_field
+ final $Val _value;
+ // ignore: unused_field
+ final $Res Function($Val) _then;
+
+ /// Create a copy of CreateRecipeState
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? entity = null,
+ }) {
+ return _then(_value.copyWith(
+ entity: null == entity
+ ? _value.entity
+ : entity // ignore: cast_nullable_to_non_nullable
+ as RecipeCreateEntity,
+ ) as $Val);
+ }
+
+ /// Create a copy of CreateRecipeState
+ /// with the given fields replaced by the non-null parameter values.
+ @override
+ @pragma('vm:prefer-inline')
+ $RecipeCreateEntityCopyWith<$Res> get entity {
+ return $RecipeCreateEntityCopyWith<$Res>(_value.entity, (value) {
+ return _then(_value.copyWith(entity: value) as $Val);
+ });
+ }
+}
+
+/// @nodoc
+abstract class _$$CreateRecipeStateImplCopyWith<$Res>
+ implements $CreateRecipeStateCopyWith<$Res> {
+ factory _$$CreateRecipeStateImplCopyWith(_$CreateRecipeStateImpl value,
+ $Res Function(_$CreateRecipeStateImpl) then) =
+ __$$CreateRecipeStateImplCopyWithImpl<$Res>;
+ @override
+ @useResult
+ $Res call({RecipeCreateEntity entity});
+
+ @override
+ $RecipeCreateEntityCopyWith<$Res> get entity;
+}
+
+/// @nodoc
+class __$$CreateRecipeStateImplCopyWithImpl<$Res>
+ extends _$CreateRecipeStateCopyWithImpl<$Res, _$CreateRecipeStateImpl>
+ implements _$$CreateRecipeStateImplCopyWith<$Res> {
+ __$$CreateRecipeStateImplCopyWithImpl(_$CreateRecipeStateImpl _value,
+ $Res Function(_$CreateRecipeStateImpl) _then)
+ : super(_value, _then);
+
+ /// Create a copy of CreateRecipeState
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? entity = null,
+ }) {
+ return _then(_$CreateRecipeStateImpl(
+ null == entity
+ ? _value.entity
+ : entity // ignore: cast_nullable_to_non_nullable
+ as RecipeCreateEntity,
+ ));
+ }
+}
+
+/// @nodoc
+
+class _$CreateRecipeStateImpl implements _CreateRecipeState {
+ const _$CreateRecipeStateImpl(this.entity);
+
+ @override
+ final RecipeCreateEntity entity;
+
+ @override
+ String toString() {
+ return 'CreateRecipeState(entity: $entity)';
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ (other.runtimeType == runtimeType &&
+ other is _$CreateRecipeStateImpl &&
+ (identical(other.entity, entity) || other.entity == entity));
+ }
+
+ @override
+ int get hashCode => Object.hash(runtimeType, entity);
+
+ /// Create a copy of CreateRecipeState
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ @pragma('vm:prefer-inline')
+ _$$CreateRecipeStateImplCopyWith<_$CreateRecipeStateImpl> get copyWith =>
+ __$$CreateRecipeStateImplCopyWithImpl<_$CreateRecipeStateImpl>(
+ this, _$identity);
+}
+
+abstract class _CreateRecipeState implements CreateRecipeState {
+ const factory _CreateRecipeState(final RecipeCreateEntity entity) =
+ _$CreateRecipeStateImpl;
+
+ @override
+ RecipeCreateEntity get entity;
+
+ /// Create a copy of CreateRecipeState
+ /// with the given fields replaced by the non-null parameter values.
+ @override
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ _$$CreateRecipeStateImplCopyWith<_$CreateRecipeStateImpl> get copyWith =>
+ throw _privateConstructorUsedError;
+}
diff --git a/lib/features/recipes/presentation/pages/recipe_create_screen.dart b/lib/features/recipes/presentation/pages/recipe_create_screen.dart
index bc95e6c3..efeace26 100644
--- a/lib/features/recipes/presentation/pages/recipe_create_screen.dart
+++ b/lib/features/recipes/presentation/pages/recipe_create_screen.dart
@@ -1,508 +1,71 @@
-import 'dart:io';
-import 'package:eatvio_app/core/theme/app_decoration.dart';
-import 'package:eatvio_app/core/widgets/custom_drop_down.dart';
-import 'package:eatvio_app/core/widgets/loader.dart';
-import 'package:eatvio_app/features/recipes/models/product.dart';
-import 'package:eatvio_app/features/recipes/models/ingredient.dart';
-import 'package:eatvio_app/features/recipes/models/recipe_model.dart';
-import 'package:eatvio_app/features/recipes/models/step.dart' as step;
-import 'package:eatvio_app/features/recipes/presentation/bloc/recipe_create_cubit.dart';
-import 'package:eatvio_app/features/recipes/presentation/bloc/recipe_create_state.dart';
import 'package:eatvio_app/core/locator_service.dart';
-import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
-import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
+import 'package:eatvio_app/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart';
+import 'package:eatvio_app/features/recipes/presentation/widgets/info_about_recipe_widget.dart';
+import 'package:eatvio_app/features/recipes/presentation/widgets/ingredients_widget.dart';
+import 'package:eatvio_app/features/recipes/presentation/widgets/steps_widget.dart';
import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
-import 'package:image_picker/image_picker.dart';
-class RecipeCreateScreen extends StatelessWidget {
- RecipeCreateScreen({Key? key}) : super(key: key);
+class RecipeCreateScreen extends StatefulWidget {
+ const RecipeCreateScreen({super.key});
+ @override
+ State createState() => _RecipeCreateScreenState();
+}
+
+class _RecipeCreateScreenState extends State {
+ @override
Widget build(BuildContext context) {
- return BlocProvider(
- create: (context) => sl()..loadRecipeCreate(),
- child: BlocBuilder(
- builder: (context, state) {
- if (state is RecipeCreateStateLoaded) {
- return _buildRecipeCreate(context, state);
- } else if (state is RecipeCreateStateLoading) {
- return Scaffold(
- body: const Loader(),
- );
- } else if (state is RecipeCreateStateError) {
- return Scaffold(
- body: Padding(
- padding: const EdgeInsets.all(16),
- child: Center(
- child: Text(state.errorMessage),
- ),
- ),
- );
- } else
- return Scaffold(
- body: Padding(
- padding: const EdgeInsets.all(16),
- child: Center(
- child: Text("Неопределенная ошибка"),
- ),
- ),
- );
- },
- ),
- );
- }
-
- Widget _buildRecipeCreate(BuildContext context, RecipeCreateStateLoaded state) {
- List _options = state.autocompleteList.map((Product item) => item.name).toList();
- return Scaffold(
- appBar: AppBar(
- actions: [
- InkWell(
- borderRadius: BorderRadius.circular(16),
- child: Ink(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- SizedBox(height: 4),
- Text(
- 'Новый рецепт',
- style: TextStyle(
- color: Colors.black,
- fontSize: 14,
- fontWeight: FontWeight.w400,
- height: 1,
- ),
- ),
- ],
- ),
+ return BlocProvider(
+ create: (BuildContext context) => sl(),
+ child: Scaffold(
+ backgroundColor: Color(0xFFF0F2F5),
+ appBar: AppBar(
+ title: Text('Добавление рецепта'),
+ centerTitle: true,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.vertical(
+ bottom: Radius.circular(12),
),
),
- ],
- ),
- backgroundColor: Color.fromARGB(255, 240, 242, 245),
- resizeToAvoidBottomInset: true,
- body: SizedBox(
- width: double.maxFinite,
- child: SingleChildScrollView(
+ ),
+ body: SingleChildScrollView(
+ physics: const BouncingScrollPhysics(),
+ padding: EdgeInsets.only(top: 8, bottom: 32),
child: Column(
+ spacing: 16,
children: [
- Container(
- padding: EdgeInsets.symmetric(
- horizontal: 24,
- vertical: 12,
- ),
+ InfoAboutRecipeWidget(),
+ IngredientsWidget(),
+ StepsWidget(),
+ InkWell(
+ onTap: () {},
+ borderRadius: BorderRadius.circular(20),
+ child: Ink(
+ width: MediaQuery.of(context).size.width - 32,
decoration: BoxDecoration(
- color: Colors.white,
- borderRadius: const BorderRadius.all(Radius.circular(8)),
+ color: appTheme.blueA400,
+ borderRadius: BorderRadius.circular(20),
),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text('Заголовок'),
- SizedBox(height: 8),
- CustomTextFormField(
- fillColor: appTheme.gray50,
- controller: state.titleController,
- hintText: 'Заголовок',
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ alignment: Alignment.center,
+ child: Text(
+ 'Опубликовать',
+ style: TextStyle(
+ fontSize: 16,
+ color: Colors.white,
+ fontWeight: FontWeight.w600,
),
- SizedBox(height: 16),
- Text('Тип блюда'),
- SizedBox(height: 8),
- CustomDropDown(
- value: state.selectedType,
- items: state.types,
- onChanged: changeType(context),
- ),
- SizedBox(height: 16),
- Text('Изображение'),
- GestureDetector(
- onTap: () async {
- final pickedFile = await state.picker.pickImage(source: ImageSource.gallery);
- context.read().uploadRecipeImage(pickedFile);
- },
- child: state.recipeImage == null
- ? Container(
- height: 128,
- decoration: AppDecoration.fillGray.copyWith(
- borderRadius: BorderRadius.circular(16),
- ),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.center,
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- Icon(
- Icons.photo_library,
- color: appTheme.blueGray300,
- ),
- SizedBox(width: 8),
- Text(
- "Добавить фото",
- selectionColor: appTheme.blueA400,
- ),
- ],
- ),
- )
- : Image.file(File(state.recipeImage!.path), height: 200, fit: BoxFit.cover),
- ),
- SizedBox(height: 16),
- Text('Время приготовления'),
- SizedBox(height: 8),
- CustomDropDown(
- items: state.cookingTimes,
- value: state.selectedCookingTime,
- onChanged: changeCookingTime(context),
- ),
- SizedBox(height: 16),
- Text('Статус'),
- SizedBox(height: 8),
- CustomDropDown(
- items: state.statuses,
- value: state.selectedStatus,
- onChanged: changeStatus(context),
- ),
- SizedBox(height: 24),
- if (!state.ingredients.isEmpty)
- for (int j = 0; j < state.ingredients.length; j++) _buildIngredient(state.ingredients[j]),
- SizedBox(height: 24),
- RawAutocomplete(
- optionsBuilder: _buildOptions(_options),
- fieldViewBuilder: (
- BuildContext context,
- TextEditingController productEditingController,
- FocusNode focusNodeName,
- VoidCallback onFieldSubmitted,
- ) {
- return CustomTextFormField(
- hintText: 'Введите название продукта',
- controller: TextEditingController(text: state.productNameController.text),
- focusNode: focusNodeName,
- onChanged: getAutocomplete(context, focusNodeName),
- );
- },
- optionsViewBuilder: (
- BuildContext context,
- AutocompleteOnSelected onSelected,
- Iterable options,
- ) {
- return Align(
- alignment: Alignment.topLeft,
- child: Material(
- elevation: 4.0,
- child: SizedBox(
- height: 200.0,
- child: ListView.builder(
- padding: const EdgeInsets.all(8.0),
- itemCount: options.length,
- itemBuilder: (BuildContext context, int index) {
- final String option = options.elementAt(index);
- return GestureDetector(
- onTap: selectProduct(onSelected, option, state),
- child: ListTile(
- title: Text(option),
- ),
- );
- },
- ),
- ),
- ),
- );
- },
- ),
- SizedBox(height: 24),
- CustomTextFormField(
- hintText: 'Порция',
- fillColor: appTheme.gray50,
- controller: state.ingredientController,
- keyboardType: TextInputType.number,
- inputFormatters: [FilteringTextInputFormatter.digitsOnly],
- ),
- SizedBox(height: 8),
- Row(children: [
- Text('гр'),
- Switch(
- value: state.ingredientAsItems,
- onChanged: (bool value) => context.read().switchUnits(value),
- activeColor: appTheme.blueA400,
- inactiveThumbColor: Colors.grey,
- inactiveTrackColor: Colors.grey.shade300,
- ),
- Text('шт'),
- ],),
- SizedBox(height: 8),
- Container(
- height: 48,
- width: MediaQuery.of(context).size.width,
- child: ElevatedButton(
- style: ElevatedButton.styleFrom(
- elevation: 0,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(8),
- ),
- backgroundColor: appTheme.blueA400,
- ),
- onPressed: () => context.read().addIngredient(
- Ingredient(
- name: state.productNameController.text,
- portion: state.ingredientController.text,
- unit: state.ingredientAsItems ? 'pcs' : 'gr',
- ),
- ),
- child: Text(
- '+',
- style: theme.textTheme.titleSmall,
- ),
- ),
- ),
- SizedBox(height: 24),
- Text('Описание рецепта'),
- SizedBox(height: 8),
- CustomTextFormField(
- maxLines: 8,
- minLines: 8,
- controller: state.stepDescriptionController,
- ),
- SizedBox(height: 24),
- for (int i = 0; i < state.numberOfSteps; i++) _buildStep(context, i, state),
- SizedBox(height: 24),
- Container(
- height: 48,
- width: MediaQuery.of(context).size.width,
- child: ElevatedButton(
- style: ElevatedButton.styleFrom(
- elevation: 0,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(8),
- ),
- backgroundColor: appTheme.blueA400,
- ),
- onPressed: () => context.read().addStep(),
- child: Text(
- '+',
- style: theme.textTheme.titleSmall,
- ),
- ),
- ),
- SizedBox(height: 8),
- Container(
- height: 48,
- width: MediaQuery.of(context).size.width,
- child: ElevatedButton(
- style: ElevatedButton.styleFrom(
- elevation: 0,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(8),
- ),
- backgroundColor: Colors.grey,
- ),
- onPressed: () => context.read().removeStep(),
- child: Text(
- '-',
- style: theme.textTheme.titleSmall,
- ),
- ),
- ),
- SizedBox(height: 24),
- Text('Ссылка на видео (video)'),
- SizedBox(height: 8),
- CustomTextFormField(
- hintText: 'Введите описание шага',
- fillColor: appTheme.gray50,
- controller: state.videoLinkController,
- ),
- SizedBox(height: 24),
- Container(
- height: 48,
- width: MediaQuery.of(context).size.width,
- child: ElevatedButton(
- style: ElevatedButton.styleFrom(
- elevation: 0,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(8),
- ),
- backgroundColor: appTheme.blueA400,
- ),
- child: Text(
- 'Опубликовать',
- style: theme.textTheme.titleSmall,
- ),
- onPressed: () => context.read().publishRecipe(createRecipeFromState(state)),
- ),
- ),
- ],
- ),),
+ ),
+ ),
+ ),
+ ),
],
),
),
),
);
}
-
- _buildStep(BuildContext context, int stepNumber, RecipeCreateStateLoaded state) {
- return Column(
- children: [
- Row(children: [Text('Шаг ${stepNumber + 1}'), Spacer(), Text('Всего шагов: ${state.numberOfSteps}')]),
- SizedBox(height: 8),
- GestureDetector(
- onTap: () async {
- final pickedFile = await state.picker.pickImage(source: ImageSource.gallery);
- context.read().uploadStepImage(pickedFile, stepNumber);
- },
- child: state.stepImages[stepNumber] == null
- ? Container(
- height: 128,
- decoration: AppDecoration.fillGray.copyWith(
- borderRadius: BorderRadius.circular(16),
- ),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.center,
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- Icon(
- Icons.photo_library,
- color: appTheme.blueGray300,
- ),
- SizedBox(width: 8),
- Text(
- "Добавить фото",
- selectionColor: appTheme.blueA400,
- ),
- SizedBox(height: 8),
- ],
- ),
- )
- : Image.file(File(state.stepImages[stepNumber]!.path), height: 150, fit: BoxFit.cover),),
- SizedBox(height: 16),
- CustomTextFormField(
- hintText: 'Введите описание шага',
- maxLines: 8,
- minLines: 8,
- fillColor: appTheme.gray50,
- controller: state.stepControllers[stepNumber],
- ),
- SizedBox(height: 24),
- ],
- );
- }
-
- _buildIngredient(Ingredient ingredient) {
- return Container(
- margin: EdgeInsets.symmetric(horizontal: 12),
- padding: EdgeInsets.symmetric(
- horizontal: 8,
- vertical: 8,
- ),
- decoration: AppDecoration.fillGray.copyWith(
- borderRadius: BorderRadiusStyle.roundedBorder5,
- ),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.end,
- mainAxisSize: MainAxisSize.min,
- children: [
- Padding(
- padding: EdgeInsets.only(
- left: 8,
- top: 8,
- ),
- child: Text(
- ingredient.name.length > 8 ? ingredient.name.substring(0, 8) : ingredient.name,
- style: CustomTextStyles.titleMedium16,
- ),
- ),
- Padding(
- padding: EdgeInsets.only(
- left: 24,
- top: 4,
- bottom: 4,
- ),
- child: Text(
- ingredient.portion.toString() + ' ' + ingredient.unit.toString(),
- ),
- ),
- Spacer(),
- GestureDetector(
- // onTap: removeIngredient(context, ingredient),
- child: Padding(
- padding: const EdgeInsets.only(right: 10),
- child: Icon(
- Icons.close,
- color: appTheme.blueGray300,
- ),
- ),
- ),
- ],
- ),
- );
- }
-
- _buildOptions(List _options) {
- return (TextEditingValue textEditingValue) {
- return _options.where((String option) {
- return true;
- });
- };
- }
-
- selectProduct(AutocompleteOnSelected onSelected, String option, RecipeCreateStateLoaded state) {
- state.productNameController.text = option;
- return () {
- onSelected(option);
- };
- }
-
- getAutocomplete(BuildContext context, FocusNode focusNode) {
- return (value) {
- if (value.length > 2) {
- context.read().inputProduct(value);
- }
- };
- }
-
- changeType(BuildContext context) {
- return (value) {
- context.read().changeType(value);
- };
- }
-
- changeStatus(BuildContext context) {
- return (value) {
- context.read().changeStatus(value);
- };
- }
-
- changeCookingTime(BuildContext context) {
- return (value) {
- context.read().changeCookingTime(value);
- };
- }
-
- RecipeModel createRecipeFromState(RecipeCreateStateLoaded state) {
- List ingredients = state.ingredients;
-
- List steps = [];
- for (int i = 0; i < state.stepControllers.length; i++) {
- if (state.stepControllers[i].text.isNotEmpty) {
- steps.add(step.Step(
- description: state.stepControllers[i].text,
- image: state.stepImages[i]?.path,
- ),);
- }
- }
-
- final RecipeModel recipe = RecipeModel(
- ingredients: ingredients,
- steps: steps,
- state: state.selectedStatus.value,
- type: state.selectedType.value,
- cooking_time: state.selectedCookingTime.value,
- description: state.stepDescriptionController.text,
- title: state.titleController.text,
- image: state.recipeImage?.path,
- video_url: state.videoLinkController.text.isNotEmpty ? state.videoLinkController.text : null,
- );
-
- return recipe;
- }
}
diff --git a/lib/features/recipes/presentation/widgets/image_widget.dart b/lib/features/recipes/presentation/widgets/image_widget.dart
new file mode 100644
index 00000000..2521ff69
--- /dev/null
+++ b/lib/features/recipes/presentation/widgets/image_widget.dart
@@ -0,0 +1,121 @@
+import 'dart:io';
+
+import 'package:eatvio_app/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_cubit.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 ImageWidget extends StatefulWidget {
+ const ImageWidget({super.key});
+
+ @override
+ State createState() => _ImageWidgetState();
+}
+
+class _ImageWidgetState extends State {
+ XFile? image;
+ late final CreateRecipeCubit createRecipeCubit;
+
+ @override
+ void initState() {
+ super.initState();
+ createRecipeCubit = context.read();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ onTap: () {
+ _showImagePicker();
+ },
+ child: image == null
+ ? SvgPicture.asset(
+ width: MediaQuery.of(context).size.width - 32,
+ 'assets/icons/upload_photo_of_portion.svg',
+ )
+ : Stack(
+ children: [
+ ClipRRect(
+ borderRadius: BorderRadius.circular(20),
+ child: Image.file(
+ File(image!.path),
+ ),
+ ),
+ Positioned(
+ right: 16,
+ top: 16,
+ child: GestureDetector(
+ onTap: () {
+ setState(() {
+ image = null;
+ });
+ createRecipeCubit.replaceImage(null);
+ },
+ child: Container(
+ padding: EdgeInsets.all(4),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ shape: BoxShape.circle,
+ ),
+ child: Icon(Icons.close_rounded),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ _showImagePicker() {
+ showCupertinoModalPopup(
+ context: context,
+ builder: (context) => CupertinoActionSheet(
+ actions: [
+ CupertinoActionSheetAction(
+ onPressed: () async {
+ final picker = ImagePicker();
+ image = await picker.pickImage(source: ImageSource.camera);
+ setState(() {});
+ if (image != null) {
+ createRecipeCubit.replaceImage(File(image!.path));
+ }
+ Navigator.of(context).pop();
+ },
+ child: const Row(
+ children: const [
+ Icon(Icons.photo_camera),
+ SizedBox(width: 8),
+ Text('Фото'),
+ ],
+ ),
+ ),
+ CupertinoActionSheetAction(
+ onPressed: () async {
+ final picker = ImagePicker();
+ image = await picker.pickImage(source: ImageSource.gallery);
+ setState(() {});
+ if (image != null) {
+ createRecipeCubit.replaceImage(File(image!.path));
+ }
+ Navigator.of(context).pop();
+ },
+ child: const Row(
+ children: const [
+ Icon(Icons.photo),
+ SizedBox(width: 8),
+ Text('Галерея'),
+ ],
+ ),
+ ),
+ ],
+ cancelButton: CupertinoActionSheetAction(
+ onPressed: () => Navigator.of(context).pop(),
+ isDestructiveAction: true,
+ child: const Text('Отмена'),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/features/recipes/presentation/widgets/info_about_recipe_widget.dart b/lib/features/recipes/presentation/widgets/info_about_recipe_widget.dart
index e69de29b..473cc04a 100644
--- a/lib/features/recipes/presentation/widgets/info_about_recipe_widget.dart
+++ b/lib/features/recipes/presentation/widgets/info_about_recipe_widget.dart
@@ -0,0 +1,113 @@
+import 'package:eatvio_app/core/data/selectionPopupModel/selection_popup_model.dart';
+import 'package:eatvio_app/core/widgets/custom_drop_down.dart';
+import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
+import 'package:eatvio_app/features/recipes/entities/recipe_create_entity.dart';
+import 'package:eatvio_app/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart';
+import 'package:eatvio_app/features/recipes/presentation/widgets/image_widget.dart';
+import 'package:eatvio_app/features/recipes/presentation/widgets/portion_widget.dart';
+import 'package:eatvio_app/features/recipes/presentation/widgets/tags_widget.dart';
+import 'package:eatvio_app/features/recipes/presentation/widgets/time_of_cook_widget.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class InfoAboutRecipeWidget extends StatefulWidget {
+ const InfoAboutRecipeWidget({super.key});
+
+ @override
+ State createState() => _InfoAboutRecipeWidgetState();
+}
+
+class _InfoAboutRecipeWidgetState extends State {
+ final List items = const [
+ SelectionPopupModel(title: 'Первые Блюда'),
+ SelectionPopupModel(title: 'Вторые Блюда'),
+ SelectionPopupModel(title: 'Салаты'),
+ SelectionPopupModel(title: 'Сладости'),
+ SelectionPopupModel(title: 'Напитки'),
+ SelectionPopupModel(title: 'Прочее'),
+ ];
+ late SelectionPopupModel selectedType;
+ late final CreateRecipeCubit createRecipeCubit;
+
+ @override
+ void initState() {
+ super.initState();
+ selectedType = items.first;
+ createRecipeCubit = context.read();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Material(
+ borderRadius: BorderRadius.circular(12),
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('Название'),
+ SizedBox(height: 4),
+ CustomTextFormField(
+ hintText: 'Например: тар-тар из говядины',
+ onChanged: (value) {
+ createRecipeCubit.replaceTitle(value);
+ },
+ ),
+ const SizedBox(height: 12),
+ Text('Тип блюда'),
+ SizedBox(height: 4),
+ CustomDropDown(
+ items: items,
+ value: selectedType,
+ onChanged: (newSelectedType) {
+ setState(() {
+ selectedType = newSelectedType;
+ });
+ createRecipeCubit.changeType(
+ switch (selectedType.title) {
+ 'Первые Блюда' => TypeOfRecipe.first_meals,
+ 'Вторые Блюда' => TypeOfRecipe.second_meals,
+ 'Салаты' => TypeOfRecipe.salads,
+ 'Сладости' => TypeOfRecipe.sweets,
+ 'Напитки' => TypeOfRecipe.drinks,
+ _ => TypeOfRecipe.other,
+ },
+ );
+ },
+ ),
+ const SizedBox(height: 12),
+ TagsWidget(),
+ const SizedBox(height: 12),
+ PortionWidget(),
+ const SizedBox(height: 12),
+ TimeOfCookWidget(),
+ const SizedBox(height: 12),
+ Text('Описание'),
+ SizedBox(height: 4),
+ CustomTextFormField(
+ hintText: 'Расскажите, почему вы выбрали именно этот рецепт? Опишите, каким будет готовое блюдо и в чём его достоинства.',
+ minLines: 5,
+ maxLines: 5,
+ onChanged: (value) {
+ createRecipeCubit.replaceDescription(value);
+ },
+ ),
+ const SizedBox(height: 12),
+ Text('Фото обложка'),
+ SizedBox(height: 4),
+ ImageWidget(),
+ const SizedBox(height: 12),
+ Text('Ссылка на видео (если есть)'),
+ SizedBox(height: 4),
+ CustomTextFormField(
+ hintText: 'Прикрепите ссылку на видео',
+ onChanged: (value) {
+ createRecipeCubit.replaceVideoUrl(value);
+ },
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/features/recipes/presentation/widgets/ingredients_widget.dart b/lib/features/recipes/presentation/widgets/ingredients_widget.dart
new file mode 100644
index 00000000..c8744492
--- /dev/null
+++ b/lib/features/recipes/presentation/widgets/ingredients_widget.dart
@@ -0,0 +1,219 @@
+import 'package:eatvio_app/core/theme/theme_helper.dart';
+import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
+import 'package:eatvio_app/features/recipes/entities/recipe_create_entity.dart';
+import 'package:eatvio_app/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart';
+import 'package:eatvio_app/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_state.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class IngredientsWidget extends StatefulWidget {
+ const IngredientsWidget({super.key});
+
+ @override
+ State createState() => _IngredientsWidgetState();
+}
+
+class _IngredientsWidgetState extends State {
+ late final CreateRecipeCubit createRecipeCubit;
+
+ @override
+ void initState() {
+ super.initState();
+ createRecipeCubit = context.read();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return BlocBuilder(
+ builder: (BuildContext context, CreateRecipeState state) {
+ return Material(
+ borderRadius: BorderRadius.circular(12),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 16),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ child: Text(
+ 'Ингредиенты ',
+ style: TextStyle(
+ fontSize: 20,
+ fontWeight: FontWeight.w600,
+ color: Color(0xFF323F4B),
+ ),
+ ),
+ ),
+ ReorderableListView(
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ onReorder: (int oldIndex, int newIndex) {
+ setState(() {
+ if (oldIndex < newIndex) {
+ newIndex -= 1;
+ }
+ });
+ createRecipeCubit.replaceIngredientPosition(oldIndex, newIndex);
+ },
+ children: [
+ for (int index = 0; index < state.entity.ingredients.length; index += 1)
+ ListTile(
+ contentPadding: EdgeInsets.symmetric(horizontal: 16),
+ key: Key('$index'),
+ leading: Icon(
+ Icons.drag_handle_rounded,
+ color: Colors.grey.shade600,
+ ),
+ title: Text(state.entity.ingredients[index].name),
+ subtitle: Text('${state.entity.ingredients[index].unit} ${state.entity.ingredients[index].portion == 'gr' ? 'гр' : 'шт'}'),
+ trailing: IconButton(
+ onPressed: () {
+ createRecipeCubit.removeIngredient(index);
+ },
+ icon: Icon(
+ Icons.delete_outline_rounded,
+ color: Colors.grey.shade600,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ListTile(
+ onTap: () => showModalBottomSheet(
+ useRootNavigator: true,
+ context: context,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
+ ),
+ isScrollControlled: true,
+ builder: (context) {
+ final nameController = TextEditingController();
+ final grammsContorller = TextEditingController();
+ final pcsController = TextEditingController();
+
+ return StatefulBuilder(
+ builder: (BuildContext context, void Function(void Function()) setStateInner) {
+ 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),
+ SizedBox(
+ width: MediaQuery.of(context).size.width - 32,
+ child: CustomTextFormField(
+ autofocus: true,
+ controller: nameController,
+ hintText: 'Введите название продукта',
+ onChanged: (query) {},
+ ),
+ ),
+ const SizedBox(height: 8),
+ Row(
+ children: [
+ SizedBox(
+ width: MediaQuery.of(context).size.width / 2 - 20,
+ child: CustomTextFormField(
+ enabled: pcsController.text.isEmpty ? true : false,
+ keyboardType: TextInputType.number,
+ controller: grammsContorller,
+ hintText: pcsController.text.isEmpty ? '100' : null,
+ suffixText: 'гр',
+ onChanged: (value) {
+ setStateInner(() {});
+ },
+ ),
+ ),
+ const SizedBox(width: 8),
+ SizedBox(
+ width: MediaQuery.of(context).size.width / 2 - 20,
+ child: CustomTextFormField(
+ enabled: grammsContorller.text.isEmpty ? true : false,
+ keyboardType: TextInputType.number,
+ controller: pcsController,
+ onChanged: (value) {
+ setStateInner(() {});
+ },
+ hintText: grammsContorller.text.isEmpty ? '1' : null,
+ suffixText: 'шт',
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ InkWell(
+ onTap: () {
+ if (nameController.text.isNotEmpty && (pcsController.text.isNotEmpty || grammsContorller.text.isNotEmpty)) {
+ createRecipeCubit.addIngredient(
+ IngredientEntity(
+ name: nameController.text,
+ unit: grammsContorller.text.isEmpty ? pcsController.text : grammsContorller.text,
+ portion: grammsContorller.text.isEmpty ? 'pcs' : 'gr',
+ ),
+ );
+ Navigator.of(context).pop();
+ setState(() {});
+ }
+ },
+ borderRadius: BorderRadius.circular(20),
+ child: Ink(
+ width: MediaQuery.of(context).size.width - 32,
+ 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,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ },
+ );
+ },
+ ),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ title: Text(
+ 'Добавить ингредиент',
+ style: TextStyle(
+ color: appTheme.blueA400,
+ ),
+ ),
+ leading: Icon(
+ Icons.add_rounded,
+ color: appTheme.blueA400,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ },
+ );
+ }
+}
diff --git a/lib/features/recipes/presentation/widgets/portion_widget.dart b/lib/features/recipes/presentation/widgets/portion_widget.dart
new file mode 100644
index 00000000..875183ff
--- /dev/null
+++ b/lib/features/recipes/presentation/widgets/portion_widget.dart
@@ -0,0 +1,69 @@
+import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
+import 'package:eatvio_app/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class PortionWidget extends StatefulWidget {
+ const PortionWidget({super.key});
+
+ @override
+ State createState() => _PortionWidgetState();
+}
+
+class _PortionWidgetState extends State {
+ late final CreateRecipeCubit createRecipeCubit;
+
+ @override
+ void initState() {
+ super.initState();
+ createRecipeCubit = context.read();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Expanded(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('Кол-во порций'),
+ SizedBox(height: 4),
+ CustomTextFormField(
+ hintText: '1',
+ onChanged: (value) {
+ createRecipeCubit.replacePortionsCount(int.parse(value));
+ },
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('Ккал в одной порции'),
+ SizedBox(height: 4),
+ CustomTextFormField(
+ hintText: '100',
+ suffixText: 'Ккал',
+ onChanged: (value) {
+ createRecipeCubit.replaceEnegryInPortion(double.parse(value));
+ },
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/features/recipes/presentation/widgets/step_image_widget.dart b/lib/features/recipes/presentation/widgets/step_image_widget.dart
new file mode 100644
index 00000000..ad4c9549
--- /dev/null
+++ b/lib/features/recipes/presentation/widgets/step_image_widget.dart
@@ -0,0 +1,126 @@
+import 'dart:io';
+
+import 'package:eatvio_app/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_cubit.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 StepImageWidget extends StatefulWidget {
+ final int index;
+
+ const StepImageWidget({
+ super.key,
+ required this.index,
+ });
+
+ @override
+ State createState() => _StepImageWidgetState();
+}
+
+class _StepImageWidgetState extends State {
+ XFile? image;
+ late final CreateRecipeCubit createRecipeCubit;
+
+ @override
+ void initState() {
+ super.initState();
+ createRecipeCubit = context.read();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ onTap: () {
+ _showImagePicker();
+ },
+ child: image == null
+ ? SvgPicture.asset(
+ width: MediaQuery.of(context).size.width - 32,
+ 'assets/icons/add_step_photo.svg',
+ )
+ : Stack(
+ children: [
+ ClipRRect(
+ borderRadius: BorderRadius.circular(20),
+ child: Image.file(
+ File(image!.path),
+ ),
+ ),
+ Positioned(
+ right: 16,
+ top: 16,
+ child: GestureDetector(
+ onTap: () {
+ setState(() {
+ image = null;
+ });
+ createRecipeCubit.replaceImageInStep(null, widget.index);
+ },
+ child: Container(
+ padding: EdgeInsets.all(4),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ shape: BoxShape.circle,
+ ),
+ child: Icon(Icons.close_rounded),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ _showImagePicker() {
+ showCupertinoModalPopup(
+ context: context,
+ builder: (context) => CupertinoActionSheet(
+ actions: [
+ CupertinoActionSheetAction(
+ onPressed: () async {
+ final picker = ImagePicker();
+ image = await picker.pickImage(source: ImageSource.camera);
+ setState(() {});
+ if (image != null) {
+ createRecipeCubit.replaceImageInStep(File(image!.path), widget.index);
+ }
+ Navigator.of(context).pop();
+ },
+ child: const Row(
+ children: const [
+ Icon(Icons.photo_camera),
+ SizedBox(width: 8),
+ Text('Фото'),
+ ],
+ ),
+ ),
+ CupertinoActionSheetAction(
+ onPressed: () async {
+ final picker = ImagePicker();
+ image = await picker.pickImage(source: ImageSource.gallery);
+ setState(() {});
+ if (image != null) {
+ createRecipeCubit.replaceImageInStep(File(image!.path), widget.index);
+ }
+ Navigator.of(context).pop();
+ },
+ child: const Row(
+ children: const [
+ Icon(Icons.photo),
+ SizedBox(width: 8),
+ Text('Галерея'),
+ ],
+ ),
+ ),
+ ],
+ cancelButton: CupertinoActionSheetAction(
+ onPressed: () => Navigator.of(context).pop(),
+ isDestructiveAction: true,
+ child: const Text('Отмена'),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/features/recipes/presentation/widgets/steps_widget.dart b/lib/features/recipes/presentation/widgets/steps_widget.dart
new file mode 100644
index 00000000..6feb9aa6
--- /dev/null
+++ b/lib/features/recipes/presentation/widgets/steps_widget.dart
@@ -0,0 +1,135 @@
+import 'package:eatvio_app/core/theme/theme_helper.dart';
+import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
+import 'package:eatvio_app/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart';
+import 'package:eatvio_app/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_state.dart';
+import 'package:eatvio_app/features/recipes/presentation/widgets/step_image_widget.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class StepsWidget extends StatefulWidget {
+ const StepsWidget({super.key});
+
+ @override
+ State createState() => _StepsWidgetState();
+}
+
+class _StepsWidgetState extends State {
+ late final CreateRecipeCubit createRecipeCubit;
+
+ @override
+ void initState() {
+ super.initState();
+ createRecipeCubit = context.read();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return BlocBuilder(
+ builder: (BuildContext context, CreateRecipeState state) {
+ return Material(
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'Пошаговая инструкция',
+ style: TextStyle(
+ fontSize: 20,
+ fontWeight: FontWeight.w600,
+ color: Color(0xFF323F4B),
+ ),
+ ),
+ const SizedBox(height: 12),
+ ListView.separated(
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ itemBuilder: (context, index) => Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Container(
+ padding: EdgeInsets.symmetric(
+ horizontal: 12,
+ vertical: 4,
+ ),
+ decoration: BoxDecoration(
+ color: appTheme.blueA400,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Text(
+ 'Шаг ${index + 1}',
+ style: TextStyle(
+ color: Colors.white,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ GestureDetector(
+ onTap: () {
+ context.read().removeStep(state.entity.steps[index]);
+ },
+ child: Icon(Icons.delete_outline_rounded),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ Text('Описание'),
+ SizedBox(height: 4),
+ CustomTextFormField(
+ hintText: 'Например: подготовьте ингредиенты',
+ maxLines: 5,
+ minLines: 3,
+ onChanged: (value) {
+ createRecipeCubit.replaceDescriptionInStep(value, index);
+ },
+ ),
+ const SizedBox(height: 12),
+ Text('Фото шага'),
+ SizedBox(height: 4),
+ StepImageWidget(
+ index: index,
+ ),
+ ],
+ ),
+ itemCount: state.entity.steps.length,
+ separatorBuilder: (BuildContext context, int index) => const SizedBox(height: 24),
+ ),
+ InkWell(
+ onTap: () {
+ context.read().addStep();
+ },
+ borderRadius: BorderRadius.circular(20),
+ child: Ink(
+ width: MediaQuery.of(context).size.width - 32,
+ decoration: BoxDecoration(
+ color: appTheme.blueA400,
+ borderRadius: BorderRadius.circular(20),
+ ),
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ alignment: Alignment.center,
+ child: Text(
+ 'Добавить шаг',
+ style: TextStyle(
+ fontSize: 16,
+ color: Colors.white,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ },
+ );
+ }
+}
diff --git a/lib/features/recipes/presentation/widgets/tags_widget.dart b/lib/features/recipes/presentation/widgets/tags_widget.dart
index e69de29b..53ef22bf 100644
--- a/lib/features/recipes/presentation/widgets/tags_widget.dart
+++ b/lib/features/recipes/presentation/widgets/tags_widget.dart
@@ -0,0 +1,82 @@
+import 'package:eatvio_app/core/theme/theme_helper.dart';
+import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
+import 'package:flutter/material.dart';
+
+class TagsWidget extends StatefulWidget {
+ const TagsWidget({super.key});
+
+ @override
+ State createState() => _TagsWidgetState();
+}
+
+class _TagsWidgetState extends State {
+ final tagController = TextEditingController();
+ final List tags = [];
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('Теги'),
+ SizedBox(height: 4),
+ CustomTextFormField(
+ hintText: 'Например: без майонеза',
+ controller: tagController,
+ suffix: IconButton(
+ onPressed: () {
+ if (tagController.text.isNotEmpty) {
+ setState(() {
+ tags.add(tagController.text);
+ tagController.clear();
+ });
+ }
+ },
+ icon: Icon(
+ Icons.done,
+ color: appTheme.blueA400,
+ ),
+ ),
+ ),
+ const SizedBox(height: 12),
+ Wrap(
+ spacing: 8,
+ runSpacing: 8,
+ children: [
+ for (int i = 0; i < tags.length; i++) ...[
+ InkWell(
+ onTap: () {
+ setState(() {
+ tags.remove(tags[i]);
+ });
+ },
+ borderRadius: BorderRadius.circular(16),
+ child: Ink(
+ padding: EdgeInsets.symmetric(vertical: 4, horizontal: 12),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(16),
+ color: Colors.grey.shade200,
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ tags[i],
+ ),
+ const SizedBox(width: 4),
+ Icon(
+ Icons.delete_outline_rounded,
+ color: Colors.grey.shade600,
+ size: 20,
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ],
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/features/recipes/presentation/widgets/time_of_cook_widget.dart b/lib/features/recipes/presentation/widgets/time_of_cook_widget.dart
new file mode 100644
index 00000000..f329f791
--- /dev/null
+++ b/lib/features/recipes/presentation/widgets/time_of_cook_widget.dart
@@ -0,0 +1,151 @@
+import 'package:eatvio_app/core/theme/theme_helper.dart';
+import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
+import 'package:eatvio_app/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class TimeOfCookWidget extends StatefulWidget {
+ const TimeOfCookWidget({super.key});
+
+ @override
+ State createState() => _TimeOfCookWidgetState();
+}
+
+class _TimeOfCookWidgetState extends State {
+ late final CreateRecipeCubit createRecipeCubit;
+
+ @override
+ void initState() {
+ super.initState();
+ createRecipeCubit = context.read();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Row(
+ children: [
+ Text('Общее время приготовления'),
+ const SizedBox(width: 8),
+ GestureDetector(
+ onTap: () {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ content: Text(
+ 'Общее время приготовления - это период времени от начала приготовления блюда до подачи его на стол.',
+ style: TextStyle(
+ fontWeight: FontWeight.w500,
+ color: Color(0xFF323F4B),
+ height: 1.2,
+ ),
+ ),
+ ),
+ );
+ },
+ child: Icon(
+ Icons.help_outline_rounded,
+ color: appTheme.blueA400,
+ size: 20,
+ ),
+ ),
+ ],
+ ),
+ SizedBox(height: 4),
+ Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Expanded(
+ child: CustomTextFormField(
+ hintText: 'Часов',
+ onChanged: (value) {
+ if (createRecipeCubit.state.entity.totalCookTime == null) {
+ createRecipeCubit.replaceTotalTime('${value.padLeft(2, '0')}:00');
+ } else {
+ createRecipeCubit.replaceTotalTime('${value.padLeft(2, '0')}:${createRecipeCubit.state.entity.totalCookTime!.substring(3, 5)}');
+ }
+ },
+ ),
+ ),
+ const SizedBox(width: 8),
+ Expanded(
+ child: CustomTextFormField(
+ hintText: 'Минут',
+ onChanged: (value) {
+ if (createRecipeCubit.state.entity.totalCookTime == null) {
+ createRecipeCubit.replaceTotalTime('00:${value.padLeft(2, '0')}');
+ } else {
+ createRecipeCubit.replaceTotalTime('${createRecipeCubit.state.entity.totalCookTime!.substring(0, 2)}:${value.padLeft(2, '0')}');
+ }
+ },
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ Row(
+ children: [
+ Text('Активное время приготовления'),
+ const SizedBox(width: 8),
+ GestureDetector(
+ onTap: () {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ content: Text(
+ 'Активное время приготовления - это период времени, требующий активного участия в приготовлении блюда. Оно включает мытье и очистку продуктов, нарезку, шинковку, взбивание, перемешивание, размешивание, перекладывание и т.п.',
+ style: TextStyle(
+ fontWeight: FontWeight.w500,
+ color: Color(0xFF323F4B),
+ height: 1.2,
+ ),
+ ),
+ ),
+ );
+ },
+ child: Icon(
+ Icons.help_outline_rounded,
+ color: appTheme.blueA400,
+ size: 20,
+ ),
+ ),
+ ],
+ ),
+ SizedBox(height: 4),
+ Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Expanded(
+ child: CustomTextFormField(
+ hintText: 'Часов',
+ onChanged: (value) {
+ if (createRecipeCubit.state.entity.activeCookTime == null) {
+ createRecipeCubit.replaceActiveTime('${value.padLeft(2, '0')}:00');
+ } else {
+ createRecipeCubit.replaceActiveTime('${value.padLeft(2, '0')}:${createRecipeCubit.state.entity.activeCookTime!.substring(3, 5)}');
+ }
+ },
+ ),
+ ),
+ const SizedBox(width: 8),
+ Expanded(
+ child: CustomTextFormField(
+ hintText: 'Минут',
+ onChanged: (value) {
+ if (createRecipeCubit.state.entity.activeCookTime == null) {
+ createRecipeCubit.replaceActiveTime('00:${value.padLeft(2, '0')}');
+ } else {
+ createRecipeCubit.replaceActiveTime('${createRecipeCubit.state.entity.activeCookTime!.substring(0, 2)}:${value.padLeft(2, '0')}');
+ }
+ },
+ ),
+ ),
+ ],
+ ),
+ ],
+ );
+ }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 341123b1..9a4a500b 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -29,7 +29,7 @@ dependencies:
go_router: ^14.6.2
keyboard_dismisser: ^3.0.0
retrofit: ^4.4.1
- get_it: ^8.0.2
+ get_it: ^8.0.3
dio: ^5.7.0
dart_pusher_channels: ^1.2.3
photo_view: ^0.15.0
@@ -39,11 +39,11 @@ dependencies:
badges: ^3.1.2
google_fonts: ^6.2.1
location: ^7.0.1
- syncfusion_flutter_charts: ^27.2.5
- syncfusion_flutter_gauges: ^27.2.5
- table_calendar: ^3.1.2
+ syncfusion_flutter_charts: ^28.1.33
+ syncfusion_flutter_gauges: ^28.1.33
+ table_calendar: ^3.1.3
flutter_keyboard_visibility: ^6.0.0
- share_plus: ^10.1.2
+ share_plus: ^10.1.3
flutter_html: ^3.0.0-beta.2
file_picker:
git:
@@ -64,7 +64,7 @@ dependencies:
http: ^1.2.2
dev_dependencies:
- build_runner: ^2.4.13
+ build_runner: ^2.4.14
freezed: ^2.5.7
json_serializable: ^6.9.0
retrofit_generator: ^9.1.5