diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index bc76a766..156d8566 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -59,7 +59,6 @@ class AppRouter { path: '/auth-screen', name: 'authScreen', redirect: (context, state) async { - // TokenStorage.removeTokenInStorage(); if (await TokenStorage.haveTokenInStorage()) { return '/my-profile-screen'; } @@ -292,7 +291,10 @@ class AppRouter { GoRoute( path: CreateDietScreen.path, name: CreateDietScreen.name, - builder: (context, state) => CreateDietScreen(), + builder: (context, state) { + final diet = state.extra as Diet?; + return CreateDietScreen(diet: diet); + }, ), GoRoute( path: DietPreviewScreen.path, diff --git a/lib/core/widgets/bottom_sheet_with_blur.dart b/lib/core/widgets/bottom_sheet_with_blur.dart index 303170c4..d6c1b61f 100644 --- a/lib/core/widgets/bottom_sheet_with_blur.dart +++ b/lib/core/widgets/bottom_sheet_with_blur.dart @@ -4,42 +4,44 @@ import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:flutter/material.dart'; Future showBottomSheetWithBlur({ - required BuildContext context, - required Widget child, - double height = 200, - bool hasBlur = true, - bool enableDrag = true, - }) { - return showModalBottomSheet( - context: context, - isScrollControlled: true, - barrierColor: appTheme.transparentBlack, - backgroundColor: Colors.transparent, - elevation: 0, - useSafeArea: true, - enableDrag: enableDrag, - builder: (context) => GestureDetector( - behavior: HitTestBehavior.opaque, - child: BackdropFilter( - filter: ImageFilter.blur( - sigmaX: hasBlur ? 24 : 0, - sigmaY: hasBlur ? 24 : 0, - tileMode: TileMode.mirror, - ), - child: SizedBox( - child: Container( - height: height, - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - color: appTheme.whiteA700, + required BuildContext context, + required Widget child, + double height = 200, + bool hasBlur = true, + bool enableDrag = true, + bool userRootNavigator = false, +}) { + return showModalBottomSheet( + context: context, + isScrollControlled: true, + barrierColor: appTheme.transparentBlack, + backgroundColor: Colors.transparent, + useRootNavigator: userRootNavigator, + elevation: 0, + useSafeArea: true, + enableDrag: enableDrag, + builder: (context) => GestureDetector( + behavior: HitTestBehavior.opaque, + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: hasBlur ? 24 : 0, + sigmaY: hasBlur ? 24 : 0, + tileMode: TileMode.mirror, + ), + child: SizedBox( + child: Container( + height: height, + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), ), - child: child, + color: appTheme.whiteA700, ), + child: child, ), ), ), - ); - } \ No newline at end of file + ), + ); +} diff --git a/lib/features/create_diet/models/create_diet_model.dart b/lib/features/create_diet/models/create_diet_model.dart index f089ff92..84639c77 100644 --- a/lib/features/create_diet/models/create_diet_model.dart +++ b/lib/features/create_diet/models/create_diet_model.dart @@ -9,12 +9,15 @@ class CreateDietModel with _$CreateDietModel { const CreateDietModel._(); const factory CreateDietModel({ + String? imageUrl, String? name, String? details, - String? state, + DietState? state, int? duration, required Map type, XFile? image, List>>? dailySchedule, }) = _CreateDietModel; } + + diff --git a/lib/features/create_diet/models/create_diet_model.freezed.dart b/lib/features/create_diet/models/create_diet_model.freezed.dart index 199f8e49..1effb5c0 100644 --- a/lib/features/create_diet/models/create_diet_model.freezed.dart +++ b/lib/features/create_diet/models/create_diet_model.freezed.dart @@ -16,9 +16,10 @@ final _privateConstructorUsedError = UnsupportedError( /// @nodoc mixin _$CreateDietModel { + String? get imageUrl => throw _privateConstructorUsedError; String? get name => throw _privateConstructorUsedError; String? get details => throw _privateConstructorUsedError; - String? get state => throw _privateConstructorUsedError; + DietState? get state => throw _privateConstructorUsedError; int? get duration => throw _privateConstructorUsedError; Map get type => throw _privateConstructorUsedError; XFile? get image => throw _privateConstructorUsedError; @@ -39,9 +40,10 @@ abstract class $CreateDietModelCopyWith<$Res> { _$CreateDietModelCopyWithImpl<$Res, CreateDietModel>; @useResult $Res call( - {String? name, + {String? imageUrl, + String? name, String? details, - String? state, + DietState? state, int? duration, Map type, XFile? image, @@ -63,6 +65,7 @@ class _$CreateDietModelCopyWithImpl<$Res, $Val extends CreateDietModel> @pragma('vm:prefer-inline') @override $Res call({ + Object? imageUrl = freezed, Object? name = freezed, Object? details = freezed, Object? state = freezed, @@ -72,6 +75,10 @@ class _$CreateDietModelCopyWithImpl<$Res, $Val extends CreateDietModel> Object? dailySchedule = freezed, }) { return _then(_value.copyWith( + imageUrl: freezed == imageUrl + ? _value.imageUrl + : imageUrl // ignore: cast_nullable_to_non_nullable + as String?, name: freezed == name ? _value.name : name // ignore: cast_nullable_to_non_nullable @@ -83,7 +90,7 @@ class _$CreateDietModelCopyWithImpl<$Res, $Val extends CreateDietModel> state: freezed == state ? _value.state : state // ignore: cast_nullable_to_non_nullable - as String?, + as DietState?, duration: freezed == duration ? _value.duration : duration // ignore: cast_nullable_to_non_nullable @@ -113,9 +120,10 @@ abstract class _$$CreateDietModelImplCopyWith<$Res> @override @useResult $Res call( - {String? name, + {String? imageUrl, + String? name, String? details, - String? state, + DietState? state, int? duration, Map type, XFile? image, @@ -135,6 +143,7 @@ class __$$CreateDietModelImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ + Object? imageUrl = freezed, Object? name = freezed, Object? details = freezed, Object? state = freezed, @@ -144,6 +153,10 @@ class __$$CreateDietModelImplCopyWithImpl<$Res> Object? dailySchedule = freezed, }) { return _then(_$CreateDietModelImpl( + imageUrl: freezed == imageUrl + ? _value.imageUrl + : imageUrl // ignore: cast_nullable_to_non_nullable + as String?, name: freezed == name ? _value.name : name // ignore: cast_nullable_to_non_nullable @@ -155,7 +168,7 @@ class __$$CreateDietModelImplCopyWithImpl<$Res> state: freezed == state ? _value.state : state // ignore: cast_nullable_to_non_nullable - as String?, + as DietState?, duration: freezed == duration ? _value.duration : duration // ignore: cast_nullable_to_non_nullable @@ -180,7 +193,8 @@ class __$$CreateDietModelImplCopyWithImpl<$Res> class _$CreateDietModelImpl extends _CreateDietModel { const _$CreateDietModelImpl( - {this.name, + {this.imageUrl, + this.name, this.details, this.state, this.duration, @@ -191,12 +205,14 @@ class _$CreateDietModelImpl extends _CreateDietModel { _dailySchedule = dailySchedule, super._(); + @override + final String? imageUrl; @override final String? name; @override final String? details; @override - final String? state; + final DietState? state; @override final int? duration; final Map _type; @@ -221,7 +237,7 @@ class _$CreateDietModelImpl extends _CreateDietModel { @override String toString() { - return 'CreateDietModel(name: $name, details: $details, state: $state, duration: $duration, type: $type, image: $image, dailySchedule: $dailySchedule)'; + return 'CreateDietModel(imageUrl: $imageUrl, name: $name, details: $details, state: $state, duration: $duration, type: $type, image: $image, dailySchedule: $dailySchedule)'; } @override @@ -229,6 +245,8 @@ class _$CreateDietModelImpl extends _CreateDietModel { return identical(this, other) || (other.runtimeType == runtimeType && other is _$CreateDietModelImpl && + (identical(other.imageUrl, imageUrl) || + other.imageUrl == imageUrl) && (identical(other.name, name) || other.name == name) && (identical(other.details, details) || other.details == details) && (identical(other.state, state) || other.state == state) && @@ -243,6 +261,7 @@ class _$CreateDietModelImpl extends _CreateDietModel { @override int get hashCode => Object.hash( runtimeType, + imageUrl, name, details, state, @@ -263,9 +282,10 @@ class _$CreateDietModelImpl extends _CreateDietModel { abstract class _CreateDietModel extends CreateDietModel { const factory _CreateDietModel( - {final String? name, + {final String? imageUrl, + final String? name, final String? details, - final String? state, + final DietState? state, final int? duration, required final Map type, final XFile? image, @@ -273,12 +293,14 @@ abstract class _CreateDietModel extends CreateDietModel { _$CreateDietModelImpl; const _CreateDietModel._() : super._(); + @override + String? get imageUrl; @override String? get name; @override String? get details; @override - String? get state; + DietState? get state; @override int? get duration; @override diff --git a/lib/features/create_diet/presentation/create_diet_screen.dart b/lib/features/create_diet/presentation/create_diet_screen.dart index 49b23577..cb1cd953 100644 --- a/lib/features/create_diet/presentation/create_diet_screen.dart +++ b/lib/features/create_diet/presentation/create_diet_screen.dart @@ -6,6 +6,7 @@ import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart'; import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart'; import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; +import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/core/widgets/main_background_bouble.dart'; import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_cubit.dart'; import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_state.dart'; @@ -16,6 +17,7 @@ import 'package:eatvio_app/features/create_diet/presentation/widgets/diet_image_ import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart'; import 'package:eatvio_app/features/create_diet/presentation/widgets/status_drop_down_widget.dart'; import 'package:eatvio_app/features/create_diet/presentation/widgets/diet_type_choice_widget.dart'; +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -24,19 +26,37 @@ import 'package:go_router/go_router.dart'; class CreateDietScreen extends StatelessWidget { static const name = 'createDietScreen'; static const path = '/create-diet-screen'; - const CreateDietScreen({super.key}); - + const CreateDietScreen({super.key, this.diet}); + final Diet? diet; @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => sl(), - child: const CreateDietScreenContent(), + create: (context) => sl()..setInitialData(diet), + child: CreateDietScreenContent(diet: diet), ); } } -class CreateDietScreenContent extends StatelessWidget { - const CreateDietScreenContent({super.key}); +class CreateDietScreenContent extends StatefulWidget { + CreateDietScreenContent({super.key, this.diet}); + final Diet? diet; + + @override + State createState() => _CreateDietScreenContentState(); +} + +class _CreateDietScreenContentState extends State { + final nameController = TextEditingController(); + final descriptionController = TextEditingController(); + + @override + void initState() { + super.initState(); + if (widget.diet != null) { + nameController.text = widget.diet?.name ?? ''; + descriptionController.text = widget.diet?.details ?? ''; + } + } @override Widget build(BuildContext context) { @@ -44,55 +64,12 @@ class CreateDietScreenContent extends StatelessWidget { return BlocBuilder( builder: (context, state) { if (state.published) { - WidgetsBinding.instance.addPostFrameCallback((_) { - context.pop(); - showBottomSheetWithBlur( - context: context, - child: Column( - children: [ - SizedBox( - height: 20, - ), - BottomSheetHandlerBar(), - SizedBox( - height: 20, - ), - Text( - '${state.dietData.name ?? 'Диета'}', - style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), - ), - Text( - 'опубликованна', - style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), - ), - SizedBox( - height: 20, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: CustomOutlinedButton( - onPressed: () { - context.pop(); - }, - text: 'Хорошо', - height: 52, - buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700), - buttonStyle: ButtonStyle( - side: WidgetStatePropertyAll(BorderSide.none), - backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), - ), - ), - ), - SizedBox( - height: 20, - ), - ], - ), - ); + Future.delayed(Duration(milliseconds: 500), () { + context.pop('${state.dietData.name ?? 'Диета'}'); }); } else if (state.drafted) { - WidgetsBinding.instance.addPostFrameCallback((_) { - context.pop(); + Future.delayed(Duration(milliseconds: 500), () { + context.pop('${state.dietData.name ?? 'Диета'}'); showBottomSheetWithBlur( context: context, child: Column( @@ -116,7 +93,7 @@ class CreateDietScreenContent extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 20), child: CustomOutlinedButton( onPressed: () { - context.pop(); + Navigator.of(context, rootNavigator: true).pop(); }, text: 'Хорошо', height: 52, @@ -141,7 +118,7 @@ class CreateDietScreenContent extends StatelessWidget { appBar: AppBar( centerTitle: true, title: Text( - s.create_diet, + widget.diet != null ? 'Редактирование диеты' : s.create_diet, textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, softWrap: true, @@ -160,6 +137,7 @@ class CreateDietScreenContent extends StatelessWidget { SizedBox(height: 16), CustomTextFormField( hintText: s.enter_diet_name, + controller: nameController, hasError: state.dietNameError != null, onChanged: (value) { context.read().setName(value); @@ -176,6 +154,7 @@ class CreateDietScreenContent extends StatelessWidget { height: 132, child: CustomTextFormField( hasError: state.dietDescriptionError != null, + controller: descriptionController, textAlignVertical: TextAlignVertical.top, maxLines: null, minLines: null, @@ -227,30 +206,19 @@ class CreateDietScreenContent extends StatelessWidget { physics: NeverScrollableScrollPhysics(), separatorBuilder: (context, index) => SizedBox(height: 8), ), - SizedBox(height: 20), + SizedBox(height: 12), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Column( children: [ CustomOutlinedButton( - onPressed: () async { - await context.read().publishDiet(toDraft: true); - }, - text: s.to_draft, - height: 52, - buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.gray), - buttonStyle: ButtonStyle( - side: WidgetStatePropertyAll(BorderSide.none), - backgroundColor: WidgetStatePropertyAll(appTheme.dottedBorder), - ), - ), - SizedBox(height: 12), - CustomOutlinedButton( - onPressed: () async { - final diet = await context.read().getDietForPreview(); - if (diet == null) return; - GoRouter.of(context).pushNamed(DietPreviewScreen.name, extra: diet); - }, + onPressed: state.isDietPublishing + ? () {} + : () async { + final diet = await context.read().getDietForPreview(); + if (diet == null) return; + GoRouter.of(context).pushNamed(DietPreviewScreen.name, extra: diet); + }, text: s.preview, height: 52, buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.gray), @@ -260,18 +228,21 @@ class CreateDietScreenContent extends StatelessWidget { ), ), SizedBox(height: 12), - CustomOutlinedButton( - onPressed: () { - context.read().publishDiet(); - }, - text: s.publish, - height: 52, - buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), - buttonStyle: ButtonStyle( - side: WidgetStatePropertyAll(BorderSide.none), - backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + if (state.isDietPublishing) + Loader() + else + CustomOutlinedButton( + onPressed: () { + context.read().publishDiet(); + }, + text: widget.diet != null ? s.save : s.publish, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + ), ), - ), ], ), ), @@ -282,5 +253,5 @@ class CreateDietScreenContent extends StatelessWidget { ); }, ); - } + } } diff --git a/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart b/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart index 666643a8..771e2531 100644 --- a/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart +++ b/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; import 'dart:developer'; +import 'package:dio/dio.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_state.dart'; @@ -16,10 +17,46 @@ class CreateDietCubit extends Cubit { final CreateDietRepository createDietRepository; final ProfileRepository profileRepository; final GlossaryRepository glossaryRepository; - static const _dietStateValues = ['active', 'personal', 'draft']; + String? tempImagePath; + static const _dietStateValues = DietState.values; CreateDietCubit({required this.profileRepository, required this.createDietRepository, required this.glossaryRepository}) : super(CreateDietState.initial()); + Future setInitialData(Diet? diet) async { + if (diet == null) return; + emit(state.copyWith(isLoading: true, isDietBeingEdited: true)); + final countOfDays = diet.dailySchedule.length; + final List>> dailySchedule = List.generate(countOfDays, (index) => [[], [], [], [], []]); + + for (final dayEntry in diet.dailySchedule.entries) { + for (final mealEntry in dayEntry.value.entries) { + for (final product in mealEntry.value.values) { + dailySchedule[dayEntry.key - 1][mealEntry.key - 1].add(product); + } + } + } + + final dietStateIndex = _dietStateValues.indexOf(diet.state); + final type = Map.fromIterable(dietTypeValues, value: (element) => false); + type['meat'] = diet.type.meat ?? false; + type['veg'] = diet.type.veg ?? false; + type['carb'] = diet.type.carb ?? false; + type['light'] = diet.type.light ?? false; + emit( + state.copyWith.dietData( + dailySchedule: dailySchedule, + details: diet.details, + state: diet.state, + name: diet.name, + duration: diet.duration, + type: type, + imageUrl: baseUrl + '/storage/' + diet.image, + ), + ); + emit(state.copyWith(dietStateIndex: dietStateIndex, durationIndex: durationValues.indexOf(diet.duration))); + emit(state.copyWith(isLoading: true)); + } + void setDetails(String text) { emit(state.copyWith(dietData: state.dietData.copyWith(details: text), dietDescriptionError: null)); } @@ -82,16 +119,16 @@ class CreateDietCubit extends Cubit { return await glossaryRepository.getGlossaryListByQuery(query); } - void setImage(XFile? image) { - emit(state.copyWith(dietData: state.dietData.copyWith(image: image), dietImageError: null)); + void setImage(XFile? image, {String? imageUrl}) { + emit(state.copyWith(dietData: state.dietData.copyWith(image: image, imageUrl: imageUrl), dietImageError: null)); } void setDailySchedule(List>> dailySchedule) { emit(state.copyWith(dietData: state.dietData.copyWith(dailySchedule: dailySchedule), dietDurationError: null)); } - void setDietState(int index) { - emit(state.copyWith(dietStateIndex: index, dietData: state.dietData.copyWith(state: _dietStateValues[index]), dietStateError: null)); + void setDietState(DietState dietState, int index) { + emit(state.copyWith(dietData: state.dietData.copyWith(state: dietState), dietStateIndex: index, dietStateError: null)); } void setDietType({required String key, required bool value}) { @@ -236,12 +273,26 @@ class CreateDietCubit extends Cubit { ); } } + final dailySchedule = >>{}; + + for (final (dayIndex, day) in state.dietData.dailySchedule!.indexed) { + final mappedDay = >{}; + for (final (mealIndex, meal) in day.indexed) { + final mappedMeal = {}; + for (final (productIndex, product) in meal.indexed) { + mappedMeal[productIndex + 1] = product; + } + mappedDay[mealIndex + 1] = mappedMeal; + } + dailySchedule[dayIndex + 1] = mappedDay; + } + final diet = Diet( id: 0, name: state.dietData.name ?? '', image: imageInBase64, duration: state.dietData.duration ?? 0, - state: state.dietData.state ?? '', + state: state.dietData.state ?? DietState.active, type: DietType( meat: state.dietData.type['meat'], veg: state.dietData.type['veg'], @@ -249,7 +300,7 @@ class CreateDietCubit extends Cubit { light: state.dietData.type['light'], ), details: state.dietData.details ?? '', - dailySchedule: state.dietData.dailySchedule ?? [], + dailySchedule: dailySchedule, author: Author( authorSlug: authorProfile.slug, authorName: authorProfile.name, @@ -267,32 +318,32 @@ class CreateDietCubit extends Cubit { return diet; } - Future publishDiet({bool toDraft = false}) async { + Future publishDiet() async { + emit(state.copyWith(isDietPublishing: true)); if (!_isFromValid()) return null; try { final image = await ImageCompressUtility.compressFile(File(state.dietData.image!.path)); - final dietState = toDraft ? _dietStateValues.last : state.dietData.state; - final diet = await createDietRepository.createDiet( - name: state.dietData.name, - duration: state.dietData.duration, + final dietState = state.dietData.state!; + final dailySchedule = state.dietData.dailySchedule; + await createDietRepository.createDiet( + name: state.dietData.name ?? '', + duration: state.dietData.duration ?? 7, state: dietState, image: image, meatType: state.dietData.type['meat'] == true ? '1' : '0', carbType: state.dietData.type['carb'] == true ? '1' : '0', vegType: state.dietData.type['veg'] == true ? '1' : '0', lightType: state.dietData.type['light'] == true ? '1' : '0', - details: state.dietData.details, + details: state.dietData.details ?? '', + dailySchedule: dailySchedule ?? [], ); - final dailySchedule = state.dietData.dailySchedule; - createDietRepository.createDietDailySchedule( - id: diet.id, - model: {'daily_schedule': dailySchedule}, - ); - emit(state.copyWith(published: !toDraft, drafted: toDraft)); + + emit(state.copyWith(published: dietState != DietState.draft, drafted: dietState == DietState.draft)); return state.dietData.name; - } catch (e, stackTrace) { + } on DioException catch (e, stackTrace) { log(e.toString(), stackTrace: stackTrace); } + emit(state.copyWith(isDietPublishing: false)); return null; } @@ -304,7 +355,7 @@ class CreateDietCubit extends Cubit { } if (state.dietData.name == null || state.dietData.name == '') { result = false; - emit(state.copyWith(dietNameError: 'Поле не заполненно')); + emit(state.copyWith(dietNameError: 'Поле не заполнено')); } if ((state.dietData.details?.length ?? 0) < 3) { result = false; @@ -312,7 +363,7 @@ class CreateDietCubit extends Cubit { } if (state.dietData.details == null || state.dietData.details == '') { result = false; - emit(state.copyWith(dietDescriptionError: 'Поле не заполненно')); + emit(state.copyWith(dietDescriptionError: 'Поле не заполнено')); } if (state.dietData.duration == null) { result = false; @@ -336,15 +387,11 @@ class CreateDietCubit extends Cubit { if (dailySchedule == null) { result = false; } else { - final dailyScheduleErrors = state.dailyScheduleErrors.map((element) { - return element.map((element) => element).toList(); - }).toList(); + final dailyScheduleErrors = [...state.dailyScheduleErrors]; for (final (dayIndex, day) in dailySchedule.indexed) { - for (final (mealIndex, meal) in day.indexed) { - if (meal.isEmpty) { - result = false; - dailyScheduleErrors[dayIndex][mealIndex] = 'Добавьте продукты'; - } + if (day.every((meal) => meal.isEmpty)) { + result = false; + dailyScheduleErrors[dayIndex] = 'Добавьте продукты'; } } emit(state.copyWith(dailyScheduleErrors: dailyScheduleErrors)); @@ -358,16 +405,13 @@ class CreateDietCubit extends Cubit { if (dailySchedule == null) { result = false; } else { - final dailyScheduleErrors = state.dailyScheduleErrors.map((element) { - return element.map((element) => element).toList(); - }).toList(); - for (final (mealIndex, meal) in dailySchedule[dayIndex].indexed) { - if (meal.isEmpty) { - result = false; - dailyScheduleErrors[dayIndex][mealIndex] = 'Добавьте продукты'; - } else { - dailyScheduleErrors[dayIndex][mealIndex] = ''; - } + final dailyScheduleErrors = [...state.dailyScheduleErrors]; + final day = dailySchedule[dayIndex]; + if (day.every((meal) => meal.isEmpty)) { + result = false; + dailyScheduleErrors[dayIndex] = 'Добавьте продукты'; + } else { + dailyScheduleErrors[dayIndex] = ''; } emit(state.copyWith(dailyScheduleErrors: dailyScheduleErrors)); } diff --git a/lib/features/create_diet/presentation/cubit/create_diet_state.dart b/lib/features/create_diet/presentation/cubit/create_diet_state.dart index b8b85b79..f0a6c2c6 100644 --- a/lib/features/create_diet/presentation/cubit/create_diet_state.dart +++ b/lib/features/create_diet/presentation/cubit/create_diet_state.dart @@ -9,6 +9,7 @@ part 'create_diet_state.freezed.dart'; class CreateDietState with _$CreateDietState { const factory CreateDietState({ required bool isLoading, + required bool isDietPublishing, required String? errorMessage, required CreateDietModel dietData, List>? copiedDay, @@ -17,15 +18,17 @@ class CreateDietState with _$CreateDietState { String? dietDurationError, String? dietStateError, String? dietImageError, - required List> dailyScheduleErrors, + required List dailyScheduleErrors, int? durationIndex, int? dietStateIndex, @Default(false) bool published, @Default(false) bool drafted, + @Default(false) bool isDietBeingEdited, }) = _CreateDietState; factory CreateDietState.initial() => CreateDietState( - dailyScheduleErrors: List.filled(21, ['', '', '', '', '']), + dailyScheduleErrors: List.filled(21, ''), + isDietPublishing: false, isLoading: false, errorMessage: null, dietData: CreateDietModel( diff --git a/lib/features/create_diet/presentation/cubit/create_diet_state.freezed.dart b/lib/features/create_diet/presentation/cubit/create_diet_state.freezed.dart index a49cbb70..cc266342 100644 --- a/lib/features/create_diet/presentation/cubit/create_diet_state.freezed.dart +++ b/lib/features/create_diet/presentation/cubit/create_diet_state.freezed.dart @@ -17,6 +17,7 @@ final _privateConstructorUsedError = UnsupportedError( /// @nodoc mixin _$CreateDietState { bool get isLoading => throw _privateConstructorUsedError; + bool get isDietPublishing => throw _privateConstructorUsedError; String? get errorMessage => throw _privateConstructorUsedError; CreateDietModel get dietData => throw _privateConstructorUsedError; List>? get copiedDay => @@ -26,12 +27,12 @@ mixin _$CreateDietState { String? get dietDurationError => throw _privateConstructorUsedError; String? get dietStateError => throw _privateConstructorUsedError; String? get dietImageError => throw _privateConstructorUsedError; - List> get dailyScheduleErrors => - throw _privateConstructorUsedError; + List get dailyScheduleErrors => throw _privateConstructorUsedError; int? get durationIndex => throw _privateConstructorUsedError; int? get dietStateIndex => throw _privateConstructorUsedError; bool get published => throw _privateConstructorUsedError; bool get drafted => throw _privateConstructorUsedError; + bool get isDietBeingEdited => throw _privateConstructorUsedError; /// Create a copy of CreateDietState /// with the given fields replaced by the non-null parameter values. @@ -48,6 +49,7 @@ abstract class $CreateDietStateCopyWith<$Res> { @useResult $Res call( {bool isLoading, + bool isDietPublishing, String? errorMessage, CreateDietModel dietData, List>? copiedDay, @@ -56,11 +58,12 @@ abstract class $CreateDietStateCopyWith<$Res> { String? dietDurationError, String? dietStateError, String? dietImageError, - List> dailyScheduleErrors, + List dailyScheduleErrors, int? durationIndex, int? dietStateIndex, bool published, - bool drafted}); + bool drafted, + bool isDietBeingEdited}); $CreateDietModelCopyWith<$Res> get dietData; } @@ -81,6 +84,7 @@ class _$CreateDietStateCopyWithImpl<$Res, $Val extends CreateDietState> @override $Res call({ Object? isLoading = null, + Object? isDietPublishing = null, Object? errorMessage = freezed, Object? dietData = null, Object? copiedDay = freezed, @@ -94,12 +98,17 @@ class _$CreateDietStateCopyWithImpl<$Res, $Val extends CreateDietState> Object? dietStateIndex = freezed, Object? published = null, Object? drafted = null, + Object? isDietBeingEdited = null, }) { return _then(_value.copyWith( isLoading: null == isLoading ? _value.isLoading : isLoading // ignore: cast_nullable_to_non_nullable as bool, + isDietPublishing: null == isDietPublishing + ? _value.isDietPublishing + : isDietPublishing // ignore: cast_nullable_to_non_nullable + as bool, errorMessage: freezed == errorMessage ? _value.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable @@ -135,7 +144,7 @@ class _$CreateDietStateCopyWithImpl<$Res, $Val extends CreateDietState> dailyScheduleErrors: null == dailyScheduleErrors ? _value.dailyScheduleErrors : dailyScheduleErrors // ignore: cast_nullable_to_non_nullable - as List>, + as List, durationIndex: freezed == durationIndex ? _value.durationIndex : durationIndex // ignore: cast_nullable_to_non_nullable @@ -152,6 +161,10 @@ class _$CreateDietStateCopyWithImpl<$Res, $Val extends CreateDietState> ? _value.drafted : drafted // ignore: cast_nullable_to_non_nullable as bool, + isDietBeingEdited: null == isDietBeingEdited + ? _value.isDietBeingEdited + : isDietBeingEdited // ignore: cast_nullable_to_non_nullable + as bool, ) as $Val); } @@ -176,6 +189,7 @@ abstract class _$$CreateDietStateImplCopyWith<$Res> @useResult $Res call( {bool isLoading, + bool isDietPublishing, String? errorMessage, CreateDietModel dietData, List>? copiedDay, @@ -184,11 +198,12 @@ abstract class _$$CreateDietStateImplCopyWith<$Res> String? dietDurationError, String? dietStateError, String? dietImageError, - List> dailyScheduleErrors, + List dailyScheduleErrors, int? durationIndex, int? dietStateIndex, bool published, - bool drafted}); + bool drafted, + bool isDietBeingEdited}); @override $CreateDietModelCopyWith<$Res> get dietData; @@ -208,6 +223,7 @@ class __$$CreateDietStateImplCopyWithImpl<$Res> @override $Res call({ Object? isLoading = null, + Object? isDietPublishing = null, Object? errorMessage = freezed, Object? dietData = null, Object? copiedDay = freezed, @@ -221,12 +237,17 @@ class __$$CreateDietStateImplCopyWithImpl<$Res> Object? dietStateIndex = freezed, Object? published = null, Object? drafted = null, + Object? isDietBeingEdited = null, }) { return _then(_$CreateDietStateImpl( isLoading: null == isLoading ? _value.isLoading : isLoading // ignore: cast_nullable_to_non_nullable as bool, + isDietPublishing: null == isDietPublishing + ? _value.isDietPublishing + : isDietPublishing // ignore: cast_nullable_to_non_nullable + as bool, errorMessage: freezed == errorMessage ? _value.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable @@ -262,7 +283,7 @@ class __$$CreateDietStateImplCopyWithImpl<$Res> dailyScheduleErrors: null == dailyScheduleErrors ? _value._dailyScheduleErrors : dailyScheduleErrors // ignore: cast_nullable_to_non_nullable - as List>, + as List, durationIndex: freezed == durationIndex ? _value.durationIndex : durationIndex // ignore: cast_nullable_to_non_nullable @@ -279,6 +300,10 @@ class __$$CreateDietStateImplCopyWithImpl<$Res> ? _value.drafted : drafted // ignore: cast_nullable_to_non_nullable as bool, + isDietBeingEdited: null == isDietBeingEdited + ? _value.isDietBeingEdited + : isDietBeingEdited // ignore: cast_nullable_to_non_nullable + as bool, )); } } @@ -288,6 +313,7 @@ class __$$CreateDietStateImplCopyWithImpl<$Res> class _$CreateDietStateImpl implements _CreateDietState { const _$CreateDietStateImpl( {required this.isLoading, + required this.isDietPublishing, required this.errorMessage, required this.dietData, final List>? copiedDay, @@ -296,17 +322,20 @@ class _$CreateDietStateImpl implements _CreateDietState { this.dietDurationError, this.dietStateError, this.dietImageError, - required final List> dailyScheduleErrors, + required final List dailyScheduleErrors, this.durationIndex, this.dietStateIndex, this.published = false, - this.drafted = false}) + this.drafted = false, + this.isDietBeingEdited = false}) : _copiedDay = copiedDay, _dailyScheduleErrors = dailyScheduleErrors; @override final bool isLoading; @override + final bool isDietPublishing; + @override final String? errorMessage; @override final CreateDietModel dietData; @@ -330,9 +359,9 @@ class _$CreateDietStateImpl implements _CreateDietState { final String? dietStateError; @override final String? dietImageError; - final List> _dailyScheduleErrors; + final List _dailyScheduleErrors; @override - List> get dailyScheduleErrors { + List get dailyScheduleErrors { if (_dailyScheduleErrors is EqualUnmodifiableListView) return _dailyScheduleErrors; // ignore: implicit_dynamic_type @@ -349,10 +378,13 @@ class _$CreateDietStateImpl implements _CreateDietState { @override @JsonKey() final bool drafted; + @override + @JsonKey() + final bool isDietBeingEdited; @override String toString() { - return 'CreateDietState(isLoading: $isLoading, errorMessage: $errorMessage, dietData: $dietData, copiedDay: $copiedDay, dietNameError: $dietNameError, dietDescriptionError: $dietDescriptionError, dietDurationError: $dietDurationError, dietStateError: $dietStateError, dietImageError: $dietImageError, dailyScheduleErrors: $dailyScheduleErrors, durationIndex: $durationIndex, dietStateIndex: $dietStateIndex, published: $published, drafted: $drafted)'; + return 'CreateDietState(isLoading: $isLoading, isDietPublishing: $isDietPublishing, errorMessage: $errorMessage, dietData: $dietData, copiedDay: $copiedDay, dietNameError: $dietNameError, dietDescriptionError: $dietDescriptionError, dietDurationError: $dietDurationError, dietStateError: $dietStateError, dietImageError: $dietImageError, dailyScheduleErrors: $dailyScheduleErrors, durationIndex: $durationIndex, dietStateIndex: $dietStateIndex, published: $published, drafted: $drafted, isDietBeingEdited: $isDietBeingEdited)'; } @override @@ -362,6 +394,8 @@ class _$CreateDietStateImpl implements _CreateDietState { other is _$CreateDietStateImpl && (identical(other.isLoading, isLoading) || other.isLoading == isLoading) && + (identical(other.isDietPublishing, isDietPublishing) || + other.isDietPublishing == isDietPublishing) && (identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage) && (identical(other.dietData, dietData) || @@ -386,13 +420,16 @@ class _$CreateDietStateImpl implements _CreateDietState { other.dietStateIndex == dietStateIndex) && (identical(other.published, published) || other.published == published) && - (identical(other.drafted, drafted) || other.drafted == drafted)); + (identical(other.drafted, drafted) || other.drafted == drafted) && + (identical(other.isDietBeingEdited, isDietBeingEdited) || + other.isDietBeingEdited == isDietBeingEdited)); } @override int get hashCode => Object.hash( runtimeType, isLoading, + isDietPublishing, errorMessage, dietData, const DeepCollectionEquality().hash(_copiedDay), @@ -405,7 +442,8 @@ class _$CreateDietStateImpl implements _CreateDietState { durationIndex, dietStateIndex, published, - drafted); + drafted, + isDietBeingEdited); /// Create a copy of CreateDietState /// with the given fields replaced by the non-null parameter values. @@ -420,6 +458,7 @@ class _$CreateDietStateImpl implements _CreateDietState { abstract class _CreateDietState implements CreateDietState { const factory _CreateDietState( {required final bool isLoading, + required final bool isDietPublishing, required final String? errorMessage, required final CreateDietModel dietData, final List>? copiedDay, @@ -428,15 +467,18 @@ abstract class _CreateDietState implements CreateDietState { final String? dietDurationError, final String? dietStateError, final String? dietImageError, - required final List> dailyScheduleErrors, + required final List dailyScheduleErrors, final int? durationIndex, final int? dietStateIndex, final bool published, - final bool drafted}) = _$CreateDietStateImpl; + final bool drafted, + final bool isDietBeingEdited}) = _$CreateDietStateImpl; @override bool get isLoading; @override + bool get isDietPublishing; + @override String? get errorMessage; @override CreateDietModel get dietData; @@ -453,7 +495,7 @@ abstract class _CreateDietState implements CreateDietState { @override String? get dietImageError; @override - List> get dailyScheduleErrors; + List get dailyScheduleErrors; @override int? get durationIndex; @override @@ -462,6 +504,8 @@ abstract class _CreateDietState implements CreateDietState { bool get published; @override bool get drafted; + @override + bool get isDietBeingEdited; /// Create a copy of CreateDietState /// with the given fields replaced by the non-null parameter values. diff --git a/lib/features/create_diet/presentation/diet_preview_screen.dart b/lib/features/create_diet/presentation/diet_preview_screen.dart index 7459be0d..879e5446 100644 --- a/lib/features/create_diet/presentation/diet_preview_screen.dart +++ b/lib/features/create_diet/presentation/diet_preview_screen.dart @@ -335,7 +335,7 @@ class _DietPreviewScreenState extends State { physics: NeverScrollableScrollPhysics(), shrinkWrap: true, itemBuilder: (context, index) { - return _dayOfPlan(widget.diet.dailySchedule[index], index, s); + return _dayOfPlan(widget.diet.dailySchedule[index] ?? {}, index, s); }, itemCount: widget.diet.dailySchedule.length, ), @@ -363,7 +363,7 @@ class _DietPreviewScreenState extends State { ); } - _dayOfPlan(List> day, int dayNumber, S s) { + _dayOfPlan(Map> day, int dayNumber, S s) { _choiceChipWidget(int index) { return ChoiceChip( selectedColor: appTheme.blueA400, @@ -431,8 +431,8 @@ class _DietPreviewScreenState extends State { ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), - itemBuilder: (context, index) => _daily(day[widget.selectedIndexes[dayNumber]][index], s), - itemCount: day[widget.selectedIndexes[dayNumber]].length, + itemBuilder: (context, index) => _daily(day[widget.selectedIndexes[dayNumber] + 1]?[index + 1], s), + itemCount: day[widget.selectedIndexes[dayNumber] + 1]?.length ?? 0, ), SizedBox(height: 8), ], @@ -450,7 +450,8 @@ class _DietPreviewScreenState extends State { ); } - _daily(DailySchedule daily, S s) { + _daily(DailySchedule? daily, S s) { + if (daily == null) return SizedBox.shrink(); return Padding( padding: const EdgeInsets.only(bottom: 12), child: Row( diff --git a/lib/features/create_diet/presentation/widgets/add_meal_widget.dart b/lib/features/create_diet/presentation/widgets/add_meal_widget.dart index f705adb4..c0c76b03 100644 --- a/lib/features/create_diet/presentation/widgets/add_meal_widget.dart +++ b/lib/features/create_diet/presentation/widgets/add_meal_widget.dart @@ -199,8 +199,7 @@ class _AddMealWidgetState extends State { final portion = DailySchedule( dailyNutrition: DailyNutrition(energy: calories, proteins: proteins, fats: fats, carbohydrates: carbohydrates), quantity: int.parse(quantityController.text), - name: glossary.name, - uniq: '', + name: glossary.name, productNutritionalValueId: glossary.id.toString(), portion: measurementUnits, ); @@ -240,7 +239,6 @@ class _AddMealWidgetState extends State { dailyNutrition: DailyNutrition(energy: calories, proteins: proteins, fats: fats, carbohydrates: carbohydrates), quantity: int.parse(quantityController.text), name: newGlossary.name, - uniq: '', productNutritionalValueId: newGlossary.id.toString(), portion: measurementUnits, ); diff --git a/lib/features/create_diet/presentation/widgets/day_widget.dart b/lib/features/create_diet/presentation/widgets/day_widget.dart index ba97708d..8ca5cc11 100644 --- a/lib/features/create_diet/presentation/widgets/day_widget.dart +++ b/lib/features/create_diet/presentation/widgets/day_widget.dart @@ -35,7 +35,7 @@ class _DayWidgetState extends State { return BlocBuilder( builder: (context, state) { return MainBackgroundBouble( - hasError: state.dailyScheduleErrors[widget.index].where((element) => element.isNotEmpty).isNotEmpty, + hasError: state.dailyScheduleErrors[widget.index].isNotEmpty, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -216,16 +216,15 @@ class _DayWidgetState extends State { ), ], ), - if (state.dailyScheduleErrors[widget.index][_selectedMealIndex].isNotEmpty) ...[ + if (state.dailyScheduleErrors[widget.index].isNotEmpty) ...[ SizedBox(height: 12), - CustomErrorWidget(error: state.dailyScheduleErrors[widget.index][_selectedMealIndex]), + CustomErrorWidget(error: state.dailyScheduleErrors[widget.index]), ], SizedBox(height: 20), ], ), ), BlocBuilder( - buildWhen: (previous, current) => previous.dietData.dailySchedule != current.dietData.dailySchedule, builder: (context, state) { if (state.dietData.dailySchedule != null) { final day = state.dietData.dailySchedule?[widget.index]; diff --git a/lib/features/create_diet/presentation/widgets/diet_image_picker_widget.dart b/lib/features/create_diet/presentation/widgets/diet_image_picker_widget.dart index 0e195a09..c15c70b6 100644 --- a/lib/features/create_diet/presentation/widgets/diet_image_picker_widget.dart +++ b/lib/features/create_diet/presentation/widgets/diet_image_picker_widget.dart @@ -71,6 +71,8 @@ class _DietImagePickerWidgetState extends State { final s = S.of(context); return BlocBuilder( builder: (context, state) { + final hasImage = state.dietData.image != null || state.dietData.imageUrl != null; + final hasFile = state.dietData.image != null; return Column( children: [ GestureDetector( @@ -81,17 +83,26 @@ class _DietImagePickerWidgetState extends State { height: 240, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), - image: state.dietData.image != null ? DecorationImage(fit: BoxFit.cover, image: FileImage(File(state.dietData.image?.path ?? ''))) : null, + image: hasImage + ? DecorationImage( + fit: BoxFit.cover, + image: (hasFile + ? FileImage( + File(state.dietData.image?.path ?? ''), + ) + : NetworkImage(state.dietData.imageUrl ?? '')) as ImageProvider, + ) + : null, color: appTheme.gray50, ), width: MediaQuery.sizeOf(context).width, - padding: state.dietData.image != null ? null : EdgeInsets.all(8), - child: state.dietData.image != null + padding: hasImage ? null : EdgeInsets.all(8), + child: hasImage ? Padding( padding: const EdgeInsets.only(top: 6, right: 6), child: GestureDetector( onTap: () { - context.read().setImage(null); + context.read().setImage(null, imageUrl: null); }, child: Align( alignment: Alignment.topRight, diff --git a/lib/features/create_diet/presentation/widgets/diet_type_choice_widget.dart b/lib/features/create_diet/presentation/widgets/diet_type_choice_widget.dart index 184f43bd..112ddbad 100644 --- a/lib/features/create_diet/presentation/widgets/diet_type_choice_widget.dart +++ b/lib/features/create_diet/presentation/widgets/diet_type_choice_widget.dart @@ -18,8 +18,7 @@ class DietTypeChoiceWidget extends StatefulWidget { class _DietTypeChoiceWidgetState extends State { final List<(String, String)> choiceContent = []; @override - void didChangeDependencies() { - // const List typeValues = ['veg', 'light', 'carb', 'meat']; + void didChangeDependencies() { super.didChangeDependencies(); final s = S.of(context); if (choiceContent.isEmpty) { diff --git a/lib/features/create_diet/presentation/widgets/status_drop_down_widget.dart b/lib/features/create_diet/presentation/widgets/status_drop_down_widget.dart index 47443b58..b178eccd 100644 --- a/lib/features/create_diet/presentation/widgets/status_drop_down_widget.dart +++ b/lib/features/create_diet/presentation/widgets/status_drop_down_widget.dart @@ -4,6 +4,7 @@ import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_c import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_state.dart'; import 'package:eatvio_app/core/widgets/custom_drop_down_button.dart'; import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart'; +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -18,14 +19,23 @@ class StatusDropDownWidget extends StatefulWidget { class _StatusDropDownWidgetState extends State { final List dietStateTitles = []; + late final List dietStates = DietState.values; @override void didChangeDependencies() { super.didChangeDependencies(); final s = S.of(context); if (dietStateTitles.isEmpty) { - dietStateTitles.add(s.active_diet_state); - dietStateTitles.add(s.personal_diet_state); + dietStates.forEach((elem) { + switch (elem) { + case DietState.active: + dietStateTitles.add(s.active_diet_state); + case DietState.draft: + dietStateTitles.add(s.draft); + case DietState.personal: + dietStateTitles.add(s.personal_diet_state); + } + }); } } @@ -45,7 +55,8 @@ class _StatusDropDownWidgetState extends State { ), onChanged: (value) { if (value != null) { - context.read().setDietState(dietStateTitles.indexOf(value)); + final index = dietStates.indexOf(value); + context.read().setDietState(value, index); } }, items: dietStateTitles @@ -55,11 +66,11 @@ class _StatusDropDownWidgetState extends State { dietStateTitles[index], style: TextStyles.textMMedium.copyWith(color: appTheme.black), ), - value: dietStateTitles[index], + value: dietStates[index], ), ) .toList(), - value: state.dietStateIndex == null ? null : dietStateTitles[state.dietStateIndex ?? 0], + value: state.dietStateIndex == null ? null : dietStates[state.dietStateIndex ?? 0], ), if (state.dietStateError != null) ...[ SizedBox( diff --git a/lib/features/create_diet/repositories/create_diet_repository.dart b/lib/features/create_diet/repositories/create_diet_repository.dart index 1dbed20d..ba9a6fa4 100644 --- a/lib/features/create_diet/repositories/create_diet_repository.dart +++ b/lib/features/create_diet/repositories/create_diet_repository.dart @@ -1,5 +1,5 @@ import 'dart:io'; - +import 'package:dio/dio.dart'; import 'package:eatvio_app/features/create_diet/services/create_diet_service.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; @@ -8,40 +8,54 @@ class CreateDietRepository { CreateDietRepository({required this.createDietService}); Future createDiet({ - String? name, - int? duration, - String? state, - String? meatType, - String? carbType, - String? vegType, - String? lightType, - String? details, - File? image, + required String name, + required int duration, + required DietState state, + required String meatType, + required String carbType, + required String vegType, + required String lightType, + required String details, + required File image, + required List>> dailySchedule, }) { - return createDietService.createDiet( - name: name, - duration: duration, - state: state, - meatType: meatType, - carbType: carbType, - vegType: vegType, - lightType: lightType, - details: details, - image: image, - ); - } + final dietMap = {}; + for (final (dayIndex, day) in dailySchedule.indexed) { + for (final (mealIndex, meal) in day.indexed) { + for (final (productIndex, product) in meal.indexed) { + final jsonProduct = product.toJson(); + dietMap.addAll({ + 'daily_schedule[${dayIndex + 1}][${mealIndex + 1}][${productIndex}][name]': jsonProduct['name'], + 'daily_schedule[${dayIndex + 1}][${mealIndex + 1}][${productIndex}][portion]': jsonProduct['portion'], + 'daily_schedule[${dayIndex + 1}][${mealIndex + 1}][${productIndex}][quantity]': jsonProduct['quantity'], + 'daily_schedule[${dayIndex + 1}][${mealIndex + 1}][${productIndex}][product_nutritional_value_id]': jsonProduct['product_nutritional_value_id'], + }); + } + } + } - Future createDietDailySchedule({ - required int id, - required Map model, - }) { - try { - return createDietService.createDietDailySchedule( - id: id, - model: model, - ); - } catch (e) { - rethrow; + dietMap.addAll({ + 'name': name, + 'details': details, + 'duration': duration, + 'type[meat]': meatType, + 'type[carb]': carbType, + 'type[veg]': vegType, + 'type[light]': lightType, + 'image': MultipartFile.fromFileSync( + image.path, + filename: image.path.split(Platform.pathSeparator).last, + ), + }); + + final formData = FormData.fromMap(dietMap); + switch (state) { + case DietState.active: + return createDietService.createPublishedDiet(formData: formData); + case DietState.draft: + return createDietService.createDraftedDiet(formData: formData); + case DietState.personal: + return createDietService.createPersonalDiet(formData: formData); } } } diff --git a/lib/features/create_diet/services/create_diet_service.dart b/lib/features/create_diet/services/create_diet_service.dart index 07432e3a..471681ee 100644 --- a/lib/features/create_diet/services/create_diet_service.dart +++ b/lib/features/create_diet/services/create_diet_service.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:dio/dio.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:retrofit/retrofit.dart'; @@ -10,23 +8,21 @@ part 'create_diet_service.g.dart'; abstract class CreateDietService { factory CreateDietService(Dio dio, {String baseUrl}) = _CreateDietService; - @POST('/api/diets/create') + @POST('/api/diets/create/published') @MultiPart() - Future createDiet({ - @Part(name: 'name') String? name, - @Part(name: 'duration') int? duration, - @Part(name: 'state') String? state, - @Part(name: 'type[meat]') String? meatType, - @Part(name: 'type[carb]') String? carbType, - @Part(name: 'type[veg]') String? vegType, - @Part(name: 'type[light]') String? lightType, - @Part(name: 'details') String? details, - @Part(name: 'image') File? image, + Future createPublishedDiet({ + @Body() required FormData formData, }); - @POST('/api/diets/create/{id}/daily_schedule') - Future createDietDailySchedule({ - @Path() required int id, - @Body() required Map model, + @POST('/api/diets/create/drafted') + @MultiPart() + Future createDraftedDiet({ + @Body() required FormData formData, + }); + + @POST('/api/diets/create/personalized') + @MultiPart() + Future createPersonalDiet({ + @Body() required FormData formData, }); } diff --git a/lib/features/create_diet/services/create_diet_service.g.dart b/lib/features/create_diet/services/create_diet_service.g.dart index 84a8c9a1..8d039872 100644 --- a/lib/features/create_diet/services/create_diet_service.g.dart +++ b/lib/features/create_diet/services/create_diet_service.g.dart @@ -18,59 +18,11 @@ class _CreateDietService implements CreateDietService { final ParseErrorLogger? errorLogger; @override - Future createDiet({ - String? name, - int? duration, - String? state, - String? meatType, - String? carbType, - String? vegType, - String? lightType, - String? details, - File? image, - }) async { + Future createPublishedDiet({required FormData formData}) async { final _extra = {}; final queryParameters = {}; - queryParameters.removeWhere((k, v) => v == null); final _headers = {}; - final _data = FormData(); - if (name != null) { - _data.fields.add(MapEntry('name', name)); - } - if (duration != null) { - _data.fields.add(MapEntry('duration', duration.toString())); - } - if (state != null) { - _data.fields.add(MapEntry('state', state)); - } - if (meatType != null) { - _data.fields.add(MapEntry('type[meat]', meatType)); - } - if (carbType != null) { - _data.fields.add(MapEntry('type[carb]', carbType)); - } - if (vegType != null) { - _data.fields.add(MapEntry('type[veg]', vegType)); - } - if (lightType != null) { - _data.fields.add(MapEntry('type[light]', lightType)); - } - if (details != null) { - _data.fields.add(MapEntry('details', details)); - } - if (image != null) { - if (image != null) { - _data.files.add( - MapEntry( - 'image', - MultipartFile.fromFileSync( - image.path, - filename: image.path.split(Platform.pathSeparator).last, - ), - ), - ); - } - } + final _data = formData; final _options = _setStreamType( Options( method: 'POST', @@ -80,7 +32,7 @@ class _CreateDietService implements CreateDietService { ) .compose( _dio.options, - '/api/diets/create', + '/api/diets/create/published', queryParameters: queryParameters, data: _data, ) @@ -98,26 +50,67 @@ class _CreateDietService implements CreateDietService { } @override - Future createDietDailySchedule({ - required int id, - required Map model, - }) async { + Future createDraftedDiet({required FormData formData}) async { final _extra = {}; final queryParameters = {}; final _headers = {}; - final _data = {}; - _data.addAll(model); - final _options = _setStreamType( - Options(method: 'POST', headers: _headers, extra: _extra) + final _data = formData; + final _options = _setStreamType( + Options( + method: 'POST', + headers: _headers, + extra: _extra, + contentType: 'multipart/form-data', + ) .compose( _dio.options, - '/api/diets/create/${id}/daily_schedule', + '/api/diets/create/drafted', queryParameters: queryParameters, data: _data, ) .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), ); - await _dio.fetch(_options); + final _result = await _dio.fetch>(_options); + late CreatedDiet _value; + try { + _value = CreatedDiet.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + return _value; + } + + @override + Future createPersonalDiet({required FormData formData}) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = formData; + final _options = _setStreamType( + Options( + method: 'POST', + headers: _headers, + extra: _extra, + contentType: 'multipart/form-data', + ) + .compose( + _dio.options, + '/api/diets/create/personalized', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + final _result = await _dio.fetch>(_options); + late CreatedDiet _value; + try { + _value = CreatedDiet.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + return _value; } RequestOptions _setStreamType(RequestOptions requestOptions) { diff --git a/lib/features/dev_tools/presentation/dev_tools_screen.dart b/lib/features/dev_tools/presentation/dev_tools_screen.dart index d3b25f40..e2e5ab48 100644 --- a/lib/features/dev_tools/presentation/dev_tools_screen.dart +++ b/lib/features/dev_tools/presentation/dev_tools_screen.dart @@ -25,7 +25,25 @@ class DevToolsScreenContent extends StatefulWidget { class _DevToolsScreenContentState extends State { String baseUrlForTest = ''; - final List testCases = [Test850task(), GetChatRoomPreviews(), GetChatRoom(), SendMessage(), GetChatMessages(), GetChatRoomIdByUserSlug()]; + final List testCases = [ + Test850task(), + GetChatRoomPreviews(), + GetChatRoom(), + SendMessage(), + GetChatMessages(), + GetChatRoomIdByUserSlug(), + GetBlogPostList(), + GetRecipes(), + GetDiets(), + GetPlans(), + GetBlogPostComments(), + SendBlogPostComment(), + GetDietsComments(), + SendDietsComment(), + GetPlansComments(), + SendPlansComment(), + GetRecipesComments(), + ]; final controller = TextEditingController(); late final List testsStates; @@ -41,7 +59,6 @@ class _DevToolsScreenContentState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(), body: SingleChildScrollView( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 10), @@ -74,76 +91,89 @@ class _DevToolsScreenContentState extends State { ), ], ), - SizedBox( + const SizedBox( height: 10, ), + if (baseUrlForTest != baseUrl) + CustomOutlinedButton( + text: 'Set dev2 base url', + onPressed: () { + setState(() { + this.baseUrlForTest = baseUrl; + }); + }, + buttonStyle: ButtonStyle(), + ), + SizedBox( + height: baseUrlForTest != baseUrl ? 40 : 10, + ), if (baseUrlForTest != 'https://eatvio.ru') ListView.builder( shrinkWrap: true, itemCount: testCases.length, - itemBuilder: (context, index) => Row( - children: [ - Text('${index + 1}. ${testCases[index].name}'), - SizedBox( - width: 50, - ), - Expanded( - child: CustomOutlinedButton( - text: 'run', - height: 20, - width: 60, - onPressed: () async { - if (testsStates[index] == TestState.loading) return; - - setState(() { - testsStates[index] = TestState.loading; - }); - final result = await testCases[index].run(baseUrl: baseUrlForTest); - if (result) { - setState(() { - testsStates[index] = TestState.success; - }); - showSuccessToast('${testCases[index].name} passed'); - } else { - setState(() { - testsStates[index] = TestState.fail; - }); - showErrorToast('${testCases[index].name} failed'); - } - }, - buttonStyle: ButtonStyle(), + itemBuilder: (context, index) => Padding( + padding: const EdgeInsets.only(bottom: 10.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text('${index + 1}. ${testCases[index].name}'), ), - ), - SizedBox( - width: 30, - ), - switch (testsStates[index]) { - TestState.success => Expanded( - child: Container( + Row( + mainAxisSize: MainAxisSize.min, + children: [ + CustomOutlinedButton( + text: 'run', height: 20, - color: Colors.green, + width: 60, + onPressed: () async { + if (testsStates[index] == TestState.loading) return; + + setState(() { + testsStates[index] = TestState.loading; + }); + final result = await testCases[index].run(baseUrl: baseUrlForTest); + if (result) { + setState(() { + testsStates[index] = TestState.success; + }); + showSuccessToast('${testCases[index].name} passed'); + } else { + setState(() { + testsStates[index] = TestState.fail; + }); + showErrorToast('${testCases[index].name} failed'); + } + }, + buttonStyle: ButtonStyle(), ), - ), - TestState.fail => Expanded( - child: Container( - height: 20, - color: Colors.red, - ), - ), - TestState.notRun => Expanded( - child: Container( - height: 20, - color: Colors.grey, - ), - ), - TestState.loading => Expanded( - child: Container( - height: 20, - child: SizedBox(height: 20, width: 20, child: Center(child: CircularProgressIndicator())), - ), - ), - }, - ], + SizedBox(width: 5), + switch (testsStates[index]) { + TestState.success => Container( + width: 20, + height: 20, + color: Colors.green, + ), + TestState.fail => Container( + width: 20, + height: 20, + color: Colors.red, + ), + TestState.notRun => Container( + width: 20, + height: 20, + color: Colors.grey, + ), + TestState.loading => Container( + width: 20, + height: 20, + child: SizedBox(height: 20, width: 20, child: Center(child: CircularProgressIndicator())), + ), + }, + ], + ), + ], + ), ), ), ], diff --git a/lib/features/dev_tools/test_cases.dart b/lib/features/dev_tools/test_cases.dart index cb812174..30eb9c6a 100644 --- a/lib/features/dev_tools/test_cases.dart +++ b/lib/features/dev_tools/test_cases.dart @@ -8,6 +8,7 @@ import 'package:eatvio_app/features/chat/services/chat_service.dart'; import 'package:eatvio_app/features/diets/services/diet_service.dart'; import 'package:eatvio_app/features/feed/services/feed_service.dart'; import 'package:eatvio_app/features/profile/services/profile_service.dart'; +import 'package:eatvio_app/features/recipes/services/recipe_service.dart'; sealed class TestCases { String get name => throw UnimplementedError(); @@ -115,6 +116,7 @@ class Test850task implements TestCases { } } +// ChatService class GetChatRoomPreviews implements TestCases { @override String get name => '/api/chat/index'; @@ -140,7 +142,7 @@ class GetChatRoom implements TestCases { @override Future run({required String baseUrl}) async { - try { + try { final dio = sl(); final chatService = ChatService(dio, baseUrl: baseUrl); @@ -159,7 +161,7 @@ class SendMessage implements TestCases { @override Future run({required String baseUrl}) async { - try { + try { final dio = sl(); final chatService = ChatService(dio, baseUrl: baseUrl); @@ -178,7 +180,7 @@ class GetChatMessages implements TestCases { @override Future run({required String baseUrl}) async { - try { + try { final dio = sl(); final chatService = ChatService(dio, baseUrl: baseUrl); @@ -197,7 +199,7 @@ class GetChatRoomIdByUserSlug implements TestCases { @override Future run({required String baseUrl}) async { - try { + try { final dio = sl(); final chatService = ChatService(dio, baseUrl: baseUrl); final consultantService = FeedService(dio, baseUrl: baseUrl); @@ -217,3 +219,230 @@ class GetChatRoomIdByUserSlug implements TestCases { } } } + +// FeedService +class GetBlogPostList implements TestCases { + @override + String get name => '/api/blogPosts/index'; + + @override + Future run({required String baseUrl}) async { + try { + final dio = sl(); + final feedService = FeedService(dio, baseUrl: baseUrl); + await feedService.getBlogPostList(page: 1); + return true; + } catch (e) { + return false; + } + } +} + +class GetRecipes implements TestCases { + @override + String get name => '/api/recipes/index'; + + @override + Future run({required String baseUrl}) async { + try { + final dio = sl(); + final feedService = FeedService(dio, baseUrl: baseUrl); + final recipeService = RecipeService(dio, baseUrl: baseUrl); + final tagsModel = await recipeService.getTags(); + final tags = tagsModel.data; + final tagsIds = tags.map((e) => e.id).toList(); + await feedService.getRecipes(page: 1, tags: tagsIds, query: null); + return true; + } catch (e) { + return false; + } + } +} + +class GetDiets implements TestCases { + @override + String get name => '/api/diets/index'; + + @override + Future run({required String baseUrl}) async { + try { + final dio = sl(); + final feedService = FeedService(dio, baseUrl: baseUrl); + await feedService.getDiets(page: 1, query: null); + return true; + } catch (e) { + return false; + } + } +} + +class GetPlans implements TestCases { + @override + String get name => '/api/plans/index'; + + @override + Future run({required String baseUrl}) async { + try { + final dio = sl(); + final feedService = FeedService(dio, baseUrl: baseUrl); + await feedService.getPlans(page: 1, query: null); + return true; + } catch (e) { + return false; + } + } +} + +class GetBlogPostComments implements TestCases { + @override + String get name => '/api/blogPosts/{id}/comments/show'; + + @override + Future run({required String baseUrl}) async { + try { + final dio = sl(); + final feedService = FeedService(dio, baseUrl: baseUrl); + final blogPostsModel = await feedService.getBlogPostList(page: 1, query: null); + final blogPosts = blogPostsModel.feedBlogPostList; + await feedService.getBlogPostComments( + id: blogPosts.first.id.toString(), + ); + return true; + } catch (e) { + return false; + } + } +} + +class SendBlogPostComment implements TestCases { + @override + String get name => '/api/blogPosts/{id}/comment/create'; + + @override + Future run({required String baseUrl}) async { + try { + final dio = sl(); + final feedService = FeedService(dio, baseUrl: baseUrl); + final blogPostsModel = await feedService.getBlogPostList(page: 1, query: null); + final blogPosts = blogPostsModel.feedBlogPostList; + await feedService.sendBlogPostComment( + id: blogPosts.first.id.toString(), + body: 'auto test message', + ); + return true; + } catch (e) { + return false; + } + } +} + +class GetDietsComments implements TestCases { + @override + String get name => '/api/diets/{id}/comments/show'; + + @override + Future run({required String baseUrl}) async { + try { + final dio = sl(); + final feedService = FeedService(dio, baseUrl: baseUrl); + final dietsModel = await feedService.getDiets(page: 1, query: null); + final diets = dietsModel.data; + await feedService.getDietsComments( + id: diets.first.id.toString(), + ); + return true; + } catch (e) { + return false; + } + } +} + +class SendDietsComment implements TestCases { + @override + String get name => '/api/diets/{id}/comment/create'; + + @override + Future run({required String baseUrl}) async { + try { + final dio = sl(); + final feedService = FeedService(dio, baseUrl: baseUrl); + final dietsModel = await feedService.getDiets(page: 1, query: null); + final diets = dietsModel.data; + await feedService.sendDietsComment( + id: diets.first.id.toString(), + body: 'auto test message', + ); + return true; + } catch (e) { + return false; + } + } +} + +class GetPlansComments implements TestCases { + @override + String get name => '/api/plans/{slug}/comments/show'; + + @override + Future run({required String baseUrl}) async { + try { + final dio = sl(); + final feedService = FeedService(dio, baseUrl: baseUrl); + final plansModel = await feedService.getPlans(page: 1, query: null); + final plans = plansModel.data; + await feedService.getPlansComments( + slug: plans.first.slug, + ); + return true; + } catch (e) { + return false; + } + } +} + +class SendPlansComment implements TestCases { + @override + String get name => '/api/plans/{slug}/comment/create'; + + @override + Future run({required String baseUrl}) async { + try { + final dio = sl(); + final feedService = FeedService(dio, baseUrl: baseUrl); + final plansModel = await feedService.getPlans(page: 1, query: null); + final plans = plansModel.data; + await feedService.sendPlansComment( + slug: plans.first.slug, + body: 'auto test message', + ); + return true; + } catch (e) { + return false; + } + } +} + +class GetRecipesComments implements TestCases { + @override + String get name => '/api/recipes/{id}/comments/show'; + + @override + Future run({required String baseUrl}) async { + try { + final dio = sl(); + final feedService = FeedService(dio, baseUrl: baseUrl); + final recipeService = RecipeService(dio, baseUrl: baseUrl); + final tagsModel = await recipeService.getTags(); + final tags = tagsModel.data; + final tagsIds = tags.map((e) => e.id).toList(); + final recipesModel = await feedService.getRecipes(page: 1, tags: tagsIds, query: null); + final recipes = recipesModel.recipesList; + await feedService.getRecipesComments( + id: recipes.first.id.toString(), + ); + return true; + } catch (e) { + return false; + } + } +} diff --git a/lib/features/diets/presentation/pages/about_diet_screen.dart b/lib/features/diets/presentation/pages/about_diet_screen.dart index 1c63c8f9..a701d978 100644 --- a/lib/features/diets/presentation/pages/about_diet_screen.dart +++ b/lib/features/diets/presentation/pages/about_diet_screen.dart @@ -454,7 +454,7 @@ class _AboutDietScreenState extends State { physics: NeverScrollableScrollPhysics(), shrinkWrap: true, itemBuilder: (context, index) { - return _dayOfPlan(widget.diet.dailySchedule[index], index, s); + return _dayOfPlan(widget.diet.dailySchedule[index + 1] ?? {}, index, s); }, itemCount: widget.diet.dailySchedule.length, ), @@ -500,7 +500,7 @@ class _AboutDietScreenState extends State { ); } - _dayOfPlan(List> day, int dayNumber, S s) { + _dayOfPlan(Map> day, int dayNumber, S s) { _choiceChipWidget(int index) { return ChoiceChip( selectedColor: appTheme.blueA400, @@ -568,8 +568,8 @@ class _AboutDietScreenState extends State { ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), - itemBuilder: (context, index) => _daily(day[widget.selectedIndexes[dayNumber]][index], s), - itemCount: day[widget.selectedIndexes[dayNumber]].length, + itemBuilder: (context, index) => _daily(day[widget.selectedIndexes[dayNumber] + 1]?[index + 1], s), + itemCount: day[widget.selectedIndexes[dayNumber] + 1]?.length ?? 0, ), SizedBox(height: 8), ], @@ -587,7 +587,8 @@ class _AboutDietScreenState extends State { ); } - _daily(DailySchedule daily, S s) { + _daily(DailySchedule? daily, S s) { + if (daily == null) return const SizedBox(); return Padding( padding: const EdgeInsets.only(bottom: 12), child: Row( diff --git a/lib/features/feed/models/diets_pagination_model.dart b/lib/features/feed/models/diets_pagination_model.dart index ba9fecf7..c67b8e07 100644 --- a/lib/features/feed/models/diets_pagination_model.dart +++ b/lib/features/feed/models/diets_pagination_model.dart @@ -23,10 +23,10 @@ class Diet with _$Diet { @JsonKey(name: "name") required String name, @JsonKey(name: "image") required String image, @JsonKey(name: "duration") required int duration, - @JsonKey(name: "state") required String state, + @JsonKey(name: "state") required DietState state, @JsonKey(name: "type") required DietType type, @JsonKey(name: "details") required String details, - @JsonKey(name: "daily_schedule") required List>> dailySchedule, + @JsonKey(name: "daily_schedule") required Map>> dailySchedule, @JsonKey(name: "author") required Author author, @JsonKey(name: "total_likes") required int totalLikes, @JsonKey(name: "total_views") required int totalViews, @@ -39,10 +39,30 @@ class Diet with _$Diet { factory Diet.fromJson(Map json) => _$DietFromJson(json); } +enum DietState { + @JsonKey(name: 'active') + active, + @JsonKey(name: 'draft') + draft, + @JsonKey(name: 'personal') + personal; + + String toString() { + switch (this) { + case DietState.active: + return 'active'; + case DietState.draft: + return 'draft'; + case DietState.personal: + return 'personal'; + } + } +} + @freezed class CreatedDiet with _$CreatedDiet { const factory CreatedDiet({ - @JsonKey(name: "id") required int id, + @JsonKey(name: "id") required int id, }) = _CreatedDiet; factory CreatedDiet.fromJson(Map json) => _$CreatedDietFromJson(json); @@ -51,9 +71,9 @@ class CreatedDiet with _$CreatedDiet { int parseDuration(dynamic duration) { if (duration is String) { return int.parse(duration); - }else { + } else { return duration; - } + } } @freezed @@ -74,9 +94,8 @@ class Author with _$Author { class DailySchedule with _$DailySchedule { const factory DailySchedule({ @JsonKey(name: "name") required String name, - @JsonKey(name: "uniq", includeToJson: false) required String uniq, @JsonKey(name: "portion") required Portion portion, - @JsonKey(name: "quantity") required int quantity, + @JsonKey(name: "quantity", fromJson: _parseQuantity) required int quantity, @JsonKey(includeToJson: false, includeFromJson: false) DailyNutrition? dailyNutrition, @JsonKey(name: 'product_nutritional_value_id', includeToJson: true, includeFromJson: false, includeIfNull: false) String? productNutritionalValueId, }) = _DailySchedule; @@ -84,8 +103,16 @@ class DailySchedule with _$DailySchedule { factory DailySchedule.fromJson(Map json) => _$DailyScheduleFromJson(json); } +int _parseQuantity(dynamic quantity) { + if (quantity is String) { + return int.parse(quantity); + } else { + return quantity; + } +} + enum Portion { - @JsonValue("grams") + @JsonValue("gr") GRAMS, @JsonValue("milliliters") MILLILITERS, diff --git a/lib/features/feed/models/diets_pagination_model.freezed.dart b/lib/features/feed/models/diets_pagination_model.freezed.dart index 37c410bc..cc185651 100644 --- a/lib/features/feed/models/diets_pagination_model.freezed.dart +++ b/lib/features/feed/models/diets_pagination_model.freezed.dart @@ -268,13 +268,13 @@ mixin _$Diet { @JsonKey(name: "duration") int get duration => throw _privateConstructorUsedError; @JsonKey(name: "state") - String get state => throw _privateConstructorUsedError; + DietState get state => throw _privateConstructorUsedError; @JsonKey(name: "type") DietType get type => throw _privateConstructorUsedError; @JsonKey(name: "details") String get details => throw _privateConstructorUsedError; @JsonKey(name: "daily_schedule") - List>> get dailySchedule => + Map>> get dailySchedule => throw _privateConstructorUsedError; @JsonKey(name: "author") Author get author => throw _privateConstructorUsedError; @@ -310,11 +310,11 @@ abstract class $DietCopyWith<$Res> { @JsonKey(name: "name") String name, @JsonKey(name: "image") String image, @JsonKey(name: "duration") int duration, - @JsonKey(name: "state") String state, + @JsonKey(name: "state") DietState state, @JsonKey(name: "type") DietType type, @JsonKey(name: "details") String details, @JsonKey(name: "daily_schedule") - List>> dailySchedule, + Map>> dailySchedule, @JsonKey(name: "author") Author author, @JsonKey(name: "total_likes") int totalLikes, @JsonKey(name: "total_views") int totalViews, @@ -378,7 +378,7 @@ class _$DietCopyWithImpl<$Res, $Val extends Diet> state: null == state ? _value.state : state // ignore: cast_nullable_to_non_nullable - as String, + as DietState, type: null == type ? _value.type : type // ignore: cast_nullable_to_non_nullable @@ -390,7 +390,7 @@ class _$DietCopyWithImpl<$Res, $Val extends Diet> dailySchedule: null == dailySchedule ? _value.dailySchedule : dailySchedule // ignore: cast_nullable_to_non_nullable - as List>>, + as Map>>, author: null == author ? _value.author : author // ignore: cast_nullable_to_non_nullable @@ -455,11 +455,11 @@ abstract class _$$DietImplCopyWith<$Res> implements $DietCopyWith<$Res> { @JsonKey(name: "name") String name, @JsonKey(name: "image") String image, @JsonKey(name: "duration") int duration, - @JsonKey(name: "state") String state, + @JsonKey(name: "state") DietState state, @JsonKey(name: "type") DietType type, @JsonKey(name: "details") String details, @JsonKey(name: "daily_schedule") - List>> dailySchedule, + Map>> dailySchedule, @JsonKey(name: "author") Author author, @JsonKey(name: "total_likes") int totalLikes, @JsonKey(name: "total_views") int totalViews, @@ -522,7 +522,7 @@ class __$$DietImplCopyWithImpl<$Res> state: null == state ? _value.state : state // ignore: cast_nullable_to_non_nullable - as String, + as DietState, type: null == type ? _value.type : type // ignore: cast_nullable_to_non_nullable @@ -534,7 +534,7 @@ class __$$DietImplCopyWithImpl<$Res> dailySchedule: null == dailySchedule ? _value._dailySchedule : dailySchedule // ignore: cast_nullable_to_non_nullable - as List>>, + as Map>>, author: null == author ? _value.author : author // ignore: cast_nullable_to_non_nullable @@ -579,7 +579,7 @@ class _$DietImpl implements _Diet { @JsonKey(name: "type") required this.type, @JsonKey(name: "details") required this.details, @JsonKey(name: "daily_schedule") - required final List>> dailySchedule, + required final Map>> dailySchedule, @JsonKey(name: "author") required this.author, @JsonKey(name: "total_likes") required this.totalLikes, @JsonKey(name: "total_views") required this.totalViews, @@ -608,20 +608,20 @@ class _$DietImpl implements _Diet { final int duration; @override @JsonKey(name: "state") - final String state; + final DietState state; @override @JsonKey(name: "type") final DietType type; @override @JsonKey(name: "details") final String details; - final List>> _dailySchedule; + final Map>> _dailySchedule; @override @JsonKey(name: "daily_schedule") - List>> get dailySchedule { - if (_dailySchedule is EqualUnmodifiableListView) return _dailySchedule; + Map>> get dailySchedule { + if (_dailySchedule is EqualUnmodifiableMapView) return _dailySchedule; // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_dailySchedule); + return EqualUnmodifiableMapView(_dailySchedule); } @override @@ -728,11 +728,11 @@ abstract class _Diet implements Diet { @JsonKey(name: "name") required final String name, @JsonKey(name: "image") required final String image, @JsonKey(name: "duration") required final int duration, - @JsonKey(name: "state") required final String state, + @JsonKey(name: "state") required final DietState state, @JsonKey(name: "type") required final DietType type, @JsonKey(name: "details") required final String details, @JsonKey(name: "daily_schedule") - required final List>> dailySchedule, + required final Map>> dailySchedule, @JsonKey(name: "author") required final Author author, @JsonKey(name: "total_likes") required final int totalLikes, @JsonKey(name: "total_views") required final int totalViews, @@ -758,7 +758,7 @@ abstract class _Diet implements Diet { int get duration; @override @JsonKey(name: "state") - String get state; + DietState get state; @override @JsonKey(name: "type") DietType get type; @@ -767,7 +767,7 @@ abstract class _Diet implements Diet { String get details; @override @JsonKey(name: "daily_schedule") - List>> get dailySchedule; + Map>> get dailySchedule; @override @JsonKey(name: "author") Author get author; @@ -1242,11 +1242,9 @@ DailySchedule _$DailyScheduleFromJson(Map json) { mixin _$DailySchedule { @JsonKey(name: "name") String get name => throw _privateConstructorUsedError; - @JsonKey(name: "uniq", includeToJson: false) - String get uniq => throw _privateConstructorUsedError; @JsonKey(name: "portion") Portion get portion => throw _privateConstructorUsedError; - @JsonKey(name: "quantity") + @JsonKey(name: "quantity", fromJson: _parseQuantity) int get quantity => throw _privateConstructorUsedError; @JsonKey(includeToJson: false, includeFromJson: false) DailyNutrition? get dailyNutrition => throw _privateConstructorUsedError; @@ -1275,9 +1273,8 @@ abstract class $DailyScheduleCopyWith<$Res> { @useResult $Res call( {@JsonKey(name: "name") String name, - @JsonKey(name: "uniq", includeToJson: false) String uniq, @JsonKey(name: "portion") Portion portion, - @JsonKey(name: "quantity") int quantity, + @JsonKey(name: "quantity", fromJson: _parseQuantity) int quantity, @JsonKey(includeToJson: false, includeFromJson: false) DailyNutrition? dailyNutrition, @JsonKey( @@ -1306,7 +1303,6 @@ class _$DailyScheduleCopyWithImpl<$Res, $Val extends DailySchedule> @override $Res call({ Object? name = null, - Object? uniq = null, Object? portion = null, Object? quantity = null, Object? dailyNutrition = freezed, @@ -1317,10 +1313,6 @@ class _$DailyScheduleCopyWithImpl<$Res, $Val extends DailySchedule> ? _value.name : name // ignore: cast_nullable_to_non_nullable as String, - uniq: null == uniq - ? _value.uniq - : uniq // ignore: cast_nullable_to_non_nullable - as String, portion: null == portion ? _value.portion : portion // ignore: cast_nullable_to_non_nullable @@ -1365,9 +1357,8 @@ abstract class _$$DailyScheduleImplCopyWith<$Res> @useResult $Res call( {@JsonKey(name: "name") String name, - @JsonKey(name: "uniq", includeToJson: false) String uniq, @JsonKey(name: "portion") Portion portion, - @JsonKey(name: "quantity") int quantity, + @JsonKey(name: "quantity", fromJson: _parseQuantity) int quantity, @JsonKey(includeToJson: false, includeFromJson: false) DailyNutrition? dailyNutrition, @JsonKey( @@ -1395,7 +1386,6 @@ class __$$DailyScheduleImplCopyWithImpl<$Res> @override $Res call({ Object? name = null, - Object? uniq = null, Object? portion = null, Object? quantity = null, Object? dailyNutrition = freezed, @@ -1406,10 +1396,6 @@ class __$$DailyScheduleImplCopyWithImpl<$Res> ? _value.name : name // ignore: cast_nullable_to_non_nullable as String, - uniq: null == uniq - ? _value.uniq - : uniq // ignore: cast_nullable_to_non_nullable - as String, portion: null == portion ? _value.portion : portion // ignore: cast_nullable_to_non_nullable @@ -1435,9 +1421,9 @@ class __$$DailyScheduleImplCopyWithImpl<$Res> class _$DailyScheduleImpl implements _DailySchedule { const _$DailyScheduleImpl( {@JsonKey(name: "name") required this.name, - @JsonKey(name: "uniq", includeToJson: false) required this.uniq, @JsonKey(name: "portion") required this.portion, - @JsonKey(name: "quantity") required this.quantity, + @JsonKey(name: "quantity", fromJson: _parseQuantity) + required this.quantity, @JsonKey(includeToJson: false, includeFromJson: false) this.dailyNutrition, @JsonKey( @@ -1454,13 +1440,10 @@ class _$DailyScheduleImpl implements _DailySchedule { @JsonKey(name: "name") final String name; @override - @JsonKey(name: "uniq", includeToJson: false) - final String uniq; - @override @JsonKey(name: "portion") final Portion portion; @override - @JsonKey(name: "quantity") + @JsonKey(name: "quantity", fromJson: _parseQuantity) final int quantity; @override @JsonKey(includeToJson: false, includeFromJson: false) @@ -1475,7 +1458,7 @@ class _$DailyScheduleImpl implements _DailySchedule { @override String toString() { - return 'DailySchedule(name: $name, uniq: $uniq, portion: $portion, quantity: $quantity, dailyNutrition: $dailyNutrition, productNutritionalValueId: $productNutritionalValueId)'; + return 'DailySchedule(name: $name, portion: $portion, quantity: $quantity, dailyNutrition: $dailyNutrition, productNutritionalValueId: $productNutritionalValueId)'; } @override @@ -1484,7 +1467,6 @@ class _$DailyScheduleImpl implements _DailySchedule { (other.runtimeType == runtimeType && other is _$DailyScheduleImpl && (identical(other.name, name) || other.name == name) && - (identical(other.uniq, uniq) || other.uniq == uniq) && (identical(other.portion, portion) || other.portion == portion) && (identical(other.quantity, quantity) || other.quantity == quantity) && @@ -1497,7 +1479,7 @@ class _$DailyScheduleImpl implements _DailySchedule { @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, name, uniq, portion, quantity, + int get hashCode => Object.hash(runtimeType, name, portion, quantity, dailyNutrition, productNutritionalValueId); /// Create a copy of DailySchedule @@ -1519,9 +1501,9 @@ class _$DailyScheduleImpl implements _DailySchedule { abstract class _DailySchedule implements DailySchedule { const factory _DailySchedule( {@JsonKey(name: "name") required final String name, - @JsonKey(name: "uniq", includeToJson: false) required final String uniq, @JsonKey(name: "portion") required final Portion portion, - @JsonKey(name: "quantity") required final int quantity, + @JsonKey(name: "quantity", fromJson: _parseQuantity) + required final int quantity, @JsonKey(includeToJson: false, includeFromJson: false) final DailyNutrition? dailyNutrition, @JsonKey( @@ -1538,13 +1520,10 @@ abstract class _DailySchedule implements DailySchedule { @JsonKey(name: "name") String get name; @override - @JsonKey(name: "uniq", includeToJson: false) - String get uniq; - @override @JsonKey(name: "portion") Portion get portion; @override - @JsonKey(name: "quantity") + @JsonKey(name: "quantity", fromJson: _parseQuantity) int get quantity; @override @JsonKey(includeToJson: false, includeFromJson: false) diff --git a/lib/features/feed/models/diets_pagination_model.g.dart b/lib/features/feed/models/diets_pagination_model.g.dart index 7a54baa0..021fbfcf 100644 --- a/lib/features/feed/models/diets_pagination_model.g.dart +++ b/lib/features/feed/models/diets_pagination_model.g.dart @@ -29,16 +29,21 @@ _$DietImpl _$$DietImplFromJson(Map json) => _$DietImpl( name: json['name'] as String, image: json['image'] as String, duration: (json['duration'] as num).toInt(), - state: json['state'] as String, + state: $enumDecode(_$DietStateEnumMap, json['state']), type: DietType.fromJson(json['type'] as Map), details: json['details'] as String, - dailySchedule: (json['daily_schedule'] as List) - .map((e) => (e as List) - .map((e) => (e as List) - .map((e) => DailySchedule.fromJson(e as Map)) - .toList()) - .toList()) - .toList(), + dailySchedule: (json['daily_schedule'] as Map).map( + (k, e) => MapEntry( + int.parse(k), + (e as Map).map( + (k, e) => MapEntry( + int.parse(k), + (e as Map).map( + (k, e) => MapEntry(int.parse(k), + DailySchedule.fromJson(e as Map)), + )), + )), + ), author: Author.fromJson(json['author'] as Map), totalLikes: (json['total_likes'] as num).toInt(), totalViews: (json['total_views'] as num).toInt(), @@ -56,10 +61,13 @@ Map _$$DietImplToJson(_$DietImpl instance) => 'name': instance.name, 'image': instance.image, 'duration': instance.duration, - 'state': instance.state, + 'state': _$DietStateEnumMap[instance.state]!, 'type': instance.type, 'details': instance.details, - 'daily_schedule': instance.dailySchedule, + 'daily_schedule': instance.dailySchedule.map((k, e) => MapEntry( + k.toString(), + e.map((k, e) => MapEntry( + k.toString(), e.map((k, e) => MapEntry(k.toString(), e)))))), 'author': instance.author, 'total_likes': instance.totalLikes, 'total_views': instance.totalViews, @@ -69,6 +77,12 @@ Map _$$DietImplToJson(_$DietImpl instance) => 'daily_nutrition': instance.dailyNutrition, }; +const _$DietStateEnumMap = { + DietState.active: 'active', + DietState.draft: 'draft', + DietState.personal: 'personal', +}; + _$CreatedDietImpl _$$CreatedDietImplFromJson(Map json) => _$CreatedDietImpl( id: (json['id'] as num).toInt(), @@ -101,9 +115,8 @@ Map _$$AuthorImplToJson(_$AuthorImpl instance) => _$DailyScheduleImpl _$$DailyScheduleImplFromJson(Map json) => _$DailyScheduleImpl( name: json['name'] as String, - uniq: json['uniq'] as String, portion: $enumDecode(_$PortionEnumMap, json['portion']), - quantity: (json['quantity'] as num).toInt(), + quantity: _parseQuantity(json['quantity']), ); Map _$$DailyScheduleImplToJson(_$DailyScheduleImpl instance) => @@ -116,7 +129,7 @@ Map _$$DailyScheduleImplToJson(_$DailyScheduleImpl instance) => }; const _$PortionEnumMap = { - Portion.GRAMS: 'grams', + Portion.GRAMS: 'gr', Portion.MILLILITERS: 'milliliters', Portion.PCS: 'pcs', }; diff --git a/lib/features/feed/models/recipes_model.dart b/lib/features/feed/models/recipes_model.dart index 8f0db8ad..ff287211 100644 --- a/lib/features/feed/models/recipes_model.dart +++ b/lib/features/feed/models/recipes_model.dart @@ -11,14 +11,14 @@ class Recipes with _$Recipes { required String image, required String state, @JsonKey(name: 'cooking_time') required int cookingTime, - required List ingredients, + required List? ingredients, required String description, - required List steps, - @JsonKey(name: 'video_url') required String videoUrl, + required List? steps, + @JsonKey(name: 'video_url') required String? videoUrl, @JsonKey(name: 'created_at') required DateTime createdAt, @JsonKey(name: 'updated_at') required DateTime updatedAt, - required String title, - required String type, + required String? title, + required String? type, required String slug, @JsonKey(name: 'total_comments') required int commentsCount, @JsonKey(name: 'was_liked_by_me') required bool wasLikedByMe, diff --git a/lib/features/feed/models/recipes_model.freezed.dart b/lib/features/feed/models/recipes_model.freezed.dart index 34d1c39a..75d6ea7b 100644 --- a/lib/features/feed/models/recipes_model.freezed.dart +++ b/lib/features/feed/models/recipes_model.freezed.dart @@ -25,17 +25,17 @@ mixin _$Recipes { String get state => throw _privateConstructorUsedError; @JsonKey(name: 'cooking_time') int get cookingTime => throw _privateConstructorUsedError; - List get ingredients => throw _privateConstructorUsedError; + List? get ingredients => throw _privateConstructorUsedError; String get description => throw _privateConstructorUsedError; - List get steps => throw _privateConstructorUsedError; + List? get steps => throw _privateConstructorUsedError; @JsonKey(name: 'video_url') - String get videoUrl => throw _privateConstructorUsedError; + String? get videoUrl => throw _privateConstructorUsedError; @JsonKey(name: 'created_at') DateTime get createdAt => throw _privateConstructorUsedError; @JsonKey(name: 'updated_at') DateTime get updatedAt => throw _privateConstructorUsedError; - String get title => throw _privateConstructorUsedError; - String get type => throw _privateConstructorUsedError; + String? get title => throw _privateConstructorUsedError; + String? get type => throw _privateConstructorUsedError; String get slug => throw _privateConstructorUsedError; @JsonKey(name: 'total_comments') int get commentsCount => throw _privateConstructorUsedError; @@ -68,14 +68,14 @@ abstract class $RecipesCopyWith<$Res> { String image, String state, @JsonKey(name: 'cooking_time') int cookingTime, - List ingredients, + List? ingredients, String description, - List steps, - @JsonKey(name: 'video_url') String videoUrl, + List? steps, + @JsonKey(name: 'video_url') String? videoUrl, @JsonKey(name: 'created_at') DateTime createdAt, @JsonKey(name: 'updated_at') DateTime updatedAt, - String title, - String type, + String? title, + String? type, String slug, @JsonKey(name: 'total_comments') int commentsCount, @JsonKey(name: 'was_liked_by_me') bool wasLikedByMe, @@ -106,14 +106,14 @@ class _$RecipesCopyWithImpl<$Res, $Val extends Recipes> Object? image = null, Object? state = null, Object? cookingTime = null, - Object? ingredients = null, + Object? ingredients = freezed, Object? description = null, - Object? steps = null, - Object? videoUrl = null, + Object? steps = freezed, + Object? videoUrl = freezed, Object? createdAt = null, Object? updatedAt = null, - Object? title = null, - Object? type = null, + Object? title = freezed, + Object? type = freezed, Object? slug = null, Object? commentsCount = null, Object? wasLikedByMe = null, @@ -139,22 +139,22 @@ class _$RecipesCopyWithImpl<$Res, $Val extends Recipes> ? _value.cookingTime : cookingTime // ignore: cast_nullable_to_non_nullable as int, - ingredients: null == ingredients + ingredients: freezed == ingredients ? _value.ingredients : ingredients // ignore: cast_nullable_to_non_nullable - as List, + as List?, description: null == description ? _value.description : description // ignore: cast_nullable_to_non_nullable as String, - steps: null == steps + steps: freezed == steps ? _value.steps : steps // ignore: cast_nullable_to_non_nullable - as List, - videoUrl: null == videoUrl + as List?, + videoUrl: freezed == videoUrl ? _value.videoUrl : videoUrl // ignore: cast_nullable_to_non_nullable - as String, + as String?, createdAt: null == createdAt ? _value.createdAt : createdAt // ignore: cast_nullable_to_non_nullable @@ -163,14 +163,14 @@ class _$RecipesCopyWithImpl<$Res, $Val extends Recipes> ? _value.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as DateTime, - title: null == title + title: freezed == title ? _value.title : title // ignore: cast_nullable_to_non_nullable - as String, - type: null == type + as String?, + type: freezed == type ? _value.type : type // ignore: cast_nullable_to_non_nullable - as String, + as String?, slug: null == slug ? _value.slug : slug // ignore: cast_nullable_to_non_nullable @@ -225,14 +225,14 @@ abstract class _$$RecipesImplCopyWith<$Res> implements $RecipesCopyWith<$Res> { String image, String state, @JsonKey(name: 'cooking_time') int cookingTime, - List ingredients, + List? ingredients, String description, - List steps, - @JsonKey(name: 'video_url') String videoUrl, + List? steps, + @JsonKey(name: 'video_url') String? videoUrl, @JsonKey(name: 'created_at') DateTime createdAt, @JsonKey(name: 'updated_at') DateTime updatedAt, - String title, - String type, + String? title, + String? type, String slug, @JsonKey(name: 'total_comments') int commentsCount, @JsonKey(name: 'was_liked_by_me') bool wasLikedByMe, @@ -262,14 +262,14 @@ class __$$RecipesImplCopyWithImpl<$Res> Object? image = null, Object? state = null, Object? cookingTime = null, - Object? ingredients = null, + Object? ingredients = freezed, Object? description = null, - Object? steps = null, - Object? videoUrl = null, + Object? steps = freezed, + Object? videoUrl = freezed, Object? createdAt = null, Object? updatedAt = null, - Object? title = null, - Object? type = null, + Object? title = freezed, + Object? type = freezed, Object? slug = null, Object? commentsCount = null, Object? wasLikedByMe = null, @@ -295,22 +295,22 @@ class __$$RecipesImplCopyWithImpl<$Res> ? _value.cookingTime : cookingTime // ignore: cast_nullable_to_non_nullable as int, - ingredients: null == ingredients + ingredients: freezed == ingredients ? _value._ingredients : ingredients // ignore: cast_nullable_to_non_nullable - as List, + as List?, description: null == description ? _value.description : description // ignore: cast_nullable_to_non_nullable as String, - steps: null == steps + steps: freezed == steps ? _value._steps : steps // ignore: cast_nullable_to_non_nullable - as List, - videoUrl: null == videoUrl + as List?, + videoUrl: freezed == videoUrl ? _value.videoUrl : videoUrl // ignore: cast_nullable_to_non_nullable - as String, + as String?, createdAt: null == createdAt ? _value.createdAt : createdAt // ignore: cast_nullable_to_non_nullable @@ -319,14 +319,14 @@ class __$$RecipesImplCopyWithImpl<$Res> ? _value.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as DateTime, - title: null == title + title: freezed == title ? _value.title : title // ignore: cast_nullable_to_non_nullable - as String, - type: null == type + as String?, + type: freezed == type ? _value.type : type // ignore: cast_nullable_to_non_nullable - as String, + as String?, slug: null == slug ? _value.slug : slug // ignore: cast_nullable_to_non_nullable @@ -367,9 +367,9 @@ class _$RecipesImpl implements _Recipes { required this.image, required this.state, @JsonKey(name: 'cooking_time') required this.cookingTime, - required final List ingredients, + required final List? ingredients, required this.description, - required final List steps, + required final List? steps, @JsonKey(name: 'video_url') required this.videoUrl, @JsonKey(name: 'created_at') required this.createdAt, @JsonKey(name: 'updated_at') required this.updatedAt, @@ -397,27 +397,31 @@ class _$RecipesImpl implements _Recipes { @override @JsonKey(name: 'cooking_time') final int cookingTime; - final List _ingredients; + final List? _ingredients; @override - List get ingredients { + List? get ingredients { + final value = _ingredients; + if (value == null) return null; if (_ingredients is EqualUnmodifiableListView) return _ingredients; // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_ingredients); + return EqualUnmodifiableListView(value); } @override final String description; - final List _steps; + final List? _steps; @override - List get steps { + List? get steps { + final value = _steps; + if (value == null) return null; if (_steps is EqualUnmodifiableListView) return _steps; // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_steps); + return EqualUnmodifiableListView(value); } @override @JsonKey(name: 'video_url') - final String videoUrl; + final String? videoUrl; @override @JsonKey(name: 'created_at') final DateTime createdAt; @@ -425,9 +429,9 @@ class _$RecipesImpl implements _Recipes { @JsonKey(name: 'updated_at') final DateTime updatedAt; @override - final String title; + final String? title; @override - final String type; + final String? type; @override final String slug; @override @@ -537,14 +541,14 @@ abstract class _Recipes implements Recipes { required final String image, required final String state, @JsonKey(name: 'cooking_time') required final int cookingTime, - required final List ingredients, + required final List? ingredients, required final String description, - required final List steps, - @JsonKey(name: 'video_url') required final String videoUrl, + required final List? steps, + @JsonKey(name: 'video_url') required final String? videoUrl, @JsonKey(name: 'created_at') required final DateTime createdAt, @JsonKey(name: 'updated_at') required final DateTime updatedAt, - required final String title, - required final String type, + required final String? title, + required final String? type, required final String slug, @JsonKey(name: 'total_comments') required final int commentsCount, @JsonKey(name: 'was_liked_by_me') required final bool wasLikedByMe, @@ -565,14 +569,14 @@ abstract class _Recipes implements Recipes { @JsonKey(name: 'cooking_time') int get cookingTime; @override - List get ingredients; + List? get ingredients; @override String get description; @override - List get steps; + List? get steps; @override @JsonKey(name: 'video_url') - String get videoUrl; + String? get videoUrl; @override @JsonKey(name: 'created_at') DateTime get createdAt; @@ -580,9 +584,9 @@ abstract class _Recipes implements Recipes { @JsonKey(name: 'updated_at') DateTime get updatedAt; @override - String get title; + String? get title; @override - String get type; + String? get type; @override String get slug; @override diff --git a/lib/features/feed/models/recipes_model.g.dart b/lib/features/feed/models/recipes_model.g.dart index cc96df42..00da0654 100644 --- a/lib/features/feed/models/recipes_model.g.dart +++ b/lib/features/feed/models/recipes_model.g.dart @@ -12,18 +12,18 @@ _$RecipesImpl _$$RecipesImplFromJson(Map json) => image: json['image'] as String, state: json['state'] as String, cookingTime: (json['cooking_time'] as num).toInt(), - ingredients: (json['ingredients'] as List) - .map((e) => Ingredient.fromJson(e as Map)) + ingredients: (json['ingredients'] as List?) + ?.map((e) => Ingredient.fromJson(e as Map)) .toList(), description: json['description'] as String, - steps: (json['steps'] as List) - .map((e) => Step.fromJson(e as Map)) + steps: (json['steps'] as List?) + ?.map((e) => Step.fromJson(e as Map)) .toList(), - videoUrl: json['video_url'] as String, + videoUrl: json['video_url'] as String?, createdAt: DateTime.parse(json['created_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String), - title: json['title'] as String, - type: json['type'] as String, + title: json['title'] as String?, + type: json['type'] as String?, slug: json['slug'] as String, commentsCount: (json['total_comments'] as num).toInt(), wasLikedByMe: json['was_liked_by_me'] as bool, diff --git a/lib/features/feed/presentation/bloc/consultants_cubit/consultants_cubit.dart b/lib/features/feed/presentation/bloc/consultants_cubit/consultants_cubit.dart index 9b30f4cc..cf96b2a6 100644 --- a/lib/features/feed/presentation/bloc/consultants_cubit/consultants_cubit.dart +++ b/lib/features/feed/presentation/bloc/consultants_cubit/consultants_cubit.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:eatvio_app/features/feed/presentation/bloc/consultants_cubit/consultants_state.dart'; import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -18,7 +20,8 @@ class ConsultantsCubit extends Cubit { isLoading: false, ), ); - } catch (e) { + } catch (e, st) { + log(e.toString(), stackTrace: st); emit(ConsultantsState.error(errorMessage: e.toString())); } } diff --git a/lib/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart b/lib/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart index 1f585375..de59de7e 100644 --- a/lib/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart +++ b/lib/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart @@ -107,9 +107,10 @@ class DietsCubit extends Cubit { errorMessage: null, ), ); - } catch (e) { + } catch (e, st) { + log(e.toString(), stackTrace: st); emit( - state.copyWith(errorMessage: e.toString()), + state.copyWith(isLoading: false), ); } } @@ -143,14 +144,13 @@ class DietsCubit extends Cubit { ); } } catch (e) { - emit(state.copyWith(errorMessage: e.toString())); + emit(state.copyWith(isLoading: false)); } } } Future likeDiet(int id, int index) async { - final currentDiets = state.dietsList; - // if (currentDiets.isEmpty) return; + final currentDiets = state.dietsList; final dietsForModification = [...currentDiets]; final currentDiet = currentDiets[index]; if (currentDiet.wasLikedByMe) { diff --git a/lib/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart b/lib/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart index 94c9bc11..ad9c01ad 100644 --- a/lib/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart +++ b/lib/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart @@ -3,6 +3,7 @@ import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; import 'package:eatvio_app/features/recipes/models/recipe_tag_model.dart'; import 'package:eatvio_app/features/recipes/reporitories/recipe_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'dart:developer'; class RecipesCubit extends Cubit { final FeedRepository feedRepository; @@ -38,7 +39,8 @@ class RecipesCubit extends Cubit { ), ); lastPage = currentPage; - } catch (e) { + } catch (e, st) { + log(e.toString(), stackTrace: st); emit(RecipesState.error(errorMessage: e.toString())); } } diff --git a/lib/features/feed/presentation/pages/feed_screen.dart b/lib/features/feed/presentation/pages/feed_screen.dart index 56252d0d..4527c0af 100644 --- a/lib/features/feed/presentation/pages/feed_screen.dart +++ b/lib/features/feed/presentation/pages/feed_screen.dart @@ -1,5 +1,6 @@ import 'package:easy_debounce/easy_debounce.dart'; import 'package:eatvio_app/core/widgets/add_item_button.dart'; +import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart'; import 'package:eatvio_app/core/widgets/custom_search_bar.dart'; import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart'; import 'package:eatvio_app/features/create_plan/presentation/create_plan_screen.dart'; @@ -14,6 +15,7 @@ import 'package:eatvio_app/features/feed/presentation/widgets/chip_group.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/consultants.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/diets.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/plans.dart'; +import 'package:eatvio_app/features/feed/presentation/widgets/success_published_diet_bottom_sheet.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/tags_modal_widget.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart'; import 'package:eatvio_app/core/locator_service.dart'; @@ -25,7 +27,6 @@ import 'package:go_router/go_router.dart'; import '../widgets/recipes.dart'; - class FeedScreen extends StatelessWidget { const FeedScreen({super.key}); @@ -260,10 +261,23 @@ class _FeedScreenContentState extends State { onTap: () => GoRouter.of(context).pushNamed(RecipeCreateScreen.name), ), 2 => AddItemButton( - onTap: state.profile!.roles.contains('consultant') ? () => GoRouter.of(context).pushNamed(CreateDietScreen.name) : null, + onTap: state.profile?.roles.contains('consultant') ?? false + ? () async { + final result = await GoRouter.of(context).pushNamed(CreateDietScreen.name); + if (result != null) { + showBottomSheetWithBlur( + userRootNavigator: true, + context: context, + child: SuccessPublishedDietBottomSheet( + title: result, + ), + ); + } + } + : null, ), 3 => AddItemButton( - onTap: state.profile!.roles.contains('consultant') ? () => GoRouter.of(context).pushNamed(CreatePlanScreen.name) : null, + onTap: state.profile?.roles.contains('consultant') ?? false ? () => GoRouter.of(context).pushNamed(CreatePlanScreen.name) : null, ), _ => AddItemButton() }, diff --git a/lib/features/feed/presentation/widgets/diets.dart b/lib/features/feed/presentation/widgets/diets.dart index 4d1ca2f3..b698065b 100644 --- a/lib/features/feed/presentation/widgets/diets.dart +++ b/lib/features/feed/presentation/widgets/diets.dart @@ -63,4 +63,4 @@ class Diets extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/features/feed/presentation/widgets/recipes_widget.dart b/lib/features/feed/presentation/widgets/recipes_widget.dart index d410793a..16681a77 100644 --- a/lib/features/feed/presentation/widgets/recipes_widget.dart +++ b/lib/features/feed/presentation/widgets/recipes_widget.dart @@ -75,7 +75,7 @@ class _RecipesWidgetState extends State { children: [ _iconButton('assets/icons/clock.svg', widget.recipes.cookingTime.toString() + ' минут'), SizedBox(width: 8), - _iconButton('assets/icons/steps.svg', widget.recipes.steps.length.toString() + ' этапов'), + _iconButton('assets/icons/steps.svg', '${widget.recipes.steps?.length.toString()}' + ' этапов'), SizedBox(width: 8), _iconButton('assets/icons/calories.svg', widget.recipes.calories.toStringAsFixed(0) + ' ккал'), ], @@ -87,7 +87,7 @@ class _RecipesWidgetState extends State { Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: Text( - widget.recipes.title, + widget.recipes.title ?? '', style: TextStyle( fontWeight: FontWeight.w600, color: Colors.black87, diff --git a/lib/features/feed/presentation/widgets/success_published_diet_bottom_sheet.dart b/lib/features/feed/presentation/widgets/success_published_diet_bottom_sheet.dart new file mode 100644 index 00000000..eade82c2 --- /dev/null +++ b/lib/features/feed/presentation/widgets/success_published_diet_bottom_sheet.dart @@ -0,0 +1,54 @@ +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart'; +import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class SuccessPublishedDietBottomSheet extends StatelessWidget { + const SuccessPublishedDietBottomSheet({super.key, required this.title}); + final String title; + @override + Widget build(BuildContext context) { + return Column( + children: [ + SizedBox( + height: 20, + ), + BottomSheetHandlerBar(), + SizedBox( + height: 20, + ), + Text( + title, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + Text( + 'опубликованна', + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + SizedBox( + height: 20, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: CustomOutlinedButton( + onPressed: () { + GoRouter.of(context).pop(); + }, + text: 'Хорошо', + height: 52, + buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + ), + ), + ), + SizedBox( + height: 20, + ), + ], + ); + } +} diff --git a/lib/features/internet_connection_checker/internet_connection_checker_screen.dart b/lib/features/internet_connection_checker/internet_connection_checker_screen.dart new file mode 100644 index 00000000..f8d258b1 --- /dev/null +++ b/lib/features/internet_connection_checker/internet_connection_checker_screen.dart @@ -0,0 +1,172 @@ +import 'dart:async'; +import 'dart:developer'; +import 'dart:io'; + +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:easy_debounce/easy_debounce.dart'; +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:http/http.dart' as http; + +class InternetConnectionCheckerScreen extends StatefulWidget { + const InternetConnectionCheckerScreen({super.key, required this.child}); + final Widget child; + + @override + State createState() => _InternetConnectionCheckerScreenState(); +} + +class _InternetConnectionCheckerScreenState extends State { + bool _hasInternet = true; + final Connectivity _connectivity = Connectivity(); + late StreamSubscription> _connectivitySubscription; + late final AppLifecycleListener _listener; + + @override + void initState() { + super.initState(); + _checkConnection(); + _connectivitySubscription = _connectivity.onConnectivityChanged.listen(_updateAccessStatus); + _listener = AppLifecycleListener( + onResume: () async { + _connectivitySubscription.resume(); + await _checkConnection(); + }, + onHide: _connectivitySubscription.pause, + onPause: _connectivitySubscription.pause, + ); + } + + Future _checkConnection() async { + late List result; + + try { + result = await _connectivity.checkConnectivity(); + } on PlatformException catch (e) { + log('Couldn\'t check connectivity status', error: e); + return; + } + + return _updateAccessStatus(result); + } + + Future _updateAccessStatus(List result) async { + final hasInternet = await _checkInternetAccess(result); + + setState(() { + _hasInternet = hasInternet; + }); + } + + Future _checkInternetAccess(List result) async { + if (result.contains(ConnectivityResult.none)) { + return false; + } + try { + final response = await Future.any([ + http.get(Uri.parse('https://google.com')), + http.get(Uri.parse('https://yandex.ru')), + http.get(Uri.parse('https://dev2.eatvio.com')), + http.get(Uri.parse('https://eatvio.ru')), + ]).timeout(Duration(seconds: 5)); + + return response.statusCode >= 100 && response.statusCode < 600; + } on SocketException catch (_) { + return false; + } on TimeoutException catch (_) { + return false; + } on HandshakeException catch (_) { + return false; + } on CertificateException catch (_) { + return false; + } on http.ClientException catch (_) { + return false; + } catch (e, st) { + log(e.toString(), stackTrace: st); + return false; + } + } + + @override + void dispose() { + _connectivitySubscription.cancel(); + _listener.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + widget.child, + if (!_hasInternet) + NoInternetScreen( + onTryAgain: () async { + EasyDebounce.debounce( + 'internet connection check', + Duration(seconds: 2), + () async { + await _checkConnection(); + }, + ); + }, + ), + ], + ); + } +} + +class NoInternetScreen extends StatelessWidget { + const NoInternetScreen({super.key, required this.onTryAgain}); + final Future Function() onTryAgain; + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + children: [ + Spacer(), + SizedBox(height: 28), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 52), + child: Text( + 'Отсутствует подключение к интернету', + textAlign: TextAlign.center, + style: TextStyles.headingXLSemibold.copyWith( + color: appTheme.black, + ), + ), + ), + SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 40), + child: Text( + 'Проверьте соединение с сетью и попробуйте ещё раз', + textAlign: TextAlign.center, + style: TextStyles.textMMedium.copyWith( + color: appTheme.gray, + ), + ), + ), + SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: CustomOutlinedButton( + onPressed: onTryAgain, + height: 52, + text: 'Попробовать ещё раз', + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + side: WidgetStatePropertyAll(BorderSide.none), + ), + ), + ), + Spacer(), + ], + ), + ); + } +} diff --git a/lib/features/my_diets/presentation/cubit/my_diets_cubit.dart b/lib/features/my_diets/presentation/cubit/my_diets_cubit.dart index 7a998163..09753d80 100644 --- a/lib/features/my_diets/presentation/cubit/my_diets_cubit.dart +++ b/lib/features/my_diets/presentation/cubit/my_diets_cubit.dart @@ -1,5 +1,8 @@ +import 'dart:developer'; + import 'package:easy_debounce/easy_debounce.dart'; import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_state.dart'; import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -28,6 +31,7 @@ class MyDietsCubit extends Cubit { 'name', 'name:desc', ]; + static const List _durationValues = [null, ...durationValues]; static const List _types = [null, ...dietTypeValues]; @@ -39,7 +43,7 @@ class MyDietsCubit extends Cubit { EasyDebounce.debounce( 'loadDiets', Duration(seconds: 1), - () => loadDiets( + () => loadCurrentDiets( query: _activeQuery, ), ); @@ -48,7 +52,7 @@ class MyDietsCubit extends Cubit { EasyDebounce.debounce( 'loadDiets', Duration(seconds: 1), - () => loadDiets( + () => loadCurrentDiets( query: _draftQuery, ), ); @@ -57,7 +61,7 @@ class MyDietsCubit extends Cubit { EasyDebounce.debounce( 'loadDiets', Duration(seconds: 1), - () => loadDiets( + () => loadCurrentDiets( query: _personalQuery, ), ); @@ -70,7 +74,7 @@ class MyDietsCubit extends Cubit { EasyDebounce.debounce( 'loadDiets', Duration(seconds: 1), - () => loadDiets( + () => loadCurrentDiets( query: _activeQuery, ), ); @@ -79,7 +83,7 @@ class MyDietsCubit extends Cubit { EasyDebounce.debounce( 'loadDiets', Duration(seconds: 1), - () => loadDiets( + () => loadCurrentDiets( query: _draftQuery, ), ); @@ -88,7 +92,7 @@ class MyDietsCubit extends Cubit { EasyDebounce.debounce( 'loadDiets', Duration(seconds: 1), - () => loadDiets( + () => loadCurrentDiets( query: _personalQuery, ), ); @@ -101,7 +105,7 @@ class MyDietsCubit extends Cubit { EasyDebounce.debounce( 'loadDiets', Duration(seconds: 1), - () => loadDiets( + () => loadCurrentDiets( query: _activeQuery, ), ); @@ -110,7 +114,7 @@ class MyDietsCubit extends Cubit { EasyDebounce.debounce( 'loadDiets', Duration(seconds: 1), - () => loadDiets( + () => loadCurrentDiets( query: _draftQuery, ), ); @@ -119,7 +123,7 @@ class MyDietsCubit extends Cubit { EasyDebounce.debounce( 'loadDiets', Duration(seconds: 1), - () => loadDiets( + () => loadCurrentDiets( query: _personalQuery, ), ); @@ -128,9 +132,34 @@ class MyDietsCubit extends Cubit { void changeSelectedStateIndex(int index) { emit(state.copyWith(selectedStateIndex: index)); + if (state.activeDiets.isEmpty) { + EasyDebounce.debounce( + 'loadActiveDiets', + Duration(seconds: 1), + () => loadCurrentDiets( + query: _activeQuery, + ), + ); + } else if (state.draftDiets.isEmpty) { + EasyDebounce.debounce( + 'loadDraftDiets', + Duration(seconds: 1), + () => loadCurrentDiets( + query: _draftQuery, + ), + ); + } else if (state.personalDiets.isEmpty) { + EasyDebounce.debounce( + 'loadPersonalDiets', + Duration(seconds: 1), + () => loadCurrentDiets( + query: _personalQuery, + ), + ); + } } - Future loadDiets({ + Future loadCurrentDiets({ String? query, }) async { switch (state.selectedStateIndex) { @@ -160,11 +189,13 @@ class MyDietsCubit extends Cubit { final type = _types[state.activeFilterState.typeIndex]; final duration = _durationValues[state.activeFilterState.durationIndex]; final sorting = _sortingValues[state.activeFilterState.sortingIndex]; + final dietState = DietState.active; final dietsPagination = await myDietsRepository.getDiets( query: query, types: type, duration: duration, sorting: sorting, + state: dietState.toString(), ); lastActivePage = dietsPagination.meta.lastPage; emit( @@ -174,7 +205,8 @@ class MyDietsCubit extends Cubit { error: null, ), ); - } catch (e) { + } catch (e, st) { + log(e.toString(), stackTrace: st); emit( state.copyWith(error: e.toString()), ); @@ -187,11 +219,13 @@ class MyDietsCubit extends Cubit { final type = _types[state.draftFilterState.typeIndex]; final duration = _durationValues[state.draftFilterState.durationIndex]; final sorting = _sortingValues[state.draftFilterState.sortingIndex]; + final dietState = DietState.draft; final dietsPagination = await myDietsRepository.getDiets( query: query, types: type, duration: duration, sorting: sorting, + state: dietState.toString(), ); lastActivePage = dietsPagination.meta.lastPage; emit( @@ -214,11 +248,13 @@ class MyDietsCubit extends Cubit { final type = _types[state.personalFilterState.typeIndex]; final duration = _durationValues[state.personalFilterState.durationIndex]; final sorting = _sortingValues[state.personalFilterState.sortingIndex]; + final dietState = DietState.personal; final dietsPagination = await myDietsRepository.getDiets( query: query, types: type, duration: duration, sorting: sorting, + state: dietState.toString(), ); lastActivePage = dietsPagination.meta.lastPage; emit( diff --git a/lib/features/my_diets/presentation/my_diets_screen.dart b/lib/features/my_diets/presentation/my_diets_screen.dart index 65ac8441..a58f3bdc 100644 --- a/lib/features/my_diets/presentation/my_diets_screen.dart +++ b/lib/features/my_diets/presentation/my_diets_screen.dart @@ -33,7 +33,7 @@ class MyDietsScreenContent extends StatelessWidget { final activeDietsScrollController = ScrollController(); final draftDietsScrollController = ScrollController(); final personalDietsScrollController = ScrollController(); - + final activeDietsSearchController = SearchController(); final draftDietsSearchController = SearchController(); final personalDietsSearchController = SearchController(); @@ -111,7 +111,7 @@ class MyDietsScreenContent extends StatelessWidget { 'searchDiets', Duration(milliseconds: 500), () { - context.read().loadDiets(query: query == '' ? null : query); + context.read().loadCurrentDiets(query: query == '' ? null : query); }, ); }, @@ -133,7 +133,7 @@ class MyDietsScreenContent extends StatelessWidget { ]; }, body: RefreshIndicator( - onRefresh: () => context.read().loadDiets(), + onRefresh: () => context.read().loadCurrentDiets(), child: BlocBuilder( builder: (BuildContext context, MyDietsState state) { if (state.isLoading) { diff --git a/lib/features/my_diets/presentation/widgets/my_diet_widget.dart b/lib/features/my_diets/presentation/widgets/my_diet_widget.dart index d173117e..ff8d060a 100644 --- a/lib/features/my_diets/presentation/widgets/my_diet_widget.dart +++ b/lib/features/my_diets/presentation/widgets/my_diet_widget.dart @@ -6,6 +6,7 @@ import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart'; import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart'; import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; import 'package:eatvio_app/core/widgets/info_row.dart'; +import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart'; import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_state.dart'; @@ -110,6 +111,13 @@ class _MyDietWidgetState extends State { children: [ Expanded( child: CustomOutlinedButton( + onPressed: () { + if (state.selectedStateIndex == 1) { + } else if (state.selectedStateIndex == 2) { + } else { + GoRouter.of(context).pushNamed(CreateDietScreen.name, extra: widget.diet); + } + }, text: switch (state.selectedStateIndex) { 1 => s.publish, 2 => s.toOffer, @@ -160,6 +168,12 @@ class _MyDietWidgetState extends State { children: [ Expanded( child: CustomOutlinedButton( + onPressed: () { + if (state.selectedStateIndex == 0) { + } else { + GoRouter.of(context).pushNamed(CreateDietScreen.name, extra: widget.diet); + } + }, text: switch (state.selectedStateIndex) { 0 => s.removeFromActive, _ => s.edit, @@ -178,51 +192,49 @@ class _MyDietWidgetState extends State { onPressed: () async { final result = await context.read().deleteDiet(widget.diet.id); if (result) - Future.delayed(Duration(seconds: 3), () { - showBottomSheetWithBlur( - context: context, - child: Column( - children: [ - SizedBox( - height: 20, - ), - BottomSheetHandlerBar(), - SizedBox( - height: 20, - ), - Text( - '${widget.diet.name}', - style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), - ), - Text( - s.deleted, - style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), - ), - SizedBox( - height: 20, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: CustomOutlinedButton( - onPressed: () { - context.pop(); - }, - text: 'Хорошо', - height: 52, - buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700), - buttonStyle: ButtonStyle( - side: WidgetStatePropertyAll(BorderSide.none), - backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), - ), + showBottomSheetWithBlur( + context: context, + child: Column( + children: [ + SizedBox( + height: 20, + ), + BottomSheetHandlerBar(), + SizedBox( + height: 20, + ), + Text( + '${widget.diet.name}', + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + Text( + s.deleted, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + SizedBox( + height: 20, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: CustomOutlinedButton( + onPressed: () { + context.pop(); + }, + text: 'Хорошо', + height: 52, + buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + side: WidgetStatePropertyAll(BorderSide.none), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), ), ), - SizedBox( - height: 20, - ), - ], - ), - ); - }); + ), + SizedBox( + height: 20, + ), + ], + ), + ); }, height: 52, width: 52, diff --git a/lib/features/my_diets/repositories/my_diets_repository.dart b/lib/features/my_diets/repositories/my_diets_repository.dart index 8c15a546..e3ec0dd1 100644 --- a/lib/features/my_diets/repositories/my_diets_repository.dart +++ b/lib/features/my_diets/repositories/my_diets_repository.dart @@ -20,6 +20,7 @@ class MyDietsRepository { String? types, int? duration, String? sorting, + String? state, }) async { try { return (await myDietsService.getDiets( @@ -28,6 +29,7 @@ class MyDietsRepository { types: types, duration: duration, sorting: sorting, + state: state, )); } catch (e) { rethrow; diff --git a/lib/features/my_diets/services/my_diets_service.dart b/lib/features/my_diets/services/my_diets_service.dart index 3a78d25e..41e3174f 100644 --- a/lib/features/my_diets/services/my_diets_service.dart +++ b/lib/features/my_diets/services/my_diets_service.dart @@ -12,13 +12,14 @@ abstract class MyDietsService { @GET('/api/profile/my_diets') Future getDiets({ @Query('page') required int page, - @Query('filters[name][\$startsWith]') String? query, + @Query('filters[name][\$startsWith]') String? query, + @Query('filters[state][\$eq]') String? state, @Query('filters[type][\$contains]') String? types, @Query('filters[duration][\$eq]') int? duration, @Query('sort') String? sorting, }); - @POST('/api/diets/delete/{dietId}') + @DELETE('/api/diets/delete/{dietId}') Future deleteDiet({ @Path() required int dietId, }); diff --git a/lib/features/my_diets/services/my_diets_service.g.dart b/lib/features/my_diets/services/my_diets_service.g.dart index f9244073..4c2ccf28 100644 --- a/lib/features/my_diets/services/my_diets_service.g.dart +++ b/lib/features/my_diets/services/my_diets_service.g.dart @@ -21,6 +21,7 @@ class _MyDietsService implements MyDietsService { Future getDiets({ required int page, String? query, + String? state, String? types, int? duration, String? sorting, @@ -29,6 +30,7 @@ class _MyDietsService implements MyDietsService { final queryParameters = { r'page': page, r'filters[name][$startsWith]': query, + r'filters[state][$eq]': state, r'filters[type][$contains]': types, r'filters[duration][$eq]': duration, r'sort': sorting, @@ -64,7 +66,7 @@ class _MyDietsService implements MyDietsService { final _headers = {}; const Map? _data = null; final _options = _setStreamType( - Options(method: 'POST', headers: _headers, extra: _extra) + Options(method: 'DELETE', headers: _headers, extra: _extra) .compose( _dio.options, '/api/diets/delete/${dietId}', diff --git a/lib/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart b/lib/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart index 79eb02a8..4b42c19b 100644 --- a/lib/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart +++ b/lib/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:eatvio_app/features/plans/repositories/plans_repository.dart'; import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_state.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -14,7 +16,7 @@ class PlansCubit extends Cubit { try { emit(PlansState.loaded(plansList: await plansRepository.getPlansBySlug(slug))); } catch (e) { - print(e); + log(e.toString()); emit(PlansState.error(errorMessage: e.toString())); } } diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index 64d4fa41..0b8b7af6 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -242,7 +242,7 @@ class _ProfileScreenState extends State { } _activePlans(ProfileStateLoaded state) { - if (state.profile.roles.contains('consultant')) { + if (!state.profile.roles.contains('consultant')) { return const SizedBox.shrink(); } return BlocBuilder( @@ -372,7 +372,7 @@ class _ProfileScreenState extends State { } _diplomasAndCertificates(ProfileStateLoaded state) { - if (state.profile.roles.contains('consultant') || state.profile.diplomasAndCertificates == null || state.profile.diplomasAndCertificates!.isEmpty) { + if (!state.profile.roles.contains('consultant') || state.profile.diplomasAndCertificates == null || state.profile.diplomasAndCertificates!.isEmpty) { return const SizedBox.shrink(); } return Padding( @@ -456,7 +456,7 @@ class _ProfileScreenState extends State { } _education(ProfileStateLoaded state) { - if (state.profile.roles.contains('consultant') || + if (!state.profile.roles.contains('consultant') || state.profile.educationResource?.completedCourses == null || state.profile.educationResource?.completedCourses?.length == 0) { return const SizedBox.shrink(); @@ -549,7 +549,7 @@ class _ProfileScreenState extends State { } _socialLinks(ProfileStateLoaded state) { - if (state.profile.roles.contains('consultant')) { + if (!state.profile.roles.contains('consultant')) { return const SizedBox.shrink(); } return Padding( diff --git a/lib/features/recipes/entities/recipe_create_entity.dart b/lib/features/recipes/entities/recipe_create_entity.dart index 75e7c783..6ca01db3 100644 --- a/lib/features/recipes/entities/recipe_create_entity.dart +++ b/lib/features/recipes/entities/recipe_create_entity.dart @@ -84,8 +84,10 @@ class RecipeCreateEntity with _$RecipeCreateEntity { TypeOfRecipe? typeOfRecipe, int? countOfPortions, double? energyInOnePortion, - String? totalCookTime, // hh:mm - String? activeCookTime, // hh:mm + int? totalCookHours, + int? totalCookMinutes, + int? activeCookHours, + int? activeCookMinutes, String? description, File? image, String? videoUrl, @@ -99,21 +101,28 @@ class RecipeCreateEntity with _$RecipeCreateEntity { type: typeOfRecipe?.toApiType(), servings: countOfPortions, energyPerServing: energyInOnePortion?.toInt(), - totalCookingTime: totalCookTime?.split(':').fold(0, (value, element) => (int.tryParse(element) ?? 0)), - activeCookTime: activeCookTime?.split(':').fold(0, (value, element) => (int.tryParse(element) ?? 0)), + totalCookingTime: ((totalCookHours ?? 0) * 60) + (totalCookMinutes ?? 0), + activeCookTime: ((activeCookHours ?? 0) * 60) + (activeCookMinutes ?? 0), description: description, image: image?.path, video_url: videoUrl, ingredients: ingredients, - steps: steps.map((step) => Step(image: step.image?.path ?? '', description: step.description ?? '')).toList(), + steps: steps.map((step) { + if (step.hasProps()) return Step(image: step.image?.path ?? '', description: step.description ?? ''); + }).toList(), tags: tags, ); } @freezed class StepEntity with _$StepEntity { + const StepEntity._(); const factory StepEntity({ String? description, File? image, }) = _StepEntity; + + bool hasProps() { + return description != null; + } } diff --git a/lib/features/recipes/entities/recipe_create_entity.freezed.dart b/lib/features/recipes/entities/recipe_create_entity.freezed.dart index aabda874..c5f4178d 100644 --- a/lib/features/recipes/entities/recipe_create_entity.freezed.dart +++ b/lib/features/recipes/entities/recipe_create_entity.freezed.dart @@ -20,8 +20,10 @@ mixin _$RecipeCreateEntity { TypeOfRecipe? get typeOfRecipe => throw _privateConstructorUsedError; int? get countOfPortions => throw _privateConstructorUsedError; double? get energyInOnePortion => throw _privateConstructorUsedError; - String? get totalCookTime => throw _privateConstructorUsedError; // hh:mm - String? get activeCookTime => throw _privateConstructorUsedError; // hh:mm + int? get totalCookHours => throw _privateConstructorUsedError; + int? get totalCookMinutes => throw _privateConstructorUsedError; + int? get activeCookHours => throw _privateConstructorUsedError; + int? get activeCookMinutes => throw _privateConstructorUsedError; String? get description => throw _privateConstructorUsedError; File? get image => throw _privateConstructorUsedError; String? get videoUrl => throw _privateConstructorUsedError; @@ -47,8 +49,10 @@ abstract class $RecipeCreateEntityCopyWith<$Res> { TypeOfRecipe? typeOfRecipe, int? countOfPortions, double? energyInOnePortion, - String? totalCookTime, - String? activeCookTime, + int? totalCookHours, + int? totalCookMinutes, + int? activeCookHours, + int? activeCookMinutes, String? description, File? image, String? videoUrl, @@ -76,8 +80,10 @@ class _$RecipeCreateEntityCopyWithImpl<$Res, $Val extends RecipeCreateEntity> Object? typeOfRecipe = freezed, Object? countOfPortions = freezed, Object? energyInOnePortion = freezed, - Object? totalCookTime = freezed, - Object? activeCookTime = freezed, + Object? totalCookHours = freezed, + Object? totalCookMinutes = freezed, + Object? activeCookHours = freezed, + Object? activeCookMinutes = freezed, Object? description = freezed, Object? image = freezed, Object? videoUrl = freezed, @@ -102,14 +108,22 @@ class _$RecipeCreateEntityCopyWithImpl<$Res, $Val extends RecipeCreateEntity> ? _value.energyInOnePortion : energyInOnePortion // ignore: cast_nullable_to_non_nullable as double?, - totalCookTime: freezed == totalCookTime - ? _value.totalCookTime - : totalCookTime // ignore: cast_nullable_to_non_nullable - as String?, - activeCookTime: freezed == activeCookTime - ? _value.activeCookTime - : activeCookTime // ignore: cast_nullable_to_non_nullable - as String?, + totalCookHours: freezed == totalCookHours + ? _value.totalCookHours + : totalCookHours // ignore: cast_nullable_to_non_nullable + as int?, + totalCookMinutes: freezed == totalCookMinutes + ? _value.totalCookMinutes + : totalCookMinutes // ignore: cast_nullable_to_non_nullable + as int?, + activeCookHours: freezed == activeCookHours + ? _value.activeCookHours + : activeCookHours // ignore: cast_nullable_to_non_nullable + as int?, + activeCookMinutes: freezed == activeCookMinutes + ? _value.activeCookMinutes + : activeCookMinutes // ignore: cast_nullable_to_non_nullable + as int?, description: freezed == description ? _value.description : description // ignore: cast_nullable_to_non_nullable @@ -151,8 +165,10 @@ abstract class _$$RecipeCreateEntityImplCopyWith<$Res> TypeOfRecipe? typeOfRecipe, int? countOfPortions, double? energyInOnePortion, - String? totalCookTime, - String? activeCookTime, + int? totalCookHours, + int? totalCookMinutes, + int? activeCookHours, + int? activeCookMinutes, String? description, File? image, String? videoUrl, @@ -178,8 +194,10 @@ class __$$RecipeCreateEntityImplCopyWithImpl<$Res> Object? typeOfRecipe = freezed, Object? countOfPortions = freezed, Object? energyInOnePortion = freezed, - Object? totalCookTime = freezed, - Object? activeCookTime = freezed, + Object? totalCookHours = freezed, + Object? totalCookMinutes = freezed, + Object? activeCookHours = freezed, + Object? activeCookMinutes = freezed, Object? description = freezed, Object? image = freezed, Object? videoUrl = freezed, @@ -204,14 +222,22 @@ class __$$RecipeCreateEntityImplCopyWithImpl<$Res> ? _value.energyInOnePortion : energyInOnePortion // ignore: cast_nullable_to_non_nullable as double?, - totalCookTime: freezed == totalCookTime - ? _value.totalCookTime - : totalCookTime // ignore: cast_nullable_to_non_nullable - as String?, - activeCookTime: freezed == activeCookTime - ? _value.activeCookTime - : activeCookTime // ignore: cast_nullable_to_non_nullable - as String?, + totalCookHours: freezed == totalCookHours + ? _value.totalCookHours + : totalCookHours // ignore: cast_nullable_to_non_nullable + as int?, + totalCookMinutes: freezed == totalCookMinutes + ? _value.totalCookMinutes + : totalCookMinutes // ignore: cast_nullable_to_non_nullable + as int?, + activeCookHours: freezed == activeCookHours + ? _value.activeCookHours + : activeCookHours // ignore: cast_nullable_to_non_nullable + as int?, + activeCookMinutes: freezed == activeCookMinutes + ? _value.activeCookMinutes + : activeCookMinutes // ignore: cast_nullable_to_non_nullable + as int?, description: freezed == description ? _value.description : description // ignore: cast_nullable_to_non_nullable @@ -248,8 +274,10 @@ class _$RecipeCreateEntityImpl extends _RecipeCreateEntity { this.typeOfRecipe, this.countOfPortions, this.energyInOnePortion, - this.totalCookTime, - this.activeCookTime, + this.totalCookHours, + this.totalCookMinutes, + this.activeCookHours, + this.activeCookMinutes, this.description, this.image, this.videoUrl, @@ -270,11 +298,13 @@ class _$RecipeCreateEntityImpl extends _RecipeCreateEntity { @override final double? energyInOnePortion; @override - final String? totalCookTime; -// hh:mm + final int? totalCookHours; @override - final String? activeCookTime; -// hh:mm + final int? totalCookMinutes; + @override + final int? activeCookHours; + @override + final int? activeCookMinutes; @override final String? description; @override @@ -307,7 +337,7 @@ class _$RecipeCreateEntityImpl extends _RecipeCreateEntity { @override String toString() { - return 'RecipeCreateEntity(title: $title, typeOfRecipe: $typeOfRecipe, countOfPortions: $countOfPortions, energyInOnePortion: $energyInOnePortion, totalCookTime: $totalCookTime, activeCookTime: $activeCookTime, description: $description, image: $image, videoUrl: $videoUrl, ingredients: $ingredients, steps: $steps, tags: $tags)'; + return 'RecipeCreateEntity(title: $title, typeOfRecipe: $typeOfRecipe, countOfPortions: $countOfPortions, energyInOnePortion: $energyInOnePortion, totalCookHours: $totalCookHours, totalCookMinutes: $totalCookMinutes, activeCookHours: $activeCookHours, activeCookMinutes: $activeCookMinutes, description: $description, image: $image, videoUrl: $videoUrl, ingredients: $ingredients, steps: $steps, tags: $tags)'; } @override @@ -322,10 +352,14 @@ class _$RecipeCreateEntityImpl extends _RecipeCreateEntity { other.countOfPortions == countOfPortions) && (identical(other.energyInOnePortion, energyInOnePortion) || other.energyInOnePortion == energyInOnePortion) && - (identical(other.totalCookTime, totalCookTime) || - other.totalCookTime == totalCookTime) && - (identical(other.activeCookTime, activeCookTime) || - other.activeCookTime == activeCookTime) && + (identical(other.totalCookHours, totalCookHours) || + other.totalCookHours == totalCookHours) && + (identical(other.totalCookMinutes, totalCookMinutes) || + other.totalCookMinutes == totalCookMinutes) && + (identical(other.activeCookHours, activeCookHours) || + other.activeCookHours == activeCookHours) && + (identical(other.activeCookMinutes, activeCookMinutes) || + other.activeCookMinutes == activeCookMinutes) && (identical(other.description, description) || other.description == description) && (identical(other.image, image) || other.image == image) && @@ -344,8 +378,10 @@ class _$RecipeCreateEntityImpl extends _RecipeCreateEntity { typeOfRecipe, countOfPortions, energyInOnePortion, - totalCookTime, - activeCookTime, + totalCookHours, + totalCookMinutes, + activeCookHours, + activeCookMinutes, description, image, videoUrl, @@ -369,8 +405,10 @@ abstract class _RecipeCreateEntity extends RecipeCreateEntity { final TypeOfRecipe? typeOfRecipe, final int? countOfPortions, final double? energyInOnePortion, - final String? totalCookTime, - final String? activeCookTime, + final int? totalCookHours, + final int? totalCookMinutes, + final int? activeCookHours, + final int? activeCookMinutes, final String? description, final File? image, final String? videoUrl, @@ -388,9 +426,13 @@ abstract class _RecipeCreateEntity extends RecipeCreateEntity { @override double? get energyInOnePortion; @override - String? get totalCookTime; // hh:mm + int? get totalCookHours; @override - String? get activeCookTime; // hh:mm + int? get totalCookMinutes; + @override + int? get activeCookHours; + @override + int? get activeCookMinutes; @override String? get description; @override @@ -506,8 +548,8 @@ class __$$StepEntityImplCopyWithImpl<$Res> /// @nodoc -class _$StepEntityImpl implements _StepEntity { - const _$StepEntityImpl({this.description, this.image}); +class _$StepEntityImpl extends _StepEntity { + const _$StepEntityImpl({this.description, this.image}) : super._(); @override final String? description; @@ -541,9 +583,10 @@ class _$StepEntityImpl implements _StepEntity { __$$StepEntityImplCopyWithImpl<_$StepEntityImpl>(this, _$identity); } -abstract class _StepEntity implements StepEntity { +abstract class _StepEntity extends StepEntity { const factory _StepEntity({final String? description, final File? image}) = _$StepEntityImpl; + const _StepEntity._() : super._(); @override String? get description; diff --git a/lib/features/recipes/models/ingredient.dart b/lib/features/recipes/models/ingredient.dart index 480f358a..26c979e6 100644 --- a/lib/features/recipes/models/ingredient.dart +++ b/lib/features/recipes/models/ingredient.dart @@ -12,5 +12,5 @@ class Ingredient with _$Ingredient { required String portion, }) = _Ingredient; - factory Ingredient.fromJson(Map json) => _$IngredientFromJson(json); + factory Ingredient.fromJson(Map json) => _$IngredientFromJson(json); } \ No newline at end of file diff --git a/lib/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart b/lib/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart index 78112360..a91e44dd 100644 --- a/lib/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart +++ b/lib/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart @@ -1,3 +1,4 @@ +import 'dart:developer'; import 'dart:io'; import 'package:eatvio_app/features/recipes/entities/recipe_create_entity.dart'; @@ -11,10 +12,10 @@ class CreateRecipeCubit extends Cubit { CreateRecipeCubit({required this.recipeRepository}) : super( CreateRecipeState( - RecipeCreateEntity( + entity: RecipeCreateEntity( typeOfRecipe: TypeOfRecipe.first_meals, ingredients: [], - steps: [], + steps: [StepEntity()], tags: [], ), ), @@ -22,13 +23,11 @@ class CreateRecipeCubit extends Cubit { addStep() { emit( - CreateRecipeState( - state.entity.copyWith( - steps: [ - ...state.entity.steps, - StepEntity(), - ], - ), + state.copyWith.entity( + steps: [ + ...state.entity.steps, + StepEntity(), + ], ), ); } @@ -36,21 +35,16 @@ class CreateRecipeCubit extends Cubit { removeStep(StepEntity step) { List steps = [...state.entity.steps]; steps.remove(step); - - emit( - CreateRecipeState(state.entity.copyWith(steps: steps)), - ); + emit(state.copyWith.entity(steps: steps)); } addIngredient(Ingredient entity) { emit( - CreateRecipeState( - state.entity.copyWith( - ingredients: [ - ...state.entity.ingredients, - entity, - ], - ), + state.copyWith.entity( + ingredients: [ + ...state.entity.ingredients, + entity, + ], ), ); } @@ -59,67 +53,35 @@ class CreateRecipeCubit extends Cubit { List ingredients = [...state.entity.ingredients]; ingredients.removeAt(index); - emit( - CreateRecipeState( - state.entity.copyWith( - ingredients: ingredients, - ), - ), - ); + emit(state.copyWith.entity(ingredients: ingredients)); } replaceIngredientPosition(int oldIndex, int newIndex) { List ingredients = [...state.entity.ingredients]; final item = ingredients.removeAt(oldIndex); ingredients.insert(newIndex, item); - emit( - CreateRecipeState( - state.entity.copyWith( - ingredients: ingredients, - ), - ), - ); + emit(state.copyWith.entity(ingredients: ingredients)); } replaceTitle(String title) { - emit( - CreateRecipeState( - state.entity.copyWith( - title: title, - ), - ), - ); + emit(state.copyWith.entity(title: title == '' ? null : title)); } replaceDescription(String description) { - emit( - CreateRecipeState( - state.entity.copyWith( - description: description, - ), - ), - ); + emit(state.copyWith.entity(description: description == '' ? null : description)); } changeType(TypeOfRecipe typeOfRecipe) { - emit( - CreateRecipeState( - state.entity.copyWith( - typeOfRecipe: typeOfRecipe, - ), - ), - ); + emit(state.copyWith.entity(typeOfRecipe: typeOfRecipe)); } addTag(String tag) { emit( - CreateRecipeState( - state.entity.copyWith( - tags: [ - ...state.entity.tags, - tag, - ], - ), + state.copyWith.entity( + tags: [ + ...state.entity.tags, + tag, + ], ), ); } @@ -128,104 +90,153 @@ class CreateRecipeCubit extends Cubit { List tags = [...state.entity.tags]; tags.remove(tag); - emit( - CreateRecipeState( - state.entity.copyWith( - tags: tags, - ), - ), - ); + emit(state.copyWith.entity(tags: tags)); } replacePortionsCount(int count) { - emit( - CreateRecipeState( - state.entity.copyWith( - countOfPortions: count, - ), - ), - ); + emit(state.copyWith.entity(countOfPortions: count)); } replaceEnegryInPortion(double energy) { - emit( - CreateRecipeState( - state.entity.copyWith( - energyInOnePortion: energy, - ), - ), - ); + emit(state.copyWith.entity(energyInOnePortion: energy)); } - replaceTotalTime(String time) { - emit( - CreateRecipeState( - state.entity.copyWith( - totalCookTime: time, - ), - ), - ); + setTotalCookingMinutes(int minutes) { + emit(state.copyWith.entity(totalCookMinutes: minutes)); } - replaceActiveTime(String time) { - emit( - CreateRecipeState( - state.entity.copyWith( - activeCookTime: time, - ), - ), - ); + setTotalCookingHours(int hours) { + emit(state.copyWith.entity(totalCookHours: hours)); + } + + setActiveCookingMinutes(int minutes) { + emit(state.copyWith.entity(activeCookMinutes: minutes)); + } + + setActiveCookingHours(int hours) { + emit(state.copyWith.entity(activeCookHours: hours)); } replaceVideoUrl(String videoUrl) { - emit( - CreateRecipeState( - state.entity.copyWith( - videoUrl: videoUrl, - ), - ), - ); + emit(state.copyWith.entity(videoUrl: videoUrl)); } replaceImage(File? image) { - emit( - CreateRecipeState( - state.entity.copyWith( - image: image, - ), - ), - ); + emit(state.copyWith.entity(image: image)); } replaceDescriptionInStep(String description, int index) { List steps = [...state.entity.steps]; + steps[index] = steps[index].copyWith(description: description); - emit( - CreateRecipeState( - state.entity.copyWith( - steps: steps, - ), - ), - ); + + emit(state.copyWith.entity(steps: steps)); } replaceImageInStep(File? image, int index) { List steps = [...state.entity.steps]; + steps[index] = steps[index].copyWith(image: image); - emit( - CreateRecipeState( - state.entity.copyWith( - steps: steps, - ), - ), - ); + + emit(state.copyWith.entity(steps: steps)); } - Future publishRecipe() async { + Future publishRecipe() async { try { - await recipeRepository.publishRecipe(state.entity.toModel()); - } catch (e) { - rethrow; + final model = state.entity.toModel(); + await recipeRepository.publishRecipe(model); + return true; + } catch (e, st) { + log(e.toString(), stackTrace: st); + return false; + } + } + + bool validateAll() { + final isTitleValid = validateTitle(); + final isDescValid = validateDescription(); + final isImageValid = validateImage(); + final areServingsValid = validateServings(); + final isCookTimeValid = validateTotalCookTime(); + final isActiveCookTimeValid = validateActiveCookTime(); + final isEnergyPerServingsValid = validateEnergyPerServing(); + + return isTitleValid && isDescValid && isImageValid && areServingsValid && isCookTimeValid && isActiveCookTimeValid && isEnergyPerServingsValid; + } + + bool validateTitle() { + if (state.entity.title == null || (state.entity.title?.isEmpty ?? true)) { + emit(state.copyWith(titleValidationError: 'Поле не заполнено')); + return false; + } else if (state.entity.title!.length < 3) { + emit(state.copyWith(titleValidationError: 'Минимальная длина 3 символа ')); + return false; + } else { + emit(state.copyWith(titleValidationError: null)); + return true; + } + } + + bool validateServings() { + if (state.entity.countOfPortions == null) { + emit(state.copyWith(servingsValidationError: 'Поле не заполнено')); + return false; + } else { + emit(state.copyWith(servingsValidationError: null)); + return true; + } + } + + bool validateEnergyPerServing() { + if (state.entity.energyInOnePortion == null) { + emit(state.copyWith(energyPerServingValidationError: 'Поле не заполнено')); + return false; + } else { + emit(state.copyWith(energyPerServingValidationError: null)); + return true; + } + } + + bool validateActiveCookTime() { + if (state.entity.activeCookHours == null && state.entity.activeCookMinutes == null) { + emit(state.copyWith(activeTimeValidationError: 'Поле не заполнено')); + return false; + } else { + emit(state.copyWith(activeTimeValidationError: null)); + return true; + } + } + + bool validateTotalCookTime() { + if (state.entity.totalCookHours == null || state.entity.totalCookMinutes == null) { + emit(state.copyWith(cookingTimeValidationError: 'Поле не заполнено')); + return false; + } else { + emit(state.copyWith(cookingTimeValidationError: null)); + return true; + } + } + + bool validateDescription() { + if (state.entity.description == null || (state.entity.description?.isEmpty ?? true)) { + emit(state.copyWith(descriptionValidationError: 'Поле не заполнено')); + return false; + } else if (state.entity.description!.length < 3) { + emit(state.copyWith(descriptionValidationError: 'Минимальная длина 3 символа ')); + return false; + } else { + emit(state.copyWith(descriptionValidationError: null)); + return true; + } + } + + bool validateImage() { + if (state.entity.image == null) { + emit(state.copyWith(imageValidationError: 'Поле не заполнено')); + return false; + } else { + emit(state.copyWith(imageValidationError: null)); + return true; } } } diff --git a/lib/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_state.dart b/lib/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_state.dart index 19adabf8..fd95a839 100644 --- a/lib/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_state.dart +++ b/lib/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_state.dart @@ -4,8 +4,16 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'create_recipe_state.freezed.dart'; @freezed -class CreateRecipeState with _$CreateRecipeState { - const factory CreateRecipeState( - RecipeCreateEntity entity, - ) = _CreateRecipeState; +class CreateRecipeState with _$CreateRecipeState { + const factory CreateRecipeState({ + required RecipeCreateEntity entity, + String? titleValidationError, + String? descriptionValidationError, + String? cookingTimeValidationError, + String? activeTimeValidationError, + String? servingsValidationError, + String? energyPerServingValidationError, + String? imageValidationError, + }) = _CreateRecipeState; + } diff --git a/lib/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_state.freezed.dart b/lib/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_state.freezed.dart index b1661b76..302f048c 100644 --- a/lib/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_state.freezed.dart +++ b/lib/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_state.freezed.dart @@ -17,6 +17,14 @@ final _privateConstructorUsedError = UnsupportedError( /// @nodoc mixin _$CreateRecipeState { RecipeCreateEntity get entity => throw _privateConstructorUsedError; + String? get titleValidationError => throw _privateConstructorUsedError; + String? get descriptionValidationError => throw _privateConstructorUsedError; + String? get cookingTimeValidationError => throw _privateConstructorUsedError; + String? get activeTimeValidationError => throw _privateConstructorUsedError; + String? get servingsValidationError => throw _privateConstructorUsedError; + String? get energyPerServingValidationError => + throw _privateConstructorUsedError; + String? get imageValidationError => throw _privateConstructorUsedError; /// Create a copy of CreateRecipeState /// with the given fields replaced by the non-null parameter values. @@ -31,7 +39,15 @@ abstract class $CreateRecipeStateCopyWith<$Res> { CreateRecipeState value, $Res Function(CreateRecipeState) then) = _$CreateRecipeStateCopyWithImpl<$Res, CreateRecipeState>; @useResult - $Res call({RecipeCreateEntity entity}); + $Res call( + {RecipeCreateEntity entity, + String? titleValidationError, + String? descriptionValidationError, + String? cookingTimeValidationError, + String? activeTimeValidationError, + String? servingsValidationError, + String? energyPerServingValidationError, + String? imageValidationError}); $RecipeCreateEntityCopyWith<$Res> get entity; } @@ -52,12 +68,48 @@ class _$CreateRecipeStateCopyWithImpl<$Res, $Val extends CreateRecipeState> @override $Res call({ Object? entity = null, + Object? titleValidationError = freezed, + Object? descriptionValidationError = freezed, + Object? cookingTimeValidationError = freezed, + Object? activeTimeValidationError = freezed, + Object? servingsValidationError = freezed, + Object? energyPerServingValidationError = freezed, + Object? imageValidationError = freezed, }) { return _then(_value.copyWith( entity: null == entity ? _value.entity : entity // ignore: cast_nullable_to_non_nullable as RecipeCreateEntity, + titleValidationError: freezed == titleValidationError + ? _value.titleValidationError + : titleValidationError // ignore: cast_nullable_to_non_nullable + as String?, + descriptionValidationError: freezed == descriptionValidationError + ? _value.descriptionValidationError + : descriptionValidationError // ignore: cast_nullable_to_non_nullable + as String?, + cookingTimeValidationError: freezed == cookingTimeValidationError + ? _value.cookingTimeValidationError + : cookingTimeValidationError // ignore: cast_nullable_to_non_nullable + as String?, + activeTimeValidationError: freezed == activeTimeValidationError + ? _value.activeTimeValidationError + : activeTimeValidationError // ignore: cast_nullable_to_non_nullable + as String?, + servingsValidationError: freezed == servingsValidationError + ? _value.servingsValidationError + : servingsValidationError // ignore: cast_nullable_to_non_nullable + as String?, + energyPerServingValidationError: freezed == + energyPerServingValidationError + ? _value.energyPerServingValidationError + : energyPerServingValidationError // ignore: cast_nullable_to_non_nullable + as String?, + imageValidationError: freezed == imageValidationError + ? _value.imageValidationError + : imageValidationError // ignore: cast_nullable_to_non_nullable + as String?, ) as $Val); } @@ -80,7 +132,15 @@ abstract class _$$CreateRecipeStateImplCopyWith<$Res> __$$CreateRecipeStateImplCopyWithImpl<$Res>; @override @useResult - $Res call({RecipeCreateEntity entity}); + $Res call( + {RecipeCreateEntity entity, + String? titleValidationError, + String? descriptionValidationError, + String? cookingTimeValidationError, + String? activeTimeValidationError, + String? servingsValidationError, + String? energyPerServingValidationError, + String? imageValidationError}); @override $RecipeCreateEntityCopyWith<$Res> get entity; @@ -100,12 +160,48 @@ class __$$CreateRecipeStateImplCopyWithImpl<$Res> @override $Res call({ Object? entity = null, + Object? titleValidationError = freezed, + Object? descriptionValidationError = freezed, + Object? cookingTimeValidationError = freezed, + Object? activeTimeValidationError = freezed, + Object? servingsValidationError = freezed, + Object? energyPerServingValidationError = freezed, + Object? imageValidationError = freezed, }) { return _then(_$CreateRecipeStateImpl( - null == entity + entity: null == entity ? _value.entity : entity // ignore: cast_nullable_to_non_nullable as RecipeCreateEntity, + titleValidationError: freezed == titleValidationError + ? _value.titleValidationError + : titleValidationError // ignore: cast_nullable_to_non_nullable + as String?, + descriptionValidationError: freezed == descriptionValidationError + ? _value.descriptionValidationError + : descriptionValidationError // ignore: cast_nullable_to_non_nullable + as String?, + cookingTimeValidationError: freezed == cookingTimeValidationError + ? _value.cookingTimeValidationError + : cookingTimeValidationError // ignore: cast_nullable_to_non_nullable + as String?, + activeTimeValidationError: freezed == activeTimeValidationError + ? _value.activeTimeValidationError + : activeTimeValidationError // ignore: cast_nullable_to_non_nullable + as String?, + servingsValidationError: freezed == servingsValidationError + ? _value.servingsValidationError + : servingsValidationError // ignore: cast_nullable_to_non_nullable + as String?, + energyPerServingValidationError: freezed == + energyPerServingValidationError + ? _value.energyPerServingValidationError + : energyPerServingValidationError // ignore: cast_nullable_to_non_nullable + as String?, + imageValidationError: freezed == imageValidationError + ? _value.imageValidationError + : imageValidationError // ignore: cast_nullable_to_non_nullable + as String?, )); } } @@ -113,14 +209,36 @@ class __$$CreateRecipeStateImplCopyWithImpl<$Res> /// @nodoc class _$CreateRecipeStateImpl implements _CreateRecipeState { - const _$CreateRecipeStateImpl(this.entity); + const _$CreateRecipeStateImpl( + {required this.entity, + this.titleValidationError, + this.descriptionValidationError, + this.cookingTimeValidationError, + this.activeTimeValidationError, + this.servingsValidationError, + this.energyPerServingValidationError, + this.imageValidationError}); @override final RecipeCreateEntity entity; + @override + final String? titleValidationError; + @override + final String? descriptionValidationError; + @override + final String? cookingTimeValidationError; + @override + final String? activeTimeValidationError; + @override + final String? servingsValidationError; + @override + final String? energyPerServingValidationError; + @override + final String? imageValidationError; @override String toString() { - return 'CreateRecipeState(entity: $entity)'; + return 'CreateRecipeState(entity: $entity, titleValidationError: $titleValidationError, descriptionValidationError: $descriptionValidationError, cookingTimeValidationError: $cookingTimeValidationError, activeTimeValidationError: $activeTimeValidationError, servingsValidationError: $servingsValidationError, energyPerServingValidationError: $energyPerServingValidationError, imageValidationError: $imageValidationError)'; } @override @@ -128,11 +246,42 @@ class _$CreateRecipeStateImpl implements _CreateRecipeState { return identical(this, other) || (other.runtimeType == runtimeType && other is _$CreateRecipeStateImpl && - (identical(other.entity, entity) || other.entity == entity)); + (identical(other.entity, entity) || other.entity == entity) && + (identical(other.titleValidationError, titleValidationError) || + other.titleValidationError == titleValidationError) && + (identical(other.descriptionValidationError, + descriptionValidationError) || + other.descriptionValidationError == + descriptionValidationError) && + (identical(other.cookingTimeValidationError, + cookingTimeValidationError) || + other.cookingTimeValidationError == + cookingTimeValidationError) && + (identical(other.activeTimeValidationError, + activeTimeValidationError) || + other.activeTimeValidationError == activeTimeValidationError) && + (identical( + other.servingsValidationError, servingsValidationError) || + other.servingsValidationError == servingsValidationError) && + (identical(other.energyPerServingValidationError, + energyPerServingValidationError) || + other.energyPerServingValidationError == + energyPerServingValidationError) && + (identical(other.imageValidationError, imageValidationError) || + other.imageValidationError == imageValidationError)); } @override - int get hashCode => Object.hash(runtimeType, entity); + int get hashCode => Object.hash( + runtimeType, + entity, + titleValidationError, + descriptionValidationError, + cookingTimeValidationError, + activeTimeValidationError, + servingsValidationError, + energyPerServingValidationError, + imageValidationError); /// Create a copy of CreateRecipeState /// with the given fields replaced by the non-null parameter values. @@ -145,11 +294,32 @@ class _$CreateRecipeStateImpl implements _CreateRecipeState { } abstract class _CreateRecipeState implements CreateRecipeState { - const factory _CreateRecipeState(final RecipeCreateEntity entity) = - _$CreateRecipeStateImpl; + const factory _CreateRecipeState( + {required final RecipeCreateEntity entity, + final String? titleValidationError, + final String? descriptionValidationError, + final String? cookingTimeValidationError, + final String? activeTimeValidationError, + final String? servingsValidationError, + final String? energyPerServingValidationError, + final String? imageValidationError}) = _$CreateRecipeStateImpl; @override RecipeCreateEntity get entity; + @override + String? get titleValidationError; + @override + String? get descriptionValidationError; + @override + String? get cookingTimeValidationError; + @override + String? get activeTimeValidationError; + @override + String? get servingsValidationError; + @override + String? get energyPerServingValidationError; + @override + String? get imageValidationError; /// Create a copy of CreateRecipeState /// with the given fields replaced by the non-null parameter values. diff --git a/lib/features/recipes/presentation/pages/about_recipes_screen.dart b/lib/features/recipes/presentation/pages/about_recipes_screen.dart index e5749b7d..3bb3d35f 100644 --- a/lib/features/recipes/presentation/pages/about_recipes_screen.dart +++ b/lib/features/recipes/presentation/pages/about_recipes_screen.dart @@ -212,7 +212,7 @@ class _AboutRecipesScreenState extends State { Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( - widget.recipes.title, + widget.recipes.title ?? '', style: TextStyle( color: Colors.black, fontSize: 24, @@ -241,7 +241,7 @@ class _AboutRecipesScreenState extends State { Icon(Icons.fast_forward_outlined), SizedBox(width: 4), Text( - widget.recipes.steps.length.toString() + ' этапов', + '${widget.recipes.steps?.length ?? 0}' + ' этапов', style: TextStyle( color: Colors.black, fontSize: 12, @@ -286,9 +286,14 @@ class _AboutRecipesScreenState extends State { padding: const EdgeInsets.symmetric(horizontal: 16), shrinkWrap: true, physics: NeverScrollableScrollPhysics(), - itemBuilder: (context, index) => _ingredient(widget.recipes.ingredients[index]), + itemBuilder: (context, index) { + if (widget.recipes.ingredients != null) { + return _ingredient(widget.recipes.ingredients![index]); + } + return SizedBox.shrink(); + }, separatorBuilder: (context, index) => SizedBox(height: 4), - itemCount: widget.recipes.ingredients.length, + itemCount: widget.recipes.ingredients?.length ?? 0, ), SizedBox(height: 12), Padding( @@ -307,7 +312,10 @@ class _AboutRecipesScreenState extends State { ListView.separated( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), - itemBuilder: (context, index) => _step(widget.recipes.steps[index], index + 1), + itemBuilder: (context, index) { + if (widget.recipes.steps == null) return SizedBox.shrink(); + return _step(widget.recipes.steps![index], index + 1); + }, separatorBuilder: (context, index) => Padding( padding: const EdgeInsets.all(16), child: Container( @@ -318,7 +326,7 @@ class _AboutRecipesScreenState extends State { ), ), ), - itemCount: widget.recipes.steps.length, + itemCount: widget.recipes.steps?.length ?? 0, ), SizedBox(height: 8), Padding( diff --git a/lib/features/recipes/presentation/pages/recipe_create_screen.dart b/lib/features/recipes/presentation/pages/recipe_create_screen.dart index 78da5627..f383da68 100644 --- a/lib/features/recipes/presentation/pages/recipe_create_screen.dart +++ b/lib/features/recipes/presentation/pages/recipe_create_screen.dart @@ -1,6 +1,8 @@ import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; +import 'package:eatvio_app/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_state.dart'; import 'package:eatvio_app/features/recipes/presentation/widgets/info_about_recipe_widget.dart'; import 'package:eatvio_app/features/recipes/presentation/widgets/ingredients_widget.dart'; import 'package:eatvio_app/features/recipes/presentation/widgets/steps_widget.dart'; @@ -42,32 +44,44 @@ class _RecipeCreateScreenState extends State { InfoAboutRecipeWidget(), IngredientsWidget(), StepsWidget(), - Builder( - builder: (context) { - return InkWell( - onTap: () { - context.read().publishRecipe(); - }, - borderRadius: BorderRadius.circular(20), - child: Ink( - width: MediaQuery.of(context).size.width - 32, - decoration: BoxDecoration( - color: appTheme.blueA400, + BlocBuilder( + builder: (context, state) { + return Builder( + builder: (context) { + return InkWell( + onTap: () async { + if (context.read().validateAll()) { + final isSuccess = await context.read().publishRecipe(); + if (isSuccess) { + showSuccessToast('Рецепт успешно опубликован'); + Navigator.pop(context); + } else { + showErrorToast('Не удалось опубликовать рецепт'); + } + } + }, borderRadius: BorderRadius.circular(20), - ), - child: Container( - padding: const EdgeInsets.all(16), - alignment: Alignment.center, - child: Text( - 'Опубликовать', - style: TextStyle( - fontSize: 16, - color: Colors.white, - fontWeight: FontWeight.w600, + child: Ink( + width: MediaQuery.of(context).size.width - 32, + decoration: BoxDecoration( + color: appTheme.blueA400, + borderRadius: BorderRadius.circular(20), + ), + child: Container( + padding: const EdgeInsets.all(16), + alignment: Alignment.center, + child: Text( + 'Опубликовать', + style: TextStyle( + fontSize: 16, + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), ), ), - ), - ), + ); + }, ); }, ), diff --git a/lib/features/recipes/presentation/widgets/image_widget.dart b/lib/features/recipes/presentation/widgets/image_widget.dart index 2521ff69..251e3345 100644 --- a/lib/features/recipes/presentation/widgets/image_widget.dart +++ b/lib/features/recipes/presentation/widgets/image_widget.dart @@ -8,14 +8,13 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:image_picker/image_picker.dart'; class ImageWidget extends StatefulWidget { - const ImageWidget({super.key}); - + const ImageWidget({super.key, required this.image}); + final File? image; @override State createState() => _ImageWidgetState(); } class _ImageWidgetState extends State { - XFile? image; late final CreateRecipeCubit createRecipeCubit; @override @@ -30,7 +29,7 @@ class _ImageWidgetState extends State { onTap: () { _showImagePicker(); }, - child: image == null + child: widget.image == null ? SvgPicture.asset( width: MediaQuery.of(context).size.width - 32, 'assets/icons/upload_photo_of_portion.svg', @@ -40,7 +39,7 @@ class _ImageWidgetState extends State { ClipRRect( borderRadius: BorderRadius.circular(20), child: Image.file( - File(image!.path), + File(widget.image!.path), ), ), Positioned( @@ -48,9 +47,6 @@ class _ImageWidgetState extends State { top: 16, child: GestureDetector( onTap: () { - setState(() { - image = null; - }); createRecipeCubit.replaceImage(null); }, child: Container( @@ -76,10 +72,11 @@ class _ImageWidgetState extends State { CupertinoActionSheetAction( onPressed: () async { final picker = ImagePicker(); - image = await picker.pickImage(source: ImageSource.camera); + final image = await picker.pickImage(source: ImageSource.camera); setState(() {}); if (image != null) { - createRecipeCubit.replaceImage(File(image!.path)); + createRecipeCubit.replaceImage(File(image.path)); + createRecipeCubit.validateImage(); } Navigator.of(context).pop(); }, @@ -94,10 +91,11 @@ class _ImageWidgetState extends State { CupertinoActionSheetAction( onPressed: () async { final picker = ImagePicker(); - image = await picker.pickImage(source: ImageSource.gallery); + final image = await picker.pickImage(source: ImageSource.gallery); setState(() {}); if (image != null) { - createRecipeCubit.replaceImage(File(image!.path)); + createRecipeCubit.replaceImage(File(image.path)); + createRecipeCubit.validateImage(); } Navigator.of(context).pop(); }, diff --git a/lib/features/recipes/presentation/widgets/info_about_recipe_widget.dart b/lib/features/recipes/presentation/widgets/info_about_recipe_widget.dart index 44ef152e..0a7cb9fe 100644 --- a/lib/features/recipes/presentation/widgets/info_about_recipe_widget.dart +++ b/lib/features/recipes/presentation/widgets/info_about_recipe_widget.dart @@ -1,8 +1,11 @@ import 'package:eatvio_app/core/data/selectionPopupModel/selection_popup_model.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/custom_drop_down.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; +import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart'; import 'package:eatvio_app/features/recipes/entities/recipe_create_entity.dart'; import 'package:eatvio_app/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; +import 'package:eatvio_app/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_state.dart'; import 'package:eatvio_app/features/recipes/presentation/widgets/image_widget.dart'; import 'package:eatvio_app/features/recipes/presentation/widgets/portion_widget.dart'; import 'package:eatvio_app/features/recipes/presentation/widgets/tags_widget.dart'; @@ -40,10 +43,29 @@ class _InfoAboutRecipeWidgetState extends State { children: [ Text('Название'), SizedBox(height: 4), - CustomTextFormField( - hintText: 'Например: тар-тар из говядины', - onChanged: (value) { - createRecipeCubit.replaceTitle(value); + BlocBuilder( + buildWhen: (previous, current) => previous.titleValidationError != current.titleValidationError, + builder: (context, state) { + return Column( + children: [ + CustomTextFormField( + hintText: 'Например: тар-тар из говядины', + hasError: state.titleValidationError != null, + onChanged: (value) { + createRecipeCubit.replaceTitle(value); + if (state.titleValidationError != null) { + createRecipeCubit.validateTitle(); + } + }, + ), + if (state.titleValidationError != null) ...[ + SizedBox(height: 4), + CustomErrorWidget( + error: state.titleValidationError!, + ), + ], + ], + ); }, ), const SizedBox(height: 12), @@ -70,18 +92,57 @@ class _InfoAboutRecipeWidgetState extends State { const SizedBox(height: 12), Text('Описание'), SizedBox(height: 4), - CustomTextFormField( - hintText: 'Расскажите, почему вы выбрали именно этот рецепт? Опишите, каким будет готовое блюдо и в чём его достоинства.', - minLines: 5, - maxLines: 5, - onChanged: (value) { - createRecipeCubit.replaceDescription(value); + BlocBuilder( + buildWhen: (previous, current) => previous.descriptionValidationError != current.descriptionValidationError, + builder: (context, state) { + return Column( + children: [ + CustomTextFormField( + hintText: 'Расскажите, почему вы выбрали именно этот рецепт? Опишите, каким будет готовое блюдо и в чём его достоинства.', + minLines: 5, + maxLines: 5, + hasError: state.descriptionValidationError != null, + onChanged: (value) { + createRecipeCubit.replaceDescription(value); + if (state.descriptionValidationError != null) { + createRecipeCubit.validateDescription(); + } + }, + ), + if (state.descriptionValidationError != null) ...[ + SizedBox(height: 4), + CustomErrorWidget( + error: state.descriptionValidationError!, + ), + ], + ], + ); }, ), const SizedBox(height: 12), Text('Фото обложка'), SizedBox(height: 4), - ImageWidget(), + BlocBuilder( + buildWhen: (previous, current) => previous.imageValidationError != current.imageValidationError || previous.entity.image != current.entity.image, + builder: (context, state) { + return Column( + children: [ + Container( + child: ImageWidget(image: state.entity.image), + decoration: state.imageValidationError != null + ? BoxDecoration(border: Border.all(width: 1, color: appTheme.red), borderRadius: BorderRadius.circular(8)) + : null, + ), + if (state.imageValidationError != null) ...[ + SizedBox(height: 4), + CustomErrorWidget( + error: state.imageValidationError!, + ), + ], + ], + ); + }, + ), const SizedBox(height: 12), Text('Ссылка на видео (если есть)'), SizedBox(height: 4), diff --git a/lib/features/recipes/presentation/widgets/ingredients_widget.dart b/lib/features/recipes/presentation/widgets/ingredients_widget.dart index 5f66ed87..3983bccf 100644 --- a/lib/features/recipes/presentation/widgets/ingredients_widget.dart +++ b/lib/features/recipes/presentation/widgets/ingredients_widget.dart @@ -69,7 +69,7 @@ class _IngredientsWidgetState extends State { color: Colors.grey.shade600, ), title: Text(state.entity.ingredients[index].name), - subtitle: Text('${state.entity.ingredients[index].unit} ${state.entity.ingredients[index].portion == 'gr' ? 'гр' : 'шт'}'), + subtitle: Text('${state.entity.ingredients[index].portion} ${state.entity.ingredients[index].unit == 'gr' ? 'гр' : 'шт'}'), trailing: IconButton( onPressed: () { createRecipeCubit.removeIngredient(index); @@ -163,8 +163,8 @@ class _IngredientsWidgetState extends State { createRecipeCubit.addIngredient( Ingredient( name: nameController.text, - unit: gramsController.text.isEmpty ? pcsController.text : gramsController.text, - portion: gramsController.text.isEmpty ? 'pcs' : 'gr', + unit: gramsController.text.isEmpty ? 'pcs' : 'gr', + portion: gramsController.text.isEmpty ? pcsController.text : gramsController.text, ), ); Navigator.of(context).pop(); diff --git a/lib/features/recipes/presentation/widgets/portion_widget.dart b/lib/features/recipes/presentation/widgets/portion_widget.dart index 875183ff..569f2c7c 100644 --- a/lib/features/recipes/presentation/widgets/portion_widget.dart +++ b/lib/features/recipes/presentation/widgets/portion_widget.dart @@ -1,5 +1,7 @@ import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; +import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart'; import 'package:eatvio_app/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; +import 'package:eatvio_app/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -12,7 +14,6 @@ class PortionWidget extends StatefulWidget { class _PortionWidgetState extends State { late final CreateRecipeCubit createRecipeCubit; - @override void initState() { super.initState(); @@ -21,49 +22,86 @@ class _PortionWidgetState extends State { @override Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( + return BlocBuilder( + buildWhen: (previous, current) => + previous.servingsValidationError != current.servingsValidationError || + previous.energyPerServingValidationError != current.energyPerServingValidationError, + builder: (context, state) { + return Column( mainAxisSize: MainAxisSize.min, children: [ - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Кол-во порций'), - SizedBox(height: 4), - CustomTextFormField( - hintText: '1', - onChanged: (value) { - createRecipeCubit.replacePortionsCount(int.parse(value)); - }, + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Кол-во порций'), + SizedBox(height: 4), + Column( + children: [ + CustomTextFormField( + hintText: '1', + hasError: state.servingsValidationError != null, + textInputType: TextInputType.number, + onChanged: (value) { + createRecipeCubit.replacePortionsCount(int.parse(value)); + if (state.servingsValidationError != null) { + createRecipeCubit.validateServings(); + } + }, + ), + if (state.servingsValidationError != null) ...[ + SizedBox(height: 4), + CustomErrorWidget( + error: state.servingsValidationError!, + ), + ], + ], + ), + ], ), - ], - ), - ), - const SizedBox(width: 8), - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Ккал в одной порции'), - SizedBox(height: 4), - CustomTextFormField( - hintText: '100', - suffixText: 'Ккал', - onChanged: (value) { - createRecipeCubit.replaceEnegryInPortion(double.parse(value)); - }, + ), + const SizedBox(width: 8), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Ккал в одной порции'), + SizedBox(height: 4), + Column( + children: [ + CustomTextFormField( + hintText: '100', + hasError: state.energyPerServingValidationError != null, + suffixText: 'Ккал', + textInputType: TextInputType.numberWithOptions(decimal: true), + onChanged: (value) { + createRecipeCubit.replaceEnegryInPortion(double.parse(value)); + if (state.energyPerServingValidationError != null) { + createRecipeCubit.validateEnergyPerServing(); + } + }, + ), + if (state.energyPerServingValidationError != null) ...[ + SizedBox(height: 4), + CustomErrorWidget( + error: state.energyPerServingValidationError!, + ), + ], + ], + ), + ], ), - ], - ), + ), + ], ), ], - ), - ], + ); + }, ); } } diff --git a/lib/features/recipes/presentation/widgets/time_of_cook_widget.dart b/lib/features/recipes/presentation/widgets/time_of_cook_widget.dart index f329f791..e845a923 100644 --- a/lib/features/recipes/presentation/widgets/time_of_cook_widget.dart +++ b/lib/features/recipes/presentation/widgets/time_of_cook_widget.dart @@ -1,6 +1,8 @@ import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; +import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart'; import 'package:eatvio_app/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; +import 'package:eatvio_app/features/recipes/presentation/bloc/create_recipe_cubit/create_recipe_state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -22,130 +24,190 @@ class _TimeOfCookWidgetState extends State { @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - Text('Общее время приготовления'), - const SizedBox(width: 8), - GestureDetector( - onTap: () { - showDialog( - context: context, - builder: (context) => AlertDialog( - content: Text( - 'Общее время приготовления - это период времени от начала приготовления блюда до подачи его на стол.', - style: TextStyle( - fontWeight: FontWeight.w500, - color: Color(0xFF323F4B), - height: 1.2, - ), - ), - ), - ); - }, - child: Icon( - Icons.help_outline_rounded, - color: appTheme.blueA400, - size: 20, - ), - ), - ], - ), - SizedBox(height: 4), - Row( + return BlocBuilder( + buildWhen: (previous, current) => + previous.activeTimeValidationError != current.activeTimeValidationError || previous.cookingTimeValidationError != current.cookingTimeValidationError, + builder: (context, state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Expanded( - child: CustomTextFormField( - hintText: 'Часов', - onChanged: (value) { - if (createRecipeCubit.state.entity.totalCookTime == null) { - createRecipeCubit.replaceTotalTime('${value.padLeft(2, '0')}:00'); - } else { - createRecipeCubit.replaceTotalTime('${value.padLeft(2, '0')}:${createRecipeCubit.state.entity.totalCookTime!.substring(3, 5)}'); - } - }, - ), - ), - const SizedBox(width: 8), - Expanded( - child: CustomTextFormField( - hintText: 'Минут', - onChanged: (value) { - if (createRecipeCubit.state.entity.totalCookTime == null) { - createRecipeCubit.replaceTotalTime('00:${value.padLeft(2, '0')}'); - } else { - createRecipeCubit.replaceTotalTime('${createRecipeCubit.state.entity.totalCookTime!.substring(0, 2)}:${value.padLeft(2, '0')}'); - } - }, - ), - ), - ], - ), - const SizedBox(height: 12), - Row( - children: [ - Text('Активное время приготовления'), - const SizedBox(width: 8), - GestureDetector( - onTap: () { - showDialog( - context: context, - builder: (context) => AlertDialog( - content: Text( - 'Активное время приготовления - это период времени, требующий активного участия в приготовлении блюда. Оно включает мытье и очистку продуктов, нарезку, шинковку, взбивание, перемешивание, размешивание, перекладывание и т.п.', - style: TextStyle( - fontWeight: FontWeight.w500, - color: Color(0xFF323F4B), - height: 1.2, + Row( + children: [ + Text('Общее время приготовления'), + const SizedBox(width: 8), + GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (context) => AlertDialog( + content: Text( + 'Общее время приготовления - это период времени от начала приготовления блюда до подачи его на стол.', + style: TextStyle( + fontWeight: FontWeight.w500, + color: Color(0xFF323F4B), + height: 1.2, + ), + ), ), - ), + ); + }, + child: Icon( + Icons.help_outline_rounded, + color: appTheme.blueA400, + size: 20, ), - ); - }, - child: Icon( - Icons.help_outline_rounded, - color: appTheme.blueA400, - size: 20, - ), + ), + ], + ), + SizedBox(height: 4), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: Column( + children: [ + CustomTextFormField( + hintText: 'Часов', + hasError: state.cookingTimeValidationError != null, + textInputType: TextInputType.number, + onChanged: (value) { + final hours = int.tryParse(value); + if (hours == null) return; + createRecipeCubit.setTotalCookingHours(hours); + if (state.cookingTimeValidationError != null) { + createRecipeCubit.validateTotalCookTime(); + } + }, + ), + if (state.cookingTimeValidationError != null) ...[ + SizedBox(height: 4), + CustomErrorWidget( + error: state.cookingTimeValidationError!, + ), + ], + ], + ), + ), + const SizedBox(width: 8), + Expanded( + child: Column( + children: [ + CustomTextFormField( + hintText: 'Минут', + hasError: state.cookingTimeValidationError != null, + textInputType: TextInputType.number, + onChanged: (value) { + final minutes = int.tryParse(value); + if (minutes == null) return; + createRecipeCubit.setTotalCookingMinutes(minutes); + if (state.cookingTimeValidationError != null) { + createRecipeCubit.validateTotalCookTime(); + } + }, + ), + if (state.cookingTimeValidationError != null) ...[ + SizedBox(height: 4), + CustomErrorWidget( + error: state.cookingTimeValidationError!, + ), + ], + ], + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Text('Активное время приготовления'), + const SizedBox(width: 8), + GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (context) => AlertDialog( + content: Text( + 'Активное время приготовления - это период времени, требующий активного участия в приготовлении блюда. Оно включает мытье и очистку продуктов, нарезку, шинковку, взбивание, перемешивание, размешивание, перекладывание и т.п.', + style: TextStyle( + fontWeight: FontWeight.w500, + color: Color(0xFF323F4B), + height: 1.2, + ), + ), + ), + ); + }, + child: Icon( + Icons.help_outline_rounded, + color: appTheme.blueA400, + size: 20, + ), + ), + ], + ), + SizedBox(height: 4), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: Column( + children: [ + CustomTextFormField( + hintText: 'Часов', + hasError: state.activeTimeValidationError != null, + textInputType: TextInputType.number, + onChanged: (value) { + final hours = int.tryParse(value); + if (hours == null) return; + createRecipeCubit.setActiveCookingHours(hours); + + if (state.activeTimeValidationError != null) { + createRecipeCubit.validateActiveCookTime(); + } + }, + ), + if (state.activeTimeValidationError != null) ...[ + SizedBox(height: 4), + CustomErrorWidget( + error: state.activeTimeValidationError!, + ), + ], + ], + ), + ), + const SizedBox(width: 8), + Expanded( + child: Column( + children: [ + CustomTextFormField( + hintText: 'Минут', + hasError: state.activeTimeValidationError != null, + textInputType: TextInputType.number, + onChanged: (value) { + final minutes = int.tryParse(value); + if (minutes == null) return; + createRecipeCubit.setActiveCookingMinutes(minutes); + + if (state.activeTimeValidationError != null) { + createRecipeCubit.validateActiveCookTime(); + } + }, + ), + if (state.activeTimeValidationError != null) ...[ + SizedBox(height: 4), + CustomErrorWidget( + error: state.activeTimeValidationError!, + ), + ], + ], + ), + ), + ], ), ], - ), - SizedBox(height: 4), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: CustomTextFormField( - hintText: 'Часов', - onChanged: (value) { - if (createRecipeCubit.state.entity.activeCookTime == null) { - createRecipeCubit.replaceActiveTime('${value.padLeft(2, '0')}:00'); - } else { - createRecipeCubit.replaceActiveTime('${value.padLeft(2, '0')}:${createRecipeCubit.state.entity.activeCookTime!.substring(3, 5)}'); - } - }, - ), - ), - const SizedBox(width: 8), - Expanded( - child: CustomTextFormField( - hintText: 'Минут', - onChanged: (value) { - if (createRecipeCubit.state.entity.activeCookTime == null) { - createRecipeCubit.replaceActiveTime('00:${value.padLeft(2, '0')}'); - } else { - createRecipeCubit.replaceActiveTime('${createRecipeCubit.state.entity.activeCookTime!.substring(0, 2)}:${value.padLeft(2, '0')}'); - } - }, - ), - ), - ], - ), - ], + ); + }, ); } } diff --git a/lib/features/recipes/reporitories/recipe_repository.dart b/lib/features/recipes/reporitories/recipe_repository.dart index b9c49aef..4dec15c0 100644 --- a/lib/features/recipes/reporitories/recipe_repository.dart +++ b/lib/features/recipes/reporitories/recipe_repository.dart @@ -1,8 +1,11 @@ import 'dart:convert'; import 'dart:io'; +import 'package:dio/dio.dart'; +import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; import 'package:eatvio_app/features/recipes/models/recipe_model.dart'; import 'package:eatvio_app/features/recipes/models/recipe_tag_model.dart'; +import 'package:eatvio_app/features/recipes/models/step.dart'; import 'package:eatvio_app/features/recipes/services/recipe_service.dart'; import 'package:eatvio_app/features/recipes/models/product.dart'; @@ -19,58 +22,56 @@ class RecipeRepository { } } - Future publishRecipe(RecipeModel recipe) async { + Future publishRecipe(RecipeModel recipe) async { try { if (recipe.image == null || (recipe.image?.isEmpty ?? true)) { - return null; + return; } - final file = File(recipe.image ?? ''); + final image = await ImageCompressUtility.compressFile(File(recipe.image ?? '')); + final multiPartFile = await MultipartFile.fromFile(image.path); final ingredientsMap = recipe.ingredients?.map((ingredient) { - return { - 'name': ingredient!.name, - 'unit': ingredient.unit, - 'portion': ingredient.portion, - }; + if (ingredient != null) { + return ingredient.toJson(); + } }).toList() ?? []; - final stepsMap = recipe.steps?.map((step) { - return { - 'description': step?.description ?? "", - }; - }).toList() ?? - []; - - // recipe.steps?.map((step) { - // return File(step?.image ?? ""); - // }).toList() ?? - // []; - - if (recipe.type != null && - recipe.description != null && - recipe.totalCookingTime != null && - recipe.activeCookTime != null && - recipe.servings != null && - recipe.energyPerServing != null && - ingredientsMap.isNotEmpty) { - return await recipeService.publishRecipe( - type: recipe.type!, - state: 'active', - title: recipe.title, - tags: recipe.tags, - description: recipe.description!, - video_url: recipe.video_url, - ingredients: ingredientsMap, - steps: stepsMap, - cooking_time: recipe.totalCookingTime!, - active_time: recipe.activeCookTime!, - image: file, - servings: recipe.servings!, - energy_per_serving: recipe.energyPerServing!, - ); + final stepsMap = []; + for (final Step? step in recipe.steps ?? []) { + if (step != null) { + if ((step.image == null || (step.image?.isEmpty ?? true)) && step.description != null) { + stepsMap.add({ + 'description': step.description ?? "", + }); + } else if (step.image != null && (step.image?.isNotEmpty ?? false)) { + final image = await ImageCompressUtility.compressFile(File(step.image ?? '')); + final multipartFile = await MultipartFile.fromFile(image.path); + stepsMap.add({ + 'description': step.description ?? "", + 'image': multipartFile, + }); + } + } } - return null; + final formMap = { + 'type': recipe.type, + 'state': 'active', + 'title': recipe.title, + 'tags': recipe.tags, + 'description': recipe.description, + 'video_url': recipe.video_url, + 'ingredients': ingredientsMap, + 'steps': stepsMap, + 'cooking_time': recipe.totalCookingTime, + 'active_time': recipe.activeCookTime, + 'image': multiPartFile, + 'servings': recipe.servings, + 'energy_per_serving': recipe.energyPerServing, + }; + formMap.removeWhere((k, v) => v == null || (v is String && v.isEmpty)); + final formData = FormData.fromMap(formMap); + await recipeService.publishRecipe(formData: formData); } catch (e) { rethrow; } diff --git a/lib/features/recipes/services/recipe_service.dart b/lib/features/recipes/services/recipe_service.dart index 2e70e730..12e9c78b 100644 --- a/lib/features/recipes/services/recipe_service.dart +++ b/lib/features/recipes/services/recipe_service.dart @@ -1,6 +1,3 @@ -import 'dart:convert'; -import 'dart:io'; - import 'package:dio/dio.dart'; import 'package:eatvio_app/features/recipes/models/recipe_model.dart'; import 'package:eatvio_app/features/recipes/models/recipe_tag_model.dart'; @@ -19,24 +16,10 @@ abstract class RecipeService { @GET('/api/diary/product_nutritional_values/filter') Future>> getInputProduct(@Query('name') String name); - @POST('/api/recipes/create') @MultiPart() - Future publishRecipe({ - @Part() required String type, - @Part() required String state, - @Part() String? title, - @Part() required String description, - @Part() String? video_url, - @Part() List>? ingredients, - @Part() List>? steps, - @Part() List? tags, - @Part() int? cooking_time, - @Part() int? active_time, - @Part() required int servings, - @Part() required int energy_per_serving, - @Part() File? image, - + Future publishRecipe({ + @Body() required FormData formData, }); @POST('/api/recipes/update/{id}') @@ -44,4 +27,4 @@ abstract class RecipeService { @GET('/api/recipes/tags') Future getTags(); -} +} \ No newline at end of file diff --git a/lib/features/recipes/services/recipe_service.g.dart b/lib/features/recipes/services/recipe_service.g.dart index 3699f878..e1e9d280 100644 --- a/lib/features/recipes/services/recipe_service.g.dart +++ b/lib/features/recipes/services/recipe_service.g.dart @@ -79,68 +79,12 @@ class _RecipeService implements RecipeService { } @override - Future publishRecipe({ - required String type, - required String state, - String? title, - required String description, - String? video_url, - List>? ingredients, - List>? steps, - List? tags, - int? cooking_time, - int? active_time, - required int servings, - required int energy_per_serving, - File? image, - }) async { + Future publishRecipe({required FormData formData}) async { final _extra = {}; final queryParameters = {}; - queryParameters.removeWhere((k, v) => v == null); final _headers = {}; - final _data = FormData(); - _data.fields.add(MapEntry('type', type)); - _data.fields.add(MapEntry('state', state)); - if (title != null) { - _data.fields.add(MapEntry('title', title)); - } - _data.fields.add(MapEntry('description', description)); - if (video_url != null) { - _data.fields.add(MapEntry('video_url', video_url)); - } - ingredients?.forEach((i) { - _data.fields.add(MapEntry('ingredients', jsonEncode(i))); - }); - steps?.forEach((i) { - _data.fields.add(MapEntry('steps', jsonEncode(i))); - }); - tags?.forEach((i) { - _data.fields.add(MapEntry('tags', i)); - }); - if (cooking_time != null) { - _data.fields.add(MapEntry('cooking_time', cooking_time.toString())); - } - if (active_time != null) { - _data.fields.add(MapEntry('active_time', active_time.toString())); - } - _data.fields.add(MapEntry('servings', servings.toString())); - _data.fields.add( - MapEntry('energy_per_serving', energy_per_serving.toString()), - ); - if (image != null) { - if (image != null) { - _data.files.add( - MapEntry( - 'image', - MultipartFile.fromFileSync( - image.path, - filename: image.path.split(Platform.pathSeparator).last, - ), - ), - ); - } - } - final _options = _setStreamType( + final _data = formData; + final _options = _setStreamType( Options( method: 'POST', headers: _headers, @@ -155,15 +99,7 @@ class _RecipeService implements RecipeService { ) .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), ); - final _result = await _dio.fetch>(_options); - late RecipeModel _value; - try { - _value = RecipeModel.fromJson(_result.data!); - } on Object catch (e, s) { - errorLogger?.logError(e, s, _options); - rethrow; - } - return _value; + await _dio.fetch(_options); } @override diff --git a/lib/features/settings/presentation/pages/settings_screen.dart b/lib/features/settings/presentation/pages/settings_screen.dart index 15ef9844..bd28f36a 100644 --- a/lib/features/settings/presentation/pages/settings_screen.dart +++ b/lib/features/settings/presentation/pages/settings_screen.dart @@ -56,7 +56,7 @@ class _SettingsScreenState extends State { const SizedBox(height: 12), _aboutEatvio(state), const SizedBox(height: 12), - if (state.profile.roles.contains('consultant')) + if (!state.profile.roles.contains('consultant')) Padding( padding: const EdgeInsets.only(bottom: 8), child: _becomeConsultantButton(state), diff --git a/lib/main.dart b/lib/main.dart index 138ffbfc..5b223f8a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_ import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/notifications_cubit/notifications_cubit.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; +import 'package:eatvio_app/features/internet_connection_checker/internet_connection_checker_screen.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -44,6 +45,7 @@ class MyApp extends StatelessWidget { child: MaterialApp.router( theme: ThemeHelper().themeData(), routerConfig: AppRouter.appRouter, + builder: (context, child) => InternetConnectionCheckerScreen(child: child!), debugShowCheckedModeBanner: false, localizationsDelegates: const [ S.delegate, diff --git a/pubspec.yaml b/pubspec.yaml index c2b1d954..dbff2756 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,6 +60,7 @@ dependencies: flutter_native_splash: ^2.4.4 webview_flutter: ^4.10.0 sentry_flutter: ^8.13.0 + connectivity_plus: ^6.1.2 dev_dependencies: build_runner: ^2.4.14