feat(recipe): Models, service and repository for eecpe create. Recipe create now uses cubit

This commit is contained in:
2024-08-14 14:36:06 +03:00
parent c53fa419d7
commit f697f403f4
26 changed files with 2817 additions and 913 deletions

View File

@@ -0,0 +1,14 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'image.freezed.dart';
part 'image.g.dart';
@freezed
class Image with _$Image {
const factory Image({
required String path,
}) = _Image;
factory Image.fromJson(Map<String, Object?> json) => _$ImageFromJson(json);
}

View File

@@ -0,0 +1,147 @@
// 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 'image.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');
Image _$ImageFromJson(Map<String, dynamic> json) {
return _Image.fromJson(json);
}
/// @nodoc
mixin _$Image {
String get path => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$ImageCopyWith<Image> get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ImageCopyWith<$Res> {
factory $ImageCopyWith(Image value, $Res Function(Image) then) =
_$ImageCopyWithImpl<$Res, Image>;
@useResult
$Res call({String path});
}
/// @nodoc
class _$ImageCopyWithImpl<$Res, $Val extends Image>
implements $ImageCopyWith<$Res> {
_$ImageCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? path = null,
}) {
return _then(_value.copyWith(
path: null == path
? _value.path
: path // ignore: cast_nullable_to_non_nullable
as String,
) as $Val);
}
}
/// @nodoc
abstract class _$$ImageImplCopyWith<$Res> implements $ImageCopyWith<$Res> {
factory _$$ImageImplCopyWith(
_$ImageImpl value, $Res Function(_$ImageImpl) then) =
__$$ImageImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({String path});
}
/// @nodoc
class __$$ImageImplCopyWithImpl<$Res>
extends _$ImageCopyWithImpl<$Res, _$ImageImpl>
implements _$$ImageImplCopyWith<$Res> {
__$$ImageImplCopyWithImpl(
_$ImageImpl _value, $Res Function(_$ImageImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? path = null,
}) {
return _then(_$ImageImpl(
path: null == path
? _value.path
: path // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
@JsonSerializable()
class _$ImageImpl implements _Image {
const _$ImageImpl({required this.path});
factory _$ImageImpl.fromJson(Map<String, dynamic> json) =>
_$$ImageImplFromJson(json);
@override
final String path;
@override
String toString() {
return 'Image(path: $path)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ImageImpl &&
(identical(other.path, path) || other.path == path));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, path);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$ImageImplCopyWith<_$ImageImpl> get copyWith =>
__$$ImageImplCopyWithImpl<_$ImageImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$ImageImplToJson(
this,
);
}
}
abstract class _Image implements Image {
const factory _Image({required final String path}) = _$ImageImpl;
factory _Image.fromJson(Map<String, dynamic> json) = _$ImageImpl.fromJson;
@override
String get path;
@override
@JsonKey(ignore: true)
_$$ImageImplCopyWith<_$ImageImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,16 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'image.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$ImageImpl _$$ImageImplFromJson(Map<String, dynamic> json) => _$ImageImpl(
path: json['path'] as String,
);
Map<String, dynamic> _$$ImageImplToJson(_$ImageImpl instance) =>
<String, dynamic>{
'path': instance.path,
};

View File

@@ -0,0 +1,18 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'ingredient.freezed.dart';
part 'ingredient.g.dart';
@freezed
class Ingredient with _$Ingredient {
const factory Ingredient({
int? id,
required String product_name,
required String unit,
required num portion,
}) = _Ingredient;
factory Ingredient.fromJson(Map<String, Object?> json) =>
_$IngredientFromJson(json);
}

View File

@@ -0,0 +1,208 @@
// 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 'ingredient.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');
Ingredient _$IngredientFromJson(Map<String, dynamic> json) {
return _Ingredient.fromJson(json);
}
/// @nodoc
mixin _$Ingredient {
int? get id => throw _privateConstructorUsedError;
String get product_name => throw _privateConstructorUsedError;
String get unit => throw _privateConstructorUsedError;
num get portion => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$IngredientCopyWith<Ingredient> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $IngredientCopyWith<$Res> {
factory $IngredientCopyWith(
Ingredient value, $Res Function(Ingredient) then) =
_$IngredientCopyWithImpl<$Res, Ingredient>;
@useResult
$Res call({int? id, String product_name, String unit, num portion});
}
/// @nodoc
class _$IngredientCopyWithImpl<$Res, $Val extends Ingredient>
implements $IngredientCopyWith<$Res> {
_$IngredientCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = freezed,
Object? product_name = null,
Object? unit = null,
Object? portion = null,
}) {
return _then(_value.copyWith(
id: freezed == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int?,
product_name: null == product_name
? _value.product_name
: product_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 num,
) as $Val);
}
}
/// @nodoc
abstract class _$$IngredientImplCopyWith<$Res>
implements $IngredientCopyWith<$Res> {
factory _$$IngredientImplCopyWith(
_$IngredientImpl value, $Res Function(_$IngredientImpl) then) =
__$$IngredientImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({int? id, String product_name, String unit, num portion});
}
/// @nodoc
class __$$IngredientImplCopyWithImpl<$Res>
extends _$IngredientCopyWithImpl<$Res, _$IngredientImpl>
implements _$$IngredientImplCopyWith<$Res> {
__$$IngredientImplCopyWithImpl(
_$IngredientImpl _value, $Res Function(_$IngredientImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = freezed,
Object? product_name = null,
Object? unit = null,
Object? portion = null,
}) {
return _then(_$IngredientImpl(
id: freezed == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int?,
product_name: null == product_name
? _value.product_name
: product_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 num,
));
}
}
/// @nodoc
@JsonSerializable()
class _$IngredientImpl implements _Ingredient {
const _$IngredientImpl(
{this.id,
required this.product_name,
required this.unit,
required this.portion});
factory _$IngredientImpl.fromJson(Map<String, dynamic> json) =>
_$$IngredientImplFromJson(json);
@override
final int? id;
@override
final String product_name;
@override
final String unit;
@override
final num portion;
@override
String toString() {
return 'Ingredient(id: $id, product_name: $product_name, unit: $unit, portion: $portion)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$IngredientImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.product_name, product_name) ||
other.product_name == product_name) &&
(identical(other.unit, unit) || other.unit == unit) &&
(identical(other.portion, portion) || other.portion == portion));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, id, product_name, unit, portion);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$IngredientImplCopyWith<_$IngredientImpl> get copyWith =>
__$$IngredientImplCopyWithImpl<_$IngredientImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$IngredientImplToJson(
this,
);
}
}
abstract class _Ingredient implements Ingredient {
const factory _Ingredient(
{final int? id,
required final String product_name,
required final String unit,
required final num portion}) = _$IngredientImpl;
factory _Ingredient.fromJson(Map<String, dynamic> json) =
_$IngredientImpl.fromJson;
@override
int? get id;
@override
String get product_name;
@override
String get unit;
@override
num get portion;
@override
@JsonKey(ignore: true)
_$$IngredientImplCopyWith<_$IngredientImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,23 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'ingredient.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$IngredientImpl _$$IngredientImplFromJson(Map<String, dynamic> json) =>
_$IngredientImpl(
id: (json['id'] as num?)?.toInt(),
product_name: json['product_name'] as String,
unit: json['unit'] as String,
portion: json['portion'] as num,
);
Map<String, dynamic> _$$IngredientImplToJson(_$IngredientImpl instance) =>
<String, dynamic>{
'id': instance.id,
'product_name': instance.product_name,
'unit': instance.unit,
'portion': instance.portion,
};

View File

@@ -0,0 +1,25 @@
import 'package:eatvio_app/features/recipes/models/image.dart';
import 'package:eatvio_app/features/recipes/models/ingredient.dart';
import 'package:eatvio_app/features/recipes/models/step.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'recipe_model.freezed.dart';
part 'recipe_model.g.dart';
@freezed
class RecipeModel with _$RecipeModel {
const factory RecipeModel({
int? id,
required List<Ingredient> ingredients,
required List<Step> steps,
required String state,
required String description,
required String type,
required Image image,
String? video_url,
num? cooking_time,
}) = _RecipeModel;
factory RecipeModel.fromJson(Map<String, Object?> json) =>
_$RecipeModelFromJson(json);
}

View File

@@ -0,0 +1,356 @@
// 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_model.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');
RecipeModel _$RecipeModelFromJson(Map<String, dynamic> json) {
return _RecipeModel.fromJson(json);
}
/// @nodoc
mixin _$RecipeModel {
int? get id => throw _privateConstructorUsedError;
List<Ingredient> get ingredients => throw _privateConstructorUsedError;
List<Step> get steps => throw _privateConstructorUsedError;
String get state => throw _privateConstructorUsedError;
String get description => throw _privateConstructorUsedError;
String get type => throw _privateConstructorUsedError;
Image get image => throw _privateConstructorUsedError;
String? get video_url => throw _privateConstructorUsedError;
num? get cooking_time => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$RecipeModelCopyWith<RecipeModel> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $RecipeModelCopyWith<$Res> {
factory $RecipeModelCopyWith(
RecipeModel value, $Res Function(RecipeModel) then) =
_$RecipeModelCopyWithImpl<$Res, RecipeModel>;
@useResult
$Res call(
{int? id,
List<Ingredient> ingredients,
List<Step> steps,
String state,
String description,
String type,
Image image,
String? video_url,
num? cooking_time});
$ImageCopyWith<$Res> get image;
}
/// @nodoc
class _$RecipeModelCopyWithImpl<$Res, $Val extends RecipeModel>
implements $RecipeModelCopyWith<$Res> {
_$RecipeModelCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = freezed,
Object? ingredients = null,
Object? steps = null,
Object? state = null,
Object? description = null,
Object? type = null,
Object? image = null,
Object? video_url = freezed,
Object? cooking_time = freezed,
}) {
return _then(_value.copyWith(
id: freezed == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int?,
ingredients: null == ingredients
? _value.ingredients
: ingredients // ignore: cast_nullable_to_non_nullable
as List<Ingredient>,
steps: null == steps
? _value.steps
: steps // ignore: cast_nullable_to_non_nullable
as List<Step>,
state: null == state
? _value.state
: state // ignore: cast_nullable_to_non_nullable
as String,
description: null == description
? _value.description
: description // ignore: cast_nullable_to_non_nullable
as String,
type: null == type
? _value.type
: type // ignore: cast_nullable_to_non_nullable
as String,
image: null == image
? _value.image
: image // ignore: cast_nullable_to_non_nullable
as Image,
video_url: freezed == video_url
? _value.video_url
: video_url // ignore: cast_nullable_to_non_nullable
as String?,
cooking_time: freezed == cooking_time
? _value.cooking_time
: cooking_time // ignore: cast_nullable_to_non_nullable
as num?,
) as $Val);
}
@override
@pragma('vm:prefer-inline')
$ImageCopyWith<$Res> get image {
return $ImageCopyWith<$Res>(_value.image, (value) {
return _then(_value.copyWith(image: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$RecipeModelImplCopyWith<$Res>
implements $RecipeModelCopyWith<$Res> {
factory _$$RecipeModelImplCopyWith(
_$RecipeModelImpl value, $Res Function(_$RecipeModelImpl) then) =
__$$RecipeModelImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{int? id,
List<Ingredient> ingredients,
List<Step> steps,
String state,
String description,
String type,
Image image,
String? video_url,
num? cooking_time});
@override
$ImageCopyWith<$Res> get image;
}
/// @nodoc
class __$$RecipeModelImplCopyWithImpl<$Res>
extends _$RecipeModelCopyWithImpl<$Res, _$RecipeModelImpl>
implements _$$RecipeModelImplCopyWith<$Res> {
__$$RecipeModelImplCopyWithImpl(
_$RecipeModelImpl _value, $Res Function(_$RecipeModelImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = freezed,
Object? ingredients = null,
Object? steps = null,
Object? state = null,
Object? description = null,
Object? type = null,
Object? image = null,
Object? video_url = freezed,
Object? cooking_time = freezed,
}) {
return _then(_$RecipeModelImpl(
id: freezed == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int?,
ingredients: null == ingredients
? _value._ingredients
: ingredients // ignore: cast_nullable_to_non_nullable
as List<Ingredient>,
steps: null == steps
? _value._steps
: steps // ignore: cast_nullable_to_non_nullable
as List<Step>,
state: null == state
? _value.state
: state // ignore: cast_nullable_to_non_nullable
as String,
description: null == description
? _value.description
: description // ignore: cast_nullable_to_non_nullable
as String,
type: null == type
? _value.type
: type // ignore: cast_nullable_to_non_nullable
as String,
image: null == image
? _value.image
: image // ignore: cast_nullable_to_non_nullable
as Image,
video_url: freezed == video_url
? _value.video_url
: video_url // ignore: cast_nullable_to_non_nullable
as String?,
cooking_time: freezed == cooking_time
? _value.cooking_time
: cooking_time // ignore: cast_nullable_to_non_nullable
as num?,
));
}
}
/// @nodoc
@JsonSerializable()
class _$RecipeModelImpl implements _RecipeModel {
const _$RecipeModelImpl(
{this.id,
required final List<Ingredient> ingredients,
required final List<Step> steps,
required this.state,
required this.description,
required this.type,
required this.image,
this.video_url,
this.cooking_time})
: _ingredients = ingredients,
_steps = steps;
factory _$RecipeModelImpl.fromJson(Map<String, dynamic> json) =>
_$$RecipeModelImplFromJson(json);
@override
final int? id;
final List<Ingredient> _ingredients;
@override
List<Ingredient> get ingredients {
if (_ingredients is EqualUnmodifiableListView) return _ingredients;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_ingredients);
}
final List<Step> _steps;
@override
List<Step> get steps {
if (_steps is EqualUnmodifiableListView) return _steps;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_steps);
}
@override
final String state;
@override
final String description;
@override
final String type;
@override
final Image image;
@override
final String? video_url;
@override
final num? cooking_time;
@override
String toString() {
return 'RecipeModel(id: $id, ingredients: $ingredients, steps: $steps, state: $state, description: $description, type: $type, image: $image, video_url: $video_url, cooking_time: $cooking_time)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$RecipeModelImpl &&
(identical(other.id, id) || other.id == id) &&
const DeepCollectionEquality()
.equals(other._ingredients, _ingredients) &&
const DeepCollectionEquality().equals(other._steps, _steps) &&
(identical(other.state, state) || other.state == state) &&
(identical(other.description, description) ||
other.description == description) &&
(identical(other.type, type) || other.type == type) &&
(identical(other.image, image) || other.image == image) &&
(identical(other.video_url, video_url) ||
other.video_url == video_url) &&
(identical(other.cooking_time, cooking_time) ||
other.cooking_time == cooking_time));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType,
id,
const DeepCollectionEquality().hash(_ingredients),
const DeepCollectionEquality().hash(_steps),
state,
description,
type,
image,
video_url,
cooking_time);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$RecipeModelImplCopyWith<_$RecipeModelImpl> get copyWith =>
__$$RecipeModelImplCopyWithImpl<_$RecipeModelImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$RecipeModelImplToJson(
this,
);
}
}
abstract class _RecipeModel implements RecipeModel {
const factory _RecipeModel(
{final int? id,
required final List<Ingredient> ingredients,
required final List<Step> steps,
required final String state,
required final String description,
required final String type,
required final Image image,
final String? video_url,
final num? cooking_time}) = _$RecipeModelImpl;
factory _RecipeModel.fromJson(Map<String, dynamic> json) =
_$RecipeModelImpl.fromJson;
@override
int? get id;
@override
List<Ingredient> get ingredients;
@override
List<Step> get steps;
@override
String get state;
@override
String get description;
@override
String get type;
@override
Image get image;
@override
String? get video_url;
@override
num? get cooking_time;
@override
@JsonKey(ignore: true)
_$$RecipeModelImplCopyWith<_$RecipeModelImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,37 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'recipe_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$RecipeModelImpl _$$RecipeModelImplFromJson(Map<String, dynamic> json) =>
_$RecipeModelImpl(
id: (json['id'] as num?)?.toInt(),
ingredients: (json['ingredients'] as List<dynamic>)
.map((e) => Ingredient.fromJson(e as Map<String, dynamic>))
.toList(),
steps: (json['steps'] as List<dynamic>)
.map((e) => Step.fromJson(e as Map<String, dynamic>))
.toList(),
state: json['state'] as String,
description: json['description'] as String,
type: json['type'] as String,
image: Image.fromJson(json['image'] as Map<String, dynamic>),
video_url: json['video_url'] as String?,
cooking_time: json['cooking_time'] as num?,
);
Map<String, dynamic> _$$RecipeModelImplToJson(_$RecipeModelImpl instance) =>
<String, dynamic>{
'id': instance.id,
'ingredients': instance.ingredients,
'steps': instance.steps,
'state': instance.state,
'description': instance.description,
'type': instance.type,
'image': instance.image,
'video_url': instance.video_url,
'cooking_time': instance.cooking_time,
};

View File

@@ -0,0 +1,17 @@
import 'package:eatvio_app/features/recipes/models/image.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'step.freezed.dart';
part 'step.g.dart';
@freezed
class Step with _$Step {
const factory Step({
int? id,
Image? image,
String? description,
}) = _Step;
factory Step.fromJson(Map<String, Object?> json) => _$StepFromJson(json);
}

View File

@@ -0,0 +1,199 @@
// 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 'step.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');
Step _$StepFromJson(Map<String, dynamic> json) {
return _Step.fromJson(json);
}
/// @nodoc
mixin _$Step {
int? get id => throw _privateConstructorUsedError;
Image? get image => throw _privateConstructorUsedError;
String? get description => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$StepCopyWith<Step> get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $StepCopyWith<$Res> {
factory $StepCopyWith(Step value, $Res Function(Step) then) =
_$StepCopyWithImpl<$Res, Step>;
@useResult
$Res call({int? id, Image? image, String? description});
$ImageCopyWith<$Res>? get image;
}
/// @nodoc
class _$StepCopyWithImpl<$Res, $Val extends Step>
implements $StepCopyWith<$Res> {
_$StepCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = freezed,
Object? image = freezed,
Object? description = freezed,
}) {
return _then(_value.copyWith(
id: freezed == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int?,
image: freezed == image
? _value.image
: image // ignore: cast_nullable_to_non_nullable
as Image?,
description: freezed == description
? _value.description
: description // ignore: cast_nullable_to_non_nullable
as String?,
) as $Val);
}
@override
@pragma('vm:prefer-inline')
$ImageCopyWith<$Res>? get image {
if (_value.image == null) {
return null;
}
return $ImageCopyWith<$Res>(_value.image!, (value) {
return _then(_value.copyWith(image: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$StepImplCopyWith<$Res> implements $StepCopyWith<$Res> {
factory _$$StepImplCopyWith(
_$StepImpl value, $Res Function(_$StepImpl) then) =
__$$StepImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({int? id, Image? image, String? description});
@override
$ImageCopyWith<$Res>? get image;
}
/// @nodoc
class __$$StepImplCopyWithImpl<$Res>
extends _$StepCopyWithImpl<$Res, _$StepImpl>
implements _$$StepImplCopyWith<$Res> {
__$$StepImplCopyWithImpl(_$StepImpl _value, $Res Function(_$StepImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = freezed,
Object? image = freezed,
Object? description = freezed,
}) {
return _then(_$StepImpl(
id: freezed == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int?,
image: freezed == image
? _value.image
: image // ignore: cast_nullable_to_non_nullable
as Image?,
description: freezed == description
? _value.description
: description // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// @nodoc
@JsonSerializable()
class _$StepImpl implements _Step {
const _$StepImpl({this.id, this.image, this.description});
factory _$StepImpl.fromJson(Map<String, dynamic> json) =>
_$$StepImplFromJson(json);
@override
final int? id;
@override
final Image? image;
@override
final String? description;
@override
String toString() {
return 'Step(id: $id, image: $image, description: $description)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$StepImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.image, image) || other.image == image) &&
(identical(other.description, description) ||
other.description == description));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, id, image, description);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$StepImplCopyWith<_$StepImpl> get copyWith =>
__$$StepImplCopyWithImpl<_$StepImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$StepImplToJson(
this,
);
}
}
abstract class _Step implements Step {
const factory _Step(
{final int? id,
final Image? image,
final String? description}) = _$StepImpl;
factory _Step.fromJson(Map<String, dynamic> json) = _$StepImpl.fromJson;
@override
int? get id;
@override
Image? get image;
@override
String? get description;
@override
@JsonKey(ignore: true)
_$$StepImplCopyWith<_$StepImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,22 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'step.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$StepImpl _$$StepImplFromJson(Map<String, dynamic> json) => _$StepImpl(
id: (json['id'] as num?)?.toInt(),
image: json['image'] == null
? null
: Image.fromJson(json['image'] as Map<String, dynamic>),
description: json['description'] as String?,
);
Map<String, dynamic> _$$StepImplToJson(_$StepImpl instance) =>
<String, dynamic>{
'id': instance.id,
'image': instance.image,
'description': instance.description,
};

View File

@@ -0,0 +1,170 @@
import 'package:eatvio_app/core/app_export.dart';
import 'package:eatvio_app/features/recipes/models/ingredient.dart';
import 'package:eatvio_app/features/recipes/presentation/bloc/recipe_create_state.dart';
import 'package:eatvio_app/features/recipes/reporitories/recipe_repository.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
class RecipeCreateCubit extends Cubit<RecipeCreateState> {
final RecipeRepository recipeRepository;
RecipeCreateCubit({required this.recipeRepository})
: super(const RecipeCreateState.loading());
loadRecipeCreate() {
emit(const RecipeCreateStateLoading());
try {
emit(RecipeCreateState.loaded(
picker: ImagePicker(),
titleController: TextEditingController(),
productNameController: TextEditingController(),
ingredientController: TextEditingController(),
stepDescriptionController: TextEditingController(),
youtubeLinkController: TextEditingController(),
stepControllers: [TextEditingController()],
stepImages: [null],
ingredients: [],
numberOfSteps: 1,
ingredientAsItems: false,
types: [
SelectionPopupModel(title: 'Первые блюда'),
SelectionPopupModel(title: 'Вторые блюда'),
SelectionPopupModel(title: 'Cалаты'),
SelectionPopupModel(title: 'Перекусы/закуски'),
SelectionPopupModel(title: 'Сладости'),
SelectionPopupModel(title: 'Напитки'),
SelectionPopupModel(title: 'Прочее'),
],
cookingTimes: [
SelectionPopupModel(title: '10 минут'),
SelectionPopupModel(title: '20 минут'),
SelectionPopupModel(title: '30 минут'),
SelectionPopupModel(title: '1 час'),
SelectionPopupModel(title: '2 часа'),
SelectionPopupModel(title: '10 дней'),
],
statuses: [
SelectionPopupModel(title: 'Активна'),
SelectionPopupModel(title: 'Неактивна'),
],
));
} catch (e) {
emit(RecipeCreateState.error(errorMessage: e.toString()));
}
}
uploadRecipeImage(XFile? pickedFile) {
try {
if (state is RecipeCreateStateLoaded) {
var currentState = state as RecipeCreateStateLoaded;
var updatedState = currentState.copyWith(
recipeImage: pickedFile,
);
emit(updatedState);
}
} catch (e) {
emit(RecipeCreateState.error(errorMessage: e.toString()));
}
}
uploadStepImage(XFile? pickedFile, int stepNumber) {
try {
if (state is RecipeCreateStateLoaded) {
var currentState = state as RecipeCreateStateLoaded;
var updatedStepImages = List<XFile?>.from(currentState.stepImages);
if (stepNumber >= 0 && stepNumber < updatedStepImages.length) {
updatedStepImages[stepNumber] = pickedFile;
}
var updatedState = currentState.copyWith(
stepImages: updatedStepImages,
);
emit(updatedState);
}
} catch (e) {
emit(RecipeCreateState.error(errorMessage: e.toString()));
}
}
switchUnits(bool value) {
try {
if (state is RecipeCreateStateLoaded) {
var currentState = state as RecipeCreateStateLoaded;
var updatedState = currentState.copyWith(ingredientAsItems: value);
emit(updatedState);
}
} catch (e) {
emit(RecipeCreateState.error(errorMessage: e.toString()));
}
}
addIngredient(Ingredient ingredient) {
try {
if (state is RecipeCreateStateLoaded) {
var currentState = state as RecipeCreateStateLoaded;
bool ingredientExists = currentState.ingredients.any(
(existingIngredient) =>
existingIngredient.product_name == ingredient.product_name);
if (ingredientExists) {
emit(RecipeCreateState.error(
errorMessage: 'Ингредиент уже существует.'));
return;
}
List<Ingredient> newIngredients =
List<Ingredient>.from(currentState.ingredients);
newIngredients.add(ingredient);
var updatedState = currentState.copyWith(ingredients: newIngredients);
emit(updatedState);
}
} catch (e) {
emit(RecipeCreateState.error(errorMessage: e.toString()));
}
}
addStep() {
try {
if (state is RecipeCreateStateLoaded) {
var currentState = state as RecipeCreateStateLoaded;
List<XFile?> newStepImages = List<XFile?>.from(currentState.stepImages);
newStepImages.add(null);
List<TextEditingController> newStepControllers =
List<TextEditingController>.from(currentState.stepControllers);
newStepControllers.add(TextEditingController());
var updatedState = currentState.copyWith(
stepImages: newStepImages,
stepControllers: newStepControllers,
numberOfSteps: currentState.numberOfSteps + 1);
emit(updatedState);
}
} catch (e) {
emit(RecipeCreateState.error(errorMessage: e.toString()));
}
}
removeStep() {
try {
if (state is RecipeCreateStateLoaded) {
var currentState = state as RecipeCreateStateLoaded;
List<XFile?> newStepImages = currentState.stepImages;
newStepImages.removeLast();
List<TextEditingController> newStepControllers =
List<TextEditingController>.from(currentState.stepControllers);
newStepControllers.removeLast();
var updatedState = currentState.copyWith(
stepImages: newStepImages,
stepControllers: newStepControllers,
numberOfSteps: currentState.numberOfSteps - 1);
emit(updatedState);
}
} catch (e) {
emit(RecipeCreateState.error(errorMessage: e.toString()));
}
}
}

View File

@@ -0,0 +1,39 @@
import 'package:eatvio_app/features/recipes/models/recipe_model.dart';
import 'package:eatvio_app/features/recipes/models/ingredient.dart';
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:image_picker/image_picker.dart';
import '../../../../data/models/selectionPopupModel/selection_popup_model.dart';
part 'recipe_create_state.freezed.dart';
@freezed
abstract class RecipeCreateState with _$RecipeCreateState {
const factory RecipeCreateState() = _RecipeCreateState;
const factory RecipeCreateState.loading() = RecipeCreateStateLoading;
const factory RecipeCreateState.loaded({
RecipeModel? recipe,
required ImagePicker picker,
required TextEditingController titleController,
required TextEditingController productNameController,
required TextEditingController ingredientController,
required TextEditingController stepDescriptionController,
required TextEditingController youtubeLinkController,
required List<TextEditingController> stepControllers,
XFile? recipeImage,
required List<XFile?> stepImages,
required List<Ingredient> ingredients,
required int numberOfSteps,
required bool ingredientAsItems,
required List<SelectionPopupModel> types,
required List<SelectionPopupModel> cookingTimes,
required List<SelectionPopupModel> statuses,
}) = RecipeCreateStateLoaded;
const factory RecipeCreateState.error({
required String errorMessage,
}) = RecipeCreateStateError;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,72 +1,56 @@
//import 'dart:io';
import 'dart:io';
import 'package:eatvio_app/core/app_export.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/image.dart' as image;
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/widgets/custom_drop_down.dart';
import 'package:eatvio_app/widgets/custom_text_form_field.dart';
import 'package:image_picker/image_picker.dart';
import 'package:flutter/material.dart';
import '../../../../presentation/feed/blog_post_create_screen/bloc/blog_post_create_bloc.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:image_picker/image_picker.dart';
class RecipeCreateScreen extends StatefulWidget {
RecipeCreateScreen({
super.key,
});
@override
State<RecipeCreateScreen> createState() => _RecipeCreateScreenState();
}
class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
final TextEditingController titleController = TextEditingController();
final TextEditingController productNameController = TextEditingController();
final TextEditingController portionController = TextEditingController();
final TextEditingController stepDescriptionController =
TextEditingController();
final TextEditingController youtubeLinkController = TextEditingController();
final List<SelectionPopupModel> types = [
SelectionPopupModel(title: 'Первые блюда'),
SelectionPopupModel(title: 'Вторые блюда'),
SelectionPopupModel(title: 'Cалаты'),
SelectionPopupModel(title: 'Перекусы/закуски'),
SelectionPopupModel(title: 'Сладости'),
SelectionPopupModel(title: 'Напитки'),
SelectionPopupModel(title: 'Прочее'),
];
final List<SelectionPopupModel> cookingTimes = [
SelectionPopupModel(title: '10 минут'),
SelectionPopupModel(title: '20 минут'),
SelectionPopupModel(title: '30 минут'),
SelectionPopupModel(title: '1 час'),
SelectionPopupModel(title: '2 часа'),
SelectionPopupModel(title: '10 дней'),
];
final List<SelectionPopupModel> statuses = [
SelectionPopupModel(title: 'Активна'),
SelectionPopupModel(title: 'Неактивна'),
];
int numberOfSteps = 1;
XFile? recipeImage; // Переменная для общей фотографии рецепта
List<XFile?> stepImages = []; // Список фотографий для каждого шага
final ImagePicker _picker = ImagePicker();
bool portionAsItems = false;
@override
void initState() {
super.initState();
stepImages.add(null); // Добавляем начальный элемент для первого шага
// ignore: must_be_immutable
class RecipeCreateScreen extends StatelessWidget {
RecipeCreateScreen({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return BlocBuilder<RecipeCreateCubit, RecipeCreateState>(
builder: (context, state) {
if (state is RecipeCreateStateLoaded) {
return _buildRecipeCreate(context, state);
} else if (state is RecipeCreateStateLoading) {
return Scaffold(
body: Center(
child: CupertinoActivityIndicator(),
),
);
} 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 build(BuildContext context) {
Widget _buildRecipeCreate(
BuildContext context, RecipeCreateStateLoaded state) {
return Scaffold(
appBar: AppBar(
toolbarHeight: 0,
@@ -94,30 +78,28 @@ class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
Text('Заголовок'),
SizedBox(height: 8),
CustomTextFormField(
validator: _validateTitle,
fillColor: appTheme.gray50,
controller: titleController,
controller: state.titleController,
hintText: 'Заголовок',
),
SizedBox(height: 16),
Text('Тип блюда'),
SizedBox(height: 8),
CustomDropDown(
value: types[0],
items: types,
value: state.types[0],
items: state.types,
),
SizedBox(height: 16),
Text('Изображение'),
GestureDetector(
onTap: () async {
final pickedFile = await _picker.pickImage(
source: ImageSource.gallery);
setState(() {
recipeImage =
pickedFile; // Сохраняем общее изображение рецепта
});
final pickedFile = await state.picker
.pickImage(source: ImageSource.gallery);
context
.read<RecipeCreateCubit>()
.uploadRecipeImage(pickedFile);
},
child: recipeImage == null
child: state.recipeImage == null
? Container(
height: 128,
decoration: AppDecoration.fillGray.copyWith(
@@ -139,49 +121,53 @@ class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
],
),
)
: Image.file(File(recipeImage!.path),
: Image.file(File(state.recipeImage!.path),
height: 200, fit: BoxFit.cover),
),
SizedBox(height: 16),
Text('Время приготовления'),
SizedBox(height: 8),
CustomDropDown(
items: cookingTimes,
value: cookingTimes[0],
items: state.cookingTimes,
value: state.cookingTimes[0],
),
SizedBox(height: 16),
Text('Статус'),
SizedBox(height: 8),
CustomDropDown(
items: statuses,
value: statuses[0],
items: state.statuses,
value: state.statuses[0],
),
SizedBox(height: 24),
if (!state.ingredients.isEmpty)
for (int j = 0; j < state.ingredients.length; j++)
_buildIngredient(state.ingredients[j]),
SizedBox(height: 24),
CustomTextFormField(
hintText: 'Введите название продукта',
controller: productNameController,
controller: state.productNameController,
),
SizedBox(height: 24),
CustomTextFormField(
hintText: 'Порция',
fillColor: appTheme.gray50,
controller: portionController,
controller: state.ingredientController,
keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly
],
),
SizedBox(height: 8),
Row(children: [
Text('гр'),
Switch(
value: portionAsItems,
onChanged: (bool value) {
setState(() {
portionAsItems = value;
});
},
activeColor: appTheme.blueA400, // Цвет активной части
inactiveThumbColor:
Colors.grey, // Цвет ползунка, когда выключен
inactiveTrackColor: Colors
.grey.shade300, // Цвет трека, когда выключен
value: state.ingredientAsItems,
onChanged: (bool value) => context
.read<RecipeCreateCubit>()
.switchUnits(value),
activeColor: appTheme.blueA400,
inactiveThumbColor: Colors.grey,
inactiveTrackColor: Colors.grey.shade300,
),
Text('шт'),
]),
@@ -190,19 +176,27 @@ class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
height: 48,
width: MediaQuery.of(context).size.width,
child: FloatingActionButton.extended(
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
backgroundColor: appTheme.blueA400,
onPressed: () {},
label: Text(
'+',
style: theme.textTheme.titleSmall,
),
),
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
backgroundColor: appTheme.blueA400,
label: Text(
'+',
style: theme.textTheme.titleSmall,
),
onPressed: () => context
.read<RecipeCreateCubit>()
.addIngredient(new Ingredient(
product_name:
state.productNameController.text,
portion: int.parse(
state.ingredientController.text),
unit: state.ingredientAsItems
? 'pcs'
: 'gr'))),
),
SizedBox(height: 24),
Text('Описание рецепта'),
@@ -210,10 +204,11 @@ class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
CustomTextFormField(
maxLines: 8,
minLines: 8,
controller: stepDescriptionController,
controller: state.stepDescriptionController,
),
SizedBox(height: 24),
for (int i = 0; i < numberOfSteps; i++) _buildStep(i),
for (int i = 0; i < state.numberOfSteps; i++)
_buildStep(context, i, state),
SizedBox(height: 24),
Container(
height: 48,
@@ -226,12 +221,8 @@ class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
borderRadius: BorderRadius.circular(8),
),
backgroundColor: appTheme.blueA400,
onPressed: () {
setState(() {
stepImages.add(null);
numberOfSteps++;
});
},
onPressed: () =>
context.read<RecipeCreateCubit>().addStep(),
label: Text(
'+',
style: theme.textTheme.titleSmall,
@@ -250,14 +241,8 @@ class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
borderRadius: BorderRadius.circular(8),
),
backgroundColor: Colors.grey,
onPressed: () {
setState(() {
if (numberOfSteps > 1) {
numberOfSteps--;
stepImages.removeLast();
}
});
},
onPressed: () =>
context.read<RecipeCreateCubit>().removeStep(),
label: Text(
'-',
style: theme.textTheme.titleSmall,
@@ -270,7 +255,7 @@ class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
CustomTextFormField(
hintText: 'Введите описание шага',
fillColor: appTheme.gray50,
controller: youtubeLinkController,
controller: state.youtubeLinkController,
),
SizedBox(height: 24),
Container(
@@ -300,25 +285,25 @@ class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
);
}
_buildStep(int stepNumber) {
_buildStep(
BuildContext context, int stepNumber, RecipeCreateStateLoaded state) {
return Column(
children: [
Row(children: [
Text('Шаг ${stepNumber + 1}'),
Spacer(),
Text('Всего шагов: $numberOfSteps')
Text('Всего шагов: ${state.numberOfSteps}')
]),
SizedBox(height: 8),
GestureDetector(
onTap: () async {
final pickedFile =
await _picker.pickImage(source: ImageSource.gallery);
setState(() {
stepImages[stepNumber] =
pickedFile; // Сохраняем изображение для текущего шага
});
await state.picker.pickImage(source: ImageSource.gallery);
context
.read<RecipeCreateCubit>()
.uploadStepImage(pickedFile, stepNumber);
},
child: stepImages[stepNumber] == null
child: state.stepImages[stepNumber] == null
? Container(
height: 128,
decoration: AppDecoration.fillGray.copyWith(
@@ -341,7 +326,7 @@ class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
],
),
)
: Image.file(File(stepImages[stepNumber]!.path),
: Image.file(File(state.stepImages[stepNumber]!.path),
height: 150, fit: BoxFit.cover)),
SizedBox(height: 16),
CustomTextFormField(
@@ -349,28 +334,96 @@ class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
maxLines: 8,
minLines: 8,
fillColor: appTheme.gray50,
controller: state.stepControllers[stepNumber],
),
SizedBox(height: 24),
],
);
}
}
addImage(BuildContext context, bool isLoading) {
if (!isLoading) {
return () async {
final picker = ImagePicker();
XFile? image = await picker.pickImage(source: ImageSource.gallery);
if (image != null) {
context.read<BlogPostCreateBloc>().add(UploadImages(image));
_buildIngredient(Ingredient ingredient) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 10.h),
padding: EdgeInsets.symmetric(
horizontal: 8.h,
vertical: 6.v,
),
decoration: AppDecoration.fillGray.copyWith(
borderRadius: BorderRadiusStyle.roundedBorder5,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: EdgeInsets.only(
left: 7.h,
top: 5.v,
),
child: Text(
ingredient.product_name.length > 8
? ingredient.product_name.substring(0, 8)
: ingredient.product_name,
style: CustomTextStyles.titleMedium16,
),
),
Padding(
padding: EdgeInsets.only(
left: 20.h,
top: 3.v,
bottom: 3.v,
),
child: Text(
ingredient.portion.toString() +
' ' +
ingredient.unit.toString().tr,
),
),
Spacer(),
GestureDetector(
// onTap: removeIngredient(context, ingredient),
child: Padding(
padding: const EdgeInsets.only(right: 10),
child: Icon(
Icons.close,
color: appTheme.blueGray300,
),
),
),
],
),
);
}
RecipeModel createRecipeFromState(RecipeCreateStateLoaded state) {
// Подготовка ингредиентов
List<Ingredient> ingredients = state.ingredients;
// Подготовка шагов (например, можно создать шаги из stepControllers)
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));
}
};
}
}
}
String? _validateTitle(value) {
if (value!.isEmpty) {
return 'Пожалуйста, введите заголовок';
// Подготовка изображения
image.Image pic = image.Image(path: state.recipeImage?.path ?? '');
// Создание экземпляра RecipeModel
return RecipeModel(
id: null,
ingredients: ingredients,
steps: steps,
state: 'draft',
description: state.titleController.text,
type: state.types.first.value,
image: pic,
video_url: state.youtubeLinkController.text.isNotEmpty
? state.youtubeLinkController.text
: null,
cooking_time:
state.cookingTimes.isNotEmpty ? state.cookingTimes.first.value : null,
);
}
return null;
}

View File

@@ -0,0 +1,14 @@
import 'package:eatvio_app/features/recipes/services/recipe_service.dart';
class RecipeRepository {
final RecipeService recipeService;
RecipeRepository({required this.recipeService});
/*Future<RecipeModel> getRecipe(int recipeID) async {
try {
return (await recipeService.getRecipe(recipeID));
} catch (e) {
rethrow;
}
}*/
}

View File

@@ -0,0 +1,13 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/recipes/models/recipe_model.dart';
import 'package:retrofit/retrofit.dart';
part 'recipe_service.g.dart';
@RestApi()
abstract class RecipeService {
factory RecipeService(Dio dio, {String baseUrl}) = _RecipeService;
@GET('/api/recipe/{id}/show/')
Future<Map<String, RecipeModel>> getRecipe(@Path() int id);
}

View File

@@ -0,0 +1,78 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'recipe_service.dart';
// **************************************************************************
// RetrofitGenerator
// **************************************************************************
// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element
class _RecipeService implements RecipeService {
_RecipeService(
this._dio, {
this.baseUrl,
});
final Dio _dio;
String? baseUrl;
@override
Future<Map<String, RecipeModel>> getRecipe(int id) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
const Map<String, dynamic>? _data = null;
final _result = await _dio.fetch<Map<String, dynamic>>(
_setStreamType<Map<String, RecipeModel>>(Options(
method: 'GET',
headers: _headers,
extra: _extra,
)
.compose(
_dio.options,
'/api/recipe/${id}/show/',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(
_dio.options.baseUrl,
baseUrl,
))));
var _value = _result.data!.map((k, dynamic v) =>
MapEntry(k, RecipeModel.fromJson(v as Map<String, dynamic>)));
return _value;
}
RequestOptions _setStreamType<T>(RequestOptions requestOptions) {
if (T != dynamic &&
!(requestOptions.responseType == ResponseType.bytes ||
requestOptions.responseType == ResponseType.stream)) {
if (T == String) {
requestOptions.responseType = ResponseType.plain;
} else {
requestOptions.responseType = ResponseType.json;
}
}
return requestOptions;
}
String _combineBaseUrls(
String dioBaseUrl,
String? baseUrl,
) {
if (baseUrl == null || baseUrl.trim().isEmpty) {
return dioBaseUrl;
}
final url = Uri.parse(baseUrl);
if (url.isAbsolute) {
return url.toString();
}
return Uri.parse(dioBaseUrl).resolveUri(url).toString();
}
}

View File

@@ -19,6 +19,9 @@ import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit
import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart';
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
import 'package:eatvio_app/features/feed/services/feed_service.dart';
import 'package:eatvio_app/features/recipes/presentation/bloc/recipe_create_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';
import 'package:eatvio_app/features/registration/repositories/registration_repository.dart';
import 'package:eatvio_app/features/registration/services/registration_service.dart';
@@ -44,6 +47,8 @@ Future<void> init() async {
sl.registerFactory<DiaryBloc>(() => DiaryBloc(diaryRepository: sl()));
sl.registerFactory<CalendarCubit>(
() => CalendarCubit(calendarRepository: sl()));
sl.registerFactory<RecipeCreateCubit>(
() => RecipeCreateCubit(recipeRepository: sl()));
// Repository
sl.registerLazySingleton<ChatRepository>(
@@ -58,6 +63,8 @@ Future<void> init() async {
() => DiaryRepository(diaryService: sl()));
sl.registerLazySingleton<CalendarRepository>(
() => CalendarRepository(calendarService: sl()));
sl.registerLazySingleton<RecipeRepository>(
() => RecipeRepository(recipeService: sl()));
// Services
sl.registerLazySingleton<ChatService>(() => ChatService(sl()));
@@ -67,6 +74,7 @@ Future<void> init() async {
() => RegistrationService(sl()));
sl.registerLazySingleton<DiaryService>(() => DiaryService(sl()));
sl.registerLazySingleton<CalendarService>(() => CalendarService(sl()));
sl.registerLazySingleton<RecipeService>(() => RecipeService(sl()));
// External
sl.registerLazySingleton(

View File

@@ -9,6 +9,7 @@ import 'package:eatvio_app/features/chat/presentation/pages/chat_select_screen/m
import 'package:eatvio_app/features/diary/presentation/bloc/diary_bloc.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart';
import 'package:eatvio_app/features/recipes/presentation/bloc/recipe_create_cubit.dart';
import 'package:eatvio_app/locator_service.dart' as di;
import 'package:eatvio_app/locator_service.dart';
import 'package:eatvio_app/presentation/profile/my_profile_screen/bloc/my_profile_bloc.dart';
@@ -45,18 +46,34 @@ class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<InitializationCubit>(create: (context) => sl<InitializationCubit>()),
BlocProvider<NewMessagesCubit>(create: (context) => sl<NewMessagesCubit>()),
BlocProvider<NotificationsCubit>(create: (context) => sl<NotificationsCubit>()),
BlocProvider<ThemeBloc>(create: (context) => ThemeBloc(ThemeState(themeType: PrefUtils().getThemeData()))),
BlocProvider<FeedCubit>(create: (context) => sl<FeedCubit>()..loadFeedBlogPostList()),
BlocProvider<RecipesCubit>(create: (context) => sl<RecipesCubit>()..loadRecipesList()),
BlocProvider<MyProfileBloc>(create: (context) => MyProfileBloc(MyProfileState(myProfileModelObj: MyProfileModel()))..add(MyProfileInitialEvent())),
BlocProvider<DiaryBloc>(create: (context) => sl<DiaryBloc>()..add(LoadDiary())),
BlocProvider<InitializationCubit>(
create: (context) => sl<InitializationCubit>()),
BlocProvider<NewMessagesCubit>(
create: (context) => sl<NewMessagesCubit>()),
BlocProvider<NotificationsCubit>(
create: (context) => sl<NotificationsCubit>()),
BlocProvider<ThemeBloc>(
create: (context) =>
ThemeBloc(ThemeState(themeType: PrefUtils().getThemeData()))),
BlocProvider<FeedCubit>(
create: (context) => sl<FeedCubit>()..loadFeedBlogPostList()),
BlocProvider<RecipesCubit>(
create: (context) => sl<RecipesCubit>()..loadRecipesList()),
BlocProvider<MyProfileBloc>(
create: (context) => MyProfileBloc(
MyProfileState(myProfileModelObj: MyProfileModel()))
..add(MyProfileInitialEvent())),
BlocProvider<DiaryBloc>(
create: (context) => sl<DiaryBloc>()..add(LoadDiary())),
BlocProvider<ChatSelectBloc>(
create: (context) => ChatSelectBloc(ChatSelectState(chatSelectModelObj: ChatSelectModel()))..add(ChatSelectInitialEvent()),
create: (context) => ChatSelectBloc(
ChatSelectState(chatSelectModelObj: ChatSelectModel()))
..add(ChatSelectInitialEvent()),
),
BlocProvider<CalendarCubit>(create: (context) => sl<CalendarCubit>()..loadCalendarList()),
BlocProvider<CalendarCubit>(
create: (context) => sl<CalendarCubit>()..loadCalendarList()),
BlocProvider<RecipeCreateCubit>(
create: (context) => sl<RecipeCreateCubit>()..loadRecipeCreate()),
],
child: BlocListener<InitializationCubit, InitializationState>(
listener: (BuildContext context, InitializationState state) {},

View File

@@ -1,193 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'package:eatvio_app/app_router.dart';
import 'package:eatvio_app/presentation/recipe/recipe_create_screen/models/recipe_create_model.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hive/hive.dart';
import 'package:http/http.dart' as http;
import '/core/app_export.dart';
part 'recipe_create_event.dart';
part 'recipe_create_state.dart';
/// A bloc that manages the state of a RecipeCreate according to the event that is dispatched to it.
class RecipeCreateBloc extends Bloc<RecipeCreateEvent, RecipeCreateState> {
RecipeCreateBloc(RecipeCreateState initialState) : super(initialState) {
on<RecipeCreateInitialEvent>(_onInitialize);
on<CreateRecipe>(_createRecipe);
on<AddStep>(_addStep);
on<RemoveStep>(_removeStep);
on<ReloadScreen>(_reloadScreen);
}
_onInitialize(
RecipeCreateInitialEvent event,
Emitter<RecipeCreateState> emit,
) async {
final box = await Hive.openBox('token');
final token = box.get('token');
if (token == null) {
AppRouter.navigatorKey.currentContext?.goNamed('authScreen');
}
if (state is RecipeCreateLoadedState) {
var currentState = state as RecipeCreateLoadedState;
emit(
currentState.copyWith(
recipeModelObj: currentState.recipeModelObj,
title: currentState.title,
token: currentState.token,
description: currentState.description,
video_url: currentState.video_url,
steps: currentState.steps,
stepDescriptions: currentState.stepDescriptions,
tags: currentState.tags,
),
);
} else {
final Map<String, dynamic> step = {
'description': '',
'image': '',
};
emit(
RecipeCreateLoadedState(
token: token,
title: TextEditingController(),
description: TextEditingController(),
video_url: TextEditingController(),
tags: TextEditingController(),
stepDescriptions: [TextEditingController()],
steps: [step],
),
);
}
}
FutureOr<void> _addStep(
AddStep event,
Emitter<RecipeCreateState> emit,
) async {
if (state is RecipeCreateLoadedState) {
var currentState = state as RecipeCreateLoadedState;
List<dynamic>? updatedSteps = currentState.steps;
List<TextEditingController>? updatedStepDescriptions = currentState.stepDescriptions;
updatedSteps!.add({
'description': '',
'image': '',
});
updatedStepDescriptions!.add(TextEditingController());
emit(
currentState.copyWith(
steps: updatedSteps,
stepDescriptions: updatedStepDescriptions,
),
);
await _reloadScreen(event, emit);
}
}
FutureOr<void> _removeStep(
RemoveStep event,
Emitter<RecipeCreateState> emit,
) async {
if (state is RecipeCreateLoadedState) {
var currentState = state as RecipeCreateLoadedState;
List<dynamic>? updatedSteps = currentState.steps;
List<TextEditingController>? updatedStepDescriptions = currentState.stepDescriptions;
updatedSteps!.removeAt(event.id);
updatedStepDescriptions!.removeAt(event.id);
emit(
currentState.copyWith(
steps: updatedSteps,
stepDescriptions: updatedStepDescriptions,
),
);
await _reloadScreen(event, emit);
}
}
FutureOr<void> _createRecipe(
CreateRecipe event,
Emitter<RecipeCreateState> emit,
) async {
if (state is RecipeCreateLoadedState) {
var currentState = state as RecipeCreateLoadedState;
var response = await sendRecipe(
currentState.title?.text,
currentState.video_url?.text,
currentState.description?.text,
currentState.steps,
);
if (response.statusCode == 200) {
AppRouter.navigatorKey.currentContext?.goNamed('recipeSelectScreen');
} else {
print(
'Failed to create blog post. Status Code: ${response.statusCode} ${response.body}',
);
}
}
}
FutureOr<http.Response> sendRecipe(
String? title,
String? video_url,
String? description,
List<dynamic>? steps,
) async {
String url = '${baseUrl}/api/recipe/create';
final Map<String, dynamic> credentials = {
'title': title,
'description': description,
'video_url': video_url,
'steps': steps,
'status': 'active',
};
var response = await http.post(
Uri.parse(url),
headers: {
'Content-Type': 'application/json',
},
body: json.encode(credentials),
encoding: utf8,
);
return response;
}
FutureOr<void> _reloadScreen(
RecipeCreateEvent? event,
Emitter<RecipeCreateState> emit,
) async {
print('Reloading..');
final box = await Hive.openBox('token');
final token = box.get('token');
if (token == null) {
AppRouter.navigatorKey.currentContext?.goNamed('authScreen');
}
if (state is RecipeCreateLoadedState) {
var currentState = state as RecipeCreateLoadedState;
emit(
currentState.copyWith(
recipeModelObj: currentState.recipeModelObj,
title: currentState.title,
token: currentState.token,
description: currentState.description,
video_url: currentState.video_url,
steps: currentState.steps,
stepDescriptions: currentState.stepDescriptions,
tags: currentState.tags,
),
);
}
}
}

View File

@@ -1,44 +0,0 @@
// ignore_for_file: must_be_immutable
part of 'recipe_create_bloc.dart';
/// Abstract class for all events that can be dispatched from the
///RecipeCreate widget.
///
/// Events must be immutable and implement the [Equatable] interface.
@immutable
abstract class RecipeCreateEvent extends Equatable {}
/// Event that is dispatched when the RecipeCreate widget is first created.
class RecipeCreateInitialEvent extends RecipeCreateEvent {
@override
List<Object?> get props => [];
}
class CreateRecipe extends RecipeCreateEvent {
@override
List<Object?> get props => [];
}
class AddStep extends RecipeCreateEvent {
AddStep();
@override
List<Object?> get props => [];
}
class RemoveStep extends RecipeCreateEvent {
RemoveStep(this.id);
final int id;
@override
List<Object?> get props => [id];
}
class ReloadScreen extends RecipeCreateEvent {
ReloadScreen();
@override
List<Object?> get props => [];
}

View File

@@ -1,92 +0,0 @@
// ignore_for_file: must_be_immutable
part of 'recipe_create_bloc.dart';
/// Represents the state of RecipeCreate in the application.
///
class RecipeCreateState extends Equatable {
RecipeCreateState({
this.recipeModelObj,
});
RecipeCreateModel? recipeModelObj;
@override
List<Object?> get props => [recipeModelObj];
RecipeCreateState copyWith({RecipeCreateModel? recipeModelObj}) {
return RecipeCreateState(
recipeModelObj: recipeModelObj ?? this.recipeModelObj,
);
}
}
class RecipeCreateLoadedState extends RecipeCreateState {
RecipeCreateLoadedState({
this.recipeModelObj,
this.token,
this.title,
this.description,
this.video_url,
this.tags,
this.stepDescriptions,
this.steps,
});
RecipeCreateModel? recipeModelObj;
String? token;
TextEditingController? title;
TextEditingController? description;
TextEditingController? video_url;
TextEditingController? tags;
List<dynamic>? steps;
List<TextEditingController>? stepDescriptions;
@override
List<Object?> get props => [
recipeModelObj,
title,
token,
description,
video_url,
steps,
tags,
stepDescriptions,
];
RecipeCreateLoadedState copyWith({
RecipeCreateModel? recipeModelObj,
String? token,
TextEditingController? title,
TextEditingController? description,
TextEditingController? video_url,
TextEditingController? tags,
List<dynamic>? steps,
List<TextEditingController>? stepDescriptions,
}) {
return RecipeCreateLoadedState(
recipeModelObj: recipeModelObj ?? this.recipeModelObj,
title: title ?? this.title,
description: description ?? this.description,
video_url: video_url ?? this.video_url,
token: token ?? this.token,
steps: steps ?? this.steps,
tags: tags ?? this.tags,
stepDescriptions: stepDescriptions ?? this.stepDescriptions,
);
}
}
class RecipeCreateErrorState extends RecipeCreateState {
final String errorMessage;
RecipeCreateErrorState(this.errorMessage);
@override
List<Object> get props => [errorMessage];
}
class RecipeCreateInitialState extends RecipeCreateState {
RecipeCreateInitialState({required RecipeCreateModel recipeModelObj}) : super(recipeModelObj: recipeModelObj);
}

View File

@@ -1,17 +0,0 @@
// ignore_for_file: must_be_immutable
import 'package:equatable/equatable.dart';
/// This class defines the variables used in the [recipe_create_screen],
/// and is typically used to hold data that is passed between different parts of the application.
class RecipeCreateModel extends Equatable {
RecipeCreateModel();
RecipeCreateModel copyWith() {
return RecipeCreateModel();
}
@override
List<Object?> get props => [];
}

View File

@@ -1,413 +0,0 @@
import 'package:eatvio_app/core/app_export.dart';
import 'package:eatvio_app/widgets/app_bar/appbar_leading_image.dart';
import 'package:eatvio_app/widgets/app_bar/appbar_subtitle.dart';
import 'package:eatvio_app/widgets/app_bar/custom_app_bar.dart';
import 'package:eatvio_app/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/widgets/custom_text_form_field.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'bloc/recipe_create_bloc.dart';
import 'models/recipe_create_model.dart';
class RecipeCreateScreen extends StatelessWidget {
const RecipeCreateScreen({Key? key}) : super(key: key);
static Widget builder(BuildContext context) {
return BlocProvider<RecipeCreateBloc>(
create: (context) => RecipeCreateBloc(
RecipeCreateState(recipeModelObj: RecipeCreateModel()))
..add(RecipeCreateInitialEvent()),
child: RecipeCreateScreen(),
);
}
Widget build(BuildContext context) {
return BlocBuilder<RecipeCreateBloc, RecipeCreateState>(
builder: (context, state) {
if (state is RecipeCreateInitialState) {
// Это начальное состояние, данные ещё не загружены.
return Container(
color: Colors.white,
);
} else if (state is RecipeCreateLoadedState) {
// Данные загружены, можно строить экран.
return _buildRecipeCreate(context, state);
} else if (state is RecipeCreateErrorState) {
// Произошла ошибка при загрузке данных.
return Center(child: Text(state.errorMessage));
} else {
// Другие состояния, если они есть.
return Container(); // Пустой контейнер для необработанных состояний.
}
},
);
}
_buildRecipeCreate(BuildContext context, RecipeCreateLoadedState state) {
mediaQueryData = MediaQuery.of(context);
List<Widget> steps = [];
if (state.steps != null) {
for (int i = 0; i < state.steps!.length; i++) {
steps.add(_buildStep(context, state, i));
}
}
return RefreshIndicator(
onRefresh: refreshPage(context),
child: Scaffold(
appBar: AppBar(
toolbarHeight: 0,
automaticallyImplyLeading: false,
),
resizeToAvoidBottomInset: false,
body: Container(
width: double.maxFinite,
decoration: BoxDecoration(color: Colors.grey[200]),
child: ListView(
children: [
_buldAppBar(context),
SizedBox(height: 20.v),
_buildRecipeInfo(state, context),
SizedBox(height: 20.v),
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: steps,
),
SizedBox(height: 20.v),
_buildAdditional(context, state),
],
),
),
),
);
}
_buldAppBar(BuildContext context) {
return Container(
margin: EdgeInsets.all(16.h),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.all(Radius.circular(12)),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 2,
blurRadius: 5,
offset: Offset(0, 3),
),
],
),
child: CustomAppBar(
height: 54.v,
leadingWidth: 28.h,
leading: AppbarLeadingImage(
imagePath: ImageConstant.imgArrowLeft,
margin: EdgeInsets.only(left: 16.h, top: 15.v, bottom: 15.v),
onTap: onTapBack(context),
),
title: Align(
alignment: Alignment.center,
child: AppbarSubtitle(
text: "Добавление рецепта",
),
),
),
);
}
_buildRecipeInfo(RecipeCreateLoadedState state, BuildContext context) {
return Container(
margin: EdgeInsets.only(
left: 16.h,
right: 16.h,
bottom: 10.h,
),
padding: EdgeInsets.all(12.h),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.all(Radius.circular(12)),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 2,
blurRadius: 5,
offset: Offset(0, 3),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 16.0),
Align(alignment: Alignment.centerLeft, child: Text("Изображение")),
_buildAddPhoto(context, state),
SizedBox(height: 16.0),
Align(
alignment: Alignment.centerLeft, child: Text("Название рецепта")),
CustomTextFormField(
controller: state.title,
hintText: "Введите название рецепта",
fillColor: appTheme.gray50,
borderDecoration: OutlineInputBorder(
borderRadius: BorderRadius.circular(5.h),
borderSide: BorderSide(
color: appTheme.gray50,
width: 1,
),
),
),
SizedBox(height: 16.0),
Align(alignment: Alignment.centerLeft, child: Text("Описание")),
CustomTextFormField(
controller: state.description,
hintText: "Введите описание рецепта",
maxLines: 4,
fillColor: appTheme.gray50,
borderDecoration: OutlineInputBorder(
borderRadius: BorderRadius.circular(5.h),
borderSide: BorderSide(
color: appTheme.gray50,
width: 1,
),
),
),
SizedBox(height: 16.0),
],
),
);
}
refreshPage(BuildContext context) {
return () async {
context.read<RecipeCreateBloc>().add(ReloadScreen());
};
}
_buildAddPhoto(BuildContext context, RecipeCreateLoadedState state) {
bool isLoading = false;
return GestureDetector(
child: Stack(
children: [
if (!isLoading)
Container(
padding: EdgeInsets.symmetric(
horizontal: 40.h,
vertical: 80.v,
),
decoration: AppDecoration.fillGray.copyWith(
borderRadius: BorderRadiusStyle.roundedBorder5,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: EdgeInsets.only(
left: 7.h,
top: 5.v,
),
child: Text(
"Добавить фото",
selectionColor: appTheme.blueA400,
),
),
Spacer(),
Icon(
Icons.photo_library,
color: appTheme.blueGray300,
),
],
),
),
/*if (isLoading)
Container(
margin: EdgeInsets.symmetric(horizontal: 10.h),
padding: EdgeInsets.symmetric(
horizontal: 40.h,
vertical: 80.v,
),
decoration: AppDecoration.fillGray.copyWith(
borderRadius: BorderRadiusStyle.roundedBorder5,
),
child: Center(
child: CircularProgressIndicator(
color: appTheme
.blueGray300), // Замените на свой прелоадер при необходимости
),
),*/
],
),
);
}
_buildStep(BuildContext context, RecipeCreateLoadedState state, int i) {
bool isLastStep = i == state.steps!.length - 1;
bool isFirstStep = i == 0;
return Container(
margin: EdgeInsets.only(
left: 16.h,
right: 16.h,
bottom: 10.h,
),
padding: EdgeInsets.all(12.h),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.all(Radius.circular(12)),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 2,
blurRadius: 5,
offset: Offset(0, 3),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Align(
alignment: Alignment.centerLeft,
child: Text(
"Шаг ${i + 1}",
style: CustomTextStyles.titleMediumSemiBold,
),
),
SizedBox(height: 8.0),
CustomTextFormField(
controller: state.stepDescriptions?[i],
hintText: "Введите описание шага",
maxLines: 4,
fillColor: appTheme.gray50,
borderDecoration: OutlineInputBorder(
borderRadius: BorderRadius.circular(5.h),
borderSide: BorderSide(
color: appTheme.gray50,
width: 1,
),
),
),
SizedBox(height: 16.0),
_buildAddPhoto(context, state),
SizedBox(height: 16.0),
if (isFirstStep && isLastStep)
CustomOutlinedButton(
onPressed: () => context.read<RecipeCreateBloc>().add(AddStep()),
text: "Добавить шаг",
buttonStyle: CustomButtonStyles.outlinePrimaryTL15,
),
if (isLastStep &&
!isFirstStep) // Добавляем кнопку "Добавить шаг" только для последнего шага
Column(
children: [
SizedBox(height: 16.0),
Row(
children: [
Flexible(
flex: 5,
child: CustomOutlinedButton(
onPressed: () =>
context.read<RecipeCreateBloc>().add(AddStep()),
text: "Добавить шаг",
buttonStyle: CustomButtonStyles.outlinePrimaryTL15,
),
),
SizedBox(width: 8), // Add some spacing between the buttons
Flexible(
flex: 1,
child: CustomOutlinedButton(
text: "х",
onPressed: () =>
context.read<RecipeCreateBloc>().add(RemoveStep(i)),
buttonStyle: CustomButtonStyles.outlinePrimaryTL15,
),
),
],
),
],
),
if (!isLastStep && !isFirstStep)
CustomOutlinedButton(
onPressed: () =>
context.read<RecipeCreateBloc>().add(RemoveStep(i)),
text: "x",
buttonStyle: CustomButtonStyles.outlinePrimaryTL15,
),
],
),
);
}
_buildAdditional(BuildContext context, RecipeCreateLoadedState state) {
return Container(
margin: EdgeInsets.only(
left: 16.h,
right: 16.h,
bottom: 10.h,
),
padding: EdgeInsets.all(12.h),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.all(Radius.circular(12)),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 2,
blurRadius: 5,
offset: Offset(0, 3),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Align(alignment: Alignment.centerLeft, child: Text("Теги")),
CustomTextFormField(
controller: state.tags,
hintText: "Введите теги, через запятую",
fillColor: appTheme.gray50,
borderDecoration: OutlineInputBorder(
borderRadius: BorderRadius.circular(5.h),
borderSide: BorderSide(
color: appTheme.gray50,
width: 1,
),
),
),
SizedBox(height: 8.0),
Align(
alignment: Alignment.centerLeft,
child: Text("Ссылка на видео (необязательно)")),
CustomTextFormField(
controller: state.video_url,
hintText: "YouTube",
fillColor: appTheme.gray50,
borderDecoration: OutlineInputBorder(
borderRadius: BorderRadius.circular(5.h),
borderSide: BorderSide(
color: appTheme.gray50,
width: 1,
),
),
),
SizedBox(height: 16.0),
CustomOutlinedButton(
text: "Добавить рецепт",
buttonStyle: CustomButtonStyles.outlinePrimaryTL15,
),
],
),
);
}
Function onTapBack(BuildContext context) {
return () {
onTapArrowLeft(context);
};
}
onTapArrowLeft(BuildContext context) {
GoRouter.of(context).pop();
}
}