forked from eatvio/eatvio_app
Merge pull request 'recipe_crud' (#107) from recipe_crud into main
Reviewed-on: maxxkim/eatvio_app#107
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
//import 'dart:io';
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:eatvio_app/core/app_export.dart';
|
||||
import 'package:eatvio_app/widgets/custom_drop_down.dart';
|
||||
import 'package:eatvio_app/widgets/custom_text_form_field.dart';
|
||||
@@ -7,12 +9,63 @@ import 'package:image_picker/image_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../presentation/feed/blog_post_create_screen/bloc/blog_post_create_bloc.dart';
|
||||
|
||||
class RecipeCreateScreen extends StatelessWidget {
|
||||
const RecipeCreateScreen({
|
||||
class RecipeCreateScreen extends StatefulWidget {
|
||||
RecipeCreateScreen({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<RecipeCreateScreen> createState() => _RecipeCreateScreenState();
|
||||
}
|
||||
|
||||
class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
|
||||
final TextEditingController titleController = TextEditingController();
|
||||
|
||||
final TextEditingController productNameController = TextEditingController();
|
||||
|
||||
final TextEditingController portionController = TextEditingController();
|
||||
|
||||
final TextEditingController stepDescriptionController =
|
||||
TextEditingController();
|
||||
|
||||
final TextEditingController youtubeLinkController = TextEditingController();
|
||||
|
||||
final List<SelectionPopupModel> types = [
|
||||
SelectionPopupModel(title: 'Первые блюда'),
|
||||
SelectionPopupModel(title: 'Вторые блюда'),
|
||||
SelectionPopupModel(title: 'Cалаты'),
|
||||
SelectionPopupModel(title: 'Перекусы/закуски'),
|
||||
SelectionPopupModel(title: 'Сладости'),
|
||||
SelectionPopupModel(title: 'Напитки'),
|
||||
SelectionPopupModel(title: 'Прочее'),
|
||||
];
|
||||
|
||||
final List<SelectionPopupModel> cookingTimes = [
|
||||
SelectionPopupModel(title: '10 минут'),
|
||||
SelectionPopupModel(title: '20 минут'),
|
||||
SelectionPopupModel(title: '30 минут'),
|
||||
SelectionPopupModel(title: '1 час'),
|
||||
SelectionPopupModel(title: '2 часа'),
|
||||
SelectionPopupModel(title: '10 дней'),
|
||||
];
|
||||
|
||||
final List<SelectionPopupModel> statuses = [
|
||||
SelectionPopupModel(title: 'Активна'),
|
||||
SelectionPopupModel(title: 'Неактивна'),
|
||||
];
|
||||
|
||||
int numberOfSteps = 1;
|
||||
XFile? recipeImage; // Переменная для общей фотографии рецепта
|
||||
List<XFile?> stepImages = []; // Список фотографий для каждого шага
|
||||
final ImagePicker _picker = ImagePicker();
|
||||
bool portionAsItems = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
stepImages.add(null); // Добавляем начальный элемент для первого шага
|
||||
}
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
@@ -23,43 +76,49 @@ class RecipeCreateScreen extends StatelessWidget {
|
||||
resizeToAvoidBottomInset: true,
|
||||
body: SizedBox(
|
||||
width: double.maxFinite,
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: 5),
|
||||
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: [
|
||||
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(
|
||||
validator: _validateTitle,
|
||||
fillColor: appTheme.gray50,
|
||||
hintText: 'Заголовок',
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text('Тип блюда'),
|
||||
SizedBox(height: 8),
|
||||
CustomDropDown(
|
||||
validator: _validateDescription,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text('Изображение'),
|
||||
Container(
|
||||
Text('Заголовок'),
|
||||
SizedBox(height: 8),
|
||||
CustomTextFormField(
|
||||
validator: _validateTitle,
|
||||
fillColor: appTheme.gray50,
|
||||
controller: titleController,
|
||||
hintText: 'Заголовок',
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text('Тип блюда'),
|
||||
SizedBox(height: 8),
|
||||
CustomDropDown(
|
||||
value: types[0],
|
||||
items: types,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text('Изображение'),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
final pickedFile = await _picker.pickImage(
|
||||
source: ImageSource.gallery);
|
||||
setState(() {
|
||||
recipeImage =
|
||||
pickedFile; // Сохраняем общее изображение рецепта
|
||||
});
|
||||
},
|
||||
child: recipeImage == null
|
||||
? Container(
|
||||
height: 128,
|
||||
decoration: AppDecoration.fillGray.copyWith(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
@@ -79,117 +138,223 @@ class RecipeCreateScreen extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text('Время приготовления'),
|
||||
SizedBox(height: 8),
|
||||
CustomDropDown(
|
||||
validator: _validateYouTubeLink,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text('Статус'),
|
||||
SizedBox(height: 8),
|
||||
CustomDropDown(
|
||||
validator: _validateYouTubeLink,
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
CustomTextFormField(
|
||||
hintText: 'Введите название продукта',
|
||||
validator: _validateYouTubeLink,
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
CustomTextFormField(
|
||||
hintText: 'Порция',
|
||||
fillColor: appTheme.gray50,
|
||||
validator: _validateYouTubeLink,
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
CustomDropDown(
|
||||
validator: _validateYouTubeLink,
|
||||
),
|
||||
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,
|
||||
onPressed: () {},
|
||||
label: Text(
|
||||
'+',
|
||||
style: theme.textTheme.titleSmall,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
Text('Описание рецепта'),
|
||||
SizedBox(height: 8),
|
||||
CustomTextFormField(
|
||||
maxLines: 8,
|
||||
minLines: 8,
|
||||
validator: _validateYouTubeLink,
|
||||
),
|
||||
SizedBox(height: 80),
|
||||
],
|
||||
)),
|
||||
)
|
||||
: Image.file(File(recipeImage!.path),
|
||||
height: 200, fit: BoxFit.cover),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text('Время приготовления'),
|
||||
SizedBox(height: 8),
|
||||
CustomDropDown(
|
||||
items: cookingTimes,
|
||||
value: cookingTimes[0],
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text('Статус'),
|
||||
SizedBox(height: 8),
|
||||
CustomDropDown(
|
||||
items: statuses,
|
||||
value: statuses[0],
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
CustomTextFormField(
|
||||
hintText: 'Введите название продукта',
|
||||
controller: productNameController,
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
CustomTextFormField(
|
||||
hintText: 'Порция',
|
||||
fillColor: appTheme.gray50,
|
||||
controller: portionController,
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Row(children: [
|
||||
Text('гр'),
|
||||
Switch(
|
||||
value: portionAsItems,
|
||||
onChanged: (bool value) {
|
||||
setState(() {
|
||||
portionAsItems = 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,
|
||||
onPressed: () {},
|
||||
label: Text(
|
||||
'+',
|
||||
style: theme.textTheme.titleSmall,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
Text('Описание рецепта'),
|
||||
SizedBox(height: 8),
|
||||
CustomTextFormField(
|
||||
maxLines: 8,
|
||||
minLines: 8,
|
||||
controller: stepDescriptionController,
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
for (int i = 0; i < numberOfSteps; i++) _buildStep(i),
|
||||
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: () {
|
||||
setState(() {
|
||||
stepImages.add(null);
|
||||
numberOfSteps++;
|
||||
});
|
||||
},
|
||||
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: () {
|
||||
setState(() {
|
||||
if (numberOfSteps > 1) {
|
||||
numberOfSteps--;
|
||||
stepImages.removeLast();
|
||||
}
|
||||
});
|
||||
},
|
||||
label: Text(
|
||||
'-',
|
||||
style: theme.textTheme.titleSmall,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
Text('Ссылка на видео (YouTube)'),
|
||||
SizedBox(height: 8),
|
||||
CustomTextFormField(
|
||||
hintText: 'Введите описание шага',
|
||||
fillColor: appTheme.gray50,
|
||||
controller: 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*_buildAddPhoto(BuildContext context, BlogPostCreateLoadedState state) {
|
||||
bool isLoading = state.isLoading;
|
||||
return GestureDetector(
|
||||
onTap: addImage(context, isLoading),
|
||||
child: Stack(
|
||||
_buildStep(int stepNumber) {
|
||||
return Column(
|
||||
children: [
|
||||
if (state.image == 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (state.image != null)
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Image(
|
||||
image: FileImage(
|
||||
File(state.image!.path),
|
||||
),
|
||||
),
|
||||
)
|
||||
Row(children: [
|
||||
Text('Шаг ${stepNumber + 1}'),
|
||||
Spacer(),
|
||||
Text('Всего шагов: $numberOfSteps')
|
||||
]),
|
||||
SizedBox(height: 8),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
final pickedFile =
|
||||
await _picker.pickImage(source: ImageSource.gallery);
|
||||
setState(() {
|
||||
stepImages[stepNumber] =
|
||||
pickedFile; // Сохраняем изображение для текущего шага
|
||||
});
|
||||
},
|
||||
child: 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(stepImages[stepNumber]!.path),
|
||||
height: 150, fit: BoxFit.cover)),
|
||||
SizedBox(height: 16),
|
||||
CustomTextFormField(
|
||||
hintText: 'Введите описание шага',
|
||||
maxLines: 8,
|
||||
minLines: 8,
|
||||
fillColor: appTheme.gray50,
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
);
|
||||
}*/
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
addImage(BuildContext context, bool isLoading) {
|
||||
if (!isLoading) {
|
||||
@@ -209,25 +374,3 @@ String? _validateTitle(value) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _validateDescription(value) {
|
||||
if (value!.isEmpty) {
|
||||
return 'Пожалуйста, введите описание';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _validateYouTubeLink(value) {
|
||||
if (value!.isEmpty) {
|
||||
return 'Пожалуйста, введите ссылку';
|
||||
} else if (!_isValidUrl(value)) {
|
||||
return 'Пожалуйста, введите корректную ссылку';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
bool _isValidUrl(String url) {
|
||||
final RegExp urlRegExp =
|
||||
new RegExp(r"^(http|https):\/\/([\w-]+\.)+[\w-]+(\/[\w-./?%&=]*)?$");
|
||||
return urlRegExp.hasMatch(url);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user