diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml
index 4f39e9ec..9d810d8e 100644
--- a/.gitea/workflows/test-flight-cd.yaml
+++ b/.gitea/workflows/test-flight-cd.yaml
@@ -16,7 +16,7 @@ jobs:
run: |
security unlock-keychain -p "$KEYCHAIN_PASSWORD" /Users/gitea_runner_ci/Library/Keychains/login.keychain-db
env:
- KEYCHAIN_PASSWORD: ${{ vars.RUNNER_CI_PASSWORD }}
+ KEYCHAIN_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }}
- name: Flutter build
run: |
@@ -30,7 +30,7 @@ jobs:
env:
LANG: en_US.UTF-8
LC_ALL: en_US.UTF-8
- MATCH_PASSWORD: ${{ vars.RUNNER_CI_PASSWORD }}
+ MATCH_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }}
MATCH_GIT_URL: http://gitea.office.local:3000/mso7nueh/fastlane
FASTLANE_USER: eatvio2023@icloud.com
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ vars.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }}
\ No newline at end of file
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index b8d3a515..485b5cb9 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -388,7 +388,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 14;
+ CURRENT_PROJECT_VERSION = 15;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -538,7 +538,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 14;
+ CURRENT_PROJECT_VERSION = 15;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -576,7 +576,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 14;
+ CURRENT_PROJECT_VERSION = 15;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 6fd068ef..93558f26 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -21,7 +21,7 @@
CFBundleSignature
????
CFBundleVersion
- 14
+ 15
ITSAppUsesNonExemptEncryption
LSRequiresIPhoneOS
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),
],