diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 39daded4..b7239f07 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -54,7 +54,7 @@ class AppRouter { initialLocation: '/auth-screen', navigatorKey: navigatorKey, routes: [ - GoRoute( + GoRoute( path: '/auth-screen', name: 'authScreen', redirect: (context, state) async { 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..67107cea 100644 --- a/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart +++ b/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart @@ -304,7 +304,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 +312,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; diff --git a/lib/features/dev_tools/presentation/dev_tools_screen.dart b/lib/features/dev_tools/presentation/dev_tools_screen.dart index ea6c3817..e2e5ab48 100644 --- a/lib/features/dev_tools/presentation/dev_tools_screen.dart +++ b/lib/features/dev_tools/presentation/dev_tools_screen.dart @@ -25,9 +25,28 @@ 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; + @override void initState() { super.initState(); @@ -40,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), @@ -73,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/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/recipes_cubit/recipes_cubit.dart b/lib/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart index d3a6a47e..60fdb41a 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/widgets/recipes.dart b/lib/features/feed/presentation/widgets/recipes.dart index 9679e81c..0f188c3b 100644 --- a/lib/features/feed/presentation/widgets/recipes.dart +++ b/lib/features/feed/presentation/widgets/recipes.dart @@ -47,4 +47,4 @@ class Recipes 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 61936531..f85d7fcc 100644 --- a/lib/features/feed/presentation/widgets/recipes_widget.dart +++ b/lib/features/feed/presentation/widgets/recipes_widget.dart @@ -61,7 +61,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 ?? 0}' + ' этапов'), SizedBox(width: 8), _iconButton('assets/icons/calories.svg', '139.9 ккал'), ], @@ -73,7 +73,7 @@ class _RecipesWidgetState extends State { Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: Text( - widget.recipes.title, + widget.recipes.title.toString(), style: TextStyle( fontWeight: FontWeight.w600, color: Colors.black87, 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/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/recipes/entities/recipe_create_entity.dart b/lib/features/recipes/entities/recipe_create_entity.dart index 75e7c783..92f7d642 100644 --- a/lib/features/recipes/entities/recipe_create_entity.dart +++ b/lib/features/recipes/entities/recipe_create_entity.dart @@ -105,15 +105,22 @@ class RecipeCreateEntity with _$RecipeCreateEntity { 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..57535cc7 100644 --- a/lib/features/recipes/entities/recipe_create_entity.freezed.dart +++ b/lib/features/recipes/entities/recipe_create_entity.freezed.dart @@ -506,8 +506,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 +541,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..00d4961b 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,145 @@ 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, - ), - ), - ); + emit(state.copyWith.entity(totalCookTime: time)); } replaceActiveTime(String time) { - emit( - CreateRecipeState( - state.entity.copyWith( - activeCookTime: time, - ), - ), - ); + emit(state.copyWith.entity(activeCookTime: time)); } 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.activeCookTime == null || state.entity.activeCookTime == '00:00') { + emit(state.copyWith(activeTimeValidationError: 'Поле не заполнено')); + return false; + } else { + emit(state.copyWith(activeTimeValidationError: null)); + return true; + } + } + + bool validateTotalCookTime() { + if (state.entity.totalCookTime == null || state.entity.totalCookTime == '00:00') { + 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..1c4e0306 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,198 @@ 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) { + 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)}'); + } + 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) { + 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')}'); + } + 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) { + 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)}'); + } + + 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) { + 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')}'); + } + + 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 396070b1..f9882b29 100644 --- a/lib/features/recipes/services/recipe_service.g.dart +++ b/lib/features/recipes/services/recipe_service.g.dart @@ -92,91 +92,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(Options( + final _data = formData; + final _options = _setStreamType(Options( method: 'POST', headers: _headers, extra: _extra, @@ -193,15 +114,7 @@ class _RecipeService implements RecipeService { _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/main.dart b/lib/main.dart index 62b91d0e..ea6fb543 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'; @@ -26,7 +27,7 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MultiBlocProvider( - providers: [ + providers: [ BlocProvider(create: (context) => sl()), BlocProvider(create: (context) => sl()), BlocProvider(create: (context) => sl()..loadFeedBlogPostList()), @@ -37,6 +38,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 89ad2dd9..5ea11531 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -58,6 +58,7 @@ dependencies: dotted_border: ^2.1.0 http: ^1.2.2 flutter_native_splash: ^2.4.4 + connectivity_plus: ^6.1.2 dev_dependencies: build_runner: ^2.4.14