forked from eatvio/eatvio_app
feat: implement recipe drafting functionality and refactor MyRecipesTab to be stateless
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -20,4 +20,5 @@ env/.env
|
|||||||
.env
|
.env
|
||||||
android/key.properties
|
android/key.properties
|
||||||
app/upload-keystore.jks
|
app/upload-keystore.jks
|
||||||
coverage/
|
coverage/
|
||||||
|
eatvio/
|
||||||
@@ -172,6 +172,23 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> saveAsDraft([bool update = false, String? slug]) async {
|
||||||
|
try {
|
||||||
|
final model = state.entity.toModel();
|
||||||
|
await recipeRepository.saveAsDraft(model, update, slug);
|
||||||
|
emit(state.copyWith(drafted: true));
|
||||||
|
return true;
|
||||||
|
} catch (e, st) {
|
||||||
|
await Sentry.captureException(e, stackTrace: st);
|
||||||
|
log(e.toString(), stackTrace: st);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool validateForDraft() {
|
||||||
|
return validateAll();
|
||||||
|
}
|
||||||
|
|
||||||
Future<File> downloadImageAsFile(String imageUrl) async {
|
Future<File> downloadImageAsFile(String imageUrl) async {
|
||||||
final response = await http.get(Uri.parse(imageUrl));
|
final response = await http.get(Uri.parse(imageUrl));
|
||||||
|
|
||||||
@@ -239,6 +256,7 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
|
|||||||
final isActiveCookTimeValid = validateActiveCookTime();
|
final isActiveCookTimeValid = validateActiveCookTime();
|
||||||
final isEnergyPerServingsValid = validateEnergyPerServing();
|
final isEnergyPerServingsValid = validateEnergyPerServing();
|
||||||
|
|
||||||
|
final isIngredientsValid = validateIngredients();
|
||||||
final isStepsValid = validateSteps();
|
final isStepsValid = validateSteps();
|
||||||
|
|
||||||
return isTitleValid &&
|
return isTitleValid &&
|
||||||
@@ -248,8 +266,19 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
|
|||||||
isCookTimeValid &&
|
isCookTimeValid &&
|
||||||
isActiveCookTimeValid &&
|
isActiveCookTimeValid &&
|
||||||
isEnergyPerServingsValid &&
|
isEnergyPerServingsValid &&
|
||||||
|
isIngredientsValid &&
|
||||||
isStepsValid;
|
isStepsValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool validateIngredients() {
|
||||||
|
if (state.entity.ingredients.isEmpty) {
|
||||||
|
emit(state.copyWith(ingredientsValidationError: 'Добавьте хотя бы один ингредиент'));
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
emit(state.copyWith(ingredientsValidationError: null));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool validateTitle() {
|
bool validateTitle() {
|
||||||
if (state.entity.title == null || (state.entity.title?.isEmpty ?? true)) {
|
if (state.entity.title == null || (state.entity.title?.isEmpty ?? true)) {
|
||||||
@@ -343,6 +372,10 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
|
|||||||
|
|
||||||
bool validateSteps() {
|
bool validateSteps() {
|
||||||
bool isValid = true;
|
bool isValid = true;
|
||||||
|
if (state.entity.steps.isEmpty) {
|
||||||
|
emit(state.copyWith(stepsValidationError: 'Добавьте хотя бы один шаг'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
for (var step in state.entity.steps) {
|
for (var step in state.entity.steps) {
|
||||||
if (step.description == null || step.description!.trim().isEmpty) {
|
if (step.description == null || step.description!.trim().isEmpty) {
|
||||||
isValid = false;
|
isValid = false;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ abstract class CreateRecipeState with _$CreateRecipeState {
|
|||||||
const factory CreateRecipeState({
|
const factory CreateRecipeState({
|
||||||
required RecipeCreateEntity entity,
|
required RecipeCreateEntity entity,
|
||||||
required bool loading,
|
required bool loading,
|
||||||
|
@Default(false) bool drafted,
|
||||||
String? titleValidationError,
|
String? titleValidationError,
|
||||||
String? descriptionValidationError,
|
String? descriptionValidationError,
|
||||||
String? cookingTimeValidationError,
|
String? cookingTimeValidationError,
|
||||||
@@ -16,6 +17,7 @@ abstract class CreateRecipeState with _$CreateRecipeState {
|
|||||||
String? energyPerServingValidationError,
|
String? energyPerServingValidationError,
|
||||||
String? imageValidationError,
|
String? imageValidationError,
|
||||||
String? stepsValidationError,
|
String? stepsValidationError,
|
||||||
|
String? ingredientsValidationError,
|
||||||
}) = _CreateRecipeState;
|
}) = _CreateRecipeState;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,55 +81,107 @@ class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
|
|||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return Builder(
|
return Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return _isRecipeBeingPublished
|
if (_isRecipeBeingPublished) {
|
||||||
? const Center(child: Loader())
|
return const Center(child: Loader());
|
||||||
: InkWell(
|
}
|
||||||
key: AppKeys.recipeCreateScreenPublishButton,
|
return Column(
|
||||||
|
children: [
|
||||||
|
InkWell(
|
||||||
|
key: AppKeys.recipeCreateScreenPublishButton,
|
||||||
|
onTap: () async {
|
||||||
|
setState(() {
|
||||||
|
_isRecipeBeingPublished = true;
|
||||||
|
});
|
||||||
|
if (context.read<CreateRecipeCubit>().validateAll()) {
|
||||||
|
final isSuccess = await context.read<CreateRecipeCubit>().publishRecipe(
|
||||||
|
widget.recipe == null ? false : true,
|
||||||
|
widget.recipe?.slug,
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
_isRecipeBeingPublished = false;
|
||||||
|
});
|
||||||
|
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}');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_isRecipeBeingPublished = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
if (widget.recipe == null || widget.recipe?.state == 'drafted')
|
||||||
|
InkWell(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isRecipeBeingPublished = true;
|
_isRecipeBeingPublished = true;
|
||||||
});
|
});
|
||||||
if (context.read<CreateRecipeCubit>().validateAll()) {
|
if (context.read<CreateRecipeCubit>().validateForDraft()) {
|
||||||
final isSuccess = await context.read<CreateRecipeCubit>().publishRecipe(
|
final isSuccess = await context.read<CreateRecipeCubit>().saveAsDraft(
|
||||||
widget.recipe == null ? false : true,
|
widget.recipe == null ? false : true,
|
||||||
widget.recipe?.slug,
|
widget.recipe?.slug,
|
||||||
);
|
);
|
||||||
setState(() {
|
setState(() {
|
||||||
_isRecipeBeingPublished = false;
|
_isRecipeBeingPublished = false;
|
||||||
});
|
});
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
Future.delayed(const Duration(milliseconds: 500), () {
|
if (!context.mounted) return;
|
||||||
if (!context.mounted) return;
|
context.pop();
|
||||||
context.pop();
|
|
||||||
showBottomSheetWithBlur(
|
|
||||||
userRootNavigator: true,
|
|
||||||
context: context,
|
|
||||||
child: SuccessPublishedRecipeBottomSheet(title: state.entity.title ?? 'Рецепт'),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
showErrorDialog(context, title: '${s.could_not_publish_recipe} ${state.entity.title}');
|
showErrorDialog(context, title: 'Не удалось сохранить черновик');
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_isRecipeBeingPublished = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
setState(() {
|
|
||||||
_isRecipeBeingPublished = false;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
child: Ink(
|
child: Ink(
|
||||||
width: MediaQuery.of(context).size.width - 32,
|
width: MediaQuery.of(context).size.width - 32,
|
||||||
decoration: BoxDecoration(color: appTheme.blueA400, borderRadius: BorderRadius.circular(20)),
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.transparent,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(color: appTheme.blueA400),
|
||||||
|
),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Text(
|
child: Text(
|
||||||
widget.recipe != null ? 'Обновить' : 'Опубликовать',
|
'Сохранить как черновик',
|
||||||
style: const TextStyle(fontSize: 16, color: Colors.white, fontWeight: FontWeight.w600),
|
style: TextStyle(fontSize: 16, color: appTheme.blueA400, fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -85,6 +85,14 @@ class _IngredientsWidgetState extends State<IngredientsWidget> {
|
|||||||
title: Text('Добавить ингредиент', style: TextStyle(color: appTheme.blueA400)),
|
title: Text('Добавить ингредиент', style: TextStyle(color: appTheme.blueA400)),
|
||||||
leading: Icon(Icons.add_rounded, color: appTheme.blueA400),
|
leading: Icon(Icons.add_rounded, color: appTheme.blueA400),
|
||||||
),
|
),
|
||||||
|
if (state.ingredientsValidationError != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 16, top: 4),
|
||||||
|
child: Text(
|
||||||
|
state.ingredientsValidationError!,
|
||||||
|
style: const TextStyle(color: Colors.red, fontSize: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:developer';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
|
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
|
||||||
@@ -23,55 +24,59 @@ class RecipeRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> publishRecipe(RecipeModel recipe, [bool update = false, String? slug]) async {
|
Future<FormData> _buildFormData(RecipeModel recipe, {String state = 'active'}) async {
|
||||||
try {
|
if (recipe.image == null || (recipe.image?.isEmpty ?? true)) {
|
||||||
if (recipe.image == null || (recipe.image?.isEmpty ?? true)) {
|
throw Exception('Recipe image is required');
|
||||||
return;
|
}
|
||||||
}
|
final image = await ImageCompressUtility.compressFile(File(recipe.image ?? ''));
|
||||||
final image = await ImageCompressUtility.compressFile(File(recipe.image ?? ''));
|
final multiPartFile = await MultipartFile.fromFile(image.path);
|
||||||
final multiPartFile = await MultipartFile.fromFile(image.path);
|
|
||||||
|
|
||||||
final ingredientsMap = recipe.ingredients?.map((ingredient) {
|
final ingredientsMap = recipe.ingredients?.map((ingredient) {
|
||||||
if (ingredient != null) {
|
if (ingredient != null) {
|
||||||
return ingredient.toJson();
|
return ingredient.toJson();
|
||||||
}
|
|
||||||
}).toList() ??
|
|
||||||
[];
|
|
||||||
|
|
||||||
final stepsMap = [];
|
|
||||||
for (final Step? step in recipe.steps ?? []) {
|
|
||||||
if (step != null) {
|
|
||||||
if ((step.image == null || (step.image?.isEmpty ?? true)) && step.description != null) {
|
|
||||||
stepsMap.add({
|
|
||||||
'description_md': step.description ?? "",
|
|
||||||
});
|
|
||||||
} else if (step.image != null && (step.image?.isNotEmpty ?? false)) {
|
|
||||||
final image = await ImageCompressUtility.compressFile(File(step.image ?? ''));
|
|
||||||
final multipartFile = await MultipartFile.fromFile(image.path);
|
|
||||||
stepsMap.add({
|
|
||||||
'description_md': step.description ?? "",
|
|
||||||
'image': multipartFile,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
}).toList() ??
|
||||||
|
[];
|
||||||
|
|
||||||
|
final stepsMap = [];
|
||||||
|
for (final Step? step in recipe.steps ?? []) {
|
||||||
|
if (step != null) {
|
||||||
|
if ((step.image == null || (step.image?.isEmpty ?? true)) && step.description != null) {
|
||||||
|
stepsMap.add({
|
||||||
|
'description_md': step.description ?? "",
|
||||||
|
});
|
||||||
|
} else if (step.image != null && (step.image?.isNotEmpty ?? false)) {
|
||||||
|
final image = await ImageCompressUtility.compressFile(File(step.image ?? ''));
|
||||||
|
final multipartFile = await MultipartFile.fromFile(image.path);
|
||||||
|
stepsMap.add({
|
||||||
|
'description_md': step.description ?? "",
|
||||||
|
'image': multipartFile,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final formMap = {
|
}
|
||||||
'type': recipe.type,
|
final formMap = {
|
||||||
'state': 'active',
|
'type': recipe.type,
|
||||||
'title': recipe.title,
|
'state': state,
|
||||||
'tags[]': recipe.tags ?? [],
|
'title': recipe.title,
|
||||||
'description_md': recipe.description,
|
'tags[]': recipe.tags ?? [],
|
||||||
'video_url': recipe.videoUrl,
|
'description_md': recipe.description,
|
||||||
'ingredients': ingredientsMap,
|
'video_url': recipe.videoUrl,
|
||||||
'steps': stepsMap,
|
'ingredients': ingredientsMap,
|
||||||
'cooking_time': recipe.totalCookingTime,
|
'steps': stepsMap,
|
||||||
'active_time': recipe.activeCookTime,
|
'cooking_time': recipe.totalCookingTime,
|
||||||
'image': multiPartFile,
|
'active_time': recipe.activeCookTime,
|
||||||
'servings': recipe.servings,
|
'image': multiPartFile,
|
||||||
'energy_per_serving': recipe.energyPerServing,
|
'servings': recipe.servings,
|
||||||
};
|
'energy_per_serving': recipe.energyPerServing,
|
||||||
formMap.removeWhere((k, v) => v == null || (v is String && v.isEmpty));
|
};
|
||||||
final formData = FormData.fromMap(formMap);
|
formMap.removeWhere((k, v) => v == null || (v is String && v.isEmpty));
|
||||||
|
return FormData.fromMap(formMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> publishRecipe(RecipeModel recipe, [bool update = false, String? slug]) async {
|
||||||
|
try {
|
||||||
|
final formData = await _buildFormData(recipe, state: 'active');
|
||||||
if (update) {
|
if (update) {
|
||||||
await recipeService.updateRecipe(slug: slug!, formData: formData);
|
await recipeService.updateRecipe(slug: slug!, formData: formData);
|
||||||
} else {
|
} else {
|
||||||
@@ -83,6 +88,38 @@ class RecipeRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> saveAsDraft(RecipeModel recipe, [bool update = false, String? slug]) async {
|
||||||
|
try {
|
||||||
|
final formData = await _buildFormData(recipe, state: 'archived');
|
||||||
|
log(formData.fields.toString());
|
||||||
|
if (update) {
|
||||||
|
final response = await recipeService.updateRecipe(slug: slug!, formData: formData);
|
||||||
|
final recipeId = response.data['data']?['id'] ?? response.data['id'];
|
||||||
|
if (recipeId != null) {
|
||||||
|
await recipeService.draftRecipe(id: recipeId as int);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final response = await recipeService.publishRecipe(formData: formData);
|
||||||
|
final recipeId = response.data['data']?['id'] ?? response.data['id'];
|
||||||
|
if (recipeId != null) {
|
||||||
|
await recipeService.draftRecipe(id: recipeId as int);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
await Sentry.captureException(e, stackTrace: s);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> draftExistingRecipe(int id) async {
|
||||||
|
try {
|
||||||
|
await recipeService.draftRecipe(id: id);
|
||||||
|
} catch (e, s) {
|
||||||
|
await Sentry.captureException(e, stackTrace: s);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<RecipeModel> getRecipe(String recipeID) async {
|
Future<RecipeModel> getRecipe(String recipeID) async {
|
||||||
try {
|
try {
|
||||||
return await recipeService.getRecipe(recipeID);
|
return await recipeService.getRecipe(recipeID);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:dio/dio.dart';
|
|||||||
import 'package:eatvio_app/features/create_recipe/models/recipe_model.dart';
|
import 'package:eatvio_app/features/create_recipe/models/recipe_model.dart';
|
||||||
import 'package:eatvio_app/features/create_recipe/models/recipe_tag_model.dart';
|
import 'package:eatvio_app/features/create_recipe/models/recipe_tag_model.dart';
|
||||||
import 'package:retrofit/retrofit.dart';
|
import 'package:retrofit/retrofit.dart';
|
||||||
|
import 'package:retrofit/http.dart';
|
||||||
import 'package:eatvio_app/features/create_recipe/models/product.dart';
|
import 'package:eatvio_app/features/create_recipe/models/product.dart';
|
||||||
|
|
||||||
part 'recipe_service.g.dart';
|
part 'recipe_service.g.dart';
|
||||||
@@ -33,7 +34,7 @@ abstract class CreateRecipeService {
|
|||||||
|
|
||||||
@POST('/api/recipes/create')
|
@POST('/api/recipes/create')
|
||||||
@MultiPart()
|
@MultiPart()
|
||||||
Future<void> publishRecipe({
|
Future<HttpResponse<dynamic>> publishRecipe({
|
||||||
@Body() required FormData formData,
|
@Body() required FormData formData,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -47,8 +48,13 @@ abstract class CreateRecipeService {
|
|||||||
|
|
||||||
@POST('/api/recipes/update/{slug}')
|
@POST('/api/recipes/update/{slug}')
|
||||||
@MultiPart()
|
@MultiPart()
|
||||||
Future<void> updateRecipe({
|
Future<HttpResponse<dynamic>> updateRecipe({
|
||||||
@Path() required String slug,
|
@Path() required String slug,
|
||||||
@Body() required FormData formData,
|
@Body() required FormData formData,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@POST('/api/recipes/draft/{id}')
|
||||||
|
Future<void> draftRecipe({
|
||||||
|
@Path() required int id,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class MyRecipesCubit extends Cubit<MyRecipesState> {
|
|||||||
|
|
||||||
static const String active = 'active';
|
static const String active = 'active';
|
||||||
static const String archived = 'archived';
|
static const String archived = 'archived';
|
||||||
|
static const String drafted = 'drafted';
|
||||||
|
|
||||||
MyRecipesCubit({
|
MyRecipesCubit({
|
||||||
required this.myRecipesRepository,
|
required this.myRecipesRepository,
|
||||||
@@ -50,6 +51,17 @@ class MyRecipesCubit extends Cubit<MyRecipesState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setDraftedState() async {
|
||||||
|
emit(const MyRecipesState.loading());
|
||||||
|
try {
|
||||||
|
final myRecipes = await myRecipesRepository.getMyRecipes();
|
||||||
|
final List<MyRecipeModel> draftedRecipes = myRecipes.where((recipe) => recipe.state == drafted).toList();
|
||||||
|
emit(MyRecipesState.loaded(recipes: draftedRecipes, selectedState: drafted));
|
||||||
|
} catch (e) {
|
||||||
|
emit(const MyRecipesState.error());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<Recipe> getRecipeById(String slug) async {
|
Future<Recipe> getRecipeById(String slug) async {
|
||||||
return await aboutRecipeRepository.getRecipe(slug);
|
return await aboutRecipeRepository.getRecipe(slug);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:eatvio_app/core/theme/theme_helper.dart';
|
|||||||
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart';
|
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart';
|
||||||
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recipes_state.dart';
|
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recipes_state.dart';
|
||||||
import 'package:eatvio_app/features/recipe/presentation/widgets/my_recipe_widget.dart';
|
import 'package:eatvio_app/features/recipe/presentation/widgets/my_recipe_widget.dart';
|
||||||
|
import 'package:eatvio_app/features/recipe/presentation/widgets/my_recipes_tab.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
@@ -32,15 +33,41 @@ class MyRecipesScreen extends StatelessWidget {
|
|||||||
create: (BuildContext context) => sl<MyRecipesCubit>()..getMyRecipes(),
|
create: (BuildContext context) => sl<MyRecipesCubit>()..getMyRecipes(),
|
||||||
child: BlocBuilder<MyRecipesCubit, MyRecipesState>(
|
child: BlocBuilder<MyRecipesCubit, MyRecipesState>(
|
||||||
builder: (BuildContext context, MyRecipesState state) {
|
builder: (BuildContext context, MyRecipesState state) {
|
||||||
if (state is MyRecipesStateLoaded) {
|
return Column(
|
||||||
return ListView.separated(
|
children: [
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
MyRecipesTab(
|
||||||
itemBuilder: (context, index) => MyRecipeWidget(recipe: state.recipes[index]),
|
selectedIndex: state is MyRecipesStateLoaded
|
||||||
separatorBuilder: (context, index) => const SizedBox(height: 12),
|
? (state.selectedState == MyRecipesCubit.drafted ? 1 : 0)
|
||||||
itemCount: state.recipes.length,
|
: 0,
|
||||||
);
|
onTabChanged: (index) {
|
||||||
}
|
if (index == 0) {
|
||||||
return Container();
|
context.read<MyRecipesCubit>().setActiveState();
|
||||||
|
} else {
|
||||||
|
context.read<MyRecipesCubit>().setDraftedState();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: state is MyRecipesStateLoaded
|
||||||
|
? state.recipes.isEmpty
|
||||||
|
? Center(
|
||||||
|
child: Text(
|
||||||
|
state.selectedState == MyRecipesCubit.drafted
|
||||||
|
? 'Нет черновиков'
|
||||||
|
: 'Нет опубликованных рецептов',
|
||||||
|
style: TextStyles.textLMedium.copyWith(color: appTheme.gray500),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: ListView.separated(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
itemBuilder: (context, index) => MyRecipeWidget(recipe: state.recipes[index]),
|
||||||
|
separatorBuilder: (context, index) => const SizedBox(height: 12),
|
||||||
|
itemCount: state.recipes.length,
|
||||||
|
)
|
||||||
|
: const Center(child: CircularProgressIndicator()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/m
|
|||||||
import 'package:eatvio_app/generated/l10n.dart';
|
import 'package:eatvio_app/generated/l10n.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart';
|
||||||
|
import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart';
|
||||||
import 'package:eatvio_app/core/widgets/markdown_html_widget.dart';
|
import 'package:eatvio_app/core/widgets/markdown_html_widget.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
@@ -99,11 +101,7 @@ class _MyRecipeWidgetState extends State<MyRecipeWidget> {
|
|||||||
? '${widget.recipe.description.substring(0, 200)}...'
|
? '${widget.recipe.description.substring(0, 200)}...'
|
||||||
: widget.recipe.description;
|
: widget.recipe.description;
|
||||||
|
|
||||||
return MarkdownHtmlWidget(
|
return MarkdownHtmlWidget(markdownText: description, fontSize: TextStyles.textSMedium.fontSize ?? 14, textColor: appTheme.black);
|
||||||
markdownText: description,
|
|
||||||
fontSize: TextStyles.textSMedium.fontSize ?? 14,
|
|
||||||
textColor: appTheme.black,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -126,7 +124,13 @@ class _MyRecipeWidgetState extends State<MyRecipeWidget> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showSuccessToast('Рецепт удален');
|
showSuccessToast('Рецепт удален');
|
||||||
if (context.mounted) context.read<MyRecipesCubit>().getMyRecipes();
|
if (context.mounted) {
|
||||||
|
if (isDraft) {
|
||||||
|
context.read<MyRecipesCubit>().setDraftedState();
|
||||||
|
} else {
|
||||||
|
context.read<MyRecipesCubit>().getMyRecipes();
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
text: 'Удалить',
|
text: 'Удалить',
|
||||||
height: 52,
|
height: 52,
|
||||||
@@ -140,30 +144,83 @@ class _MyRecipeWidgetState extends State<MyRecipeWidget> {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: CustomOutlinedButton(
|
child: PopupMenuButton<int>(
|
||||||
onPressed: () async {
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
final box = context.findRenderObject() as RenderBox?;
|
offset: const Offset(0, 50),
|
||||||
await SharePlus.instance.share(
|
itemBuilder: (context) => [
|
||||||
ShareParams(
|
if (!isDraft)
|
||||||
uri: Uri.parse('${sl<AppConfig>().baseUrl}/ru/recipe/${widget.recipe.slug}'),
|
PopupMenuItem(
|
||||||
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
|
onTap: () async {
|
||||||
|
final box = context.findRenderObject() as RenderBox?;
|
||||||
|
await SharePlus.instance.share(
|
||||||
|
ShareParams(
|
||||||
|
uri: Uri.parse('${sl<AppConfig>().baseUrl}/ru/recipe/${widget.recipe.slug}'),
|
||||||
|
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.share, size: 20, color: appTheme.black),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(S.of(context).share, style: TextStyles.textSMedium),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
if (widget.recipe.state == 'active' || isDraft)
|
||||||
},
|
PopupMenuItem(
|
||||||
height: 52,
|
onTap: () {
|
||||||
width: 52,
|
context.push(RecipeCreateScreen.path, extra: widget.recipe);
|
||||||
buttonStyle: ButtonStyle(
|
},
|
||||||
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
|
child: Row(
|
||||||
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
|
children: [
|
||||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
Icon(Icons.edit, size: 20, color: appTheme.black),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(S.of(context).edit, style: TextStyles.textSMedium),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: CustomOutlinedButton(
|
||||||
|
onPressed: () {},
|
||||||
|
height: 52,
|
||||||
|
width: 52,
|
||||||
|
buttonStyle: ButtonStyle(
|
||||||
|
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
|
||||||
|
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
|
||||||
|
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||||
|
),
|
||||||
|
text: isDraft ? '...' : s.share,
|
||||||
|
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||||
),
|
),
|
||||||
text: s.share,
|
|
||||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
if (isDraft)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 12),
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
// Publish draft
|
||||||
|
context.read<CreateRecipeCubit>().publishRecipe(true, widget.recipe.slug).then((success) {
|
||||||
|
if (success) {
|
||||||
|
context.read<MyRecipesCubit>().setDraftedState();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: appTheme.blueA400,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
),
|
||||||
|
child: Text('Опубликовать', style: TextStyles.textSMedium.copyWith(color: Colors.white)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -172,6 +229,8 @@ class _MyRecipeWidgetState extends State<MyRecipeWidget> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get isDraft => widget.recipe.state == 'drafted';
|
||||||
|
|
||||||
Container _iconButton(String iconPath, [String? text]) {
|
Container _iconButton(String iconPath, [String? text]) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
@@ -192,4 +251,3 @@ class _MyRecipeWidgetState extends State<MyRecipeWidget> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,30 +4,21 @@ import 'package:eatvio_app/core/widgets/animated_option_button.dart';
|
|||||||
import 'package:eatvio_app/generated/l10n.dart';
|
import 'package:eatvio_app/generated/l10n.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
class MyRecipesTab extends StatefulWidget {
|
class MyRecipesTab extends StatelessWidget {
|
||||||
const MyRecipesTab({super.key, this.additionalOnTap});
|
const MyRecipesTab({
|
||||||
|
super.key,
|
||||||
|
required this.selectedIndex,
|
||||||
|
required this.onTabChanged,
|
||||||
|
});
|
||||||
|
|
||||||
final void Function()? additionalOnTap;
|
final int selectedIndex;
|
||||||
|
final ValueChanged<int> onTabChanged;
|
||||||
@override
|
|
||||||
State<MyRecipesTab> createState() => _MyRecipesTabState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MyRecipesTabState extends State<MyRecipesTab> {
|
|
||||||
final List<String> tabTitles = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didChangeDependencies() {
|
|
||||||
final s = S.of(context);
|
|
||||||
if (tabTitles.isEmpty) {
|
|
||||||
tabTitles.add(s.published);
|
|
||||||
tabTitles.add(s.drafted);
|
|
||||||
}
|
|
||||||
super.didChangeDependencies();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final s = S.of(context);
|
||||||
|
final tabTitles = [s.published, s.drafted];
|
||||||
|
|
||||||
return Align(
|
return Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -44,18 +35,14 @@ class _MyRecipesTabState extends State<MyRecipesTab> {
|
|||||||
children: [
|
children: [
|
||||||
...List.generate(
|
...List.generate(
|
||||||
2,
|
2,
|
||||||
(index) =>
|
(index) => Expanded(
|
||||||
Expanded(
|
child: AnimatedOptionButton(
|
||||||
child: AnimatedOptionButton(
|
textStyle: TextStyles.textSMedium,
|
||||||
textStyle: TextStyles.textSMedium,
|
isSelected: selectedIndex == index,
|
||||||
isSelected: false, // state.selectedStateIndex == index,
|
text: tabTitles[index],
|
||||||
text: tabTitles[index],
|
onTap: () => onTabChanged(index),
|
||||||
onTap: () {
|
),
|
||||||
widget.additionalOnTap?.call();
|
),
|
||||||
// context.read<MyPlansCubit>().changeSelectedStateIndex(index);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -10,8 +10,15 @@ import 'package:eatvio_app/features/create_recipe/services/recipe_service.dart';
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
|
import 'package:retrofit/retrofit.dart';
|
||||||
|
|
||||||
class MockCreateRecipeService extends Mock implements CreateRecipeService {}
|
class MockCreateRecipeService extends Mock implements CreateRecipeService {}
|
||||||
|
|
||||||
|
final mockHttpResponse = HttpResponse(
|
||||||
|
null,
|
||||||
|
Response(requestOptions: RequestOptions(path: ''), statusCode: 200),
|
||||||
|
);
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
setUpAll(() {
|
setUpAll(() {
|
||||||
registerFallbackValue(FormData());
|
registerFallbackValue(FormData());
|
||||||
@@ -109,7 +116,7 @@ void main() {
|
|||||||
test('deleteRecipe - удаляет рецепт', () async {
|
test('deleteRecipe - удаляет рецепт', () async {
|
||||||
const slug = 'recipe-slug';
|
const slug = 'recipe-slug';
|
||||||
|
|
||||||
when(() => mockService.deleteRecipe(slug: slug)).thenAnswer((_) async => Future.value());
|
when(() => mockService.deleteRecipe(slug: slug)).thenAnswer((_) async => Future.value(mockHttpResponse));
|
||||||
|
|
||||||
await repository.deleteRecipe(slug);
|
await repository.deleteRecipe(slug);
|
||||||
|
|
||||||
@@ -188,7 +195,7 @@ void main() {
|
|||||||
// Мокируем ImageCompressUtility.compressFile
|
// Мокируем ImageCompressUtility.compressFile
|
||||||
// Это сложно, так как это статический метод, но мы можем проверить, что вызывается updateRecipe
|
// Это сложно, так как это статический метод, но мы можем проверить, что вызывается updateRecipe
|
||||||
when(() => mockService.updateRecipe(slug: slug, formData: any(named: 'formData')))
|
when(() => mockService.updateRecipe(slug: slug, formData: any(named: 'formData')))
|
||||||
.thenAnswer((_) async => Future.value());
|
.thenAnswer((_) async => Future.value(mockHttpResponse));
|
||||||
|
|
||||||
// Этот тест может упасть из-за ImageCompressUtility, но мы проверяем логику
|
// Этот тест может упасть из-за ImageCompressUtility, но мы проверяем логику
|
||||||
try {
|
try {
|
||||||
@@ -216,7 +223,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
|
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
|
||||||
.thenAnswer((_) async => Future.value());
|
.thenAnswer((_) async => Future.value(mockHttpResponse));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await repository.publishRecipe(recipe);
|
await repository.publishRecipe(recipe);
|
||||||
@@ -243,7 +250,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
|
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
|
||||||
.thenAnswer((_) async => Future.value());
|
.thenAnswer((_) async => Future.value(mockHttpResponse));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await repository.publishRecipe(recipe);
|
await repository.publishRecipe(recipe);
|
||||||
@@ -270,7 +277,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
|
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
|
||||||
.thenAnswer((_) async => Future.value());
|
.thenAnswer((_) async => Future.value(mockHttpResponse));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await repository.publishRecipe(recipe);
|
await repository.publishRecipe(recipe);
|
||||||
@@ -297,7 +304,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
|
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
|
||||||
.thenAnswer((_) async => Future.value());
|
.thenAnswer((_) async => Future.value(mockHttpResponse));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await repository.publishRecipe(recipe);
|
await repository.publishRecipe(recipe);
|
||||||
@@ -364,7 +371,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
|
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
|
||||||
.thenAnswer((_) async => Future.value());
|
.thenAnswer((_) async => Future.value(mockHttpResponse));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await repository.publishRecipe(recipe);
|
await repository.publishRecipe(recipe);
|
||||||
@@ -399,7 +406,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
|
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
|
||||||
.thenAnswer((_) async => Future.value());
|
.thenAnswer((_) async => Future.value(mockHttpResponse));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await repository.publishRecipe(recipe);
|
await repository.publishRecipe(recipe);
|
||||||
@@ -435,7 +442,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
|
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
|
||||||
.thenAnswer((_) async => Future.value());
|
.thenAnswer((_) async => Future.value(mockHttpResponse));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await repository.publishRecipe(recipe);
|
await repository.publishRecipe(recipe);
|
||||||
@@ -472,7 +479,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
|
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
|
||||||
.thenAnswer((_) async => Future.value());
|
.thenAnswer((_) async => Future.value(mockHttpResponse));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await repository.publishRecipe(recipe);
|
await repository.publishRecipe(recipe);
|
||||||
@@ -499,7 +506,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
|
when(() => mockService.publishRecipe(formData: any(named: 'formData')))
|
||||||
.thenAnswer((_) async => Future.value());
|
.thenAnswer((_) async => Future.value(mockHttpResponse));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await repository.publishRecipe(recipe);
|
await repository.publishRecipe(recipe);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('MyRecipesTab smoke', () {
|
group('MyRecipesTab smoke', () {
|
||||||
Widget wrap(Widget child) {
|
Widget wrap() {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
theme: ThemeHelper().themeData(),
|
theme: ThemeHelper().themeData(),
|
||||||
localizationsDelegates: const [
|
localizationsDelegates: const [
|
||||||
@@ -17,12 +17,17 @@ void main() {
|
|||||||
GlobalCupertinoLocalizations.delegate,
|
GlobalCupertinoLocalizations.delegate,
|
||||||
],
|
],
|
||||||
supportedLocales: const [Locale('ru')],
|
supportedLocales: const [Locale('ru')],
|
||||||
home: Scaffold(body: child),
|
home: Material(
|
||||||
|
child: MyRecipesTab(
|
||||||
|
selectedIndex: 0,
|
||||||
|
onTabChanged: (_) {},
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
testWidgets('renders my recipes tab', (tester) async {
|
testWidgets('renders my recipes tab', (tester) async {
|
||||||
await tester.pumpWidget(wrap(const MyRecipesTab()));
|
await tester.pumpWidget(wrap());
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(find.byType(MyRecipesTab), findsOneWidget);
|
expect(find.byType(MyRecipesTab), findsOneWidget);
|
||||||
|
|||||||
Reference in New Issue
Block a user