forked from eatvio/eatvio_app
feat(recipe): add creating recipes
This commit is contained in:
10
assets/icons/add_step_photo.svg
Normal file
10
assets/icons/add_step_photo.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 14 KiB |
10
assets/icons/upload_photo_of_portion.svg
Normal file
10
assets/icons/upload_photo_of_portion.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 21 KiB |
@@ -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',
|
||||
|
||||
@@ -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<void> init() async {
|
||||
sl.registerFactory<CalendarCubit>(() => CalendarCubit(calendarRepository: sl()));
|
||||
sl.registerFactory<ProfileCubit>(() => ProfileCubit(profileRepository: sl(), chatRepository: sl()));
|
||||
sl.registerFactory<profilePlans.PlansCubit>(() => profilePlans.PlansCubit(plansRepository: sl()));
|
||||
sl.registerFactory<RecipeCreateCubit>(() => RecipeCreateCubit(recipeRepository: sl()));
|
||||
sl.registerFactory<DietsCubit>(() => DietsCubit(feedRepository: sl(), dietsRepository: sl()));
|
||||
sl.registerFactory<CommentsCubit>(() => CommentsCubit(feedRepository: sl()));
|
||||
sl.registerFactory<ChatSelectCubit>(() => ChatSelectCubit(chatRepository: sl()));
|
||||
@@ -90,6 +89,7 @@ Future<void> init() async {
|
||||
sl.registerFactory<CreatePlanCubit>(() => CreatePlanCubit());
|
||||
sl.registerFactory<FeedProfileCubit>(() => FeedProfileCubit(profileRepository: sl()));
|
||||
sl.registerFactory<AboutPlanCubit>(() => AboutPlanCubit(planRepository: sl()));
|
||||
sl.registerFactory<CreateRecipeCubit>(() => CreateRecipeCubit());
|
||||
|
||||
// Repository
|
||||
sl.registerLazySingleton<ChatRepository>(() => ChatRepository(chatService: sl()));
|
||||
|
||||
@@ -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<SelectionPopupModel>(
|
||||
value: item,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -34,8 +34,9 @@ class _DiaryScreenState extends State<DiaryScreen> {
|
||||
children: [
|
||||
CalendarWidget(),
|
||||
if (state is DiaryStateLoading) const Loader(),
|
||||
if (state is DiaryStateLoaded) MealWidget(),
|
||||
CaloriesChartWidget(),
|
||||
const SizedBox(height: 12),
|
||||
if (state is DiaryStateLoaded) MealWidget(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -152,6 +152,9 @@ class _FeedScreenState extends State<FeedScreen> {
|
||||
_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,
|
||||
),
|
||||
|
||||
@@ -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<IngredientEntity> ingredients,
|
||||
required List<StepEntity> steps,
|
||||
required List<String> 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;
|
||||
}
|
||||
|
||||
722
lib/features/recipes/entities/recipe_create_entity.freezed.dart
Normal file
722
lib/features/recipes/entities/recipe_create_entity.freezed.dart
Normal file
@@ -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>(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<IngredientEntity> get ingredients => throw _privateConstructorUsedError;
|
||||
List<StepEntity> get steps => throw _privateConstructorUsedError;
|
||||
List<String> 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<RecipeCreateEntity> 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<IngredientEntity> ingredients,
|
||||
List<StepEntity> steps,
|
||||
List<String> 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<IngredientEntity>,
|
||||
steps: null == steps
|
||||
? _value.steps
|
||||
: steps // ignore: cast_nullable_to_non_nullable
|
||||
as List<StepEntity>,
|
||||
tags: null == tags
|
||||
? _value.tags
|
||||
: tags // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
) 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<IngredientEntity> ingredients,
|
||||
List<StepEntity> steps,
|
||||
List<String> 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<IngredientEntity>,
|
||||
steps: null == steps
|
||||
? _value._steps
|
||||
: steps // ignore: cast_nullable_to_non_nullable
|
||||
as List<StepEntity>,
|
||||
tags: null == tags
|
||||
? _value._tags
|
||||
: tags // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @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<IngredientEntity> ingredients,
|
||||
required final List<StepEntity> steps,
|
||||
required final List<String> 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<IngredientEntity> _ingredients;
|
||||
@override
|
||||
List<IngredientEntity> get ingredients {
|
||||
if (_ingredients is EqualUnmodifiableListView) return _ingredients;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_ingredients);
|
||||
}
|
||||
|
||||
final List<StepEntity> _steps;
|
||||
@override
|
||||
List<StepEntity> get steps {
|
||||
if (_steps is EqualUnmodifiableListView) return _steps;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_steps);
|
||||
}
|
||||
|
||||
final List<String> _tags;
|
||||
@override
|
||||
List<String> 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<IngredientEntity> ingredients,
|
||||
required final List<StepEntity> steps,
|
||||
required final List<String> 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<IngredientEntity> get ingredients;
|
||||
@override
|
||||
List<StepEntity> get steps;
|
||||
@override
|
||||
List<String> 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<IngredientEntity> 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<StepEntity> 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;
|
||||
}
|
||||
@@ -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<CreateRecipeState> {
|
||||
CreateRecipeCubit()
|
||||
: super(
|
||||
CreateRecipeState(
|
||||
RecipeCreateEntity(
|
||||
ingredients: [],
|
||||
steps: [],
|
||||
tags: [],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
addStep() {
|
||||
emit(
|
||||
CreateRecipeState(
|
||||
state.entity.copyWith(
|
||||
steps: [
|
||||
...state.entity.steps,
|
||||
StepEntity(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
removeStep(StepEntity step) {
|
||||
List<StepEntity> 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<IngredientEntity> ingredients = [...state.entity.ingredients];
|
||||
ingredients.removeAt(index);
|
||||
|
||||
emit(
|
||||
CreateRecipeState(
|
||||
state.entity.copyWith(
|
||||
ingredients: ingredients,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
replaceIngredientPosition(int oldIndex, int newIndex) {
|
||||
List<IngredientEntity> 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<String> 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<StepEntity> steps = [...state.entity.steps];
|
||||
steps[index] = steps[index].copyWith(description: description);
|
||||
emit(
|
||||
CreateRecipeState(
|
||||
state.entity.copyWith(
|
||||
steps: steps,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
replaceImageInStep(File? image, int index) {
|
||||
List<StepEntity> steps = [...state.entity.steps];
|
||||
steps[index] = steps[index].copyWith(image: image);
|
||||
emit(
|
||||
CreateRecipeState(
|
||||
state.entity.copyWith(
|
||||
steps: steps,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>(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<CreateRecipeState> 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;
|
||||
}
|
||||
@@ -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<RecipeCreateScreen> createState() => _RecipeCreateScreenState();
|
||||
}
|
||||
|
||||
class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<RecipeCreateCubit>(
|
||||
create: (context) => sl<RecipeCreateCubit>()..loadRecipeCreate(),
|
||||
child: BlocBuilder<RecipeCreateCubit, RecipeCreateState>(
|
||||
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<String> _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<CreateRecipeCubit>(
|
||||
create: (BuildContext context) => sl<CreateRecipeCubit>(),
|
||||
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<RecipeCreateCubit>().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<String>(
|
||||
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<String> onSelected,
|
||||
Iterable<String> 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: <TextInputFormatter>[FilteringTextInputFormatter.digitsOnly],
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Row(children: [
|
||||
Text('гр'),
|
||||
Switch(
|
||||
value: state.ingredientAsItems,
|
||||
onChanged: (bool value) => context.read<RecipeCreateCubit>().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<RecipeCreateCubit>().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<RecipeCreateCubit>().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<RecipeCreateCubit>().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<RecipeCreateCubit>().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<RecipeCreateCubit>().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<String> _options) {
|
||||
return (TextEditingValue textEditingValue) {
|
||||
return _options.where((String option) {
|
||||
return true;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
selectProduct(AutocompleteOnSelected<String> 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<RecipeCreateCubit>().inputProduct(value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
changeType(BuildContext context) {
|
||||
return (value) {
|
||||
context.read<RecipeCreateCubit>().changeType(value);
|
||||
};
|
||||
}
|
||||
|
||||
changeStatus(BuildContext context) {
|
||||
return (value) {
|
||||
context.read<RecipeCreateCubit>().changeStatus(value);
|
||||
};
|
||||
}
|
||||
|
||||
changeCookingTime(BuildContext context) {
|
||||
return (value) {
|
||||
context.read<RecipeCreateCubit>().changeCookingTime(value);
|
||||
};
|
||||
}
|
||||
|
||||
RecipeModel createRecipeFromState(RecipeCreateStateLoaded state) {
|
||||
List<Ingredient> ingredients = state.ingredients;
|
||||
|
||||
List<step.Step> 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;
|
||||
}
|
||||
}
|
||||
|
||||
121
lib/features/recipes/presentation/widgets/image_widget.dart
Normal file
121
lib/features/recipes/presentation/widgets/image_widget.dart
Normal file
@@ -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<ImageWidget> createState() => _ImageWidgetState();
|
||||
}
|
||||
|
||||
class _ImageWidgetState extends State<ImageWidget> {
|
||||
XFile? image;
|
||||
late final CreateRecipeCubit createRecipeCubit;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
createRecipeCubit = context.read<CreateRecipeCubit>();
|
||||
}
|
||||
|
||||
@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<void>(
|
||||
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('Отмена'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<InfoAboutRecipeWidget> createState() => _InfoAboutRecipeWidgetState();
|
||||
}
|
||||
|
||||
class _InfoAboutRecipeWidgetState extends State<InfoAboutRecipeWidget> {
|
||||
final List<SelectionPopupModel> 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<CreateRecipeCubit>();
|
||||
}
|
||||
|
||||
@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);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<IngredientsWidget> createState() => _IngredientsWidgetState();
|
||||
}
|
||||
|
||||
class _IngredientsWidgetState extends State<IngredientsWidget> {
|
||||
late final CreateRecipeCubit createRecipeCubit;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
createRecipeCubit = context.read<CreateRecipeCubit>();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<CreateRecipeCubit, CreateRecipeState>(
|
||||
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: <Widget>[
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<PortionWidget> createState() => _PortionWidgetState();
|
||||
}
|
||||
|
||||
class _PortionWidgetState extends State<PortionWidget> {
|
||||
late final CreateRecipeCubit createRecipeCubit;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
createRecipeCubit = context.read<CreateRecipeCubit>();
|
||||
}
|
||||
|
||||
@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));
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
126
lib/features/recipes/presentation/widgets/step_image_widget.dart
Normal file
126
lib/features/recipes/presentation/widgets/step_image_widget.dart
Normal file
@@ -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<StepImageWidget> createState() => _StepImageWidgetState();
|
||||
}
|
||||
|
||||
class _StepImageWidgetState extends State<StepImageWidget> {
|
||||
XFile? image;
|
||||
late final CreateRecipeCubit createRecipeCubit;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
createRecipeCubit = context.read<CreateRecipeCubit>();
|
||||
}
|
||||
|
||||
@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<void>(
|
||||
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('Отмена'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
135
lib/features/recipes/presentation/widgets/steps_widget.dart
Normal file
135
lib/features/recipes/presentation/widgets/steps_widget.dart
Normal file
@@ -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<StepsWidget> createState() => _StepsWidgetState();
|
||||
}
|
||||
|
||||
class _StepsWidgetState extends State<StepsWidget> {
|
||||
late final CreateRecipeCubit createRecipeCubit;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
createRecipeCubit = context.read<CreateRecipeCubit>();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<CreateRecipeCubit, CreateRecipeState>(
|
||||
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<CreateRecipeCubit>().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<CreateRecipeCubit>().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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<TagsWidget> createState() => _TagsWidgetState();
|
||||
}
|
||||
|
||||
class _TagsWidgetState extends State<TagsWidget> {
|
||||
final tagController = TextEditingController();
|
||||
final List<String> 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<TimeOfCookWidget> createState() => _TimeOfCookWidgetState();
|
||||
}
|
||||
|
||||
class _TimeOfCookWidgetState extends State<TimeOfCookWidget> {
|
||||
late final CreateRecipeCubit createRecipeCubit;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
createRecipeCubit = context.read<CreateRecipeCubit>();
|
||||
}
|
||||
|
||||
@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')}');
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
12
pubspec.yaml
12
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
|
||||
|
||||
Reference in New Issue
Block a user