feat: implement recipe drafting functionality and refactor MyRecipesTab to be stateless

This commit is contained in:
mso7nueh
2026-02-16 12:46:55 +03:00
parent 516dfa4158
commit d4f41fe6cf
13 changed files with 388 additions and 153 deletions

View File

@@ -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 {
final response = await http.get(Uri.parse(imageUrl));
@@ -239,6 +256,7 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
final isActiveCookTimeValid = validateActiveCookTime();
final isEnergyPerServingsValid = validateEnergyPerServing();
final isIngredientsValid = validateIngredients();
final isStepsValid = validateSteps();
return isTitleValid &&
@@ -248,8 +266,19 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
isCookTimeValid &&
isActiveCookTimeValid &&
isEnergyPerServingsValid &&
isIngredientsValid &&
isStepsValid;
}
bool validateIngredients() {
if (state.entity.ingredients.isEmpty) {
emit(state.copyWith(ingredientsValidationError: 'Добавьте хотя бы один ингредиент'));
return false;
} else {
emit(state.copyWith(ingredientsValidationError: null));
return true;
}
}
bool validateTitle() {
if (state.entity.title == null || (state.entity.title?.isEmpty ?? true)) {
@@ -343,6 +372,10 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
bool validateSteps() {
bool isValid = true;
if (state.entity.steps.isEmpty) {
emit(state.copyWith(stepsValidationError: 'Добавьте хотя бы один шаг'));
return false;
}
for (var step in state.entity.steps) {
if (step.description == null || step.description!.trim().isEmpty) {
isValid = false;

View File

@@ -8,6 +8,7 @@ abstract class CreateRecipeState with _$CreateRecipeState {
const factory CreateRecipeState({
required RecipeCreateEntity entity,
required bool loading,
@Default(false) bool drafted,
String? titleValidationError,
String? descriptionValidationError,
String? cookingTimeValidationError,
@@ -16,6 +17,7 @@ abstract class CreateRecipeState with _$CreateRecipeState {
String? energyPerServingValidationError,
String? imageValidationError,
String? stepsValidationError,
String? ingredientsValidationError,
}) = _CreateRecipeState;
}

View File

@@ -81,55 +81,107 @@ class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
builder: (context, state) {
return Builder(
builder: (context) {
return _isRecipeBeingPublished
? const Center(child: Loader())
: InkWell(
key: AppKeys.recipeCreateScreenPublishButton,
if (_isRecipeBeingPublished) {
return const Center(child: Loader());
}
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 {
setState(() {
_isRecipeBeingPublished = true;
});
if (context.read<CreateRecipeCubit>().validateAll()) {
final isSuccess = await context.read<CreateRecipeCubit>().publishRecipe(
widget.recipe == null ? false : true,
widget.recipe?.slug,
);
if (context.read<CreateRecipeCubit>().validateForDraft()) {
final isSuccess = await context.read<CreateRecipeCubit>().saveAsDraft(
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 ?? 'Рецепт'),
);
});
if (!context.mounted) return;
context.pop();
} else {
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),
child: Ink(
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(
padding: const EdgeInsets.all(16),
alignment: Alignment.center,
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),
),
),
),
);
),
],
);
},
);
},

View File

@@ -85,6 +85,14 @@ class _IngredientsWidgetState extends State<IngredientsWidget> {
title: Text('Добавить ингредиент', style: TextStyle(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),
),
),
],
),
),