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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -31,7 +31,6 @@ import 'package:eatvio_app/features/profile/presentation/pages/profile_screen.da
import 'package:eatvio_app/features/profile/presentation/pages/subs_screen.dart';
import 'package:eatvio_app/features/recipes/presentation/pages/about_recipes_screen.dart';
import 'package:eatvio_app/features/recipes/presentation/pages/recipe_create_screen.dart';
import 'package:eatvio_app/features/recipes/presentation/pages/recipe_edit_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/registration_screen.dart';
import 'package:eatvio_app/features/settings/entities/settings_entity.dart';
import 'package:eatvio_app/features/settings/presentation/pages/change_email_screen.dart';
@@ -135,13 +134,6 @@ class AppRouter {
name: 'recipeCreateScreen',
builder: (context, state) => RecipeCreateScreen(),
),
GoRoute(
path: '/recipe-edit-screen',
name: 'recipeEditScreen',
builder: (context, state) => RecipeEditScreen(
recipeID: state.extra as String,
),
),
GoRoute(
path: '/profile-screen',
name: 'profileScreen',

View File

@@ -1,4 +1,4 @@
const String baseUrl = 'https://dev2.eatvio.com';
String baseUrl = 'https://dev2.eatvio.com';
const List<int> durationValues = [7, 14, 21];

View File

@@ -52,7 +52,7 @@ import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/prof
import 'package:eatvio_app/features/profile/presentation/bloc/sub_cubit/sub_cubit.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:eatvio_app/features/profile/services/profile_service.dart';
import 'package:eatvio_app/features/recipes/presentation/bloc/recipe_create_cubit.dart';
import 'package:eatvio_app/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart';
import 'package:eatvio_app/features/recipes/reporitories/recipe_repository.dart';
import 'package:eatvio_app/features/recipes/services/recipe_service.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart';
@@ -81,7 +81,6 @@ Future<void> init() async {
sl.registerFactory<CalendarCubit>(() => CalendarCubit(calendarRepository: sl()));
sl.registerFactory<ProfileCubit>(() => ProfileCubit(profileRepository: sl(), chatRepository: sl()));
sl.registerFactory<profilePlans.PlansCubit>(() => profilePlans.PlansCubit(plansRepository: sl()));
sl.registerFactory<RecipeCreateCubit>(() => RecipeCreateCubit(recipeRepository: sl()));
sl.registerFactory<DietsCubit>(() => DietsCubit(feedRepository: sl(), dietsRepository: sl()));
sl.registerFactory<CommentsCubit>(() => CommentsCubit(feedRepository: sl()));
sl.registerFactory<ChatSelectCubit>(() => ChatSelectCubit(chatRepository: sl()));
@@ -96,6 +95,7 @@ Future<void> init() async {
sl.registerFactory<CreatePlanCubit>(() => CreatePlanCubit());
sl.registerFactory<FeedProfileCubit>(() => FeedProfileCubit(profileRepository: sl()));
sl.registerFactory<AboutPlanCubit>(() => AboutPlanCubit(planRepository: sl()));
sl.registerFactory<CreateRecipeCubit>(() => CreateRecipeCubit());
sl.registerFactory<MyDietsCubit>(() => MyDietsCubit(myDietsRepository: sl()));
sl.registerFactory<MyPlansCubit>(() => MyPlansCubit(myPlansRepository: sl()));

View File

@@ -64,11 +64,6 @@ class CustomDropDown extends StatelessWidget {
value: value,
isDense: true,
isExpanded: true,
style: TextStyle(
fontSize: 16,
color: Colors.black,
fontWeight: FontWeight.w400,
),
items: items?.map((SelectionPopupModel item) {
return DropdownMenuItem<SelectionPopupModel>(
value: item,

View File

@@ -38,10 +38,12 @@ class CustomTextFormField extends StatelessWidget {
this.expands = false,
this.textAlignVertical,
this.counter = const SizedBox.shrink(),
this.enabled = true,
super.key,
this.hasError = false,
this.readOnly = false,
});
final Widget? counter;
final Alignment? alignment;
final double? width;
@@ -77,12 +79,15 @@ class CustomTextFormField extends StatelessWidget {
final bool expands;
final TextAlignVertical? textAlignVertical;
final bool hasError;
final bool enabled;
final bool readOnly;
@override
Widget build(BuildContext context) {
return Align(
alignment: alignment ?? Alignment.center,
child: TextFormField(
enabled: enabled,
readOnly: readOnly,
textAlignVertical: textAlignVertical,
expands: expands,
@@ -114,6 +119,7 @@ class CustomTextFormField extends StatelessWidget {
prefixIcon: prefix,
prefixIconConstraints: prefixConstraints,
suffixIcon: suffix,
floatingLabelBehavior: FloatingLabelBehavior.always,
suffixStyle: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,

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,

View File

@@ -17,16 +17,20 @@ import 'package:intl/message_lookup_by_library.dart';
import 'package:intl/src/intl_helpers.dart';
import 'messages_en.dart' as messages_en;
import 'messages_ru.dart' as messages_ru;
typedef Future<dynamic> LibraryLoader();
Map<String, LibraryLoader> _deferredLibraries = {
'en': () => new SynchronousFuture(null),
'ru': () => new SynchronousFuture(null),
};
MessageLookupByLibrary? _findExact(String localeName) {
switch (localeName) {
case 'en':
return messages_en.messages;
case 'ru':
return messages_ru.messages;
default:
return null;
}

View File

@@ -0,0 +1,306 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a ru locale. All the
// messages from the main program should be duplicated here with the same
// function name.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
final messages = new MessageLookup();
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'ru';
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"Moscow": MessageLookupByLibrary.simpleMessage("Москва"),
"Russia": MessageLookupByLibrary.simpleMessage("Россия"),
"about_diet": MessageLookupByLibrary.simpleMessage("Об этой диете"),
"about_plan": MessageLookupByLibrary.simpleMessage("Об этом плане"),
"active": MessageLookupByLibrary.simpleMessage("Активен"),
"active_diet_state": MessageLookupByLibrary.simpleMessage("Активная"),
"add": MessageLookupByLibrary.simpleMessage("Добавить"),
"add_cover": MessageLookupByLibrary.simpleMessage("Добавьте обложку"),
"add_measure": MessageLookupByLibrary.simpleMessage("Добавить замер"),
"adding_plan": MessageLookupByLibrary.simpleMessage("Добавление плана"),
"additional_information":
MessageLookupByLibrary.simpleMessage("Дополнительная информация"),
"admin": MessageLookupByLibrary.simpleMessage("Админ"),
"all": MessageLookupByLibrary.simpleMessage("Все"),
"allergies_presence":
MessageLookupByLibrary.simpleMessage("Наличие аллергии"),
"another_form_of_comment":
MessageLookupByLibrary.simpleMessage("комментария"),
"another_form_of_day": MessageLookupByLibrary.simpleMessage("дня"),
"answer": MessageLookupByLibrary.simpleMessage("Ответить"),
"archived": MessageLookupByLibrary.simpleMessage("В архиве"),
"authentificate": MessageLookupByLibrary.simpleMessage("Войти"),
"average_pressure": MessageLookupByLibrary.simpleMessage(
"Средние показатели давления:",),
"bodymass_chart": MessageLookupByLibrary.simpleMessage("График ИМТ"),
"by_default": MessageLookupByLibrary.simpleMessage("По умолчянию"),
"calories": MessageLookupByLibrary.simpleMessage("Калории"),
"carb_type": MessageLookupByLibrary.simpleMessage("Мучное"),
"carb_type_desc": MessageLookupByLibrary.simpleMessage(
"Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.",),
"carbohydrates": MessageLookupByLibrary.simpleMessage("Углеводы"),
"chat": MessageLookupByLibrary.simpleMessage("Чат"),
"chest": MessageLookupByLibrary.simpleMessage("Грудь"),
"chest_circumference":
MessageLookupByLibrary.simpleMessage("Обхват груди"),
"choose_diet_duration":
MessageLookupByLibrary.simpleMessage("Выберите срок диеты"),
"choose_plan_duration":
MessageLookupByLibrary.simpleMessage("Выберите срок плана"),
"choose_start_diet_date": MessageLookupByLibrary.simpleMessage(
"Выберите дату старты диеты. Она отобразиться у Вас в дневнике",),
"circumference_empty": MessageLookupByLibrary.simpleMessage(
"Все замеры обязательны к заполнению",),
"circumference_is_not_a_number": MessageLookupByLibrary.simpleMessage(
"Все замеры должны быть числами",),
"circumferences_chart":
MessageLookupByLibrary.simpleMessage("График обхватов"),
"city": MessageLookupByLibrary.simpleMessage("Город"),
"city_of_living":
MessageLookupByLibrary.simpleMessage("Город проживания"),
"comfort": MessageLookupByLibrary.simpleMessage("Комфорт"),
"comment": MessageLookupByLibrary.simpleMessage("комментарий"),
"comments": MessageLookupByLibrary.simpleMessage("Комментарии"),
"comments_plural": MessageLookupByLibrary.simpleMessage("комментариев"),
"complain": MessageLookupByLibrary.simpleMessage("Пожаловаться"),
"continueButton": MessageLookupByLibrary.simpleMessage("Продолжить"),
"continue_": MessageLookupByLibrary.simpleMessage("Продолжить"),
"copied": MessageLookupByLibrary.simpleMessage("cкопирован"),
"copy_link": MessageLookupByLibrary.simpleMessage("Скопировать ссылку"),
"country": MessageLookupByLibrary.simpleMessage("Страна"),
"create_diet": MessageLookupByLibrary.simpleMessage("Создание диеты"),
"customer_price":
MessageLookupByLibrary.simpleMessage("Цена для покупателя"),
"date_of_birth": MessageLookupByLibrary.simpleMessage("Дата рождения"),
"day": MessageLookupByLibrary.simpleMessage("День"),
"days": MessageLookupByLibrary.simpleMessage("дней"),
"diary": MessageLookupByLibrary.simpleMessage("Дневник"),
"diet_author": MessageLookupByLibrary.simpleMessage("Автор диеты"),
"diet_plan": MessageLookupByLibrary.simpleMessage("План диеты"),
"diet_state": MessageLookupByLibrary.simpleMessage("Статус диеты"),
"diet_time": MessageLookupByLibrary.simpleMessage("Время прохождения"),
"diet_type": MessageLookupByLibrary.simpleMessage("Тип диеты"),
"do_you_have_hypertension_tendency":
MessageLookupByLibrary.simpleMessage("Есть ли у вас гипертония?"),
"do_you_have_hypotension_tendency":
MessageLookupByLibrary.simpleMessage("Есть ли у вас гипотония?"),
"do_you_take_pills": MessageLookupByLibrary.simpleMessage(
"Принимаете ли вы лекарства или бады?",),
"down_pressure":
MessageLookupByLibrary.simpleMessage("Нижние показатели давления:"),
"draft": MessageLookupByLibrary.simpleMessage("Черновик"),
"eating_behavior":
MessageLookupByLibrary.simpleMessage("Пищевое поведение"),
"eating_behavior_type": MessageLookupByLibrary.simpleMessage(
"Формирование пищевого поведения",),
"eating_behavior_type_desc": MessageLookupByLibrary.simpleMessage(
"Формирование здорового пищевого поведения — это процесс, который включает в себя изменение привычек и отношения к еде. Он направлен на то, чтобы помочь людям научиться правильно питаться и поддерживать здоровый образ жизни.Основные принципы формирования здорового пищевого поведения: \n● Разнообразие продуктов; \n● Сбалансированный рацион; \n● Питьевой режим; \n● Ограничение вредных продуктов; \n● Регулярные приёмы пищи; \n● Умеренность;Осознанное питание; \n● Физическая активность;",),
"economy": MessageLookupByLibrary.simpleMessage("Эконом"),
"email": MessageLookupByLibrary.simpleMessage("Элетронная почта"),
"email_empty": MessageLookupByLibrary.simpleMessage(
"Необходимо ввести адрес электронной почты",),
"email_invalid": MessageLookupByLibrary.simpleMessage(
"Некорректный адрес электронной почты",),
"enter_additional_information": MessageLookupByLibrary.simpleMessage(
"Введите любую дополнительную информацию на своё усмотрение:",),
"enter_diet_description":
MessageLookupByLibrary.simpleMessage("Введите описание диеты"),
"enter_diet_name":
MessageLookupByLibrary.simpleMessage("Выведите название диеты"),
"enter_information":
MessageLookupByLibrary.simpleMessage("Введите информацию..."),
"exit": MessageLookupByLibrary.simpleMessage("Выйти"),
"expand": MessageLookupByLibrary.simpleMessage("Показать полностью"),
"fats": MessageLookupByLibrary.simpleMessage("Жиры"),
"favorite_foods":
MessageLookupByLibrary.simpleMessage("Любимые продукты"),
"favorite_unfavorite_foods": MessageLookupByLibrary.simpleMessage(
"Ваши любимые/нелюбимые продукты питания?",),
"female": MessageLookupByLibrary.simpleMessage("Женский"),
"fill_in_information":
MessageLookupByLibrary.simpleMessage("Заполните информацию:"),
"finish": MessageLookupByLibrary.simpleMessage("Завершить"),
"first_name": MessageLookupByLibrary.simpleMessage("Имя"),
"first_name_empty": MessageLookupByLibrary.simpleMessage(
"Имя обязательно для заполнения",),
"first_name_short": MessageLookupByLibrary.simpleMessage(
"В имени должен быть минимум 1 символ",),
"fit_type": MessageLookupByLibrary.simpleMessage("Поддержание формы"),
"fit_type_desc": MessageLookupByLibrary.simpleMessage(
"План питания для поддержания формы сбалансированный и разнообразный, чтобы обеспечить организм всеми необходимыми питательными веществами. Общие принципы: \n● Дробное питание; \n● Разнообразие продуктов; \n● Контроль порций; \n● Питьевой режим; \n● Ограничение вредных продуктов;",),
"fitness": MessageLookupByLibrary.simpleMessage("Поддержка формы"),
"from_a_to_z": MessageLookupByLibrary.simpleMessage("Название: А-Я"),
"from_z_to_a": MessageLookupByLibrary.simpleMessage("Название: Я-А"),
"gallery": MessageLookupByLibrary.simpleMessage("Галерея"),
"gr": MessageLookupByLibrary.simpleMessage("гр"),
"grams": MessageLookupByLibrary.simpleMessage("гр"),
"height_measurements":
MessageLookupByLibrary.simpleMessage("Замер роста"),
"hide": MessageLookupByLibrary.simpleMessage("Свернуть"),
"hip_circumference":
MessageLookupByLibrary.simpleMessage("Обхват бедер"),
"hips": MessageLookupByLibrary.simpleMessage("Бедра"),
"in_progress": MessageLookupByLibrary.simpleMessage("В процессе"),
"inactive_diet_state": MessageLookupByLibrary.simpleMessage("Черновик"),
"initial_measurements":
MessageLookupByLibrary.simpleMessage("Первичные замеры"),
"label": MessageLookupByLibrary.simpleMessage("Название"),
"language": MessageLookupByLibrary.simpleMessage("Язык"),
"last_name": MessageLookupByLibrary.simpleMessage("Фамилия"),
"last_name_empty": MessageLookupByLibrary.simpleMessage(
"Фамилия обязательна для заполнения",),
"last_name_short": MessageLookupByLibrary.simpleMessage(
"В фамилии должен быть минимум 6 символов",),
"lbl_english": MessageLookupByLibrary.simpleMessage("English"),
"light_type": MessageLookupByLibrary.simpleMessage("Легкая"),
"light_type_desc": MessageLookupByLibrary.simpleMessage(
"С некоторыми видами диет можно употреблять алкоголь в умеренных количествах. Рекомендуется совмещать вместе с низкокалорийными закусками.",),
"link_was_copied":
MessageLookupByLibrary.simpleMessage("Ссылка успешно скопирована"),
"loading": MessageLookupByLibrary.simpleMessage("Загрузка..."),
"male": MessageLookupByLibrary.simpleMessage("Мужской"),
"meal": MessageLookupByLibrary.simpleMessage("Приём пищи"),
"meat": MessageLookupByLibrary.simpleMessage("Мясо"),
"meat_type": MessageLookupByLibrary.simpleMessage("С мясом"),
"meat_type_desc": MessageLookupByLibrary.simpleMessage(
"Диета с мясом полезная и сбалансированная. Включает в себя нежирное мясо: курицу, индейку, кролика или говядину. В сочетании с овощами получаете все необходимые витамины и минералы. Не забывайте о порциях и старайтесь есть медленно, прислушиваясь к своему организму.",),
"milliliter": MessageLookupByLibrary.simpleMessage("мл"),
"minimal_customer_price": MessageLookupByLibrary.simpleMessage(
"Минимальная цена для покупателя: 1200₽",),
"most_new": MessageLookupByLibrary.simpleMessage("Самые новые"),
"most_old": MessageLookupByLibrary.simpleMessage("Самые старые"),
"msg_network_err":
MessageLookupByLibrary.simpleMessage("Сетевая ошибка!"),
"msg_something_went_wrong":
MessageLookupByLibrary.simpleMessage("Что то пошло не так!"),
"my_blog": MessageLookupByLibrary.simpleMessage("Мой блог"),
"my_settings": MessageLookupByLibrary.simpleMessage("Мои настройки"),
"name_empty": MessageLookupByLibrary.simpleMessage(
"Имя пользователя обязательно для заполнения",),
"name_short": MessageLookupByLibrary.simpleMessage(
"В имени пользователя должно быть минимум 6 символов",),
"no": MessageLookupByLibrary.simpleMessage("Нет"),
"no_account":
MessageLookupByLibrary.simpleMessage("У меня нет аккаунта"),
"one_option": MessageLookupByLibrary.simpleMessage("1 опция"),
"option": MessageLookupByLibrary.simpleMessage("Опция"),
"option_content":
MessageLookupByLibrary.simpleMessage("Содержание опции"),
"option_content_hint_text":
MessageLookupByLibrary.simpleMessage("На связи 7 дней в неделю"),
"option_desc": MessageLookupByLibrary.simpleMessage("Описание опции"),
"option_desc_hint_text": MessageLookupByLibrary.simpleMessage(
"Этот план питания для похудения на 7 дней сбалансированный и разнообразный",),
"option_duration":
MessageLookupByLibrary.simpleMessage("Продолжительность опции"),
"option_personal_diet_desc": MessageLookupByLibrary.simpleMessage(
"Опция подразумевает создание индивидуальной диеты",),
"option_title": MessageLookupByLibrary.simpleMessage("Заголовок опции"),
"order": MessageLookupByLibrary.simpleMessage("Порядок"),
"password": MessageLookupByLibrary.simpleMessage("Пароль"),
"password_confirmation":
MessageLookupByLibrary.simpleMessage("Подтвердите пароль"),
"password_confirmation_mismatch":
MessageLookupByLibrary.simpleMessage("Пароли не совпадают"),
"password_empty": MessageLookupByLibrary.simpleMessage(
"Пароль обязателен для заполнения",),
"password_short": MessageLookupByLibrary.simpleMessage(
"В пароле должно быть минимум 6 символов",),
"pcs": MessageLookupByLibrary.simpleMessage("шт"),
"personal_diet_state":
MessageLookupByLibrary.simpleMessage("Персональная"),
"photo": MessageLookupByLibrary.simpleMessage("Фото"),
"place_of_living":
MessageLookupByLibrary.simpleMessage("Место проживания"),
"plan_author": MessageLookupByLibrary.simpleMessage("Автор плана"),
"plan_desc": MessageLookupByLibrary.simpleMessage("Описание плана"),
"plan_desc_example": MessageLookupByLibrary.simpleMessage(
"Большое подробное, привлекательное описание плана",),
"plan_duration": MessageLookupByLibrary.simpleMessage("Срок плана"),
"plan_label_desc":
MessageLookupByLibrary.simpleMessage("План для поддержания формы"),
"plans_complete":
MessageLookupByLibrary.simpleMessage("Пройденные планы"),
"popular": MessageLookupByLibrary.simpleMessage("Популярные"),
"preferred_language":
MessageLookupByLibrary.simpleMessage("Предпочитаемый язык общения"),
"pressure_empty": MessageLookupByLibrary.simpleMessage(
"Все показатели давления обязательны к заполнению",),
"pressure_is_not_a_number": MessageLookupByLibrary.simpleMessage(
"Все показатели давления должны быть числами",),
"preview": MessageLookupByLibrary.simpleMessage("Превью"),
"price_explanation": MessageLookupByLibrary.simpleMessage(
"Разница в ценах, это комиссия Итвио",),
"profile": MessageLookupByLibrary.simpleMessage("Профиль"),
"proteins": MessageLookupByLibrary.simpleMessage("Белки"),
"publish": MessageLookupByLibrary.simpleMessage("Опубликовать"),
"save": MessageLookupByLibrary.simpleMessage("Сохранить"),
"services": MessageLookupByLibrary.simpleMessage("Оказание услуг"),
"settings": MessageLookupByLibrary.simpleMessage("Настройки"),
"share": MessageLookupByLibrary.simpleMessage("Поделиться"),
"skip": MessageLookupByLibrary.simpleMessage("Пропустить"),
"slim": MessageLookupByLibrary.simpleMessage("Похудение"),
"slim_type": MessageLookupByLibrary.simpleMessage("Похудение"),
"slim_type_desc": MessageLookupByLibrary.simpleMessage(
"План питания для похудения должен быть разработан с учётом индивидуальных особенностей организма, возраста, пола, уровня физической активности и состояния здоровья. Общие принципы: \n● Снижение калорийности; \n● Сбалансированный рацион; \n● Разнообразие продуктов; \n● Питьевой режим; \n● Ограничение вредных продуктов;",),
"standard": MessageLookupByLibrary.simpleMessage("Стандарт"),
"startDiet": MessageLookupByLibrary.simpleMessage("Начать диету"),
"tell_your_schedule": MessageLookupByLibrary.simpleMessage(
"Расскажите в свободной форме о том как просыпаетесь, когда едите, когда ложитесь спать:",),
"three_options": MessageLookupByLibrary.simpleMessage("3 опции"),
"to_draft": MessageLookupByLibrary.simpleMessage("В черновик"),
"unfavorite_foods":
MessageLookupByLibrary.simpleMessage("Нелюбимые продукты"),
"up_pressure": MessageLookupByLibrary.simpleMessage(
"Верхние показатели давления:",),
"user_authorization":
MessageLookupByLibrary.simpleMessage("Аутентификация"),
"user_registration":
MessageLookupByLibrary.simpleMessage("Регистрация пользователя"),
"username": MessageLookupByLibrary.simpleMessage("Имя пользователя"),
"vegan": MessageLookupByLibrary.simpleMessage("Веган."),
"vegan_type": MessageLookupByLibrary.simpleMessage("Вегетарианская"),
"vegan_type_desc": MessageLookupByLibrary.simpleMessage(
"Диета основана на потреблении растительной пищи и отказе от продуктов животного происхождения. Такой тип питания может быть разнообразным и вкусным, а также полезным для здоровья.",),
"waist": MessageLookupByLibrary.simpleMessage("Талия"),
"waist_circumference":
MessageLookupByLibrary.simpleMessage("Обхват талии"),
"weight": MessageLookupByLibrary.simpleMessage("Вес"),
"weight_chart": MessageLookupByLibrary.simpleMessage("График веса"),
"weight_measurements":
MessageLookupByLibrary.simpleMessage("Замер веса"),
"wellness":
MessageLookupByLibrary.simpleMessage("Улучшение самочувствия"),
"wellness_type":
MessageLookupByLibrary.simpleMessage("Улучшение самочувствия"),
"wellness_type_desc": MessageLookupByLibrary.simpleMessage(
"Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.",),
"what_is_your_name":
MessageLookupByLibrary.simpleMessage("Как вас зовут?"),
"wrong_credentials": MessageLookupByLibrary.simpleMessage(
"Неверные данные пользователя",),
"you_will_get": MessageLookupByLibrary.simpleMessage("Вы получите"),
"your_blood_pressure":
MessageLookupByLibrary.simpleMessage("Ваше артериальное давление?"),
"your_favorite_sports":
MessageLookupByLibrary.simpleMessage("Ваши спортивные увлечения?"),
"your_gender": MessageLookupByLibrary.simpleMessage("Ваш пол?"),
"your_height": MessageLookupByLibrary.simpleMessage("Ваш рост:"),
"your_schedule": MessageLookupByLibrary.simpleMessage("Ваш режим дня?"),
"your_weight": MessageLookupByLibrary.simpleMessage("Ваш вес:"),
};
}

View File

@@ -2177,6 +2177,7 @@ class AppLocalizationDelegate extends LocalizationsDelegate<S> {
List<Locale> get supportedLocales {
return const <Locale>[
Locale.fromSubtags(languageCode: 'en'),
Locale.fromSubtags(languageCode: 'ru'),
];
}

View File

@@ -98,14 +98,14 @@
"gr": "гр",
"milliliter": "мл",
"grams": "гр",
"startDiet": "Начать диету",
"startDiet": "Начать диету",
"comments": "Комментарии",
"comment": "комментарий",
"another_form_of_comment": "комментария",
"about_diet": "Об этой диете",
"days": "дней",
"day": "День",
"another_form_of_day": "дня",
"another_form_of_day": "дня",
"comments_plural": "комментариев",
"meal": "Приём пищи",
"diet_author": "Автор диеты",
@@ -113,7 +113,7 @@
"expand": "Показать полностью",
"copy_link": "Скопировать ссылку",
"share": "Поделиться",
"complain": "Пожаловаться",
"complain": "Пожаловаться",
"vegan_type": "Вегетарианская",
"light_type": "Легкая",
"meat_type": "С мясом",
@@ -127,7 +127,7 @@
"proteins": "Белки",
"fats": "Жиры",
"carbohydrates": "Углеводы",
"answer": "Ответить",
"answer": "Ответить",
"link_was_copied": "Ссылка успешно скопирована",
"choose_start_diet_date": "Выберите дату старты диеты. Она отобразиться у Вас в дневнике",
"continue_": "Продолжить",
@@ -150,7 +150,7 @@
"active_diet_state": "Активная",
"inactive_diet_state": "Черновик",
"personal_diet_state": "Персональная",
"meat": "Мясо",
"meat": "Мясо",
"vegan": "Веган.",
"photo": "Фото",
"gallery": "Галерея",

201
lib/l10n/intl_ru.arb Normal file
View File

@@ -0,0 +1,201 @@
{
"user_registration": "Регистрация пользователя",
"email": "Элетронная почта",
"username": "Имя пользователя",
"password": "Пароль",
"password_confirmation": "Подтвердите пароль",
"wrong_credentials": "Неверные данные пользователя",
"first_name": "Имя",
"last_name": "Фамилия",
"what_is_your_name": "Как вас зовут?",
"date_of_birth": "Дата рождения",
"country": "Страна",
"Russia": "Россия",
"city": "Город",
"Moscow": "Москва",
"place_of_living": "Место проживания",
"hip_circumference": "Обхват бедер",
"chest_circumference": "Обхват груди",
"waist_circumference": "Обхват талии",
"initial_measurements": "Первичные замеры",
"male": "Мужской",
"female": "Женский",
"weight_measurements": "Замер веса",
"your_weight": "Ваш вес:",
"height_measurements": "Замер роста",
"your_height": "Ваш рост:",
"allergies_presence": "Наличие аллергии",
"do_you_take_pills": "Принимаете ли вы лекарства или бады?",
"your_gender": "Ваш пол?",
"no": "Нет",
"do_you_have_hypertension_tendency": "Есть ли у вас гипертония?",
"your_blood_pressure": "Ваше артериальное давление?",
"average_pressure": "Средние показатели давления:",
"up_pressure": "Верхние показатели давления:",
"down_pressure": "Нижние показатели давления:",
"do_you_have_hypotension_tendency": "Есть ли у вас гипотония?",
"your_favorite_sports": "Ваши спортивные увлечения?",
"favorite_foods": "Любимые продукты",
"favorite_unfavorite_foods": "Ваши любимые/нелюбимые продукты питания?",
"unfavorite_foods": "Нелюбимые продукты",
"your_schedule": "Ваш режим дня?",
"tell_your_schedule": "Расскажите в свободной форме о том как просыпаетесь, когда едите, когда ложитесь спать:",
"additional_information": "Дополнительная информация",
"enter_additional_information": "Введите любую дополнительную информацию на своё усмотрение:",
"lbl_english": "English",
"finish": "Завершить",
"preferred_language": "Предпочитаемый язык общения",
"continueButton": "Продолжить",
"skip": "Пропустить",
"fill_in_information": "Заполните информацию:",
"enter_information": "Введите информацию...",
"msg_network_err": "Сетевая ошибка!",
"msg_something_went_wrong": "Что то пошло не так!",
"email_empty": "Необходимо ввести адрес электронной почты",
"email_invalid": "Некорректный адрес электронной почты",
"password_empty": "Пароль обязателен для заполнения",
"password_short": "В пароле должно быть минимум 6 символов",
"name_empty": "Имя пользователя обязательно для заполнения",
"name_short": "В имени пользователя должно быть минимум 6 символов",
"password_confirmation_mismatch": "Пароли не совпадают",
"first_name_empty": "Имя обязательно для заполнения",
"first_name_short": "В имени должен быть минимум 1 символ",
"last_name_empty": "Фамилия обязательна для заполнения",
"last_name_short": "В фамилии должен быть минимум 6 символов",
"circumference_empty": "Все замеры обязательны к заполнению",
"circumference_is_not_a_number": "Все замеры должны быть числами",
"pressure_empty": "Все показатели давления обязательны к заполнению",
"pressure_is_not_a_number": "Все показатели давления должны быть числами",
"no_account": "У меня нет аккаунта",
"authentificate": "Войти",
"user_authorization": "Аутентификация",
"loading": "Загрузка...",
"weight_chart": "График веса",
"circumferences_chart": "График обхватов",
"city_of_living": "Город проживания",
"bodymass_chart": "График ИМТ",
"chest": "Грудь",
"waist": "Талия",
"hips": "Бедра",
"exit": "Выйти",
"active": "Активен",
"archived": "В архиве",
"draft": "Черновик",
"my_blog": "Мой блог",
"language": "Язык",
"in_progress": "В процессе",
"add_measure": "Добавить замер",
"weight": "Вес",
"plans_complete": "Пройденные планы",
"my_settings": "Мои настройки",
"settings": "Настройки",
"profile": "Профиль",
"chat": "Чат",
"admin": "Админ",
"diary": "Дневник",
"save": "Сохранить",
"pcs": "шт",
"gr": "гр",
"milliliter": "мл",
"grams": "гр",
"startDiet": "Начать диету",
"comments": "Комментарии",
"comment": "комментарий",
"another_form_of_comment": "комментария",
"about_diet": "Об этой диете",
"days": "дней",
"day": "День",
"another_form_of_day": "дня",
"comments_plural": "комментариев",
"meal": "Приём пищи",
"diet_author": "Автор диеты",
"hide": "Свернуть",
"expand": "Показать полностью",
"copy_link": "Скопировать ссылку",
"share": "Поделиться",
"complain": "Пожаловаться",
"vegan_type": "Вегетарианская",
"light_type": "Легкая",
"meat_type": "С мясом",
"carb_type": "Мучное",
"vegan_type_desc": "Диета основана на потреблении растительной пищи и отказе от продуктов животного происхождения. Такой тип питания может быть разнообразным и вкусным, а также полезным для здоровья.",
"light_type_desc": "С некоторыми видами диет можно употреблять алкоголь в умеренных количествах. Рекомендуется совмещать вместе с низкокалорийными закусками.",
"carb_type_desc": "Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.",
"meat_type_desc": "Диета с мясом полезная и сбалансированная. Включает в себя нежирное мясо: курицу, индейку, кролика или говядину. В сочетании с овощами получаете все необходимые витамины и минералы. Не забывайте о порциях и старайтесь есть медленно, прислушиваясь к своему организму.",
"diet_plan": "План диеты",
"calories": "Калории",
"proteins": "Белки",
"fats": "Жиры",
"carbohydrates": "Углеводы",
"answer": "Ответить",
"link_was_copied": "Ссылка успешно скопирована",
"choose_start_diet_date": "Выберите дату старты диеты. Она отобразиться у Вас в дневнике",
"continue_": "Продолжить",
"order": "Порядок",
"diet_time": "Время прохождения",
"all": "Все",
"diet_type": "Тип диеты",
"popular": "Популярные",
"most_new": "Самые новые",
"most_old": "Самые старые",
"from_a_to_z": "Название: А-Я",
"from_z_to_a": "Название: Я-А",
"by_default": "По умолчянию",
"create_diet": "Создание диеты",
"enter_diet_name": "Выведите название диеты",
"enter_diet_description": "Введите описание диеты",
"choose_diet_duration": "Выберите срок диеты",
"diet_state": "Статус диеты",
"active_diet_state": "Активная",
"inactive_diet_state": "Черновик",
"personal_diet_state": "Персональная",
"meat": "Мясо",
"vegan": "Веган.",
"photo": "Фото",
"gallery": "Галерея",
"add_cover": "Добавьте обложку",
"add": "Добавить",
"copied": "cкопирован",
"to_draft": "В черновик",
"preview": "Превью",
"publish": "Опубликовать",
"adding_plan": "Добавление плана",
"label": "Название",
"plan_label_desc": "План для поддержания формы",
"plan_duration": "Срок плана",
"slim": "Похудение",
"fitness": "Поддержка формы",
"wellness": "Улучшение самочувствия",
"eating_behavior": "Пищевое поведение",
"choose_plan_duration": "Выберите срок плана",
"plan_desc": "Описание плана",
"plan_desc_example": "Большое подробное, привлекательное описание плана",
"one_option": "1 опция",
"three_options": "3 опции",
"option": "Опция",
"services": "Оказание услуг",
"economy": "Эконом",
"standard": "Стандарт",
"comfort": "Комфорт",
"option_title": "Заголовок опции",
"option_desc": "Описание опции",
"option_desc_hint_text": "Этот план питания для похудения на 7 дней сбалансированный и разнообразный",
"you_will_get": "Вы получите",
"customer_price": "Цена для покупателя",
"minimal_customer_price": "Минимальная цена для покупателя: 1200₽",
"price_explanation": "Разница в ценах, это комиссия Итвио",
"option_content": "Содержание опции",
"option_content_hint_text": "На связи 7 дней в неделю",
"option_personal_diet_desc": "Опция подразумевает создание индивидуальной диеты",
"about_plan": "Об этом плане",
"plan_author": "Автор плана",
"option_duration": "Продолжительность опции",
"fit_type": "Поддержание формы",
"fit_type_desc": "План питания для поддержания формы сбалансированный и разнообразный, чтобы обеспечить организм всеми необходимыми питательными веществами. Общие принципы: \n● Дробное питание; \n● Разнообразие продуктов; \n● Контроль порций; \n● Питьевой режим; \n● Ограничение вредных продуктов;",
"slim_type": "Похудение",
"slim_type_desc": "План питания для похудения должен быть разработан с учётом индивидуальных особенностей организма, возраста, пола, уровня физической активности и состояния здоровья. Общие принципы: \n● Снижение калорийности; \n● Сбалансированный рацион; \n● Разнообразие продуктов; \n● Питьевой режим; \n● Ограничение вредных продуктов;",
"wellness_type": "Улучшение самочувствия",
"wellness_type_desc": "Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.",
"eating_behavior_type": "Формирование пищевого поведения",
"eating_behavior_type_desc": "Формирование здорового пищевого поведения — это процесс, который включает в себя изменение привычек и отношения к еде. Он направлен на то, чтобы помочь людям научиться правильно питаться и поддерживать здоровый образ жизни.Основные принципы формирования здорового пищевого поведения: \n● Разнообразие продуктов; \n● Сбалансированный рацион; \n● Питьевой режим; \n● Ограничение вредных продуктов; \n● Регулярные приёмы пищи; \n● Умеренность;Осознанное питание; \n● Физическая активность;"
}

View File

@@ -45,7 +45,9 @@ class MyApp extends StatelessWidget {
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
supportedLocales: [
Locale.fromSubtags(languageCode: 'ru'),
],
),
),
),

View File

@@ -29,7 +29,7 @@ dependencies:
go_router: ^14.6.2
keyboard_dismisser: ^3.0.0
retrofit: ^4.4.1
get_it: ^8.0.2
get_it: ^8.0.3
dio: ^5.7.0
dart_pusher_channels: ^1.2.3
photo_view: ^0.15.0
@@ -39,11 +39,11 @@ dependencies:
badges: ^3.1.2
google_fonts: ^6.2.1
location: ^7.0.1
syncfusion_flutter_charts: ^27.2.5
syncfusion_flutter_gauges: ^27.2.5
table_calendar: ^3.1.2
syncfusion_flutter_charts: ^28.1.33
syncfusion_flutter_gauges: ^28.1.33
table_calendar: ^3.1.3
flutter_keyboard_visibility: ^6.0.0
share_plus: ^10.1.2
share_plus: ^10.1.3
flutter_html: ^3.0.0-beta.2
file_picker:
git:
@@ -64,7 +64,7 @@ dependencies:
http: ^1.2.2
dev_dependencies:
build_runner: ^2.4.13
build_runner: ^2.4.14
freezed: ^2.5.7
json_serializable: ^6.9.0
retrofit_generator: ^9.1.5