forked from eatvio/eatvio_app
505 lines
20 KiB
Dart
505 lines
20 KiB
Dart
import 'dart:convert';
|
||
import 'dart:io';
|
||
import 'package:eatvio_app/core/app_export.dart';
|
||
import 'package:eatvio_app/features/diary/models/product.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/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/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) {
|
||
context.read<RecipeCreateCubit>().loadRecipeCreate();
|
||
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: CircularProgressIndicator(),
|
||
),
|
||
);
|
||
} 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) {
|
||
List<String> _options = state.autocompleteList.map((Product item) => item.name).toList();
|
||
return Scaffold(
|
||
appBar: AppBar(
|
||
actions: [
|
||
InkWell(
|
||
borderRadius: BorderRadius.circular(16),
|
||
child: Ink(
|
||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
SizedBox(height: 4),
|
||
Text(
|
||
'Новый рецепт',
|
||
style: TextStyle(
|
||
color: Colors.black,
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w400,
|
||
height: 1,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
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.selectedType,
|
||
items: state.types,
|
||
onChanged: changeType(context),
|
||
),
|
||
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.selectedCookingTime,
|
||
onChanged: changeCookingTime(context),
|
||
),
|
||
SizedBox(height: 16),
|
||
Text('Статус'),
|
||
SizedBox(height: 8),
|
||
CustomDropDown(
|
||
items: state.statuses,
|
||
value: state.selectedStatus,
|
||
onChanged: changeStatus(context),
|
||
),
|
||
SizedBox(height: 24),
|
||
if (!state.ingredients.isEmpty)
|
||
for (int j = 0; j < state.ingredients.length; j++) _buildIngredient(state.ingredients[j]),
|
||
SizedBox(height: 24),
|
||
RawAutocomplete<String>(
|
||
optionsBuilder: _buildOptions(_options),
|
||
fieldViewBuilder: (
|
||
BuildContext context,
|
||
TextEditingController productEditingController,
|
||
FocusNode focusNodeName,
|
||
VoidCallback onFieldSubmitted,
|
||
) {
|
||
return CustomTextFormField(
|
||
hintText: 'Введите название продукта',
|
||
controller: TextEditingController(text: state.productNameController.text),
|
||
focusNode: focusNodeName,
|
||
onChanged: getAutocomplete(context, focusNodeName),
|
||
);
|
||
},
|
||
optionsViewBuilder: (
|
||
BuildContext context,
|
||
AutocompleteOnSelected<String> onSelected,
|
||
Iterable<String> options,
|
||
) {
|
||
return Align(
|
||
alignment: Alignment.topLeft,
|
||
child: Material(
|
||
elevation: 4.0,
|
||
child: SizedBox(
|
||
height: 200.0,
|
||
child: ListView.builder(
|
||
padding: const EdgeInsets.all(8.0),
|
||
itemCount: options.length,
|
||
itemBuilder: (BuildContext context, int index) {
|
||
final String option = options.elementAt(index);
|
||
return GestureDetector(
|
||
onTap: selectProduct(onSelected, option, state),
|
||
child: ListTile(
|
||
title: Text(option),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
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: ElevatedButton(
|
||
style: ElevatedButton.styleFrom(
|
||
elevation: 0,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
backgroundColor: appTheme.blueA400,
|
||
),
|
||
onPressed: () => context.read<RecipeCreateCubit>().addIngredient(
|
||
Ingredient(
|
||
name: state.productNameController.text,
|
||
portion: state.ingredientController.text,
|
||
unit: state.ingredientAsItems ? 'pcs' : 'gr',
|
||
),
|
||
),
|
||
child: Text(
|
||
'+',
|
||
style: theme.textTheme.titleSmall,
|
||
),
|
||
),
|
||
),
|
||
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: ElevatedButton(
|
||
style: ElevatedButton.styleFrom(
|
||
elevation: 0,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
backgroundColor: appTheme.blueA400,
|
||
),
|
||
onPressed: () => context.read<RecipeCreateCubit>().addStep(),
|
||
child: Text(
|
||
'+',
|
||
style: theme.textTheme.titleSmall,
|
||
),
|
||
),
|
||
),
|
||
SizedBox(height: 8),
|
||
Container(
|
||
height: 48,
|
||
width: MediaQuery.of(context).size.width,
|
||
child: ElevatedButton(
|
||
style: ElevatedButton.styleFrom(
|
||
elevation: 0,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
backgroundColor: Colors.grey,
|
||
),
|
||
onPressed: () => context.read<RecipeCreateCubit>().removeStep(),
|
||
child: 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: ElevatedButton(
|
||
style: ElevatedButton.styleFrom(
|
||
elevation: 0,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
backgroundColor: appTheme.blueA400,
|
||
),
|
||
child: Text(
|
||
'Опубликовать',
|
||
style: theme.textTheme.titleSmall,
|
||
),
|
||
onPressed: () => context.read<RecipeCreateCubit>().publishRecipe(createRecipeFromState(state)),
|
||
),
|
||
),
|
||
],
|
||
)),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
_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.name.length > 8 ? ingredient.name.substring(0, 8) : ingredient.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,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
_buildOptions(List<String> _options) {
|
||
return (TextEditingValue textEditingValue) {
|
||
return _options.where((String option) {
|
||
return true;
|
||
});
|
||
};
|
||
}
|
||
|
||
selectProduct(AutocompleteOnSelected<String> onSelected, String option, RecipeCreateStateLoaded state) {
|
||
state.productNameController.text = option;
|
||
return () {
|
||
onSelected(option);
|
||
};
|
||
}
|
||
|
||
getAutocomplete(BuildContext context, FocusNode focusNode) {
|
||
return (value) {
|
||
if (value.length > 2) {
|
||
context.read<RecipeCreateCubit>().inputProduct(value);
|
||
}
|
||
};
|
||
}
|
||
|
||
changeType(BuildContext context) {
|
||
return (value) {
|
||
context.read<RecipeCreateCubit>().changeType(value);
|
||
};
|
||
}
|
||
|
||
changeStatus(BuildContext context) {
|
||
return (value) {
|
||
context.read<RecipeCreateCubit>().changeStatus(value);
|
||
};
|
||
}
|
||
|
||
changeCookingTime(BuildContext context) {
|
||
return (value) {
|
||
context.read<RecipeCreateCubit>().changeCookingTime(value);
|
||
};
|
||
}
|
||
|
||
RecipeModel createRecipeFromState(RecipeCreateStateLoaded state) {
|
||
List<Ingredient> ingredients = state.ingredients;
|
||
|
||
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: state.stepImages[i]?.path,
|
||
));
|
||
}
|
||
}
|
||
|
||
final RecipeModel recipe = RecipeModel(
|
||
ingredients: ingredients,
|
||
steps: steps,
|
||
state: state.selectedStatus.value,
|
||
type: state.selectedType.value,
|
||
cooking_time: state.selectedCookingTime.value,
|
||
description: state.stepDescriptionController.text,
|
||
title: state.titleController.text,
|
||
image: state.recipeImage?.path,
|
||
video_url: state.youtubeLinkController.text.isNotEmpty ? state.youtubeLinkController.text : null,
|
||
);
|
||
print(jsonDecode(jsonEncode(recipe)));
|
||
return recipe;
|
||
}
|
||
}
|