Merge remote-tracking branch 'upstream/main'

This commit is contained in:
Username1548
2024-12-23 15:39:02 +07:00
54 changed files with 3149 additions and 2929 deletions

View File

@@ -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(

View File

@@ -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(),
],
),

View File

@@ -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,

View File

@@ -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,
),

View File

@@ -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) {

View File

@@ -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),
),
),
),

View 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;
}

View 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;
}

View File

@@ -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,
),
),
);
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View 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('Отмена'),
),
),
);
}
}

View File

@@ -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);
},
),
],
),
),
);
}
}

View File

@@ -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,
),
),
],
),
),
);
},
);
}
}

View File

@@ -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));
},
),
],
),
),
],
),
],
);
}
}

View 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('Отмена'),
),
),
);
}
}

View 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,
),
),
),
),
),
],
),
),
);
},
);
}
}

View 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,
),
],
),
),
),
],
],
),
],
);
}
}

View File

@@ -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')}');
}
},
),
),
],
),
],
);
}
}

View File

@@ -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,

View File

@@ -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

View File

@@ -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,

View File

@@ -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) {

View File

@@ -33,7 +33,7 @@ class _AdditionalInformationScreenState extends State<AdditionalInformationScree
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Дополнительная информация',
'Дополнительная информация (не обязательно)',
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.start,

View File

@@ -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,

View File

@@ -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(

View File

@@ -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],

View File

@@ -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(),
),
),
],
),
],

View File

@@ -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>();

View File

@@ -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,
),
),
),
),
),
),
),
],
),
),
),
);

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,
),
);
},
),
],
),
);
}
}

View File

@@ -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,