diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 1803c603..1c7210ac 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -196,7 +196,13 @@ class AppRouter { return AboutRecipeScreen(recipe: data['recipe'] as Recipe, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); }, ), - GoRoute(path: RecipeCreateScreen.path, name: RecipeCreateScreen.name, builder: (context, state) => const RecipeCreateScreen()), + GoRoute( + path: RecipeCreateScreen.path, + name: RecipeCreateScreen.name, + builder: (context, state) => RecipeCreateScreen( + recipe: state.extra != null ? state.extra as Recipe : null, + ), + ), GoRoute(path: '/profile-screen', name: 'profileScreen', builder: (context, state) => ProfileScreen(author: state.extra as Author)), GoRoute( path: '/subs-screen/:slug', @@ -224,7 +230,10 @@ class AppRouter { final data = state.extra as Map; final chatId = data['chatId'] as String; final sendInitialMessage = data['sendInitialMessage'] as bool?; - return ChatScreen(chatId: chatId, sendInitialMessage: sendInitialMessage,); + return ChatScreen( + chatId: chatId, + sendInitialMessage: sendInitialMessage, + ); }, ), GoRoute( diff --git a/lib/core/const.dart b/lib/core/const.dart index 31c47e9c..4f38a54e 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -1,5 +1,5 @@ -String baseUrl = 'https://eatvio.ru'; -// String baseUrl = 'https://dev2.eatvio.com'; +// String baseUrl = 'https://eatvio.ru'; +String baseUrl = 'https://dev2.eatvio.com'; const List durationValues = [7, 14, 21]; diff --git a/lib/core/widgets/custom_popup.dart b/lib/core/widgets/custom_popup.dart index a4167f11..837c165b 100644 --- a/lib/core/widgets/custom_popup.dart +++ b/lib/core/widgets/custom_popup.dart @@ -6,9 +6,10 @@ import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/core/widgets/show_error_dialog.dart'; import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart'; import 'package:eatvio_app/features/complaining/presentation/complaining_bottom_sheet.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart'; import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; +import 'package:eatvio_app/features/feed/models/recipes_model.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart'; import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart'; import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart'; @@ -26,6 +27,7 @@ class CustomPopup extends StatelessWidget { final String? slug; final int? id; final FeedBlogPostModel? feedBlogPostModel; + final Recipe? recipe; const CustomPopup({ super.key, @@ -35,6 +37,7 @@ class CustomPopup extends StatelessWidget { this.id, this.slug, this.feedBlogPostModel, + this.recipe, }); @override @@ -94,6 +97,8 @@ class CustomPopup extends StatelessWidget { addItem('assets/icons/edit_paint.svg', 'Редактировать', () { if (complainedContentType == ComplainedContent.posts) { GoRouter.of(context).pushNamed('blogPostCreateScreen', extra: feedBlogPostModel); + } else if (complainedContentType == ComplainedContent.recipe) { + GoRouter.of(context).pushNamed(RecipeCreateScreen.name, extra: recipe); } else { showInfoToast('В разработке'); return; @@ -103,8 +108,6 @@ class CustomPopup extends StatelessWidget { addItem('assets/icons/remove_from_public.svg', 'Снять с публикации', () async { if (complainedContentType == ComplainedContent.diet) { final result = await sl().unpublishDiet(id!); - if (!context.mounted) return; - context.read().loadFeedBlogPostList(); if (!result) { showErrorDialog(context, title: S.current.could_not_delete_blog_post); return; @@ -112,8 +115,6 @@ class CustomPopup extends StatelessWidget { showSuccessToast('Диета перенесена в архив, обновите ленту'); } else if (complainedContentType == ComplainedContent.plan) { final result = await sl().unpublish(slug!); - if (!context.mounted) return; - context.read().loadFeedBlogPostList(); if (!result) { showErrorDialog(context, title: S.current.could_not_delete_blog_post); return; @@ -125,8 +126,6 @@ class CustomPopup extends StatelessWidget { addItem('assets/icons/red_trash.svg', 'Удалить', () async { if (complainedContentType == ComplainedContent.posts) { final result = await sl().deleteBlogPost(id!); - if (!context.mounted) return; - context.read().loadFeedBlogPostList(); if (!result) { showErrorDialog(context, title: S.current.could_not_delete_blog_post); return; @@ -134,8 +133,6 @@ class CustomPopup extends StatelessWidget { showSuccessToast('Пост удален, обновите ленту'); } else if (complainedContentType == ComplainedContent.recipe) { final result = await sl().deleteRecipe(slug!); - if (!context.mounted) return; - context.read().loadFeedBlogPostList(); if (!result) { showErrorDialog(context, title: S.current.could_not_delete_blog_post); return; @@ -143,8 +140,6 @@ class CustomPopup extends StatelessWidget { showSuccessToast('Рецепт удален, обновите ленту'); } else if (complainedContentType == ComplainedContent.diet) { final result = await sl().deleteDiet(id!); - if (!context.mounted) return; - context.read().loadFeedBlogPostList(); if (!result) { showErrorDialog(context, title: S.current.could_not_delete_blog_post); return; @@ -152,8 +147,6 @@ class CustomPopup extends StatelessWidget { showSuccessToast('Диета удалена, обновите ленту'); } else if (complainedContentType == ComplainedContent.plan) { final result = await sl().deletePlan(slug!); - if (!context.mounted) return; - context.read().loadFeedBlogPostList(); if (!result) { showErrorDialog(context, title: S.current.could_not_delete_blog_post); return; diff --git a/lib/features/create_recipe/entities/recipe_create_entity.dart b/lib/features/create_recipe/entities/recipe_create_entity.dart index 91fec435..69d04e8c 100644 --- a/lib/features/create_recipe/entities/recipe_create_entity.dart +++ b/lib/features/create_recipe/entities/recipe_create_entity.dart @@ -55,6 +55,27 @@ enum TypeOfRecipe { } } + static TypeOfRecipe? fromApiType(String value) { + switch (value) { + case 'first_meals': + return TypeOfRecipe.firstMeals; + case 'second_meals': + return TypeOfRecipe.secondMeals; + case 'salads': + return TypeOfRecipe.salads; + case 'snacks': + return TypeOfRecipe.snacks; + case 'sweets': + return TypeOfRecipe.sweets; + case 'drinks': + return TypeOfRecipe.drinks; + case 'other': + return TypeOfRecipe.other; + default: + return null; + } + } + static TypeOfRecipe? fromString(String value) { switch (value) { case 'Первые блюда': @@ -80,6 +101,7 @@ enum TypeOfRecipe { @freezed class RecipeCreateEntity with _$RecipeCreateEntity { const RecipeCreateEntity._(); + const factory RecipeCreateEntity({ String? title, TypeOfRecipe? typeOfRecipe, @@ -94,7 +116,7 @@ class RecipeCreateEntity with _$RecipeCreateEntity { String? videoUrl, required List ingredients, required List steps, - required List tags, // tags for search + required List tags, }) = _RecipeCreateEntity; RecipeModel toModel() => RecipeModel( @@ -118,6 +140,7 @@ class RecipeCreateEntity with _$RecipeCreateEntity { @freezed class StepEntity with _$StepEntity { const StepEntity._(); + const factory StepEntity({ String? description, File? image, diff --git a/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart b/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart index 809944b9..0aaf687f 100644 --- a/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart +++ b/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart @@ -1,17 +1,24 @@ import 'dart:developer'; import 'dart:io'; +import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/features/create_recipe/entities/recipe_create_entity.dart'; -import 'package:eatvio_app/features/create_recipe/models/ingredient.dart'; +import 'package:eatvio_app/features/create_recipe/models/ingredient.dart' as i; import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart'; import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart'; +import 'package:eatvio_app/features/feed/models/recipes_model.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:http/http.dart' as http; +import 'package:path_provider/path_provider.dart'; class CreateRecipeCubit extends Cubit { final RecipeRepository recipeRepository; - CreateRecipeCubit({required this.recipeRepository}) - : super( + + CreateRecipeCubit({ + required this.recipeRepository, + }) : super( const CreateRecipeState( + loading: true, entity: RecipeCreateEntity( typeOfRecipe: TypeOfRecipe.firstMeals, ingredients: [], @@ -38,7 +45,7 @@ class CreateRecipeCubit extends Cubit { emit(state.copyWith.entity(steps: steps)); } - addIngredient(Ingredient entity) { + addIngredient(i.Ingredient entity) { emit( state.copyWith.entity( ingredients: [ @@ -50,14 +57,14 @@ class CreateRecipeCubit extends Cubit { } removeIngredient(int index) { - List ingredients = [...state.entity.ingredients]; + List ingredients = [...state.entity.ingredients]; ingredients.removeAt(index); emit(state.copyWith.entity(ingredients: ingredients)); } replaceIngredientPosition(int oldIndex, int newIndex) { - List ingredients = [...state.entity.ingredients]; + List ingredients = [...state.entity.ingredients]; final item = ingredients.removeAt(oldIndex); ingredients.insert(newIndex, item); emit(state.copyWith.entity(ingredients: ingredients)); @@ -141,10 +148,10 @@ class CreateRecipeCubit extends Cubit { emit(state.copyWith.entity(steps: steps)); } - Future publishRecipe() async { + Future publishRecipe([bool update = false, String? slug]) async { try { final model = state.entity.toModel(); - await recipeRepository.publishRecipe(model); + await recipeRepository.publishRecipe(model, update, slug); return true; } catch (e, st) { log(e.toString(), stackTrace: st); @@ -152,6 +159,57 @@ class CreateRecipeCubit extends Cubit { } } + Future downloadImageAsFile(String imageUrl) async { + final response = await http.get(Uri.parse(imageUrl)); + + if (response.statusCode == 200) { + final tempDir = await getTemporaryDirectory(); + final filePath = '${tempDir.path}/${imageUrl.split('/').last}'; + final file = File(filePath); + return await file.writeAsBytes(response.bodyBytes); + } else { + throw Exception('Не удалось загрузить изображение'); + } + } + + disableLoading() { + emit(state.copyWith(loading: false)); + } + + Future recipeToEntity(Recipe recipe) async { + final file = await downloadImageAsFile('$baseUrl/storage/${recipe.image}'); + + final List steps = await Future.wait( + recipe.steps!.map( + (e) async => StepEntity( + description: e.description, + image: await downloadImageAsFile('$baseUrl/storage/${e.image}'), + ), + ), + ); + emit( + CreateRecipeState( + entity: RecipeCreateEntity( + title: recipe.title, + typeOfRecipe: TypeOfRecipe.fromApiType(recipe.type!), + countOfPortions: recipe.servings, + energyInOnePortion: recipe.calories, + totalCookHours: recipe.cookingTime ~/ 60, + totalCookMinutes: recipe.cookingTime % 60, + activeCookHours: recipe.activeCookingTime ~/ 60, + activeCookMinutes: recipe.activeCookingTime % 60, + description: recipe.description, + image: file, + videoUrl: recipe.videoUrl, + ingredients: recipe.ingredients!.map((e) => i.Ingredient(name: e.name, unit: e.unit!, portion: e.portion)).toList(), + steps: steps, + tags: recipe.tags.map((e) => (e.name)).toList(), + ), + loading: false, + ), + ); + } + bool validateAll() { final isTitleValid = validateTitle(); final isDescValid = validateDescription(); @@ -239,4 +297,13 @@ class CreateRecipeCubit extends Cubit { return true; } } + + Future deleteRecipe(String slug) async { + try { + await recipeRepository.deleteRecipe(slug); + return true; + } catch (e) { + return false; + } + } } diff --git a/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart b/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart index 6bf9e961..5aac99df 100644 --- a/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart +++ b/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart @@ -7,6 +7,7 @@ part 'create_recipe_state.freezed.dart'; class CreateRecipeState with _$CreateRecipeState { const factory CreateRecipeState({ required RecipeCreateEntity entity, + required bool loading, String? titleValidationError, String? descriptionValidationError, String? cookingTimeValidationError, @@ -15,5 +16,5 @@ class CreateRecipeState with _$CreateRecipeState { String? energyPerServingValidationError, String? imageValidationError, }) = _CreateRecipeState; - + } diff --git a/lib/features/create_recipe/presentation/pages/recipe_create_screen.dart b/lib/features/create_recipe/presentation/pages/recipe_create_screen.dart index 7d5ee805..76683d79 100644 --- a/lib/features/create_recipe/presentation/pages/recipe_create_screen.dart +++ b/lib/features/create_recipe/presentation/pages/recipe_create_screen.dart @@ -1,6 +1,7 @@ import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart'; +import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/core/widgets/show_error_dialog.dart'; import 'package:eatvio_app/core/widgets/success_published_recipe_bottom_sheet.dart'; import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; @@ -8,13 +9,19 @@ import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recip import 'package:eatvio_app/features/create_recipe/presentation/widgets/info_about_recipe_widget.dart'; import 'package:eatvio_app/features/create_recipe/presentation/widgets/ingredients_widget.dart'; import 'package:eatvio_app/features/create_recipe/presentation/widgets/steps_widget.dart'; +import 'package:eatvio_app/features/feed/models/recipes_model.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; class RecipeCreateScreen extends StatefulWidget { - const RecipeCreateScreen({super.key}); + final Recipe? recipe; + + const RecipeCreateScreen({ + super.key, + this.recipe, + }); static const name = 'recipeCreateScreen'; static const path = '/recipe-create-screen'; @@ -24,85 +31,115 @@ class RecipeCreateScreen extends StatefulWidget { } class _RecipeCreateScreenState extends State { + late final CreateRecipeCubit createRecipeCubit; + + @override + void initState() { + super.initState(); + createRecipeCubit = sl(); + if (widget.recipe != null) { + createRecipeCubit.recipeToEntity(widget.recipe!); + } else { + createRecipeCubit.disableLoading(); + } + } + @override Widget build(BuildContext context) { final s = S.of(context); return BlocProvider( - create: (BuildContext context) => sl(), - child: Scaffold( - backgroundColor: const Color(0xFFF0F2F5), - appBar: AppBar( - title: const Text('Добавление рецепта'), - centerTitle: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - bottom: Radius.circular(12), - ), - ), - ), - body: SingleChildScrollView( - physics: const BouncingScrollPhysics(), - padding: const EdgeInsets.only(top: 8, bottom: 32), - child: Column( - spacing: 16, - children: [ - const InfoAboutRecipeWidget(), - const IngredientsWidget(), - const StepsWidget(), - BlocBuilder( - builder: (context, state) { - return Builder( - builder: (context) { - return InkWell( - onTap: () async { - if (context.read().validateAll()) { - final isSuccess = await context.read().publishRecipe(); - if (isSuccess) { - Future.delayed(const Duration(milliseconds: 500), () { - if (!context.mounted) return; - context.pop(); - showBottomSheetWithBlur( - userRootNavigator: true, - context: context, - child: SuccessPublishedRecipeBottomSheet( - title: state.entity.title ?? 'Рецепт', - ), - ); - }); - } else { - if (!context.mounted) return; - showErrorDialog(context, title: '${s.could_not_publish_recipe} ${state.entity.title}'); - } - } - }, - 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: const Text( - 'Опубликовать', - style: TextStyle( - fontSize: 16, - color: Colors.white, - fontWeight: FontWeight.w600, - ), - ), - ), - ), - ); - }, - ); - }, + create: (BuildContext context) => createRecipeCubit, + child: BlocBuilder( + builder: (BuildContext context, CreateRecipeState state) { + if (state.loading) { + return const Scaffold( + body: Loader(), + ); + } else { + return Scaffold( + backgroundColor: const Color(0xFFF0F2F5), + appBar: AppBar( + title: widget.recipe != null ? const Text('Редактирование рецепта') : const Text('Добавление рецепта'), + centerTitle: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + bottom: Radius.circular(12), + ), + ), ), - ], - ), - ), + body: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.only(top: 8, bottom: 32), + child: Column( + spacing: 16, + children: [ + InfoAboutRecipeWidget( + createRecipeCubit: createRecipeCubit, + ), + const IngredientsWidget(), + StepsWidget( + createRecipeCubit: createRecipeCubit, + ), + BlocBuilder( + builder: (context, state) { + return Builder( + builder: (context) { + return InkWell( + onTap: () async { + if (context.read().validateAll()) { + final isSuccess = await context.read().publishRecipe( + widget.recipe == null ? false : true, + widget.recipe?.slug, + ); + if (isSuccess) { + Future.delayed(const Duration(milliseconds: 500), () { + if (!context.mounted) return; + context.pop(); + showBottomSheetWithBlur( + userRootNavigator: true, + context: context, + child: SuccessPublishedRecipeBottomSheet( + title: state.entity.title ?? 'Рецепт', + ), + ); + }); + } else { + if (!context.mounted) return; + showErrorDialog(context, title: '${s.could_not_publish_recipe} ${state.entity.title}'); + } + } + }, + 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( + widget.recipe != null ? 'Обновить' : 'Опубликовать', + style: const TextStyle( + fontSize: 16, + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ); + }, + ); + }, + ), + ], + ), + ), + ); + } + }, ), ); } diff --git a/lib/features/create_recipe/presentation/widgets/info_about_recipe_widget.dart b/lib/features/create_recipe/presentation/widgets/info_about_recipe_widget.dart index bc32ca54..38a4c4ee 100644 --- a/lib/features/create_recipe/presentation/widgets/info_about_recipe_widget.dart +++ b/lib/features/create_recipe/presentation/widgets/info_about_recipe_widget.dart @@ -14,7 +14,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class InfoAboutRecipeWidget extends StatefulWidget { - const InfoAboutRecipeWidget({super.key}); + final CreateRecipeCubit createRecipeCubit; + + const InfoAboutRecipeWidget({super.key, required this.createRecipeCubit}); @override State createState() => _InfoAboutRecipeWidgetState(); @@ -23,13 +25,19 @@ class InfoAboutRecipeWidget extends StatefulWidget { class _InfoAboutRecipeWidgetState extends State { final List items = TypeOfRecipe.values.map((e) => SelectionPopupModel(title: e.toString())).toList(); late SelectionPopupModel selectedType; - late final CreateRecipeCubit createRecipeCubit; @override void initState() { super.initState(); - selectedType = items.first; - createRecipeCubit = context.read(); + if (widget.createRecipeCubit.state.entity.typeOfRecipe != null) { + for (int i = 0; i < items.length; i++) { + if (items[i].title == widget.createRecipeCubit.state.entity.typeOfRecipe!.toString()) { + selectedType = items[i]; + } + } + } else { + selectedType = items.first; + } } @override @@ -44,6 +52,7 @@ class _InfoAboutRecipeWidgetState extends State { const Text('Название'), const SizedBox(height: 4), BlocBuilder( + bloc: widget.createRecipeCubit, buildWhen: (previous, current) => previous.titleValidationError != current.titleValidationError, builder: (context, state) { return Column( @@ -51,10 +60,11 @@ class _InfoAboutRecipeWidgetState extends State { CustomTextFormField( hintText: 'Например: тар-тар из говядины', hasError: state.titleValidationError != null, + initialValue: state.entity.title, onChanged: (value) { - createRecipeCubit.replaceTitle(value); + widget.createRecipeCubit.replaceTitle(value); if (state.titleValidationError != null) { - createRecipeCubit.validateTitle(); + widget.createRecipeCubit.validateTitle(); } }, ), @@ -78,7 +88,7 @@ class _InfoAboutRecipeWidgetState extends State { setState(() { selectedType = newSelectedType; }); - createRecipeCubit.changeType( + widget.createRecipeCubit.changeType( TypeOfRecipe.fromString(selectedType.title) ?? TypeOfRecipe.other, ); }, @@ -100,12 +110,13 @@ class _InfoAboutRecipeWidgetState extends State { CustomTextFormField( hintText: 'Расскажите, почему вы выбрали именно этот рецепт? Опишите, каким будет готовое блюдо и в чём его достоинства.', minLines: 5, + initialValue: state.entity.description, maxLines: 5, hasError: state.descriptionValidationError != null, onChanged: (value) { - createRecipeCubit.replaceDescription(value); + widget.createRecipeCubit.replaceDescription(value); if (state.descriptionValidationError != null) { - createRecipeCubit.validateDescription(); + widget.createRecipeCubit.validateDescription(); } }, ), @@ -148,8 +159,9 @@ class _InfoAboutRecipeWidgetState extends State { const SizedBox(height: 4), CustomTextFormField( hintText: 'Прикрепите ссылку на видео', + initialValue: widget.createRecipeCubit.state.entity.videoUrl, onChanged: (value) { - createRecipeCubit.replaceVideoUrl(value); + widget.createRecipeCubit.replaceVideoUrl(value); }, ), ], diff --git a/lib/features/create_recipe/presentation/widgets/portion_widget.dart b/lib/features/create_recipe/presentation/widgets/portion_widget.dart index 99372193..abd593c2 100644 --- a/lib/features/create_recipe/presentation/widgets/portion_widget.dart +++ b/lib/features/create_recipe/presentation/widgets/portion_widget.dart @@ -14,6 +14,7 @@ class PortionWidget extends StatefulWidget { class _PortionWidgetState extends State { late final CreateRecipeCubit createRecipeCubit; + @override void initState() { super.initState(); @@ -45,6 +46,7 @@ class _PortionWidgetState extends State { CustomTextFormField( hintText: '1', hasError: state.servingsValidationError != null, + initialValue: state.entity.countOfPortions?.toString(), textInputType: TextInputType.number, onChanged: (value) { createRecipeCubit.replacePortionsCount(int.parse(value)); @@ -77,6 +79,7 @@ class _PortionWidgetState extends State { CustomTextFormField( hintText: '100', hasError: state.energyPerServingValidationError != null, + initialValue: state.entity.energyInOnePortion?.toString(), suffixText: 'Ккал', textInputType: const TextInputType.numberWithOptions(decimal: true), onChanged: (value) { diff --git a/lib/features/create_recipe/presentation/widgets/step_image_widget.dart b/lib/features/create_recipe/presentation/widgets/step_image_widget.dart index 35f534a5..1189f0f8 100644 --- a/lib/features/create_recipe/presentation/widgets/step_image_widget.dart +++ b/lib/features/create_recipe/presentation/widgets/step_image_widget.dart @@ -3,16 +3,16 @@ import 'dart:io'; import 'package:eatvio_app/features/create_recipe/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 CreateRecipeCubit createRecipeCubit; final int index; const StepImageWidget({ super.key, - required this.index, + required this.index, required this.createRecipeCubit, }); @override @@ -21,12 +21,15 @@ class StepImageWidget extends StatefulWidget { class _StepImageWidgetState extends State { XFile? image; - late final CreateRecipeCubit createRecipeCubit; @override void initState() { super.initState(); - createRecipeCubit = context.read(); + if (widget.createRecipeCubit.state.entity.steps.length > widget.index) { + if (widget.createRecipeCubit.state.entity.steps[widget.index].image != null) { + image = XFile(widget.createRecipeCubit.state.entity.steps[widget.index].image!.path); + } + } } @override @@ -56,7 +59,7 @@ class _StepImageWidgetState extends State { setState(() { image = null; }); - createRecipeCubit.replaceImageInStep(null, widget.index); + widget.createRecipeCubit.replaceImageInStep(null, widget.index); }, child: Container( padding: const EdgeInsets.all(4), @@ -84,7 +87,7 @@ class _StepImageWidgetState extends State { image = await picker.pickImage(source: ImageSource.camera); setState(() {}); if (image != null) { - createRecipeCubit.replaceImageInStep(File(image!.path), widget.index); + widget.createRecipeCubit.replaceImageInStep(File(image!.path), widget.index); } if (!context.mounted) return; Navigator.of(context).pop(); @@ -103,7 +106,7 @@ class _StepImageWidgetState extends State { image = await picker.pickImage(source: ImageSource.gallery); setState(() {}); if (image != null) { - createRecipeCubit.replaceImageInStep(File(image!.path), widget.index); + widget.createRecipeCubit.replaceImageInStep(File(image!.path), widget.index); } if (!context.mounted) return; Navigator.of(context).pop(); diff --git a/lib/features/create_recipe/presentation/widgets/steps_widget.dart b/lib/features/create_recipe/presentation/widgets/steps_widget.dart index 934510f4..07474c72 100644 --- a/lib/features/create_recipe/presentation/widgets/steps_widget.dart +++ b/lib/features/create_recipe/presentation/widgets/steps_widget.dart @@ -7,24 +7,19 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class StepsWidget extends StatefulWidget { - const StepsWidget({super.key}); + final CreateRecipeCubit createRecipeCubit; + + const StepsWidget({super.key, required this.createRecipeCubit}); @override State createState() => _StepsWidgetState(); } class _StepsWidgetState extends State { - late final CreateRecipeCubit createRecipeCubit; - - @override - void initState() { - super.initState(); - createRecipeCubit = context.read(); - } - @override Widget build(BuildContext context) { return BlocBuilder( + bloc: widget.createRecipeCubit, builder: (BuildContext context, CreateRecipeState state) { return Material( shape: RoundedRectangleBorder( @@ -83,10 +78,11 @@ class _StepsWidgetState extends State { const SizedBox(height: 4), CustomTextFormField( hintText: 'Например: подготовьте ингредиенты', + initialValue: widget.createRecipeCubit.state.entity.steps[index].description, maxLines: 5, minLines: 3, onChanged: (value) { - createRecipeCubit.replaceDescriptionInStep(value, index); + widget.createRecipeCubit.replaceDescriptionInStep(value, index); }, ), const SizedBox(height: 12), @@ -94,6 +90,7 @@ class _StepsWidgetState extends State { const SizedBox(height: 4), StepImageWidget( index: index, + createRecipeCubit: widget.createRecipeCubit, ), ], ), diff --git a/lib/features/create_recipe/presentation/widgets/tags_widget.dart b/lib/features/create_recipe/presentation/widgets/tags_widget.dart index 26b9d23b..baac0c3b 100644 --- a/lib/features/create_recipe/presentation/widgets/tags_widget.dart +++ b/lib/features/create_recipe/presentation/widgets/tags_widget.dart @@ -1,6 +1,8 @@ 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/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class TagsWidget extends StatefulWidget { const TagsWidget({super.key}); @@ -10,8 +12,16 @@ class TagsWidget extends StatefulWidget { } class _TagsWidgetState extends State { + late final CreateRecipeCubit createRecipeCubit; final tagController = TextEditingController(); - final List tags = []; + late final List tags; + + @override + void initState() { + super.initState(); + createRecipeCubit = context.read(); + tags = createRecipeCubit.state.entity.tags; + } @override Widget build(BuildContext context) { diff --git a/lib/features/create_recipe/presentation/widgets/time_of_cook_widget.dart b/lib/features/create_recipe/presentation/widgets/time_of_cook_widget.dart index dacd76cb..d995207d 100644 --- a/lib/features/create_recipe/presentation/widgets/time_of_cook_widget.dart +++ b/lib/features/create_recipe/presentation/widgets/time_of_cook_widget.dart @@ -71,6 +71,7 @@ class _TimeOfCookWidgetState extends State { hintText: 'Часов', hasError: state.cookingTimeValidationError != null, textInputType: TextInputType.number, + initialValue: state.entity.totalCookHours?.toString(), onChanged: (value) { final hours = int.tryParse(value); if (hours == null) return; @@ -97,6 +98,7 @@ class _TimeOfCookWidgetState extends State { hintText: 'Минут', hasError: state.cookingTimeValidationError != null, textInputType: TextInputType.number, + initialValue: state.entity.totalCookMinutes?.toString(), onChanged: (value) { final minutes = int.tryParse(value); if (minutes == null) return; @@ -156,6 +158,7 @@ class _TimeOfCookWidgetState extends State { CustomTextFormField( hintText: 'Часов', hasError: state.activeTimeValidationError != null, + initialValue: state.entity.activeCookHours?.toString(), textInputType: TextInputType.number, onChanged: (value) { final hours = int.tryParse(value); @@ -183,6 +186,7 @@ class _TimeOfCookWidgetState extends State { CustomTextFormField( hintText: 'Минут', hasError: state.activeTimeValidationError != null, + initialValue: state.entity.activeCookMinutes?.toString(), textInputType: TextInputType.number, onChanged: (value) { final minutes = int.tryParse(value); diff --git a/lib/features/create_recipe/reporitories/recipe_repository.dart b/lib/features/create_recipe/reporitories/recipe_repository.dart index a0c2a08b..f0f860e9 100644 --- a/lib/features/create_recipe/reporitories/recipe_repository.dart +++ b/lib/features/create_recipe/reporitories/recipe_repository.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:convert'; import 'dart:io'; import 'package:dio/dio.dart'; import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; @@ -11,7 +10,6 @@ import 'package:eatvio_app/features/create_recipe/models/product.dart'; class RecipeRepository { final CreateRecipeService recipeService; - RecipeRepository({required this.recipeService}); @@ -23,7 +21,7 @@ class RecipeRepository { } } - Future publishRecipe(RecipeModel recipe) async { + Future publishRecipe(RecipeModel recipe, [bool update = false, String? slug]) async { try { if (recipe.image == null || (recipe.image?.isEmpty ?? true)) { return; @@ -72,15 +70,11 @@ class RecipeRepository { }; formMap.removeWhere((k, v) => v == null || (v is String && v.isEmpty)); final formData = FormData.fromMap(formMap); - await recipeService.publishRecipe(formData: formData); - } catch (e) { - rethrow; - } - } - - Future editRecipe(RecipeModel recipe, String id) async { - try { - return await recipeService.editRecipe(jsonDecode(jsonEncode(recipe)), id); + if (update) { + await recipeService.updateRecipe(slug: slug!, formData: formData); + } else { + await recipeService.publishRecipe(formData: formData); + } } catch (e) { rethrow; } @@ -100,5 +94,13 @@ class RecipeRepository { } catch (e) { rethrow; } - } + } + + Future deleteRecipe(String slug) async { + try { + return await recipeService.deleteRecipe(slug: slug); + } catch (e) { + rethrow; + } + } } diff --git a/lib/features/create_recipe/services/recipe_service.dart b/lib/features/create_recipe/services/recipe_service.dart index 4bc9c85f..41ee957a 100644 --- a/lib/features/create_recipe/services/recipe_service.dart +++ b/lib/features/create_recipe/services/recipe_service.dart @@ -37,11 +37,18 @@ abstract class CreateRecipeService { @Body() required FormData formData, }); - @POST('/api/recipes/update/{id}') - Future editRecipe(@Part() Map recipe, String id); - @GET('/api/recipes/tags') Future getTags(); + @DELETE('/api/recipes/delete/{slug}') + Future deleteRecipe({ + @Path() required String slug, + }); -} \ No newline at end of file + @POST('/api/recipes/update/{slug}') + @MultiPart() + Future updateRecipe({ + @Path() required String slug, + @Body() required FormData formData, + }); +} diff --git a/lib/features/feed/models/recipes_model.dart b/lib/features/feed/models/recipes_model.dart index 6a5668bd..b999af60 100644 --- a/lib/features/feed/models/recipes_model.dart +++ b/lib/features/feed/models/recipes_model.dart @@ -25,14 +25,23 @@ class Recipe with _$Recipe { @JsonKey(name: 'was_liked_by_me') required bool wasLikedByMe, @JsonKey(name: 'is_favorite') required bool isFavorite, @JsonKey(name: 'energy_per_serving') required double calories, + int? servings, @JsonKey(name: 'total_likes') required int likesCount, @JsonKey(name: 'total_views') required int viewsCount, + required List tags, required Author author, }) = _Recipes; factory Recipe.fromJson(Map json) => _$RecipeFromJson(json); } +@freezed +class Tag with _$Tag { + const factory Tag({required String name}) = _Tag; + + factory Tag.fromJson(Map json) => _$TagFromJson(json); +} + @freezed class Ingredient with _$Ingredient { const factory Ingredient({required String name, String? unit, required String portion}) = _Ingredient; diff --git a/lib/features/feed/presentation/widgets/recipes_widget.dart b/lib/features/feed/presentation/widgets/recipes_widget.dart index 39ca3ee1..53022a92 100644 --- a/lib/features/feed/presentation/widgets/recipes_widget.dart +++ b/lib/features/feed/presentation/widgets/recipes_widget.dart @@ -127,6 +127,7 @@ class _RecipesWidgetState extends State { path: 'recipe/${widget.recipe.slug}', slug: widget.recipe.slug, complainedContentType: ComplainedContent.recipe, + recipe: widget.recipe, ), ), ), diff --git a/lib/features/recipe/presentation/about_recipe_screen.dart b/lib/features/recipe/presentation/about_recipe_screen.dart index 2436ba0b..6906b3da 100644 --- a/lib/features/recipe/presentation/about_recipe_screen.dart +++ b/lib/features/recipe/presentation/about_recipe_screen.dart @@ -218,6 +218,7 @@ class _AboutRecipeScreenState extends State { path: 'recipe/${widget.recipe.slug}', complainedContentType: ComplainedContent.recipe, slug: widget.recipe.slug, + recipe: widget.recipe, ), const SizedBox(width: 16), ], @@ -401,6 +402,7 @@ class _AboutRecipeScreenState extends State { path: 'recipe/${widget.recipe.slug}', complainedContentType: ComplainedContent.recipe, slug: widget.recipe.slug, + recipe: widget.recipe, ), const SizedBox(width: 16), ], @@ -425,6 +427,7 @@ class _AboutRecipeScreenState extends State { path: 'recipe/${widget.recipe.slug}', complainedContentType: ComplainedContent.recipe, slug: widget.recipe.slug, + recipe: widget.recipe, ), const SizedBox(width: 16), ],