forked from eatvio/eatvio_app
feat(recipe): edit recipe
This commit is contained in:
@@ -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<String, dynamic>;
|
||||
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(
|
||||
|
||||
@@ -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<int> durationValues = [7, 14, 21];
|
||||
|
||||
|
||||
@@ -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<MyDietsCubit>().unpublishDiet(id!);
|
||||
if (!context.mounted) return;
|
||||
context.read<FeedCubit>().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<MyPlansCubit>().unpublish(slug!);
|
||||
if (!context.mounted) return;
|
||||
context.read<FeedCubit>().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<BlogPostCreateCubit>().deleteBlogPost(id!);
|
||||
if (!context.mounted) return;
|
||||
context.read<FeedCubit>().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<RecipesCubit>().deleteRecipe(slug!);
|
||||
if (!context.mounted) return;
|
||||
context.read<FeedCubit>().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<DietsCubit>().deleteDiet(id!);
|
||||
if (!context.mounted) return;
|
||||
context.read<FeedCubit>().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<MyPlansCubit>().deletePlan(slug!);
|
||||
if (!context.mounted) return;
|
||||
context.read<FeedCubit>().loadFeedBlogPostList();
|
||||
if (!result) {
|
||||
showErrorDialog(context, title: S.current.could_not_delete_blog_post);
|
||||
return;
|
||||
|
||||
@@ -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<Ingredient> ingredients,
|
||||
required List<StepEntity> steps,
|
||||
required List<String> tags, // tags for search
|
||||
required List<String> 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,
|
||||
|
||||
@@ -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<CreateRecipeState> {
|
||||
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<CreateRecipeState> {
|
||||
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<CreateRecipeState> {
|
||||
}
|
||||
|
||||
removeIngredient(int index) {
|
||||
List<Ingredient> ingredients = [...state.entity.ingredients];
|
||||
List<i.Ingredient> ingredients = [...state.entity.ingredients];
|
||||
ingredients.removeAt(index);
|
||||
|
||||
emit(state.copyWith.entity(ingredients: ingredients));
|
||||
}
|
||||
|
||||
replaceIngredientPosition(int oldIndex, int newIndex) {
|
||||
List<Ingredient> ingredients = [...state.entity.ingredients];
|
||||
List<i.Ingredient> 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<CreateRecipeState> {
|
||||
emit(state.copyWith.entity(steps: steps));
|
||||
}
|
||||
|
||||
Future<bool> publishRecipe() async {
|
||||
Future<bool> 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<CreateRecipeState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<File> 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<void> recipeToEntity(Recipe recipe) async {
|
||||
final file = await downloadImageAsFile('$baseUrl/storage/${recipe.image}');
|
||||
|
||||
final List<StepEntity> 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<CreateRecipeState> {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> deleteRecipe(String slug) async {
|
||||
try {
|
||||
await recipeRepository.deleteRecipe(slug);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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<RecipeCreateScreen> {
|
||||
late final CreateRecipeCubit createRecipeCubit;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
createRecipeCubit = sl<CreateRecipeCubit>();
|
||||
if (widget.recipe != null) {
|
||||
createRecipeCubit.recipeToEntity(widget.recipe!);
|
||||
} else {
|
||||
createRecipeCubit.disableLoading();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return BlocProvider<CreateRecipeCubit>(
|
||||
create: (BuildContext context) => sl<CreateRecipeCubit>(),
|
||||
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<CreateRecipeCubit, CreateRecipeState>(
|
||||
builder: (context, state) {
|
||||
return Builder(
|
||||
builder: (context) {
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
if (context.read<CreateRecipeCubit>().validateAll()) {
|
||||
final isSuccess = await context.read<CreateRecipeCubit>().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<CreateRecipeCubit, CreateRecipeState>(
|
||||
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<CreateRecipeCubit, CreateRecipeState>(
|
||||
builder: (context, state) {
|
||||
return Builder(
|
||||
builder: (context) {
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
if (context.read<CreateRecipeCubit>().validateAll()) {
|
||||
final isSuccess = await context.read<CreateRecipeCubit>().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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<InfoAboutRecipeWidget> createState() => _InfoAboutRecipeWidgetState();
|
||||
@@ -23,13 +25,19 @@ class InfoAboutRecipeWidget extends StatefulWidget {
|
||||
class _InfoAboutRecipeWidgetState extends State<InfoAboutRecipeWidget> {
|
||||
final List<SelectionPopupModel> 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<CreateRecipeCubit>();
|
||||
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<InfoAboutRecipeWidget> {
|
||||
const Text('Название'),
|
||||
const SizedBox(height: 4),
|
||||
BlocBuilder<CreateRecipeCubit, CreateRecipeState>(
|
||||
bloc: widget.createRecipeCubit,
|
||||
buildWhen: (previous, current) => previous.titleValidationError != current.titleValidationError,
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
@@ -51,10 +60,11 @@ class _InfoAboutRecipeWidgetState extends State<InfoAboutRecipeWidget> {
|
||||
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<InfoAboutRecipeWidget> {
|
||||
setState(() {
|
||||
selectedType = newSelectedType;
|
||||
});
|
||||
createRecipeCubit.changeType(
|
||||
widget.createRecipeCubit.changeType(
|
||||
TypeOfRecipe.fromString(selectedType.title) ?? TypeOfRecipe.other,
|
||||
);
|
||||
},
|
||||
@@ -100,12 +110,13 @@ class _InfoAboutRecipeWidgetState extends State<InfoAboutRecipeWidget> {
|
||||
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<InfoAboutRecipeWidget> {
|
||||
const SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
hintText: 'Прикрепите ссылку на видео',
|
||||
initialValue: widget.createRecipeCubit.state.entity.videoUrl,
|
||||
onChanged: (value) {
|
||||
createRecipeCubit.replaceVideoUrl(value);
|
||||
widget.createRecipeCubit.replaceVideoUrl(value);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
@@ -14,6 +14,7 @@ class PortionWidget extends StatefulWidget {
|
||||
|
||||
class _PortionWidgetState extends State<PortionWidget> {
|
||||
late final CreateRecipeCubit createRecipeCubit;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -45,6 +46,7 @@ class _PortionWidgetState extends State<PortionWidget> {
|
||||
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<PortionWidget> {
|
||||
CustomTextFormField(
|
||||
hintText: '100',
|
||||
hasError: state.energyPerServingValidationError != null,
|
||||
initialValue: state.entity.energyInOnePortion?.toString(),
|
||||
suffixText: 'Ккал',
|
||||
textInputType: const TextInputType.numberWithOptions(decimal: true),
|
||||
onChanged: (value) {
|
||||
|
||||
@@ -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<StepImageWidget> {
|
||||
XFile? image;
|
||||
late final CreateRecipeCubit createRecipeCubit;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
createRecipeCubit = context.read<CreateRecipeCubit>();
|
||||
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<StepImageWidget> {
|
||||
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<StepImageWidget> {
|
||||
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<StepImageWidget> {
|
||||
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();
|
||||
|
||||
@@ -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<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>(
|
||||
bloc: widget.createRecipeCubit,
|
||||
builder: (BuildContext context, CreateRecipeState state) {
|
||||
return Material(
|
||||
shape: RoundedRectangleBorder(
|
||||
@@ -83,10 +78,11 @@ class _StepsWidgetState extends State<StepsWidget> {
|
||||
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<StepsWidget> {
|
||||
const SizedBox(height: 4),
|
||||
StepImageWidget(
|
||||
index: index,
|
||||
createRecipeCubit: widget.createRecipeCubit,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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<TagsWidget> {
|
||||
late final CreateRecipeCubit createRecipeCubit;
|
||||
final tagController = TextEditingController();
|
||||
final List<String> tags = [];
|
||||
late final List<String> tags;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
createRecipeCubit = context.read<CreateRecipeCubit>();
|
||||
tags = createRecipeCubit.state.entity.tags;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -71,6 +71,7 @@ class _TimeOfCookWidgetState extends State<TimeOfCookWidget> {
|
||||
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<TimeOfCookWidget> {
|
||||
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<TimeOfCookWidget> {
|
||||
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<TimeOfCookWidget> {
|
||||
CustomTextFormField(
|
||||
hintText: 'Минут',
|
||||
hasError: state.activeTimeValidationError != null,
|
||||
initialValue: state.entity.activeCookMinutes?.toString(),
|
||||
textInputType: TextInputType.number,
|
||||
onChanged: (value) {
|
||||
final minutes = int.tryParse(value);
|
||||
|
||||
@@ -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<void> publishRecipe(RecipeModel recipe) async {
|
||||
Future<void> 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<RecipeModel> 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<void> deleteRecipe(String slug) async {
|
||||
try {
|
||||
return await recipeService.deleteRecipe(slug: slug);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,11 +37,18 @@ abstract class CreateRecipeService {
|
||||
@Body() required FormData formData,
|
||||
});
|
||||
|
||||
@POST('/api/recipes/update/{id}')
|
||||
Future<RecipeModel> editRecipe(@Part() Map<String, Object?> recipe, String id);
|
||||
|
||||
@GET('/api/recipes/tags')
|
||||
Future<RecipeTagModel> getTags();
|
||||
|
||||
@DELETE('/api/recipes/delete/{slug}')
|
||||
Future<void> deleteRecipe({
|
||||
@Path() required String slug,
|
||||
});
|
||||
|
||||
}
|
||||
@POST('/api/recipes/update/{slug}')
|
||||
@MultiPart()
|
||||
Future<void> updateRecipe({
|
||||
@Path() required String slug,
|
||||
@Body() required FormData formData,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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<Tag> tags,
|
||||
required Author author,
|
||||
}) = _Recipes;
|
||||
|
||||
factory Recipe.fromJson(Map<String, Object?> json) => _$RecipeFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class Tag with _$Tag {
|
||||
const factory Tag({required String name}) = _Tag;
|
||||
|
||||
factory Tag.fromJson(Map<String, Object?> json) => _$TagFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class Ingredient with _$Ingredient {
|
||||
const factory Ingredient({required String name, String? unit, required String portion}) = _Ingredient;
|
||||
|
||||
@@ -127,6 +127,7 @@ class _RecipesWidgetState extends State<RecipesWidget> {
|
||||
path: 'recipe/${widget.recipe.slug}',
|
||||
slug: widget.recipe.slug,
|
||||
complainedContentType: ComplainedContent.recipe,
|
||||
recipe: widget.recipe,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -218,6 +218,7 @@ class _AboutRecipeScreenState extends State<AboutRecipeScreen> {
|
||||
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<AboutRecipeScreen> {
|
||||
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<AboutRecipeScreen> {
|
||||
path: 'recipe/${widget.recipe.slug}',
|
||||
complainedContentType: ComplainedContent.recipe,
|
||||
slug: widget.recipe.slug,
|
||||
recipe: widget.recipe,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user