Files
eatvio_app/lib/features/recipes/presentation/pages/recipe_edit_screen.dart
2024-09-09 18:18:35 +03:00

513 lines
20 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 RecipeEditScreen extends StatelessWidget {
final String recipeID;
const RecipeEditScreen({
required this.recipeID,
super.key,
});
Widget build(BuildContext context) {
if (recipeID != "0") {
context.read<RecipeCreateCubit>().loadRecipeById(recipeID);
}
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(
state.titleController.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.network(
baseUrl + '/storage/' + state.recipeImage!.path,
height: 150,
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,
),
child: Text(
'+',
style: theme.textTheme.titleSmall,
),
onPressed: () => context.read<RecipeCreateCubit>().addIngredient(new Ingredient(
name: state.productNameController.text,
portion: 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: 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>().addStep(),
),
),
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,
),
child: Text(
'-',
style: theme.textTheme.titleSmall,
),
onPressed: () => context.read<RecipeCreateCubit>().removeStep(),
),
),
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>().editRecipe(createRecipeFromState(state), recipeID),
),
),
],
)),
],
),
),
),
);
}
_buildStep(BuildContext context, int stepNumber, RecipeCreateStateLoaded state) {
print(baseUrl + '/storage/' + state.stepImages[stepNumber]!.path);
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.network(
baseUrl + '/storage/' + 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,
);
return recipe;
}
}