forked from eatvio/eatvio_app
Merge remote-tracking branch 'upstream/main'
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/token_storage.dart';
|
||||
@@ -41,6 +42,7 @@ class AuthScreen extends StatefulWidget {
|
||||
class _AuthScreenState extends State<AuthScreen> {
|
||||
bool checkboxState = false;
|
||||
bool hidePassword = true;
|
||||
final baseUrlController = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -49,10 +51,54 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: SvgPicture.asset(
|
||||
ImageConstant.imgLogoEatvio,
|
||||
height: 48,
|
||||
fit: BoxFit.contain,
|
||||
title: GestureDetector(
|
||||
onLongPress: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('Base url без https:// и /api'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CustomTextFormField(
|
||||
controller: baseUrlController,
|
||||
hintText: 'dev2.eatvio.com',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
baseUrl = 'https://${baseUrlController.text}';
|
||||
sl<Dio>().options.baseUrl = baseUrl;
|
||||
},
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Ink(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: appTheme.blueA400,
|
||||
),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'Сохранить',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: SvgPicture.asset(
|
||||
ImageConstant.imgLogoEatvio,
|
||||
height: 48,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
|
||||
@@ -35,6 +35,7 @@ class _DiaryScreenState extends State<DiaryScreen> {
|
||||
CalendarWidget(),
|
||||
if (state is DiaryStateLoading) const Loader(),
|
||||
if (state is DiaryStateLoaded) MealWidget(),
|
||||
const SizedBox(height: 12),
|
||||
CaloriesChartWidget(),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -12,8 +12,8 @@ class CaloriesChartWidget extends StatelessWidget {
|
||||
builder: (BuildContext context, DiaryState state) {
|
||||
if (state is DiaryStateLoaded) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
margin: const EdgeInsets.only(top: 12),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: Colors.white,
|
||||
|
||||
@@ -215,6 +215,9 @@ class _FeedScreenState extends State<FeedScreen> {
|
||||
),
|
||||
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,
|
||||
),
|
||||
|
||||
@@ -11,7 +11,7 @@ import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class MenuScreen extends StatelessWidget {
|
||||
const MenuScreen({super.key});
|
||||
MenuScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -192,10 +192,10 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
),
|
||||
_subscriptionsAndSubscribers(state),
|
||||
if (state.profile.badges.isNotEmpty) Padding(padding: EdgeInsets.only(bottom: 16), child: _badges(state)),
|
||||
if (state.profile.role == 'consultant') Padding(padding: EdgeInsets.only(bottom: 16), child: _socialLinks(state)),
|
||||
if (state.profile.role == 'consultant') Padding(padding: EdgeInsets.only(bottom: 16), child: _education(state)),
|
||||
if (state.profile.role == 'consultant') Padding(padding: EdgeInsets.only(bottom: 16), child: _diplomasAndCertificates(state)),
|
||||
if (state.profile.role == 'consultant') Padding(padding: EdgeInsets.only(bottom: 16), child: _activePlans(state)),
|
||||
if (state.profile.role == 'consultant') _socialLinks(state),
|
||||
if (state.profile.role == 'consultant') _education(state),
|
||||
if (state.profile.role == 'consultant') _diplomasAndCertificates(state),
|
||||
if (state.profile.role == 'consultant') _activePlans(state),
|
||||
_blogPosts(state),
|
||||
],
|
||||
),
|
||||
@@ -228,11 +228,11 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
),
|
||||
_subscriptionsAndSubscribers(state),
|
||||
if (state.profile.badges.isNotEmpty) Padding(padding: EdgeInsets.only(bottom: 16), child: _badges(state)),
|
||||
if (state.profile.role == 'consultant') Padding(padding: EdgeInsets.only(bottom: 16), child: _socialLinks(state)),
|
||||
if (state.profile.role == 'consultant') Padding(padding: EdgeInsets.only(bottom: 16), child: _education(state)),
|
||||
if (state.profile.role == 'consultant') Padding(padding: EdgeInsets.only(bottom: 16), child: _diplomasAndCertificates(state)),
|
||||
if (state.profile.role == 'consultant') _socialLinks(state),
|
||||
if (state.profile.role == 'consultant') _education(state),
|
||||
if (state.profile.role == 'consultant') _diplomasAndCertificates(state),
|
||||
Padding(padding: EdgeInsets.only(bottom: 16), child: _charts()),
|
||||
if (state.profile.role == 'consultant') Padding(padding: EdgeInsets.only(bottom: 16), child: _activePlans(state)),
|
||||
if (state.profile.role == 'consultant') _activePlans(state),
|
||||
_newBlogPostButton(),
|
||||
_blogPosts(state),
|
||||
],
|
||||
@@ -249,6 +249,9 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
bloc: plansCubit,
|
||||
builder: (BuildContext context, PlansState state) {
|
||||
if (state is PlansStateLoaded) {
|
||||
if (state.plansList.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
@@ -366,57 +369,60 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
}
|
||||
|
||||
_diplomasAndCertificates(ProfileStateLoaded state) {
|
||||
if (state.profile.role != 'consultant') {
|
||||
if (state.profile.role != 'consultant' || state.profile.diplomasAndCertificates == null || state.profile.diplomasAndCertificates!.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Дипломы\nи сертификаты',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF323F4B),
|
||||
fontSize: 24,
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w600,
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Дипломы\nи сертификаты',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF323F4B),
|
||||
fontSize: 24,
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
widget.author == null ? 'Редактировать' : '',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF2F80ED),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
Text(
|
||||
widget.author == null ? 'Редактировать' : '',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF2F80ED),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: MediaQuery.of(context).size.width - 2,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Material(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
if (state.profile.diplomasAndCertificates != null)
|
||||
...state.profile.diplomasAndCertificates!.map((d) => _diplomaImage(baseUrl + '/storage/' + d.path)).toList(),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: MediaQuery.of(context).size.width - 2,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Material(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
if (state.profile.diplomasAndCertificates != null)
|
||||
...state.profile.diplomasAndCertificates!.map((d) => _diplomaImage(baseUrl + '/storage/' + d.path)).toList(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -447,53 +453,56 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
}
|
||||
|
||||
_education(ProfileStateLoaded state) {
|
||||
if (state.profile.role != 'consultant') {
|
||||
if (state.profile.role != 'consultant' || state.profile.educationResource?.completedCourses == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Образование',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF323F4B),
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w600,
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Образование',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF323F4B),
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
widget.author == null ? 'Редактировать' : '',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF2F80ED),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
Text(
|
||||
widget.author == null ? 'Редактировать' : '',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF2F80ED),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Material(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Column(
|
||||
children: [
|
||||
if (state.profile.educationResource?.completedCourses != null)
|
||||
...state.profile.educationResource!.completedCourses!.map((c) => _educationBlock(c.values.first.title, c.values.first.date)).toList(),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Material(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Column(
|
||||
children: [
|
||||
if (state.profile.educationResource?.completedCourses != null)
|
||||
...state.profile.educationResource!.completedCourses!.map((c) => _educationBlock(c.values.first.title, c.values.first.date)).toList(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -539,7 +548,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4) + const EdgeInsets.only(bottom: 16),
|
||||
child: Material(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
@@ -556,17 +565,19 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
);
|
||||
},
|
||||
'assets/icons/vk.svg',
|
||||
state.profile.educationResource?.vkLink == null ? true : false,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
_socialLinkButton(
|
||||
() {
|
||||
launchUrl(
|
||||
Uri.parse(
|
||||
state.profile.educationResource?.tgLink ?? '',
|
||||
state.profile.educationResource?.vkLink ?? '',
|
||||
),
|
||||
);
|
||||
},
|
||||
'assets/icons/telegram.svg',
|
||||
state.profile.educationResource?.vkLink == null ? true : false,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
_socialLinkButton(
|
||||
@@ -578,6 +589,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
);
|
||||
},
|
||||
'assets/icons/web.svg',
|
||||
state.profile.educationResource?.personalSiteLink == null ? true : false,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -586,9 +598,9 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
_socialLinkButton(Function() onTap, String icon) {
|
||||
_socialLinkButton(Function() onTap, String icon, bool disabled) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
onTap: disabled ? null : onTap,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Ink(
|
||||
width: MediaQuery.of(context).size.width / 3 - (56 / 3),
|
||||
@@ -601,6 +613,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
child: SvgPicture.asset(
|
||||
icon,
|
||||
width: 24,
|
||||
colorFilter: ColorFilter.mode(Colors.black12, BlendMode.srcIn),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
50
lib/features/recipes/entities/recipe_create_entity.dart
Normal file
50
lib/features/recipes/entities/recipe_create_entity.dart
Normal file
@@ -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,300 +0,0 @@
|
||||
import 'package:eatvio_app/core/data/selectionPopupModel/selection_popup_model.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/presentation/bloc/recipe_create_state.dart';
|
||||
import 'package:eatvio_app/features/recipes/reporitories/recipe_repository.dart';
|
||||
import 'package:eatvio_app/features/recipes/models/product.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
class RecipeCreateCubit extends Cubit<RecipeCreateState> {
|
||||
final RecipeRepository recipeRepository;
|
||||
|
||||
RecipeCreateCubit({required this.recipeRepository}) : super(const RecipeCreateState.loading());
|
||||
|
||||
loadRecipeById(String recipeID) async {
|
||||
emit(const RecipeCreateStateLoading());
|
||||
try {
|
||||
final RecipeModel recipe = await recipeRepository.getRecipe(recipeID);
|
||||
final List<SelectionPopupModel> types = [
|
||||
SelectionPopupModel(title: 'Первые блюда', value: 'first_meals'),
|
||||
SelectionPopupModel(title: 'Вторые блюда', value: 'second_meals'),
|
||||
SelectionPopupModel(title: 'Cалаты', value: 'salads'),
|
||||
SelectionPopupModel(title: 'Перекусы/закуски', value: 'snacks'),
|
||||
SelectionPopupModel(title: 'Сладости', value: 'sweets'),
|
||||
SelectionPopupModel(title: 'Напитки', value: 'drinks'),
|
||||
SelectionPopupModel(title: 'Прочее', value: 'other'),
|
||||
];
|
||||
final List<SelectionPopupModel> cookingTimes = [
|
||||
SelectionPopupModel(title: '10 минут', value: 10),
|
||||
SelectionPopupModel(title: '20 минут', value: 20),
|
||||
SelectionPopupModel(title: '30 минут', value: 30),
|
||||
SelectionPopupModel(title: '1 час', value: 60),
|
||||
SelectionPopupModel(title: '2 часа', value: 120),
|
||||
SelectionPopupModel(title: '10 дней', value: 14400),
|
||||
];
|
||||
final List<SelectionPopupModel> statuses = [
|
||||
SelectionPopupModel(title: 'Активна', value: 'active'),
|
||||
SelectionPopupModel(title: 'Неактивна', value: 'inactive'),
|
||||
];
|
||||
final selectedType = types.firstWhere(
|
||||
(type) => type.value == recipe.type,
|
||||
orElse: () => types[0],
|
||||
);
|
||||
|
||||
final selectedCookingTime = cookingTimes.firstWhere(
|
||||
(time) => time.value == recipe.cooking_time,
|
||||
orElse: () => cookingTimes[0],
|
||||
);
|
||||
|
||||
final selectedStatus = statuses.firstWhere(
|
||||
(status) => status.value == recipe.state,
|
||||
orElse: () => statuses[0],
|
||||
);
|
||||
|
||||
final stepControllers = recipe.steps?.map((step) {
|
||||
return TextEditingController(text: step?.description);
|
||||
}).toList();
|
||||
|
||||
final stepImages = recipe.steps?.map((step) {
|
||||
return step?.image != null ? XFile(step!.image!) : null;
|
||||
}).toList();
|
||||
|
||||
final ingredients = recipe.ingredients?.map((ingredient) {
|
||||
return Ingredient(
|
||||
name: ingredient!.name,
|
||||
portion: ingredient.portion,
|
||||
unit: ingredient.unit,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
emit(RecipeCreateState.loaded(
|
||||
picker: ImagePicker(),
|
||||
recipeImage: XFile(recipe.image!),
|
||||
titleController: TextEditingController(text: recipe.title),
|
||||
productNameController: TextEditingController(),
|
||||
ingredientController: TextEditingController(),
|
||||
stepDescriptionController: TextEditingController(text: recipe.description),
|
||||
videoLinkController: TextEditingController(text: recipe.video_url),
|
||||
stepControllers: stepControllers ?? [],
|
||||
stepImages: stepImages ?? [],
|
||||
autocompleteList: [],
|
||||
ingredients: ingredients ?? [],
|
||||
numberOfSteps: 1,
|
||||
ingredientAsItems: false,
|
||||
types: types,
|
||||
cookingTimes: cookingTimes,
|
||||
statuses: statuses,
|
||||
selectedType: selectedType,
|
||||
selectedCookingTime: selectedCookingTime,
|
||||
selectedStatus: selectedStatus,),);
|
||||
} catch (e) {
|
||||
emit(RecipeCreateState.error(errorMessage: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
loadRecipeCreate() {
|
||||
emit(const RecipeCreateStateLoading());
|
||||
try {
|
||||
final List<SelectionPopupModel> types = [
|
||||
SelectionPopupModel(title: 'Первые блюда', value: 'first_meals'),
|
||||
SelectionPopupModel(title: 'Вторые блюда', value: 'second_meals'),
|
||||
SelectionPopupModel(title: 'Cалаты', value: 'salads'),
|
||||
SelectionPopupModel(title: 'Перекусы/закуски', value: 'snacks'),
|
||||
SelectionPopupModel(title: 'Сладости', value: 'sweets'),
|
||||
SelectionPopupModel(title: 'Напитки', value: 'drinks'),
|
||||
SelectionPopupModel(title: 'Прочее', value: 'other'),
|
||||
];
|
||||
final List<SelectionPopupModel> cookingTimes = [
|
||||
SelectionPopupModel(title: '10 минут', value: 10),
|
||||
SelectionPopupModel(title: '20 минут', value: 20),
|
||||
SelectionPopupModel(title: '30 минут', value: 30),
|
||||
SelectionPopupModel(title: '1 час', value: 60),
|
||||
SelectionPopupModel(title: '2 часа', value: 120),
|
||||
SelectionPopupModel(title: '10 дней', value: 14400),
|
||||
];
|
||||
final List<SelectionPopupModel> statuses = [
|
||||
SelectionPopupModel(title: 'Активна', value: 'active'),
|
||||
SelectionPopupModel(title: 'Неактивна', value: 'inactive'),
|
||||
];
|
||||
emit(RecipeCreateState.loaded(
|
||||
picker: ImagePicker(),
|
||||
titleController: TextEditingController(),
|
||||
productNameController: TextEditingController(),
|
||||
ingredientController: TextEditingController(),
|
||||
stepDescriptionController: TextEditingController(),
|
||||
videoLinkController: TextEditingController(),
|
||||
stepControllers: [TextEditingController()],
|
||||
stepImages: [null],
|
||||
autocompleteList: [],
|
||||
ingredients: [],
|
||||
numberOfSteps: 1,
|
||||
ingredientAsItems: false,
|
||||
types: types,
|
||||
cookingTimes: cookingTimes,
|
||||
statuses: statuses,
|
||||
selectedType: types[0],
|
||||
selectedCookingTime: cookingTimes[0],
|
||||
selectedStatus: statuses[0],),);
|
||||
} catch (e) {
|
||||
emit(RecipeCreateState.error(errorMessage: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
publishRecipe(RecipeModel recipe) async {
|
||||
await recipeRepository.publishRecipe(recipe);
|
||||
}
|
||||
|
||||
editRecipe(RecipeModel recipe, String id) async {
|
||||
await recipeRepository.editRecipe(recipe, id);
|
||||
}
|
||||
|
||||
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.name == ingredient.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 = List<XFile?>.from(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()));
|
||||
}
|
||||
}
|
||||
|
||||
inputProduct(String input) async {
|
||||
if (state is RecipeCreateStateLoaded) {
|
||||
var currentState = state as RecipeCreateStateLoaded;
|
||||
|
||||
final Map<String, List<Product>> productNutritionalValues = await recipeRepository.inputProduct(input);
|
||||
|
||||
var updatedState = currentState.copyWith(
|
||||
productNameController: TextEditingController(text: input),
|
||||
autocompleteList: productNutritionalValues['product_nutritional_value'] ?? [],
|
||||
);
|
||||
|
||||
emit(updatedState);
|
||||
}
|
||||
}
|
||||
|
||||
changeType(SelectionPopupModel type) {
|
||||
if (state is RecipeCreateStateLoaded) {
|
||||
var currentState = state as RecipeCreateStateLoaded;
|
||||
RecipeCreateStateLoaded updatedState = currentState.copyWith(
|
||||
selectedType: type,
|
||||
);
|
||||
emit(updatedState);
|
||||
}
|
||||
}
|
||||
|
||||
changeStatus(SelectionPopupModel status) {
|
||||
if (state is RecipeCreateStateLoaded) {
|
||||
var currentState = state as RecipeCreateStateLoaded;
|
||||
RecipeCreateStateLoaded updatedState = currentState.copyWith(
|
||||
selectedStatus: status,
|
||||
);
|
||||
emit(updatedState);
|
||||
}
|
||||
}
|
||||
|
||||
changeCookingTime(SelectionPopupModel cookingTime) {
|
||||
if (state is RecipeCreateStateLoaded) {
|
||||
var currentState = state as RecipeCreateStateLoaded;
|
||||
RecipeCreateStateLoaded updatedState = currentState.copyWith(
|
||||
selectedCookingTime: cookingTime,
|
||||
);
|
||||
emit(updatedState);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import 'package:eatvio_app/core/data/selectionPopupModel/selection_popup_model.dart';
|
||||
import 'package:eatvio_app/features/recipes/models/recipe_model.dart';
|
||||
import 'package:eatvio_app/features/recipes/models/ingredient.dart';
|
||||
import 'package:eatvio_app/features/recipes/models/product.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:image_picker/image_picker.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 videoLinkController,
|
||||
required List<TextEditingController> stepControllers,
|
||||
XFile? recipeImage,
|
||||
required List<XFile?> stepImages,
|
||||
required List<Ingredient> ingredients,
|
||||
required List<Product> autocompleteList,
|
||||
required int numberOfSteps,
|
||||
required bool ingredientAsItems,
|
||||
required List<SelectionPopupModel> types,
|
||||
required List<SelectionPopupModel> cookingTimes,
|
||||
required List<SelectionPopupModel> statuses,
|
||||
required SelectionPopupModel selectedType,
|
||||
required SelectionPopupModel selectedCookingTime,
|
||||
required SelectionPopupModel selectedStatus,
|
||||
}) = RecipeCreateStateLoaded;
|
||||
|
||||
const factory RecipeCreateState.error({
|
||||
required String errorMessage,
|
||||
}) = RecipeCreateStateError;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,516 +0,0 @@
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
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/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:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class RecipeEditScreen extends StatelessWidget {
|
||||
final String recipeID;
|
||||
|
||||
const RecipeEditScreen({
|
||||
required this.recipeID,
|
||||
super.key,
|
||||
});
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
if (recipeID != "0") {
|
||||
context.read<RecipeCreateCubit>().loadRecipeById(recipeID);
|
||||
}
|
||||
return BlocBuilder<RecipeCreateCubit, RecipeCreateState>(
|
||||
builder: (context, state) {
|
||||
if (state is RecipeCreateStateLoaded) {
|
||||
return _buildRecipeCreate(context, state);
|
||||
} else if (state is RecipeCreateStateLoading) {
|
||||
return Scaffold(
|
||||
body: 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(
|
||||
state.titleController.text,
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: Color.fromARGB(255, 240, 242, 245),
|
||||
resizeToAvoidBottomInset: true,
|
||||
body: SizedBox(
|
||||
width: double.maxFinite,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Заголовок'),
|
||||
SizedBox(height: 8),
|
||||
CustomTextFormField(
|
||||
fillColor: appTheme.gray50,
|
||||
controller: state.titleController,
|
||||
hintText: 'Заголовок',
|
||||
),
|
||||
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.network(
|
||||
baseUrl + '/storage/' + state.recipeImage!.path,
|
||||
height: 150,
|
||||
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,
|
||||
),
|
||||
child: Text(
|
||||
'+',
|
||||
style: theme.textTheme.titleSmall,
|
||||
),
|
||||
onPressed: () => context.read<RecipeCreateCubit>().addIngredient(new Ingredient(
|
||||
name: state.productNameController.text,
|
||||
portion: state.ingredientController.text,
|
||||
unit: state.ingredientAsItems ? 'pcs' : 'gr',),),),
|
||||
),
|
||||
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,
|
||||
),
|
||||
child: Text(
|
||||
'+',
|
||||
style: theme.textTheme.titleSmall,
|
||||
),
|
||||
onPressed: () => context.read<RecipeCreateCubit>().addStep(),
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
child: Text(
|
||||
'-',
|
||||
style: theme.textTheme.titleSmall,
|
||||
),
|
||||
onPressed: () => context.read<RecipeCreateCubit>().removeStep(),
|
||||
),
|
||||
),
|
||||
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>().editRecipe(createRecipeFromState(state), recipeID),
|
||||
),
|
||||
),
|
||||
],
|
||||
),),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_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.network(
|
||||
baseUrl + '/storage/' + 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,223 @@
|
||||
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;
|
||||
final nameController = TextEditingController();
|
||||
final gramsController = TextEditingController();
|
||||
final pcsController = TextEditingController();
|
||||
|
||||
@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: () {
|
||||
nameController.clear();
|
||||
gramsController.clear();
|
||||
pcsController.clear();
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
|
||||
),
|
||||
isScrollControlled: true,
|
||||
builder: (context) {
|
||||
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: gramsController,
|
||||
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: gramsController.text.isEmpty ? true : false,
|
||||
keyboardType: TextInputType.number,
|
||||
controller: pcsController,
|
||||
onChanged: (value) {
|
||||
setStateInner(() {});
|
||||
},
|
||||
hintText: gramsController.text.isEmpty ? '1' : null,
|
||||
suffixText: 'шт',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
if (nameController.text.isNotEmpty && (pcsController.text.isNotEmpty || gramsController.text.isNotEmpty)) {
|
||||
createRecipeCubit.addIngredient(
|
||||
IngredientEntity(
|
||||
name: nameController.text,
|
||||
unit: gramsController.text.isEmpty ? pcsController.text : gramsController.text,
|
||||
portion: gramsController.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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
82
lib/features/recipes/presentation/widgets/tags_widget.dart
Normal file
82
lib/features/recipes/presentation/widgets/tags_widget.dart
Normal file
@@ -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')}');
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ class UserRegistrationSheet with _$UserRegistrationSheet {
|
||||
required String lang,
|
||||
required int weight,
|
||||
required int height,
|
||||
required String phone,
|
||||
@JsonKey(name: 'chest_circumference') required int chestCircumference,
|
||||
@JsonKey(name: 'waist_circumference') required int waistCircumference,
|
||||
@JsonKey(name: 'hip_circumference') required int hipCircumference,
|
||||
|
||||
@@ -39,6 +39,7 @@ mixin _$UserRegistrationSheet {
|
||||
String get lang => throw _privateConstructorUsedError;
|
||||
int get weight => throw _privateConstructorUsedError;
|
||||
int get height => throw _privateConstructorUsedError;
|
||||
String get phone => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: 'chest_circumference')
|
||||
int get chestCircumference => throw _privateConstructorUsedError;
|
||||
@JsonKey(name: 'waist_circumference')
|
||||
@@ -98,6 +99,7 @@ abstract class $UserRegistrationSheetCopyWith<$Res> {
|
||||
String lang,
|
||||
int weight,
|
||||
int height,
|
||||
String phone,
|
||||
@JsonKey(name: 'chest_circumference') int chestCircumference,
|
||||
@JsonKey(name: 'waist_circumference') int waistCircumference,
|
||||
@JsonKey(name: 'hip_circumference') int hipCircumference,
|
||||
@@ -143,6 +145,7 @@ class _$UserRegistrationSheetCopyWithImpl<$Res,
|
||||
Object? lang = null,
|
||||
Object? weight = null,
|
||||
Object? height = null,
|
||||
Object? phone = null,
|
||||
Object? chestCircumference = null,
|
||||
Object? waistCircumference = null,
|
||||
Object? hipCircumference = null,
|
||||
@@ -211,6 +214,10 @@ class _$UserRegistrationSheetCopyWithImpl<$Res,
|
||||
? _value.height
|
||||
: height // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
phone: null == phone
|
||||
? _value.phone
|
||||
: phone // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
chestCircumference: null == chestCircumference
|
||||
? _value.chestCircumference
|
||||
: chestCircumference // ignore: cast_nullable_to_non_nullable
|
||||
@@ -294,6 +301,7 @@ abstract class _$$UserRegistrationSheetImplCopyWith<$Res>
|
||||
String lang,
|
||||
int weight,
|
||||
int height,
|
||||
String phone,
|
||||
@JsonKey(name: 'chest_circumference') int chestCircumference,
|
||||
@JsonKey(name: 'waist_circumference') int waistCircumference,
|
||||
@JsonKey(name: 'hip_circumference') int hipCircumference,
|
||||
@@ -337,6 +345,7 @@ class __$$UserRegistrationSheetImplCopyWithImpl<$Res>
|
||||
Object? lang = null,
|
||||
Object? weight = null,
|
||||
Object? height = null,
|
||||
Object? phone = null,
|
||||
Object? chestCircumference = null,
|
||||
Object? waistCircumference = null,
|
||||
Object? hipCircumference = null,
|
||||
@@ -405,6 +414,10 @@ class __$$UserRegistrationSheetImplCopyWithImpl<$Res>
|
||||
? _value.height
|
||||
: height // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
phone: null == phone
|
||||
? _value.phone
|
||||
: phone // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
chestCircumference: null == chestCircumference
|
||||
? _value.chestCircumference
|
||||
: chestCircumference // ignore: cast_nullable_to_non_nullable
|
||||
@@ -483,6 +496,7 @@ class _$UserRegistrationSheetImpl implements _UserRegistrationSheet {
|
||||
required this.lang,
|
||||
required this.weight,
|
||||
required this.height,
|
||||
required this.phone,
|
||||
@JsonKey(name: 'chest_circumference') required this.chestCircumference,
|
||||
@JsonKey(name: 'waist_circumference') required this.waistCircumference,
|
||||
@JsonKey(name: 'hip_circumference') required this.hipCircumference,
|
||||
@@ -533,6 +547,8 @@ class _$UserRegistrationSheetImpl implements _UserRegistrationSheet {
|
||||
@override
|
||||
final int height;
|
||||
@override
|
||||
final String phone;
|
||||
@override
|
||||
@JsonKey(name: 'chest_circumference')
|
||||
final int chestCircumference;
|
||||
@override
|
||||
@@ -577,7 +593,7 @@ class _$UserRegistrationSheetImpl implements _UserRegistrationSheet {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'UserRegistrationSheet(firstName: $firstName, lastName: $lastName, email: $email, password: $password, passwordConfirmation: $passwordConfirmation, termsAccepted: $termsAccepted, birthdate: $birthdate, gender: $gender, location: $location, responseBody: $responseBody, lang: $lang, weight: $weight, height: $height, chestCircumference: $chestCircumference, waistCircumference: $waistCircumference, hipCircumference: $hipCircumference, bloodPressureUp: $bloodPressureUp, bloodPressureDown: $bloodPressureDown, hasHypo: $hasHypo, hasHyper: $hasHyper, aboutAllergy: $aboutAllergy, aboutTakingPils: $aboutTakingPils, aboutSportHobbies: $aboutSportHobbies, favoriteFoods: $favoriteFoods, unfavoriteFoods: $unfavoriteFoods, additionalInfo: $additionalInfo, activityCoefficient: $activityCoefficient)';
|
||||
return 'UserRegistrationSheet(firstName: $firstName, lastName: $lastName, email: $email, password: $password, passwordConfirmation: $passwordConfirmation, termsAccepted: $termsAccepted, birthdate: $birthdate, gender: $gender, location: $location, responseBody: $responseBody, lang: $lang, weight: $weight, height: $height, phone: $phone, chestCircumference: $chestCircumference, waistCircumference: $waistCircumference, hipCircumference: $hipCircumference, bloodPressureUp: $bloodPressureUp, bloodPressureDown: $bloodPressureDown, hasHypo: $hasHypo, hasHyper: $hasHyper, aboutAllergy: $aboutAllergy, aboutTakingPils: $aboutTakingPils, aboutSportHobbies: $aboutSportHobbies, favoriteFoods: $favoriteFoods, unfavoriteFoods: $unfavoriteFoods, additionalInfo: $additionalInfo, activityCoefficient: $activityCoefficient)';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -606,6 +622,7 @@ class _$UserRegistrationSheetImpl implements _UserRegistrationSheet {
|
||||
(identical(other.lang, lang) || other.lang == lang) &&
|
||||
(identical(other.weight, weight) || other.weight == weight) &&
|
||||
(identical(other.height, height) || other.height == height) &&
|
||||
(identical(other.phone, phone) || other.phone == phone) &&
|
||||
(identical(other.chestCircumference, chestCircumference) ||
|
||||
other.chestCircumference == chestCircumference) &&
|
||||
(identical(other.waistCircumference, waistCircumference) ||
|
||||
@@ -652,6 +669,7 @@ class _$UserRegistrationSheetImpl implements _UserRegistrationSheet {
|
||||
lang,
|
||||
weight,
|
||||
height,
|
||||
phone,
|
||||
chestCircumference,
|
||||
waistCircumference,
|
||||
hipCircumference,
|
||||
@@ -701,6 +719,7 @@ abstract class _UserRegistrationSheet implements UserRegistrationSheet {
|
||||
required final String lang,
|
||||
required final int weight,
|
||||
required final int height,
|
||||
required final String phone,
|
||||
@JsonKey(name: 'chest_circumference')
|
||||
required final int chestCircumference,
|
||||
@JsonKey(name: 'waist_circumference')
|
||||
@@ -756,6 +775,8 @@ abstract class _UserRegistrationSheet implements UserRegistrationSheet {
|
||||
@override
|
||||
int get height;
|
||||
@override
|
||||
String get phone;
|
||||
@override
|
||||
@JsonKey(name: 'chest_circumference')
|
||||
int get chestCircumference;
|
||||
@override
|
||||
|
||||
@@ -22,6 +22,7 @@ _$UserRegistrationSheetImpl _$$UserRegistrationSheetImplFromJson(
|
||||
lang: json['lang'] as String,
|
||||
weight: (json['weight'] as num).toInt(),
|
||||
height: (json['height'] as num).toInt(),
|
||||
phone: json['phone'] as String,
|
||||
chestCircumference: (json['chest_circumference'] as num).toInt(),
|
||||
waistCircumference: (json['waist_circumference'] as num).toInt(),
|
||||
hipCircumference: (json['hip_circumference'] as num).toInt(),
|
||||
@@ -55,6 +56,7 @@ Map<String, dynamic> _$$UserRegistrationSheetImplToJson(
|
||||
'lang': instance.lang,
|
||||
'weight': instance.weight,
|
||||
'height': instance.height,
|
||||
'phone': instance.phone,
|
||||
'chest_circumference': instance.chestCircumference,
|
||||
'waist_circumference': instance.waistCircumference,
|
||||
'hip_circumference': instance.hipCircumference,
|
||||
|
||||
@@ -24,7 +24,7 @@ class RegistrationCubit extends Cubit<RegistrationState> {
|
||||
gender: '',
|
||||
location: '',
|
||||
responseBody: '',
|
||||
lang: '',
|
||||
lang: 'Русский',
|
||||
weight: 0,
|
||||
height: 0,
|
||||
chestCircumference: 0,
|
||||
@@ -40,6 +40,7 @@ class RegistrationCubit extends Cubit<RegistrationState> {
|
||||
favoriteFoods: '',
|
||||
unfavoriteFoods: '',
|
||||
additionalInfo: '',
|
||||
phone: '',
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -48,7 +49,11 @@ class RegistrationCubit extends Cubit<RegistrationState> {
|
||||
|
||||
Future<void> tryRegistration() async {
|
||||
try {
|
||||
final token = await registrationRepository.tryRegister(state.sheet);
|
||||
final token = await registrationRepository.tryRegister(
|
||||
state.sheet.copyWith(
|
||||
phone: state.sheet.phone.replaceAll(' ', '').replaceAll('(', '').replaceAll(')', ''),
|
||||
),
|
||||
);
|
||||
await TokenStorage.saveTokenInStorage(token);
|
||||
AppRouter.navigatorKey.currentContext?.go('/my-profile-screen');
|
||||
} catch (e) {
|
||||
|
||||
@@ -33,7 +33,7 @@ class _AdditionalInformationScreenState extends State<AdditionalInformationScree
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Дополнительная информация',
|
||||
'Дополнительная информация (не обязательно)',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.start,
|
||||
|
||||
@@ -33,7 +33,7 @@ class _AllergiesScreenState extends State<AllergiesScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(s.allergies_presence),
|
||||
Text(s.allergies_presence + ' (не обязательно)'),
|
||||
SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
controller: enterInformationController,
|
||||
|
||||
@@ -58,7 +58,7 @@ class _BloodPressureScreenState extends State<BloodPressureScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(s.up_pressure),
|
||||
Text(s.up_pressure + ' (не обязательно)'),
|
||||
SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
inputFormatters: [digitsOnly],
|
||||
@@ -72,7 +72,7 @@ class _BloodPressureScreenState extends State<BloodPressureScreen> {
|
||||
},
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(s.down_pressure),
|
||||
Text(s.down_pressure + ' (не обязательно)'),
|
||||
SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
inputFormatters: [digitsOnly],
|
||||
@@ -84,7 +84,7 @@ class _BloodPressureScreenState extends State<BloodPressureScreen> {
|
||||
},
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(s.do_you_have_hypotension_tendency),
|
||||
Text(s.do_you_have_hypotension_tendency + ' (не обязательно)'),
|
||||
SizedBox(height: 4),
|
||||
CustomDropDown(
|
||||
icon: Container(
|
||||
@@ -110,7 +110,7 @@ class _BloodPressureScreenState extends State<BloodPressureScreen> {
|
||||
},
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(s.do_you_have_hypertension_tendency),
|
||||
Text(s.do_you_have_hypertension_tendency + ' (не обязательно)'),
|
||||
SizedBox(height: 4),
|
||||
CustomDropDown(
|
||||
icon: Container(
|
||||
|
||||
@@ -39,7 +39,7 @@ class _BodyCircumferencesScreenState extends State<BodyCircumferencesScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(s.chest_circumference),
|
||||
Text(s.chest_circumference + ' (не обязательно)'),
|
||||
SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
inputFormatters: [digitsOnly],
|
||||
@@ -53,7 +53,7 @@ class _BodyCircumferencesScreenState extends State<BodyCircumferencesScreen> {
|
||||
},
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(s.waist_circumference),
|
||||
Text(s.waist_circumference + ' (не обязательно)'),
|
||||
SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
inputFormatters: [digitsOnly],
|
||||
@@ -65,7 +65,7 @@ class _BodyCircumferencesScreenState extends State<BodyCircumferencesScreen> {
|
||||
},
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(s.hip_circumference),
|
||||
Text(s.hip_circumference + ' (не обязательно)'),
|
||||
SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
inputFormatters: [digitsOnly],
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'package:eatvio_app/features/registration/models/user_registration_model.
|
||||
import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||
@@ -34,6 +33,22 @@ class _CityOfLivingScreenState extends State<CityOfLivingScreen> {
|
||||
if (widget.sheet.location.isNotEmpty) {
|
||||
cityController.text = widget.sheet.location;
|
||||
}
|
||||
tryGetLocation();
|
||||
}
|
||||
|
||||
tryGetLocation() async {
|
||||
final LocationData? locationData = await getLocationData();
|
||||
if (locationData != null) {
|
||||
final List<String> locations = await context
|
||||
.read<RegistrationCubit>()
|
||||
.getLocationsByCoordinates(locationData.latitude.toString(), locationData.longitude.toString());
|
||||
final registrationCubit = context.read<RegistrationCubit>();
|
||||
registrationCubit.updateSheet(registrationCubit.state.sheet.copyWith(location: locations.first));
|
||||
setState(() {
|
||||
cityController.text = locations.first;
|
||||
suggestionsController.suggestions = locations;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<LocationData?> getLocationData() async {
|
||||
@@ -76,19 +91,21 @@ class _CityOfLivingScreenState extends State<CityOfLivingScreen> {
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width - 80,
|
||||
width: MediaQuery.of(context).size.width - 32,
|
||||
child: TypeAheadField(
|
||||
debounceDuration: Duration(milliseconds: 300),
|
||||
controller: cityController,
|
||||
suggestionsController: suggestionsController,
|
||||
builder: (context, controller, focus) => CustomTextFormField(
|
||||
controller: cityController,
|
||||
focusNode: focus,
|
||||
autofocus: true,
|
||||
textStyle: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.black,
|
||||
),),
|
||||
controller: cityController,
|
||||
focusNode: focus,
|
||||
autofocus: true,
|
||||
hintText: 'Москва',
|
||||
textStyle: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
itemBuilder: (BuildContext context, String item) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
@@ -117,46 +134,6 @@ class _CityOfLivingScreenState extends State<CityOfLivingScreen> {
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: !isLoading
|
||||
? () async {
|
||||
setState(() {
|
||||
isLoading = true;
|
||||
});
|
||||
final LocationData? locationData = await getLocationData();
|
||||
if (locationData != null) {
|
||||
final List<String> locations = await context
|
||||
.read<RegistrationCubit>()
|
||||
.getLocationsByCoordinates(locationData.latitude.toString(), locationData.longitude.toString());
|
||||
final registrationCubit = context.read<RegistrationCubit>();
|
||||
registrationCubit.updateSheet(registrationCubit.state.sheet.copyWith(location: locations.first));
|
||||
setState(() {
|
||||
cityController.text = locations.first;
|
||||
suggestionsController.suggestions = locations;
|
||||
});
|
||||
}
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
: null,
|
||||
child: Ink(
|
||||
height: 40,
|
||||
width: 40,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Color(0xFFEAF2FD),
|
||||
),
|
||||
child: !isLoading
|
||||
? Icon(
|
||||
Icons.near_me_rounded,
|
||||
color: appTheme.blueA400,
|
||||
)
|
||||
: CupertinoActivityIndicator(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -20,7 +20,7 @@ class _CredentialsScreenState extends State<CredentialsScreen> {
|
||||
final passwordController = TextEditingController();
|
||||
|
||||
final passwordConfirmationController = TextEditingController();
|
||||
|
||||
final emailFocusNode = FocusNode();
|
||||
bool hidePassword = true;
|
||||
|
||||
@override
|
||||
@@ -29,6 +29,7 @@ class _CredentialsScreenState extends State<CredentialsScreen> {
|
||||
emailController.text = widget.sheet.email;
|
||||
passwordController.text = widget.sheet.password;
|
||||
passwordConfirmationController.text = widget.sheet.passwordConfirmation;
|
||||
emailFocusNode.requestFocus();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -44,9 +45,11 @@ class _CredentialsScreenState extends State<CredentialsScreen> {
|
||||
SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
controller: emailController,
|
||||
focusNode: emailFocusNode,
|
||||
textInputType: TextInputType.emailAddress,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
hintText: 'Введите email',
|
||||
autofocus: true,
|
||||
textInputAction: TextInputAction.next,
|
||||
onChanged: (newValue) {
|
||||
final registrationCubit = context.read<RegistrationCubit>();
|
||||
|
||||
@@ -85,7 +85,7 @@ class _DateOfBirthScreenState extends State<DateOfBirthScreen> {
|
||||
showCupertinoModalPopup<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => Container(
|
||||
height: 216,
|
||||
height: 314,
|
||||
padding: const EdgeInsets.only(top: 6),
|
||||
margin: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
@@ -93,7 +93,41 @@ class _DateOfBirthScreenState extends State<DateOfBirthScreen> {
|
||||
color: CupertinoColors.systemBackground.resolveFrom(context),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: child,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(height: 216, child: child,),
|
||||
const SizedBox(height: 16),
|
||||
Material(
|
||||
child: SizedBox(
|
||||
height: 52,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
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(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -35,7 +35,7 @@ class _FavoriteFoodsScreenState extends State<FavoriteFoodsScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(s.favorite_foods),
|
||||
Text(s.favorite_foods + ' (не обязательно)'),
|
||||
SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
controller: favoriteFoodsController,
|
||||
@@ -50,7 +50,7 @@ class _FavoriteFoodsScreenState extends State<FavoriteFoodsScreen> {
|
||||
},
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(s.unfavorite_foods),
|
||||
Text(s.unfavorite_foods + ' (не обязательно)'),
|
||||
SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
controller: unFavoriteFoodsController,
|
||||
|
||||
@@ -33,7 +33,7 @@ class _FavoriteSportsScreenState extends State<FavoriteSportsScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(s.your_favorite_sports),
|
||||
Text(s.your_favorite_sports + ' (не обязательно)'),
|
||||
SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
controller: enterInformationController,
|
||||
|
||||
@@ -33,7 +33,7 @@ class _MedicineIntakeScreenState extends State<MedicineIntakeScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(s.do_you_take_pills),
|
||||
Text(s.do_you_take_pills + ' (не обязательно)'),
|
||||
SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
controller: enterInformationController,
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
|
||||
import 'package:eatvio_app/features/registration/models/user_registration_model.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:mask_text_input_formatter/mask_text_input_formatter.dart';
|
||||
|
||||
class PhoneScreen extends StatefulWidget {
|
||||
final UserRegistrationSheet sheet;
|
||||
|
||||
const PhoneScreen({
|
||||
super.key,
|
||||
required this.sheet,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PhoneScreen> createState() => _PhoneScreenState();
|
||||
}
|
||||
|
||||
class _PhoneScreenState extends State<PhoneScreen> {
|
||||
final phoneController = TextEditingController();
|
||||
final phoneFocus = FocusNode();
|
||||
|
||||
final maskFormatter = MaskTextInputFormatter(
|
||||
mask: '+7 (###) ### ## ##',
|
||||
filter: {"#": RegExp(r'[0-9]')},
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.sheet.phone.isNotEmpty) phoneController.text = widget.sheet.phone;
|
||||
phoneFocus.requestFocus();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
padding: EdgeInsets.only(bottom: 80, left: 16, right: 16, top: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Номер телефона'),
|
||||
SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
textInputAction: TextInputAction.done,
|
||||
autofocus: true,
|
||||
focusNode: phoneFocus,
|
||||
hintText: '+7 (999) 999 99 99',
|
||||
controller: phoneController,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [maskFormatter],
|
||||
onChanged: (newValue) {
|
||||
final registrationCubit = context.read<RegistrationCubit>();
|
||||
registrationCubit.updateSheet(
|
||||
registrationCubit.state.sheet.copyWith(
|
||||
phone: newValue,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,9 @@ import 'package:eatvio_app/features/registration/presentation/pages/favorite_foo
|
||||
import 'package:eatvio_app/features/registration/presentation/pages/favorite_sports_screen.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/pages/gender_screen.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/pages/height_screen.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/pages/language_select_screen.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/pages/medicine_intake_screen.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/pages/name_screen.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/pages/phone_screen.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/pages/weight_screen.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
@@ -159,7 +159,8 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
|
||||
});
|
||||
},
|
||||
children: [
|
||||
LanguageSelectScreen(sheet: state.sheet),
|
||||
// LanguageSelectScreen(sheet: state.sheet),
|
||||
PhoneScreen(sheet: state.sheet),
|
||||
CredentialsScreen(sheet: state.sheet),
|
||||
NameScreen(sheet: state.sheet),
|
||||
GenderScreen(sheet: state.sheet),
|
||||
@@ -184,6 +185,25 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
|
||||
|
||||
Future<bool> canGoNext(RegistrationState state) async {
|
||||
switch (step) {
|
||||
case 1:
|
||||
if (state.sheet.phone.length != 18) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('Ошибка'),
|
||||
content: Text('Введите корректный номер телефона'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text('OK'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
case 2:
|
||||
List<String> errors = validateCredentialsFields(
|
||||
state.sheet.email,
|
||||
|
||||
Reference in New Issue
Block a user