import 'dart:io'; import 'package:eatvio_app/features/recipes/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/locator_service.dart'; import 'package:eatvio_app/theme/app_decoration.dart'; import 'package:eatvio_app/theme/custom_text_style.dart'; import 'package:eatvio_app/theme/theme_helper.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:flutter_bloc/flutter_bloc.dart'; import 'package:image_picker/image_picker.dart'; import 'package:rive/rive.dart' as rive; class RecipeCreateScreen extends StatelessWidget { RecipeCreateScreen({Key? key}) : super(key: key); Widget build(BuildContext context) { return BlocProvider( create: (context) => sl()..loadRecipeCreate(), child: BlocBuilder( builder: (context, state) { if (state is RecipeCreateStateLoaded) { return _buildRecipeCreate(context, state); } else if (state is RecipeCreateStateLoading) { return Scaffold( body: Center( child: SizedBox( height: 32, width: 32, child: rive.RiveAnimation.asset( 'assets/rive_animations/loading.riv', ), ), ), ); } 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 _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().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( 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 onSelected, Iterable 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: [FilteringTextInputFormatter.digitsOnly], ), SizedBox(height: 8), Row(children: [ Text('гр'), Switch( value: state.ingredientAsItems, onChanged: (bool value) => context.read().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().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().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().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().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().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: 12), padding: EdgeInsets.symmetric( horizontal: 8, vertical: 8, ), decoration: AppDecoration.fillGray.copyWith( borderRadius: BorderRadiusStyle.roundedBorder5, ), child: Row( mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.min, children: [ Padding( padding: EdgeInsets.only( left: 8, top: 8, ), child: Text( ingredient.name.length > 8 ? ingredient.name.substring(0, 8) : ingredient.name, style: CustomTextStyles.titleMedium16, ), ), Padding( padding: EdgeInsets.only( left: 24, top: 4, bottom: 4, ), child: Text( ingredient.portion.toString() + ' ' + ingredient.unit.toString(), ), ), Spacer(), GestureDetector( // onTap: removeIngredient(context, ingredient), child: Padding( padding: const EdgeInsets.only(right: 10), child: Icon( Icons.close, color: appTheme.blueGray300, ), ), ), ], ), ); } _buildOptions(List _options) { return (TextEditingValue textEditingValue) { return _options.where((String option) { return true; }); }; } selectProduct(AutocompleteOnSelected 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().inputProduct(value); } }; } changeType(BuildContext context) { return (value) { context.read().changeType(value); }; } changeStatus(BuildContext context) { return (value) { context.read().changeStatus(value); }; } changeCookingTime(BuildContext context) { return (value) { context.read().changeCookingTime(value); }; } RecipeModel createRecipeFromState(RecipeCreateStateLoaded state) { List ingredients = state.ingredients; List 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; } }