forked from eatvio/eatvio_app
430 lines
17 KiB
Dart
430 lines
17 KiB
Dart
import 'dart:io';
|
||
import 'package:eatvio_app/core/app_export.dart';
|
||
import 'package:eatvio_app/features/recipes/models/ingredient.dart';
|
||
import 'package:eatvio_app/features/recipes/models/recipe_model.dart';
|
||
import 'package:eatvio_app/features/recipes/models/image.dart' as image;
|
||
import 'package:eatvio_app/features/recipes/models/step.dart' as step;
|
||
import 'package:eatvio_app/features/recipes/presentation/bloc/recipe_create_cubit.dart';
|
||
import 'package:eatvio_app/features/recipes/presentation/bloc/recipe_create_state.dart';
|
||
import 'package:eatvio_app/widgets/custom_drop_down.dart';
|
||
import 'package:eatvio_app/widgets/custom_text_form_field.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter/cupertino.dart';
|
||
import 'package:flutter/services.dart';
|
||
import 'package:image_picker/image_picker.dart';
|
||
|
||
// ignore: must_be_immutable
|
||
class RecipeCreateScreen extends StatelessWidget {
|
||
RecipeCreateScreen({Key? key}) : super(key: key);
|
||
Widget build(BuildContext context) {
|
||
return BlocBuilder<RecipeCreateCubit, RecipeCreateState>(
|
||
builder: (context, state) {
|
||
if (state is RecipeCreateStateLoaded) {
|
||
return _buildRecipeCreate(context, state);
|
||
} else if (state is RecipeCreateStateLoading) {
|
||
return Scaffold(
|
||
body: Center(
|
||
child: CupertinoActivityIndicator(),
|
||
),
|
||
);
|
||
} else if (state is RecipeCreateStateError) {
|
||
return Scaffold(
|
||
body: Padding(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Center(
|
||
child: Text(state.errorMessage),
|
||
),
|
||
),
|
||
);
|
||
} else
|
||
return Scaffold(
|
||
body: Padding(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Center(
|
||
child: Text("Неопределенная ошибка"),
|
||
),
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
Widget _buildRecipeCreate(
|
||
BuildContext context, RecipeCreateStateLoaded state) {
|
||
return Scaffold(
|
||
appBar: AppBar(
|
||
toolbarHeight: 0,
|
||
automaticallyImplyLeading: false,
|
||
),
|
||
backgroundColor: Color.fromARGB(255, 240, 242, 245),
|
||
resizeToAvoidBottomInset: true,
|
||
body: SizedBox(
|
||
width: double.maxFinite,
|
||
child: SingleChildScrollView(
|
||
child: Column(
|
||
children: [
|
||
Container(
|
||
padding: EdgeInsets.symmetric(
|
||
horizontal: 24,
|
||
vertical: 12,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text('Заголовок'),
|
||
SizedBox(height: 8),
|
||
CustomTextFormField(
|
||
fillColor: appTheme.gray50,
|
||
controller: state.titleController,
|
||
hintText: 'Заголовок',
|
||
),
|
||
SizedBox(height: 16),
|
||
Text('Тип блюда'),
|
||
SizedBox(height: 8),
|
||
CustomDropDown(
|
||
value: state.types[0],
|
||
items: state.types,
|
||
),
|
||
SizedBox(height: 16),
|
||
Text('Изображение'),
|
||
GestureDetector(
|
||
onTap: () async {
|
||
final pickedFile = await state.picker
|
||
.pickImage(source: ImageSource.gallery);
|
||
context
|
||
.read<RecipeCreateCubit>()
|
||
.uploadRecipeImage(pickedFile);
|
||
},
|
||
child: state.recipeImage == null
|
||
? Container(
|
||
height: 128,
|
||
decoration: AppDecoration.fillGray.copyWith(
|
||
borderRadius: BorderRadius.circular(16),
|
||
),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
crossAxisAlignment: CrossAxisAlignment.center,
|
||
children: [
|
||
Icon(
|
||
Icons.photo_library,
|
||
color: appTheme.blueGray300,
|
||
),
|
||
SizedBox(width: 8),
|
||
Text(
|
||
"Добавить фото",
|
||
selectionColor: appTheme.blueA400,
|
||
),
|
||
],
|
||
),
|
||
)
|
||
: Image.file(File(state.recipeImage!.path),
|
||
height: 200, fit: BoxFit.cover),
|
||
),
|
||
SizedBox(height: 16),
|
||
Text('Время приготовления'),
|
||
SizedBox(height: 8),
|
||
CustomDropDown(
|
||
items: state.cookingTimes,
|
||
value: state.cookingTimes[0],
|
||
),
|
||
SizedBox(height: 16),
|
||
Text('Статус'),
|
||
SizedBox(height: 8),
|
||
CustomDropDown(
|
||
items: state.statuses,
|
||
value: state.statuses[0],
|
||
),
|
||
SizedBox(height: 24),
|
||
if (!state.ingredients.isEmpty)
|
||
for (int j = 0; j < state.ingredients.length; j++)
|
||
_buildIngredient(state.ingredients[j]),
|
||
SizedBox(height: 24),
|
||
CustomTextFormField(
|
||
hintText: 'Введите название продукта',
|
||
controller: state.productNameController,
|
||
),
|
||
SizedBox(height: 24),
|
||
CustomTextFormField(
|
||
hintText: 'Порция',
|
||
fillColor: appTheme.gray50,
|
||
controller: state.ingredientController,
|
||
keyboardType: TextInputType.number,
|
||
inputFormatters: <TextInputFormatter>[
|
||
FilteringTextInputFormatter.digitsOnly
|
||
],
|
||
),
|
||
SizedBox(height: 8),
|
||
Row(children: [
|
||
Text('гр'),
|
||
Switch(
|
||
value: state.ingredientAsItems,
|
||
onChanged: (bool value) => context
|
||
.read<RecipeCreateCubit>()
|
||
.switchUnits(value),
|
||
activeColor: appTheme.blueA400,
|
||
inactiveThumbColor: Colors.grey,
|
||
inactiveTrackColor: Colors.grey.shade300,
|
||
),
|
||
Text('шт'),
|
||
]),
|
||
SizedBox(height: 8),
|
||
Container(
|
||
height: 48,
|
||
width: MediaQuery.of(context).size.width,
|
||
child: FloatingActionButton.extended(
|
||
highlightElevation: 0,
|
||
elevation: 0,
|
||
focusElevation: 0,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
backgroundColor: appTheme.blueA400,
|
||
label: Text(
|
||
'+',
|
||
style: theme.textTheme.titleSmall,
|
||
),
|
||
onPressed: () => context
|
||
.read<RecipeCreateCubit>()
|
||
.addIngredient(new Ingredient(
|
||
product_name:
|
||
state.productNameController.text,
|
||
portion: int.parse(
|
||
state.ingredientController.text),
|
||
unit: state.ingredientAsItems
|
||
? 'pcs'
|
||
: 'gr'))),
|
||
),
|
||
SizedBox(height: 24),
|
||
Text('Описание рецепта'),
|
||
SizedBox(height: 8),
|
||
CustomTextFormField(
|
||
maxLines: 8,
|
||
minLines: 8,
|
||
controller: state.stepDescriptionController,
|
||
),
|
||
SizedBox(height: 24),
|
||
for (int i = 0; i < state.numberOfSteps; i++)
|
||
_buildStep(context, i, state),
|
||
SizedBox(height: 24),
|
||
Container(
|
||
height: 48,
|
||
width: MediaQuery.of(context).size.width,
|
||
child: FloatingActionButton.extended(
|
||
highlightElevation: 0,
|
||
elevation: 0,
|
||
focusElevation: 0,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
backgroundColor: appTheme.blueA400,
|
||
onPressed: () =>
|
||
context.read<RecipeCreateCubit>().addStep(),
|
||
label: Text(
|
||
'+',
|
||
style: theme.textTheme.titleSmall,
|
||
),
|
||
),
|
||
),
|
||
SizedBox(height: 8),
|
||
Container(
|
||
height: 48,
|
||
width: MediaQuery.of(context).size.width,
|
||
child: FloatingActionButton.extended(
|
||
highlightElevation: 0,
|
||
elevation: 0,
|
||
focusElevation: 0,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
backgroundColor: Colors.grey,
|
||
onPressed: () =>
|
||
context.read<RecipeCreateCubit>().removeStep(),
|
||
label: Text(
|
||
'-',
|
||
style: theme.textTheme.titleSmall,
|
||
),
|
||
),
|
||
),
|
||
SizedBox(height: 24),
|
||
Text('Ссылка на видео (YouTube)'),
|
||
SizedBox(height: 8),
|
||
CustomTextFormField(
|
||
hintText: 'Введите описание шага',
|
||
fillColor: appTheme.gray50,
|
||
controller: state.youtubeLinkController,
|
||
),
|
||
SizedBox(height: 24),
|
||
Container(
|
||
height: 48,
|
||
width: MediaQuery.of(context).size.width,
|
||
child: FloatingActionButton.extended(
|
||
highlightElevation: 0,
|
||
elevation: 0,
|
||
focusElevation: 0,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
backgroundColor: appTheme.blueA400,
|
||
onPressed: () {},
|
||
label: Text(
|
||
'Опубликовать',
|
||
style: theme.textTheme.titleSmall,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
)),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
_buildStep(
|
||
BuildContext context, int stepNumber, RecipeCreateStateLoaded state) {
|
||
return Column(
|
||
children: [
|
||
Row(children: [
|
||
Text('Шаг ${stepNumber + 1}'),
|
||
Spacer(),
|
||
Text('Всего шагов: ${state.numberOfSteps}')
|
||
]),
|
||
SizedBox(height: 8),
|
||
GestureDetector(
|
||
onTap: () async {
|
||
final pickedFile =
|
||
await state.picker.pickImage(source: ImageSource.gallery);
|
||
context
|
||
.read<RecipeCreateCubit>()
|
||
.uploadStepImage(pickedFile, stepNumber);
|
||
},
|
||
child: state.stepImages[stepNumber] == null
|
||
? Container(
|
||
height: 128,
|
||
decoration: AppDecoration.fillGray.copyWith(
|
||
borderRadius: BorderRadius.circular(16),
|
||
),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
crossAxisAlignment: CrossAxisAlignment.center,
|
||
children: [
|
||
Icon(
|
||
Icons.photo_library,
|
||
color: appTheme.blueGray300,
|
||
),
|
||
SizedBox(width: 8),
|
||
Text(
|
||
"Добавить фото",
|
||
selectionColor: appTheme.blueA400,
|
||
),
|
||
SizedBox(height: 8),
|
||
],
|
||
),
|
||
)
|
||
: Image.file(File(state.stepImages[stepNumber]!.path),
|
||
height: 150, fit: BoxFit.cover)),
|
||
SizedBox(height: 16),
|
||
CustomTextFormField(
|
||
hintText: 'Введите описание шага',
|
||
maxLines: 8,
|
||
minLines: 8,
|
||
fillColor: appTheme.gray50,
|
||
controller: state.stepControllers[stepNumber],
|
||
),
|
||
SizedBox(height: 24),
|
||
],
|
||
);
|
||
}
|
||
|
||
_buildIngredient(Ingredient ingredient) {
|
||
return Container(
|
||
margin: EdgeInsets.symmetric(horizontal: 10.h),
|
||
padding: EdgeInsets.symmetric(
|
||
horizontal: 8.h,
|
||
vertical: 6.v,
|
||
),
|
||
decoration: AppDecoration.fillGray.copyWith(
|
||
borderRadius: BorderRadiusStyle.roundedBorder5,
|
||
),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.end,
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Padding(
|
||
padding: EdgeInsets.only(
|
||
left: 7.h,
|
||
top: 5.v,
|
||
),
|
||
child: Text(
|
||
ingredient.product_name.length > 8
|
||
? ingredient.product_name.substring(0, 8)
|
||
: ingredient.product_name,
|
||
style: CustomTextStyles.titleMedium16,
|
||
),
|
||
),
|
||
Padding(
|
||
padding: EdgeInsets.only(
|
||
left: 20.h,
|
||
top: 3.v,
|
||
bottom: 3.v,
|
||
),
|
||
child: Text(
|
||
ingredient.portion.toString() +
|
||
' ' +
|
||
ingredient.unit.toString().tr,
|
||
),
|
||
),
|
||
Spacer(),
|
||
GestureDetector(
|
||
// onTap: removeIngredient(context, ingredient),
|
||
child: Padding(
|
||
padding: const EdgeInsets.only(right: 10),
|
||
child: Icon(
|
||
Icons.close,
|
||
color: appTheme.blueGray300,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
RecipeModel createRecipeFromState(RecipeCreateStateLoaded state) {
|
||
// Подготовка ингредиентов
|
||
List<Ingredient> ingredients = state.ingredients;
|
||
|
||
// Подготовка шагов (например, можно создать шаги из stepControllers)
|
||
List<step.Step> steps = [];
|
||
for (int i = 0; i < state.stepControllers.length; i++) {
|
||
if (state.stepControllers[i].text.isNotEmpty) {
|
||
steps.add(step.Step(description: state.stepControllers[i].text));
|
||
}
|
||
}
|
||
|
||
// Подготовка изображения
|
||
image.Image pic = image.Image(path: state.recipeImage?.path ?? '');
|
||
|
||
// Создание экземпляра RecipeModel
|
||
return RecipeModel(
|
||
id: null,
|
||
ingredients: ingredients,
|
||
steps: steps,
|
||
state: 'draft',
|
||
description: state.titleController.text,
|
||
type: state.types.first.value,
|
||
image: pic,
|
||
video_url: state.youtubeLinkController.text.isNotEmpty
|
||
? state.youtubeLinkController.text
|
||
: null,
|
||
cooking_time:
|
||
state.cookingTimes.isNotEmpty ? state.cookingTimes.first.value : null,
|
||
);
|
||
}
|
||
}
|