diff --git a/lib/presentation/recipe/recipe_create_screen/bloc/recipe_create_bloc.dart b/lib/presentation/recipe/recipe_create_screen/bloc/recipe_create_bloc.dart index f637dbfe..05535dde 100644 --- a/lib/presentation/recipe/recipe_create_screen/bloc/recipe_create_bloc.dart +++ b/lib/presentation/recipe/recipe_create_screen/bloc/recipe_create_bloc.dart @@ -16,6 +16,9 @@ class RecipeCreateBloc extends Bloc { RecipeCreateBloc(RecipeCreateState initialState) : super(initialState) { on(_onInitialize); on(_createRecipe); + on(_addStep); + on(_removeStep); + on(_reloadScreen); } _onInitialize( @@ -34,22 +37,65 @@ class RecipeCreateBloc extends Bloc { emit(currentState.copyWith( recipeModelObj: currentState.recipeModelObj, title: currentState.title, + token: currentState.token, description: currentState.description, video_url: currentState.video_url, - steps: currentState.steps)); + steps: currentState.steps, + stepDescriptions: currentState.stepDescriptions, + tags: currentState.tags)); } else { final Map step = { 'description': '', 'image': '', }; emit(RecipeCreateLoadedState( + token: token, title: TextEditingController(), description: TextEditingController(), video_url: TextEditingController(), + tags: TextEditingController(), + stepDescriptions: [TextEditingController()], steps: [step])); } } + FutureOr _addStep( + AddStep event, Emitter emit) async { + if (state is RecipeCreateLoadedState) { + var currentState = state as RecipeCreateLoadedState; + + List? updatedSteps = currentState.steps; + List? updatedStepDescriptions = + currentState.stepDescriptions; + + updatedSteps!.add({ + 'description': '', + 'image': '', + }); + updatedStepDescriptions!.add(TextEditingController()); + emit(currentState.copyWith( + steps: updatedSteps, stepDescriptions: updatedStepDescriptions)); + await _reloadScreen(event, emit); + } + } + + FutureOr _removeStep( + RemoveStep event, Emitter emit) async { + if (state is RecipeCreateLoadedState) { + var currentState = state as RecipeCreateLoadedState; + + List? updatedSteps = currentState.steps; + List? updatedStepDescriptions = + currentState.stepDescriptions; + + updatedSteps!.removeAt(event.id); + updatedStepDescriptions!.removeAt(event.id); + emit(currentState.copyWith( + steps: updatedSteps, stepDescriptions: updatedStepDescriptions)); + await _reloadScreen(event, emit); + } + } + FutureOr _createRecipe( CreateRecipe event, Emitter emit) async { if (state is RecipeCreateLoadedState) { @@ -59,6 +105,7 @@ class RecipeCreateBloc extends Bloc { currentState.title?.text, currentState.video_url?.text, currentState.description?.text, + currentState.steps, ); if (response.statusCode == 200) { @@ -70,17 +117,14 @@ class RecipeCreateBloc extends Bloc { } } - FutureOr sendRecipe( - String? title, - String? video_url, - String? description, - ) async { + FutureOr sendRecipe(String? title, String? video_url, + String? description, List? steps) async { String url = '${dotenv.env['SERVER_ROOT']!}/recipe/create'; - final Map credentials = { 'title': title, 'description': description, 'video_url': video_url, + 'steps': steps, 'status': 'active', }; @@ -95,4 +139,28 @@ class RecipeCreateBloc extends Bloc { return response; } + + FutureOr _reloadScreen( + RecipeCreateEvent? event, Emitter emit) async { + print('Reloading..'); + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + if (state is RecipeCreateLoadedState) { + var currentState = state as RecipeCreateLoadedState; + emit(currentState.copyWith( + recipeModelObj: currentState.recipeModelObj, + title: currentState.title, + token: currentState.token, + description: currentState.description, + video_url: currentState.video_url, + steps: currentState.steps, + stepDescriptions: currentState.stepDescriptions, + tags: currentState.tags)); + } + } } diff --git a/lib/presentation/recipe/recipe_create_screen/bloc/recipe_create_event.dart b/lib/presentation/recipe/recipe_create_screen/bloc/recipe_create_event.dart index fcea8078..085045d4 100644 --- a/lib/presentation/recipe/recipe_create_screen/bloc/recipe_create_event.dart +++ b/lib/presentation/recipe/recipe_create_screen/bloc/recipe_create_event.dart @@ -19,3 +19,22 @@ class CreateRecipe extends RecipeCreateEvent { @override List get props => []; } + +class AddStep extends RecipeCreateEvent { + AddStep(); + @override + List get props => []; +} + +class RemoveStep extends RecipeCreateEvent { + RemoveStep(this.id); + final int id; + @override + List get props => [id]; +} + +class ReloadScreen extends RecipeCreateEvent { + ReloadScreen(); + @override + List get props => []; +} diff --git a/lib/presentation/recipe/recipe_create_screen/bloc/recipe_create_state.dart b/lib/presentation/recipe/recipe_create_screen/bloc/recipe_create_state.dart index 2a331cb4..8545b837 100644 --- a/lib/presentation/recipe/recipe_create_screen/bloc/recipe_create_state.dart +++ b/lib/presentation/recipe/recipe_create_screen/bloc/recipe_create_state.dart @@ -24,33 +24,52 @@ class RecipeCreateState extends Equatable { class RecipeCreateLoadedState extends RecipeCreateState { RecipeCreateLoadedState( {this.recipeModelObj, + this.token, this.title, this.description, this.video_url, + this.tags, + this.stepDescriptions, this.steps}); RecipeCreateModel? recipeModelObj; + String? token; TextEditingController? title; TextEditingController? description; TextEditingController? video_url; + TextEditingController? tags; List? steps; + List? stepDescriptions; @override - List get props => - [recipeModelObj, title, description, video_url, steps]; + List get props => [ + recipeModelObj, + title, + token, + description, + video_url, + steps, + tags, + stepDescriptions + ]; RecipeCreateLoadedState copyWith({ RecipeCreateModel? recipeModelObj, + String? token, TextEditingController? title, TextEditingController? description, TextEditingController? video_url, + TextEditingController? tags, List? steps, + List? stepDescriptions, }) { return RecipeCreateLoadedState( - recipeModelObj: recipeModelObj ?? this.recipeModelObj, - title: title ?? this.title, - description: description ?? this.description, - video_url: video_url ?? this.video_url, - steps: steps ?? this.steps, - ); + recipeModelObj: recipeModelObj ?? this.recipeModelObj, + title: title ?? this.title, + description: description ?? this.description, + video_url: video_url ?? this.video_url, + token: token ?? this.token, + steps: steps ?? this.steps, + tags: tags ?? this.tags, + stepDescriptions: stepDescriptions ?? this.stepDescriptions); } } diff --git a/lib/presentation/recipe/recipe_create_screen/recipe_create_screen.dart b/lib/presentation/recipe/recipe_create_screen/recipe_create_screen.dart index 88654074..24268793 100644 --- a/lib/presentation/recipe/recipe_create_screen/recipe_create_screen.dart +++ b/lib/presentation/recipe/recipe_create_screen/recipe_create_screen.dart @@ -10,12 +10,12 @@ import 'package:eatvio_app/widgets/custom_outlined_button.dart'; class RecipeCreateScreen extends StatelessWidget { const RecipeCreateScreen({Key? key}) : super(key: key); - static Widget builder(BuildContext context) { final recipeID = ModalRoute.of(context)!.settings.arguments; return BlocProvider( - create: (context) => - RecipeCreateBloc(RecipeCreateState(recipeModelObj: RecipeCreateModel(recipeID: recipeID.toString())))..add(RecipeCreateInitialEvent()), + create: (context) => RecipeCreateBloc(RecipeCreateState( + recipeModelObj: RecipeCreateModel(recipeID: recipeID.toString()))) + ..add(RecipeCreateInitialEvent()), child: RecipeCreateScreen()); } @@ -50,29 +50,27 @@ class RecipeCreateScreen extends StatelessWidget { } } - return SafeArea( - child: Scaffold( - resizeToAvoidBottomInset: false, - body: Container( - width: double.maxFinite, - decoration: BoxDecoration(color: Colors.grey[200]), - child: ListView( - children: [ - _buldAppBar(context), - SizedBox(height: 20.v), - _buildRecipeInfo(state, context), - SizedBox(height: 20.v), - Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: steps, - ), - SizedBox(height: 20.v), - _buildAdditional(context, state) - ], - ), - ), - ), - ); + return RefreshIndicator( + onRefresh: refreshPage(context), + child: SafeArea( + child: Scaffold( + resizeToAvoidBottomInset: false, + body: Container( + width: double.maxFinite, + decoration: BoxDecoration(color: Colors.grey[200]), + child: ListView(children: [ + _buldAppBar(context), + SizedBox(height: 20.v), + _buildRecipeInfo(state, context), + SizedBox(height: 20.v), + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: steps, + ), + SizedBox(height: 20.v), + _buildAdditional(context, state) + ]), + )))); } _buldAppBar(BuildContext context) { @@ -164,45 +162,50 @@ class RecipeCreateScreen extends StatelessWidget { ]), ); } -} -_buildAddPhoto(BuildContext context, RecipeCreateLoadedState state) { - bool isLoading = false; - return GestureDetector( - child: Stack( - children: [ - if (!isLoading) - Container( - padding: EdgeInsets.symmetric( - horizontal: 40.h, - vertical: 80.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, + refreshPage(BuildContext context) { + return () async { + context.read().add(ReloadScreen()); + }; + } + + _buildAddPhoto(BuildContext context, RecipeCreateLoadedState state) { + bool isLoading = false; + return GestureDetector( + child: Stack( + children: [ + if (!isLoading) + Container( + padding: EdgeInsets.symmetric( + horizontal: 40.h, + vertical: 80.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( + "Добавить фото", + selectionColor: appTheme.blueA400, + ), ), - child: Text( - "Добавить фото", - selectionColor: appTheme.blueA400, - ), - ), - Spacer(), - Icon( - Icons.photo_library, - color: appTheme.blueGray300, - ) - ], + Spacer(), + Icon( + Icons.photo_library, + color: appTheme.blueGray300, + ) + ], + ), ), - ), - /*if (isLoading) + /*if (isLoading) Container( margin: EdgeInsets.symmetric(horizontal: 10.h), padding: EdgeInsets.symmetric( @@ -218,155 +221,173 @@ _buildAddPhoto(BuildContext context, RecipeCreateLoadedState state) { .blueGray300), // Замените на свой прелоадер при необходимости ), ),*/ - ], - ), - ); -} - -_buildStep(BuildContext context, RecipeCreateLoadedState state, int i) { - bool isLastStep = i == state.steps!.length - 1; - bool isFirstStep = i == 0; - - return Container( - margin: EdgeInsets.only( - left: 16.h, - right: 16.h, - bottom: 10.h, - ), - padding: EdgeInsets.all(12.h), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: const BorderRadius.all(Radius.circular(12)), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 2, - blurRadius: 5, - offset: Offset(0, 3), - ), - ], - ), - child: Column(mainAxisSize: MainAxisSize.min, children: [ - Align( - alignment: Alignment.centerLeft, - child: Text( - "Шаг ${i + 1}", - style: CustomTextStyles.titleMediumSemiBold, - ), + ], ), - SizedBox(height: 8.0), - CustomTextFormField( - controller: state.description, - hintText: "Введите описание шага", - maxLines: 4, - fillColor: appTheme.gray50, - borderDecoration: OutlineInputBorder( - borderRadius: BorderRadius.circular(5.h), - borderSide: BorderSide( - color: appTheme.gray50, - width: 1, + ); + } + + _buildStep(BuildContext context, RecipeCreateLoadedState state, int i) { + bool isLastStep = i == state.steps!.length - 1; + bool isFirstStep = i == 0; + + return Container( + margin: EdgeInsets.only( + left: 16.h, + right: 16.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Align( + alignment: Alignment.centerLeft, + child: Text( + "Шаг ${i + 1}", + style: CustomTextStyles.titleMediumSemiBold, ), ), - ), - SizedBox(height: 16.0), - _buildAddPhoto(context, state), - if (!isFirstStep) - Row( - children: [ - Flexible( - flex: 5, - child: CustomOutlinedButton( - text: "Добавить шаг", - buttonStyle: CustomButtonStyles.outlinePrimaryTL15, - ), + SizedBox(height: 8.0), + CustomTextFormField( + controller: state.stepDescriptions?[i], + hintText: "Введите описание шага", + maxLines: 4, + fillColor: appTheme.gray50, + borderDecoration: OutlineInputBorder( + borderRadius: BorderRadius.circular(5.h), + borderSide: BorderSide( + color: appTheme.gray50, + width: 1, ), - SizedBox(width: 8), // Add some spacing between the buttons - Flexible( - flex: 1, - child: CustomOutlinedButton( - text: "х", - onPressed: () => Navigator.pushNamed( - context, - AppRoutes.blogPostCreateScreen, - ), - buttonStyle: CustomButtonStyles.outlinePrimaryTL15, - ), - ), - ], + ), ), - if (isLastStep) // Добавляем кнопку "Добавить шаг" только для последнего шага SizedBox(height: 16.0), - CustomOutlinedButton( - text: "Добавить шаг", - buttonStyle: CustomButtonStyles.outlinePrimaryTL15, - ), - ]), - ); -} + _buildAddPhoto(context, state), + SizedBox(height: 16.0), + if (isFirstStep && isLastStep) + CustomOutlinedButton( + onPressed: () => context.read().add(AddStep()), + text: "Добавить шаг", + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + ), + if (isLastStep && + !isFirstStep) // Добавляем кнопку "Добавить шаг" только для последнего шага + Column( + children: [ + SizedBox(height: 16.0), + Row( + children: [ + Flexible( + flex: 5, + child: CustomOutlinedButton( + onPressed: () => + context.read().add(AddStep()), + text: "Добавить шаг", + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + ), + ), + SizedBox(width: 8), // Add some spacing between the buttons + Flexible( + flex: 1, + child: CustomOutlinedButton( + text: "х", + onPressed: () => + context.read().add(RemoveStep(i)), + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + ), + ), + ], + ), + ], + ), + if (!isLastStep && !isFirstStep) + CustomOutlinedButton( + onPressed: () => + context.read().add(RemoveStep(i)), + text: "x", + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + ), + ]), + ); + } -_buildAdditional(BuildContext context, RecipeCreateLoadedState state) { - return Container( - margin: EdgeInsets.only( - left: 16.h, - right: 16.h, - bottom: 10.h, - ), - padding: EdgeInsets.all(12.h), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: const BorderRadius.all(Radius.circular(12)), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 2, - blurRadius: 5, - offset: Offset(0, 3), - ), - ], - ), - child: Column(mainAxisSize: MainAxisSize.min, children: [ - Align(alignment: Alignment.centerLeft, child: Text("Теги")), - CustomTextFormField( - controller: state.title, - hintText: "Введите теги, через запятую", - fillColor: appTheme.gray50, - borderDecoration: OutlineInputBorder( - borderRadius: BorderRadius.circular(5.h), - borderSide: BorderSide( - color: appTheme.gray50, - width: 1, + _buildAdditional(BuildContext context, RecipeCreateLoadedState state) { + return Container( + margin: EdgeInsets.only( + left: 16.h, + right: 16.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Align(alignment: Alignment.centerLeft, child: Text("Теги")), + CustomTextFormField( + controller: state.tags, + hintText: "Введите теги, через запятую", + fillColor: appTheme.gray50, + borderDecoration: OutlineInputBorder( + borderRadius: BorderRadius.circular(5.h), + borderSide: BorderSide( + color: appTheme.gray50, + width: 1, + ), ), ), - ), - SizedBox(height: 8.0), - Align(alignment: Alignment.centerLeft, child: Text("Ссылка на видео (необязательно)")), - CustomTextFormField( - controller: state.description, - hintText: "YouTube", - fillColor: appTheme.gray50, - borderDecoration: OutlineInputBorder( - borderRadius: BorderRadius.circular(5.h), - borderSide: BorderSide( - color: appTheme.gray50, - width: 1, + SizedBox(height: 8.0), + Align( + alignment: Alignment.centerLeft, + child: Text("Ссылка на видео (необязательно)")), + CustomTextFormField( + controller: state.video_url, + hintText: "YouTube", + fillColor: appTheme.gray50, + borderDecoration: OutlineInputBorder( + borderRadius: BorderRadius.circular(5.h), + borderSide: BorderSide( + color: appTheme.gray50, + width: 1, + ), ), ), - ), - SizedBox(height: 16.0), - CustomOutlinedButton( - text: "Добавить рецепт", - buttonStyle: CustomButtonStyles.outlinePrimaryTL15, - ), - ]), - ); -} + SizedBox(height: 16.0), + CustomOutlinedButton( + text: "Добавить рецепт", + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + ), + ]), + ); + } -Function onTapBack(BuildContext context) { - return () { - onTapArrowLeft(context); - }; -} -onTapArrowLeft(BuildContext context) { - NavigatorService.goBack(); + Function onTapBack(BuildContext context) { + return () { + onTapArrowLeft(context); + }; + } + + onTapArrowLeft(BuildContext context) { + NavigatorService.goBack(); + } } diff --git a/lib/presentation/recipe/recipe_edit_screen/bloc/recipe_edit_bloc.dart b/lib/presentation/recipe/recipe_edit_screen/bloc/recipe_edit_bloc.dart index 8cafde30..1e971c2d 100644 --- a/lib/presentation/recipe/recipe_edit_screen/bloc/recipe_edit_bloc.dart +++ b/lib/presentation/recipe/recipe_edit_screen/bloc/recipe_edit_bloc.dart @@ -16,6 +16,9 @@ class RecipeEditBloc extends Bloc { RecipeEditBloc(RecipeEditState initialState) : super(initialState) { on(_onInitialize); on(_editRecipe); + on(_addStep); + on(_removeStep); + on(_reloadScreen); } _onInitialize( @@ -44,16 +47,44 @@ class RecipeEditBloc extends Bloc { var currentState = state as RecipeEditLoadedState; emit(currentState.copyWith( recipeModelObj: currentState.recipeModelObj, - recipe: currentState.recipe, title: currentState.title, + token: currentState.token, + recipe: currentState.recipe, description: currentState.description, - video_url: currentState.video_url)); + video_url: currentState.video_url, + steps: currentState.steps, + stepDescriptions: currentState.stepDescriptions, + tags: currentState.tags)); } else { + final Map defaultStep = { + 'description': '', + 'image': '', + }; + + List steps = data['steps']; + List stepDescriptions = []; + + if (data['steps'] == null) { + stepDescriptions.add(TextEditingController()); + data['steps'].add(defaultStep); + } else { + for (var step in steps) { + stepDescriptions + .add(TextEditingController(text: step['description'])); + } + } + emit(RecipeEditLoadedState( - recipe: data, - title: TextEditingController(text: data['title']), - description: TextEditingController(text: data['description']), - video_url: TextEditingController(text: data['video_url']))); + token: token, + recipe: data, + title: TextEditingController(text: data['title']), + description: TextEditingController(text: data['description']), + video_url: TextEditingController(text: data['video_url']), + tagsController: TextEditingController(), + tags: await _fetchTags(data['id']), + stepDescriptions: stepDescriptions, + steps: steps, + )); } } else { // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку @@ -92,6 +123,42 @@ class RecipeEditBloc extends Bloc { } } + FutureOr _addStep(AddStep event, Emitter emit) async { + if (state is RecipeEditLoadedState) { + var currentState = state as RecipeEditLoadedState; + + List? updatedSteps = currentState.steps; + List? updatedStepDescriptions = + currentState.stepDescriptions; + + updatedSteps!.add({ + 'description': '', + 'image': '', + }); + updatedStepDescriptions!.add(TextEditingController()); + emit(currentState.copyWith( + steps: updatedSteps, stepDescriptions: updatedStepDescriptions)); + await _reloadScreen(event, emit); + } + } + + FutureOr _removeStep( + RemoveStep event, Emitter emit) async { + if (state is RecipeEditLoadedState) { + var currentState = state as RecipeEditLoadedState; + + List? updatedSteps = currentState.steps; + List? updatedStepDescriptions = + currentState.stepDescriptions; + + updatedSteps!.removeAt(event.id); + updatedStepDescriptions!.removeAt(event.id); + emit(currentState.copyWith( + steps: updatedSteps, stepDescriptions: updatedStepDescriptions)); + await _reloadScreen(event, emit); + } + } + FutureOr sendRecipe( Map? recipe, String? title, @@ -123,4 +190,49 @@ class RecipeEditBloc extends Bloc { return response; } + + _fetchTags(int id) async { + final box = await Hive.openBox('token'); + final token = box.get('token'); + + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/recipe/getTags/$id'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + final data = jsonDecode(response.body); + return data; + } + + FutureOr _reloadScreen( + RecipeEditEvent? event, Emitter emit) async { + print('Reloading..'); + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + if (state is RecipeEditLoadedState) { + var currentState = state as RecipeEditLoadedState; + emit(currentState.copyWith( + recipeModelObj: currentState.recipeModelObj, + recipe: currentState.recipe, + title: currentState.title, + token: currentState.token, + description: currentState.description, + video_url: currentState.video_url, + steps: currentState.steps, + stepDescriptions: currentState.stepDescriptions, + tags: currentState.tags)); + } + } } diff --git a/lib/presentation/recipe/recipe_edit_screen/bloc/recipe_edit_event.dart b/lib/presentation/recipe/recipe_edit_screen/bloc/recipe_edit_event.dart index bcbe219c..c6d49d38 100644 --- a/lib/presentation/recipe/recipe_edit_screen/bloc/recipe_edit_event.dart +++ b/lib/presentation/recipe/recipe_edit_screen/bloc/recipe_edit_event.dart @@ -19,3 +19,22 @@ class EditRecipe extends RecipeEditEvent { @override List get props => []; } + +class AddStep extends RecipeEditEvent { + AddStep(); + @override + List get props => []; +} + +class RemoveStep extends RecipeEditEvent { + RemoveStep(this.id); + final int id; + @override + List get props => [id]; +} + +class ReloadScreen extends RecipeEditEvent { + ReloadScreen(); + @override + List get props => []; +} diff --git a/lib/presentation/recipe/recipe_edit_screen/bloc/recipe_edit_state.dart b/lib/presentation/recipe/recipe_edit_screen/bloc/recipe_edit_state.dart index ec001270..af03fcfe 100644 --- a/lib/presentation/recipe/recipe_edit_screen/bloc/recipe_edit_state.dart +++ b/lib/presentation/recipe/recipe_edit_screen/bloc/recipe_edit_state.dart @@ -24,32 +24,52 @@ class RecipeEditState extends Equatable { class RecipeEditLoadedState extends RecipeEditState { RecipeEditLoadedState( {this.recipeModelObj, + this.token, this.recipe, this.title, this.description, - this.video_url}); + this.video_url, + this.tagsController, + this.stepDescriptions, + this.tags, + this.steps}); RecipeEditModel? recipeModelObj; Map? recipe; + String? token; TextEditingController? title; TextEditingController? description; TextEditingController? video_url; + TextEditingController? tagsController; + List? steps; + List? tags; + List? stepDescriptions; @override List get props => [recipeModelObj, recipe, title, description, video_url]; RecipeEditLoadedState copyWith({ RecipeEditModel? recipeModelObj, Map? recipe, + String? token, TextEditingController? title, TextEditingController? description, TextEditingController? video_url, + TextEditingController? tagsController, + List? steps, + List? tags, + List? stepDescriptions, }) { return RecipeEditLoadedState( recipeModelObj: recipeModelObj ?? this.recipeModelObj, + token: token ?? this.token, recipe: recipe ?? this.recipe, title: title ?? this.title, description: description ?? this.description, video_url: video_url ?? this.video_url, + tagsController: tagsController ?? this.tagsController, + steps: steps ?? this.steps, + tags: tags ?? this.tags, + stepDescriptions: stepDescriptions ?? this.stepDescriptions, ); } } diff --git a/lib/presentation/recipe/recipe_edit_screen/recipe_edit_screen.dart b/lib/presentation/recipe/recipe_edit_screen/recipe_edit_screen.dart index 9ee0075f..c2344e1a 100644 --- a/lib/presentation/recipe/recipe_edit_screen/recipe_edit_screen.dart +++ b/lib/presentation/recipe/recipe_edit_screen/recipe_edit_screen.dart @@ -1,17 +1,23 @@ import 'bloc/recipe_edit_bloc.dart'; import 'models/recipe_edit_model.dart'; -import 'package:eatvio_app/core/app_export.dart'; import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:eatvio_app/widgets/app_bar/custom_app_bar.dart'; +import 'package:eatvio_app/widgets/app_bar/appbar_leading_image.dart'; +import 'package:eatvio_app/widgets/app_bar/appbar_subtitle.dart'; +import 'package:eatvio_app/widgets/custom_text_form_field.dart'; +import 'package:eatvio_app/widgets/custom_outlined_button.dart'; class RecipeEditScreen extends StatelessWidget { const RecipeEditScreen({Key? key}) : super(key: key); - static Widget builder(BuildContext context) { final recipeID = ModalRoute.of(context)!.settings.arguments; return BlocProvider( - create: (context) => RecipeEditBloc(RecipeEditState(recipeModelObj: RecipeEditModel(recipeID: recipeID.toString())))..add(RecipeEditInitialEvent()), + create: (context) => RecipeEditBloc(RecipeEditState( + recipeModelObj: RecipeEditModel(recipeID: recipeID.toString()))) + ..add(RecipeEditInitialEvent()), child: RecipeEditScreen()); } @@ -39,78 +45,385 @@ class RecipeEditScreen extends StatelessWidget { _buildRecipeEdit(BuildContext context, RecipeEditLoadedState state) { mediaQueryData = MediaQuery.of(context); - return SafeArea( - child: Scaffold( - resizeToAvoidBottomInset: false, - body: Container( - width: double.maxFinite, - decoration: AppDecoration.fillGray, - child: ListView( - children: [ - SizedBox(height: 30.v), - Container( - margin: EdgeInsets.only( - left: 16.h, - right: 20.h, - bottom: 10.h, - ), - padding: EdgeInsets.all(12.h), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: const BorderRadius.all(Radius.circular(12)), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 2, - blurRadius: 5, - offset: Offset(0, 3), + List steps = []; + if (state.steps != null) { + for (int i = 0; i < state.steps!.length; i++) { + steps.add(_buildStep(context, state, i)); + } + } + + return RefreshIndicator( + onRefresh: refreshPage(context), + child: SafeArea( + child: Scaffold( + resizeToAvoidBottomInset: false, + body: Container( + width: double.maxFinite, + decoration: BoxDecoration(color: Colors.grey[200]), + child: ListView(children: [ + _buldAppBar(context), + SizedBox(height: 20.v), + _buildRecipeInfo(state, context), + SizedBox(height: 20.v), + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: steps, ), - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - CachedNetworkImage( - imageUrl: dotenv.env['STORAGE_URL']! + (state.recipe?['image'] ?? ''), - imageBuilder: (context, imageProvider) => CircleAvatar( - backgroundImage: imageProvider, - radius: 25, - ), - placeholder: (context, url) => CircularProgressIndicator(), - errorWidget: (context, url, error) => CircleAvatar( - child: Icon(Icons.person), - backgroundColor: Colors.white, - radius: 25, - ), - ), - SizedBox(height: 16.0), - TextFormField( - controller: state.title, - decoration: InputDecoration(labelText: 'Заголовок'), - ), - SizedBox(height: 16.0), - TextFormField( - controller: state.description, - decoration: InputDecoration(labelText: 'Описание'), - ), - SizedBox(height: 16.0), - TextFormField( - controller: state.video_url, - decoration: InputDecoration(labelText: 'Ссылка'), - ), - SizedBox(height: 16.0), - // Здесь нужно добавить виджет для загрузки картинки - ElevatedButton( - onPressed: () => context.read().add(EditRecipe()), - child: Text('Сохранить'), - ) - ], - ), - ) - ], + SizedBox(height: 20.v), + _buildAdditional(context, state) + ]), + )))); + } + + _buldAppBar(BuildContext context) { + return Container( + margin: EdgeInsets.all(16.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), ), + ], + ), + child: CustomAppBar( + height: 54.v, + leadingWidth: 28.h, + leading: AppbarLeadingImage( + imagePath: ImageConstant.imgArrowLeft, + margin: EdgeInsets.only(left: 16.h, top: 15.v, bottom: 15.v), + onTap: onTapBack(context), ), + title: Align( + alignment: Alignment.center, + child: AppbarSubtitle( + text: "Редактирование рецепта", + )), ), ); } + + _buildRecipeInfo(RecipeEditLoadedState state, BuildContext context) { + return Container( + margin: EdgeInsets.only( + left: 16.h, + right: 16.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + SizedBox(height: 16.0), + Align(alignment: Alignment.centerLeft, child: Text("Изображение")), + if (state.recipe?['image'] != null) + CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + (state.recipe?['image']), + width: double + .infinity, // Установите ширину равной ширине родительского элемента + height: 200.0, + fit: BoxFit.cover, + placeholder: (context, url) => CircularProgressIndicator(), + errorWidget: (context, url, error) => Icon(Icons.error), + ) + else + _buildAddPhoto(context, state), + SizedBox(height: 16.0), + Align(alignment: Alignment.centerLeft, child: Text("Название рецепта")), + CustomTextFormField( + controller: state.title, + hintText: "Введите название рецепта", + fillColor: appTheme.gray50, + borderDecoration: OutlineInputBorder( + borderRadius: BorderRadius.circular(5.h), + borderSide: BorderSide( + color: appTheme.gray50, + width: 1, + ), + ), + ), + SizedBox(height: 16.0), + Align(alignment: Alignment.centerLeft, child: Text("Описание")), + CustomTextFormField( + controller: state.description, + hintText: "Введите описание рецепта", + maxLines: 4, + fillColor: appTheme.gray50, + borderDecoration: OutlineInputBorder( + borderRadius: BorderRadius.circular(5.h), + borderSide: BorderSide( + color: appTheme.gray50, + width: 1, + ), + ), + ), + SizedBox(height: 16.0), + ]), + ); + } + + refreshPage(BuildContext context) { + return () async { + context.read().add(ReloadScreen()); + }; + } + + _buildAddPhoto(BuildContext context, RecipeEditLoadedState state) { + bool isLoading = false; + return GestureDetector( + child: Stack( + children: [ + if (!isLoading) + Container( + padding: EdgeInsets.symmetric( + horizontal: 40.h, + vertical: 80.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( + "Добавить фото", + selectionColor: appTheme.blueA400, + ), + ), + Spacer(), + Icon( + Icons.photo_library, + color: appTheme.blueGray300, + ) + ], + ), + ), + /*if (isLoading) + Container( + margin: EdgeInsets.symmetric(horizontal: 10.h), + padding: EdgeInsets.symmetric( + horizontal: 40.h, + vertical: 80.v, + ), + decoration: AppDecoration.fillGray.copyWith( + borderRadius: BorderRadiusStyle.roundedBorder5, + ), + child: Center( + child: CircularProgressIndicator( + color: appTheme + .blueGray300), // Замените на свой прелоадер при необходимости + ), + ),*/ + ], + ), + ); + } + + _buildStep(BuildContext context, RecipeEditLoadedState state, int i) { + bool isLastStep = i == state.steps!.length - 1; + bool isFirstStep = i == 0; + + return Container( + margin: EdgeInsets.only( + left: 16.h, + right: 16.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Align( + alignment: Alignment.centerLeft, + child: Text( + "Шаг ${i + 1}", + style: CustomTextStyles.titleMediumSemiBold, + ), + ), + SizedBox(height: 8.0), + CustomTextFormField( + controller: state.stepDescriptions?[i], + hintText: "Введите описание шага", + maxLines: 4, + fillColor: appTheme.gray50, + borderDecoration: OutlineInputBorder( + borderRadius: BorderRadius.circular(5.h), + borderSide: BorderSide( + color: appTheme.gray50, + width: 1, + ), + ), + ), + SizedBox(height: 16.0), + if (state.steps?[i]['image'] != null) + CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + (state.steps?[i]['image']), + width: double + .infinity, // Установите ширину равной ширине родительского элемента + height: 200.0, // Задайте постоянную высоту элемента + fit: BoxFit.cover, + placeholder: (context, url) => CircularProgressIndicator(), + errorWidget: (context, url, error) => Icon(Icons.error), + ) + else + _buildAddPhoto(context, state), + SizedBox(height: 16.0), + if (isFirstStep && isLastStep) + CustomOutlinedButton( + onPressed: () => context.read().add(AddStep()), + text: "Добавить шаг", + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + ), + if (isLastStep && + !isFirstStep) // Добавляем кнопку "Добавить шаг" только для последнего шага + Column( + children: [ + SizedBox(height: 16.0), + Row( + children: [ + Flexible( + flex: 5, + child: CustomOutlinedButton( + onPressed: () => + context.read().add(AddStep()), + text: "Добавить шаг", + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + ), + ), + SizedBox(width: 8), // Add some spacing between the buttons + Flexible( + flex: 1, + child: CustomOutlinedButton( + text: "х", + onPressed: () => + context.read().add(RemoveStep(i)), + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + ), + ), + ], + ), + ], + ), + if (!isLastStep && !isFirstStep) + CustomOutlinedButton( + onPressed: () => context.read().add(RemoveStep(i)), + text: "x", + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + ), + ]), + ); + } + + _buildAdditional(BuildContext context, RecipeEditLoadedState state) { + return Container( + margin: EdgeInsets.only( + left: 16.h, + right: 16.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Align(alignment: Alignment.centerLeft, child: Text("Теги")), + _buildEditableTags(state.tags), + SizedBox(height: 8.0), + Align( + alignment: Alignment.centerLeft, + child: Text("Ссылка на видео (необязательно)")), + CustomTextFormField( + controller: state.video_url, + hintText: "YouTube", + fillColor: appTheme.gray50, + borderDecoration: OutlineInputBorder( + borderRadius: BorderRadius.circular(5.h), + borderSide: BorderSide( + color: appTheme.gray50, + width: 1, + ), + ), + ), + SizedBox(height: 16.0), + CustomOutlinedButton( + text: "Добавить рецепт", + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + ), + ]), + ); + } + + _buildEditableTags(List? tags) { + String initialText = tags!.map((tag) => tag['name']).join(', '); + + return CustomTextFormField( + initialValue: initialText, + fillColor: appTheme.gray50, + borderDecoration: OutlineInputBorder( + borderRadius: BorderRadius.circular(5.h), + borderSide: BorderSide( + color: appTheme.gray50, + width: 1, + ), + ), + onChanged: _trimTags, + ); + } + + void _trimTags(newValue) { + List newTagNames = + newValue!.split(',').map((tag) => tag.trim()).toList(); + List> newTags = + newTagNames.map((tagName) => {'name': tagName}).toList(); + print(newTags); + } + + Function onTapBack(BuildContext context) { + return () { + onTapArrowLeft(context); + }; + } + + onTapArrowLeft(BuildContext context) { + NavigatorService.goBack(); + } } diff --git a/lib/presentation/recipe/recipe_show_screen/bloc/recipe_show_bloc.dart b/lib/presentation/recipe/recipe_show_screen/bloc/recipe_show_bloc.dart index a65348ed..982266c0 100644 --- a/lib/presentation/recipe/recipe_show_screen/bloc/recipe_show_bloc.dart +++ b/lib/presentation/recipe/recipe_show_screen/bloc/recipe_show_bloc.dart @@ -5,9 +5,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:hive/hive.dart'; import 'package:http/http.dart' as http; +import 'package:intl/intl.dart'; import 'dart:convert'; import '/core/app_export.dart'; import 'package:eatvio_app/presentation/recipe/recipe_show_screen/models/recipe_show_model.dart'; +import 'package:eatvio_app/presentation/feed/blog_post_screen/models/comment_model.dart'; part 'recipe_show_event.dart'; part 'recipe_show_state.dart'; @@ -16,6 +18,7 @@ class RecipeBloc extends Bloc { RecipeBloc(RecipeState initialState) : super(initialState) { on(_onInitialize); on(_deleteEntry); + on(_createComment); } _onInitialize( @@ -44,9 +47,21 @@ class RecipeBloc extends Bloc { var currentState = state as RecipeLoadedState; emit(currentState.copyWith( recipeModelObj: currentState.recipeModelObj, - recipe: currentState.recipe)); + recipe: currentState.recipe, + token: currentState.token, + commentController: currentState.commentController, + comments: currentState.comments, + steps: currentState.steps, + author: currentState.author)); } else { - emit(RecipeLoadedState(recipe: data)); + emit(RecipeLoadedState( + token: token, + recipe: data, + steps: data['steps'], + author: await _fetchAuthor(data['id']), + comments: fillCommentList(await _fetchComments(data['id'])), + tags: await _fetchTags(data['id']), + commentController: TextEditingController())); } } else { // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку @@ -85,4 +100,115 @@ class RecipeBloc extends Bloc { emit(RecipeErrorState('Ошибка: ${response.statusCode}')); } } + + FutureOr _createComment( + CreateComment event, Emitter emit) async { + if (state is RecipeLoadedState) { + var currentState = state as RecipeLoadedState; + // Отправляем данные по HTTP маршруту и ждем ответ + var response = await _sendCommentData( + currentState.token, event.body, currentState.recipe?['id']); + if (response.statusCode == 200) { + var updatedComments = + fillCommentList(await _fetchComments(currentState.recipe?['id'])); + var updatedState = currentState.copyWith( + comments: updatedComments, + commentController: + TextEditingController(), // Clear the comment input field + ); + emit(updatedState); // Emit the updated state to trigger a screen update + } + } + } + + _fetchAuthor(int id) async { + final box = await Hive.openBox('token'); + final token = box.get('token'); + + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/recipe/getAuthor/$id'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + final data = jsonDecode(response.body); + return data; + } + + Future _fetchComments(int id) async { + final box = await Hive.openBox('token'); + final token = box.get('token'); + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/recipe/getComments/$id'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + final data = jsonDecode(response.body); + return data; + } + + _fetchTags(int id) async { + final box = await Hive.openBox('token'); + final token = box.get('token'); + + if (token == null) { + NavigatorService.popAndPushNamed( + AppRoutes.authScreen, + ); + } + + final response = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/recipe/getTags/$id'), + headers: { + 'Authorization': 'Bearer $token', + }, + ); + final data = jsonDecode(response.body); + return data; + } + + List fillCommentList(List comments) { + List list = []; + for (int i = 0; i < comments.length; i++) { + list.add(CommentModel( + id: comments[i]['id'].toString(), + userImage: comments[i]['profile_photo_path'], + username: comments[i]['name'], + dateText: DateFormat('dd.MM') + .format(DateTime.parse(comments[i]['updated_at'])), + greetingText: comments[i]['body'])); + } + return list; + } + + Future _sendCommentData( + String? token, String body, int id) async { + String url = dotenv.env['SERVER_ROOT']! + '/recipe/createComment/$id'; + final Map credentials = { + 'id': id, + 'body': body, + }; + + var response = await http.post(Uri.parse(url), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token', + }, + body: json.encode(credentials), + encoding: utf8); + + return response; + } } diff --git a/lib/presentation/recipe/recipe_show_screen/bloc/recipe_show_event.dart b/lib/presentation/recipe/recipe_show_screen/bloc/recipe_show_event.dart index 6257e893..7510c3a1 100644 --- a/lib/presentation/recipe/recipe_show_screen/bloc/recipe_show_event.dart +++ b/lib/presentation/recipe/recipe_show_screen/bloc/recipe_show_event.dart @@ -21,3 +21,10 @@ class DeleteEntry extends RecipeEvent { @override List get props => [id]; } + +class CreateComment extends RecipeEvent { + CreateComment(this.body); + final String body; + @override + List get props => [body]; +} diff --git a/lib/presentation/recipe/recipe_show_screen/bloc/recipe_show_state.dart b/lib/presentation/recipe/recipe_show_screen/bloc/recipe_show_state.dart index e8c1807f..fc3a9f49 100644 --- a/lib/presentation/recipe/recipe_show_screen/bloc/recipe_show_state.dart +++ b/lib/presentation/recipe/recipe_show_screen/bloc/recipe_show_state.dart @@ -22,17 +22,52 @@ class RecipeState extends Equatable { } class RecipeLoadedState extends RecipeState { - RecipeLoadedState({this.recipeModelObj, this.recipe}); + RecipeLoadedState( + {this.recipeModelObj, + this.recipe, + this.author, + this.comments, + this.token, + this.commentController, + this.tags, + this.steps}); RecipeModel? recipeModelObj; Map? recipe; + Map? author; + List? comments; + TextEditingController? commentController; + List? steps; + List? tags; + String? token; @override - List get props => [recipeModelObj, recipe]; + List get props => [ + recipeModelObj, + recipe, + author, + comments, + token, + commentController, + steps + ]; RecipeLoadedState copyWith( - {RecipeModel? recipeModelObj, Map? recipe}) { + {RecipeModel? recipeModelObj, + Map? recipe, + Map? author, + TextEditingController? commentController, + String? token, + List? tags, + List? steps, + List? comments}) { return RecipeLoadedState( recipeModelObj: recipeModelObj ?? this.recipeModelObj, recipe: recipe ?? this.recipe, + author: author ?? this.author, + tags: tags ?? this.tags, + token: token ?? this.token, + commentController: commentController ?? this.commentController, + steps: steps ?? this.steps, + comments: comments ?? this.comments, ); } } diff --git a/lib/presentation/recipe/recipe_show_screen/recipe_show_screen.dart b/lib/presentation/recipe/recipe_show_screen/recipe_show_screen.dart index 4bdea28d..ef38afff 100644 --- a/lib/presentation/recipe/recipe_show_screen/recipe_show_screen.dart +++ b/lib/presentation/recipe/recipe_show_screen/recipe_show_screen.dart @@ -1,17 +1,22 @@ import 'bloc/recipe_show_bloc.dart'; import 'models/recipe_show_model.dart'; +import 'package:eatvio_app/presentation/feed/blog_post_screen/widgets/comment_widget.dart'; import 'package:eatvio_app/core/app_export.dart'; import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:eatvio_app/widgets/custom_edit_text_form_field.dart'; +import 'package:eatvio_app/widgets/custom_outlined_button.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; class RecipeScreen extends StatelessWidget { const RecipeScreen({Key? key}) : super(key: key); - static Widget builder(BuildContext context) { final recipeID = ModalRoute.of(context)!.settings.arguments; return BlocProvider( - create: (context) => RecipeBloc(RecipeState(recipeModelObj: RecipeModel(recipeID: recipeID.toString())))..add(RecipeInitialEvent()), + create: (context) => RecipeBloc(RecipeState( + recipeModelObj: RecipeModel(recipeID: recipeID.toString()))) + ..add(RecipeInitialEvent()), child: RecipeScreen()); } @@ -39,87 +44,365 @@ class RecipeScreen extends StatelessWidget { _buildRecipe(BuildContext context, RecipeLoadedState state) { mediaQueryData = MediaQuery.of(context); + List steps = []; + if (state.steps != null) { + for (int i = 0; i < state.steps!.length; i++) { + steps.add(_buildStep(context, state, i)); + } + } return SafeArea( - child: Scaffold( - resizeToAvoidBottomInset: false, - body: Container( - width: double.maxFinite, - decoration: AppDecoration.fillGray, - child: ListView( - children: [ - SizedBox(height: 30.v), - Container( - margin: EdgeInsets.only( - left: 16.h, - right: 20.h, - bottom: 10.h, - ), - padding: EdgeInsets.all(12.h), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: const BorderRadius.all(Radius.circular(12)), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 2, - blurRadius: 5, - offset: Offset(0, 3), - ), - ], - ), - child: Column(mainAxisSize: MainAxisSize.min, children: [ - CachedNetworkImage( - imageUrl: dotenv.env['STORAGE_URL']! + (state.recipe?['image'] ?? ''), - imageBuilder: (context, imageProvider) => CircleAvatar( - backgroundImage: imageProvider, - radius: 25, - ), - placeholder: (context, url) => CircularProgressIndicator(), - errorWidget: (context, url, error) => CircleAvatar( - child: Icon(Icons.person), - backgroundColor: Colors.white, - radius: 25, - ), + child: Scaffold( + resizeToAvoidBottomInset: false, + body: Container( + width: double.maxFinite, + decoration: AppDecoration.fillGray, + child: ListView( + children: [ + SizedBox(height: 15.v), + _buildHeaderRow(context, state), + SizedBox(height: 15.v), + _buildRecipeData(context, state), + SizedBox(height: 15.v), + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: steps, ), - SizedBox(height: 16.0), - Text(state.recipe?['title'], style: CustomTextStyles.titleMediumBlack), - SizedBox(height: 8.0), - Text( - state.recipe?['description'], - ), - SizedBox(height: 16.0), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - ElevatedButton( - onPressed: () => Navigator.pushNamed(context, AppRoutes.recipeEditScreen, arguments: state.recipe?['id'].toString()), - child: Text('Редактировать'), - ), - ElevatedButton( - onPressed: () => context.read().add(DeleteEntry(state.recipe?['id'])), - child: Text('Удалить'), - ), - ], - ) - ]), - ) - ], + SizedBox(height: 15.v), + _buildTextField(context, state), + SizedBox(height: 15.v), + _buildCommentSection(context, state), + ], + ), + ))); + } + + _buildHeaderRow(BuildContext context, RecipeLoadedState state) { + final String name = state.author?['name']; + return Container( + margin: EdgeInsets.symmetric(horizontal: 16.h, vertical: 16.h), + padding: EdgeInsets.symmetric( + horizontal: 24.h, + vertical: 12.v, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), ), - ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CustomImageView( + imagePath: ImageConstant.imgArrowLeft, + onTap: () => NavigatorService.goBack()), + Padding( + padding: EdgeInsets.only(top: 1.v), + child: Row( + children: [ + Align( + alignment: Alignment.centerRight, + child: Text( + "$name", + style: CustomTextStyles.titleMediumBlack, + ), + ), + CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + + (state.author?['profile_photo_path'] ?? ''), + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundImage: imageProvider, + radius: 25, + ), + placeholder: (context, url) => CircularProgressIndicator(), + errorWidget: (context, url, error) => CircleAvatar( + child: Icon(Icons.person), + backgroundColor: Colors.white, + radius: 25, + ), + ), + ], + ), + ), + ], ), ); } - ///Handling page based on route - Widget getCurrentPage( - BuildContext context, - String currentRoute, - ) { - switch (currentRoute) { - case AppRoutes.recipeScreen: - return RecipeScreen.builder(context); - default: - return RecipeScreen(); + _buildRecipeData(BuildContext context, RecipeLoadedState state) { + return Container( + margin: EdgeInsets.only( + left: 16.h, + right: 20.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Row( + children: [ + Text( + state.recipe?['title'], + style: CustomTextStyles.titleMediumBlack, + ), + Spacer(), + PopupMenuButton( + onSelected: (value) => tapMenu(value, context, state), + itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + value: 'edit', + child: Text('Редактировать'), + ), + PopupMenuItem( + value: 'delete', + child: Text('Удалить'), + ), + ], + ), + ], + ), + SizedBox(height: 8.0), + _buildImage(state), + SizedBox(height: 16.0), + Text( + state.recipe?['description'], + style: CustomTextStyles.bodyMediumBluegray800, + ), + SizedBox(height: 16.0), + Align( + alignment: Alignment.topLeft, + child: Text( + DateFormat('dd.MM.yyyy') + .format(DateTime.parse(state.recipe?['updated_at'])), + style: theme.textTheme.bodyMedium, + )), + SizedBox(height: 16.0), + _buildTags(state.tags), + SizedBox(height: 16.0), + Row(children: [ + Padding( + padding: EdgeInsets.only(left: 24), + child: Row(children: [ + Padding( + padding: EdgeInsets.only(right: 8), + child: Icon(Icons.favorite_outline), + ), + Text( + "Нравится", + style: CustomTextStyles.titleMediumSemiBold, + ) + ]), + ), + Spacer(), + Padding( + padding: EdgeInsets.only(right: 24), + child: Row(children: [ + Padding( + padding: const EdgeInsets.only(right: 8), + child: Icon(Icons.share_outlined), + ), + Text( + "Поделиться", + style: CustomTextStyles.titleMediumSemiBold, + ) + ])) + ]), + ]), + ); + } + + _buildTags(List? tags) { + return Wrap( + spacing: 8.0, + runSpacing: 4.0, + children: tags!.map((tag) { + return Chip( + label: Text(tag['name']), + ); + }).toList(), + ); + } + + _buildImage(RecipeLoadedState state) { + return CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + (state.recipe?['image'] ?? ''), + imageBuilder: (context, imageProvider) => Stack( + children: [ + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(8.0)), + child: Image( + image: imageProvider, + ), + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 48.0), + child: Row( + children: [ + Icon( + Icons.remove_red_eye, + color: Colors.white, + ), + SizedBox(width: 4.0), + Text( + '24', + style: TextStyle(color: Colors.white), + ), + ], + ), + ), + Row( + children: [ + Icon( + Icons.favorite, + color: Colors.white, + ), + SizedBox(width: 4.0), + Text( + '24', + style: TextStyle(color: Colors.white), + ), + ], + ), + Padding( + padding: const EdgeInsets.only(right: 48.0), + child: Row( + children: [ + Icon( + Icons.comment_outlined, + color: Colors.white, + ), + SizedBox(width: 4.0), + Text( + '${state.comments?.length}', + style: TextStyle(color: Colors.white), + ), + ], + ), + ), + ], + ), + ), + ), + ], + ), + placeholder: (context, url) => CircularProgressIndicator(), + ); + } + + void tapMenu(String value, BuildContext context, RecipeLoadedState state) { + if (value == 'edit') { + Navigator.pushNamed( + context, + AppRoutes.recipeEditScreen, + arguments: state.recipe?['id'].toString(), + ); + } else if (value == 'delete') { + context.read().add(DeleteEntry(state.recipe?['id'])); + } + } + + _buildTextField(BuildContext context, RecipeLoadedState state) { + return Container( + margin: EdgeInsets.only( + left: 16.h, + right: 20.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Column( + children: [ + CustomTextFormField( + controller: state.commentController, + maxLines: 4, + ), + SizedBox(height: 15.h), + CustomOutlinedButton( + text: "Оставить отзыв", + margin: EdgeInsets.only(bottom: 5), + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + decoration: BoxDecoration( + color: Colors.transparent, + ), + onPressed: () => context + .read() + .add(CreateComment(state.commentController!.text)), + // Установите прозрачный фон + ) + ], + ), + ], + ), + ); + } + + _buildCommentSection(BuildContext context, RecipeLoadedState state) { + if (state.comments != null && state.comments!.isNotEmpty) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + for (int i = 0; i < state.comments!.length; i++) + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + CommentWidget(state.comments![i]), + SizedBox(height: 10.0), // Separator + ], + ), + ], + ); + } else { + // Return a message or placeholder widget when there are no comments + return Column( + children: [ + Center( + child: Text('No comments available'), + ), + SizedBox( + height: 20, + ) + ], + ); } } @@ -127,4 +410,48 @@ class RecipeScreen extends StatelessWidget { onTapArrowleftone(BuildContext context) { NavigatorService.goBack(); } + + Widget _buildStep(BuildContext context, RecipeLoadedState state, int i) { + return Container( + margin: EdgeInsets.only( + left: 16.h, + right: 16.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Align( + alignment: Alignment.centerLeft, + child: Text( + "Шаг ${i + 1}", + style: CustomTextStyles.titleMediumSemiBold, + ), + ), + SizedBox(height: 16.0), + CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + (state.steps?[i]['image']), + width: double + .infinity, // Установите ширину равной ширине родительского элемента + height: 200.0, // Задайте постоянную высоту элемента + fit: BoxFit.cover, + placeholder: (context, url) => CircularProgressIndicator(), + errorWidget: (context, url, error) => Icon(Icons.error), + ), + SizedBox(height: 16.0), + Text(state.steps?[i]['description']) + ]), + ); + } } diff --git a/lib/presentation/recipe/recipe_show_screen/widgets/comment_widget.dart b/lib/presentation/recipe/recipe_show_screen/widgets/comment_widget.dart new file mode 100644 index 00000000..c224966c --- /dev/null +++ b/lib/presentation/recipe/recipe_show_screen/widgets/comment_widget.dart @@ -0,0 +1,107 @@ +// ignore_for_file: must_be_immutable + +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:eatvio_app/core/app_export.dart'; +import 'package:eatvio_app/presentation/feed/blog_post_screen/models/comment_model.dart'; +import 'package:cached_network_image/cached_network_image.dart'; + +class CommentWidget extends StatelessWidget { + CommentWidget(this.commentModelObj, {Key? key}) + : super( + key: key, + ); + + CommentModel commentModelObj; + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.only( + left: 16.h, + right: 20.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment + .spaceBetween, // Move the main axis alignment to space between + crossAxisAlignment: + CrossAxisAlignment.center, // Center the items vertically + children: [ + CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + commentModelObj.userImage!, + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundImage: imageProvider, + radius: 25, + ), + placeholder: (context, url) => CircularProgressIndicator(), + errorWidget: (context, url, error) => CircleAvatar( + child: Icon(Icons.person), + backgroundColor: Colors.white, + radius: 25, + ), + ), + Expanded( + child: Padding( + padding: EdgeInsets.only( + left: 12.h, + top: 2.v, + bottom: 11.v, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 227.h, + child: Row( + children: [ + Text( + commentModelObj.username!.length <= 15 + ? commentModelObj.username! + : commentModelObj.username!.substring(0, 15) + + '...', + style: theme.textTheme.titleMedium, + ), + Spacer(), + Padding( + padding: EdgeInsets.only(left: 6.h), + child: Text( + commentModelObj.dateText!, + style: theme.textTheme.bodyMedium, + ), + ) + ], + ), + ), + SizedBox(height: 6.v), + Text( + commentModelObj.greetingText!, + style: CustomTextStyles.bodyMediumBluegray800, + ), + ], + ), + ), + ), + CustomImageView( + imagePath: ImageConstant.imgSend, + height: 30.v, + width: 30.h, + margin: EdgeInsets.only(left: 5)), + ], + ), + ); + } +} diff --git a/lib/widgets/custom_text_form_field.dart b/lib/widgets/custom_text_form_field.dart index 86a2a237..69416278 100644 --- a/lib/widgets/custom_text_form_field.dart +++ b/lib/widgets/custom_text_form_field.dart @@ -30,6 +30,8 @@ class CustomTextFormField extends StatelessWidget { this.keyboardType, this.onFieldSubmitted, this.onSaved, + this.initialValue, + this.onChanged, }) : super( key: key, ); @@ -57,8 +59,10 @@ class CustomTextFormField extends StatelessWidget { final bool? filled; final TextInputType? keyboardType; final FormFieldValidator? validator; + final String? initialValue; final Function(String)? onFieldSubmitted; final void Function(String?)? onSaved; + final void Function(String?)? onChanged; @override Widget build(BuildContext context) { @@ -76,6 +80,7 @@ class CustomTextFormField extends StatelessWidget { textInputAction: textInputAction, maxLines: maxLines ?? 1, onFieldSubmitted: onFieldSubmitted, + onChanged: onChanged, decoration: InputDecoration( hintText: hintText ?? "", hintStyle: hintStyle ?? theme.textTheme.bodyLarge, @@ -116,6 +121,7 @@ class CustomTextFormField extends StatelessWidget { validator: validator, onSaved: onSaved, inputFormatters: inputFormatters, + initialValue: initialValue, ), ), ) @@ -129,6 +135,7 @@ class CustomTextFormField extends StatelessWidget { obscureText: obscureText!, textInputAction: textInputAction, maxLines: maxLines ?? 1, + initialValue: initialValue, decoration: InputDecoration( hintText: hintText ?? "", hintStyle: hintStyle ?? theme.textTheme.bodyLarge,