internet connection handling

This commit is contained in:
Username1548
2025-02-03 15:32:12 +07:00
parent 4242265830
commit 589122581b
30 changed files with 1384 additions and 666 deletions

View File

@@ -54,7 +54,7 @@ class AppRouter {
initialLocation: '/auth-screen',
navigatorKey: navigatorKey,
routes: [
GoRoute(
GoRoute(
path: '/auth-screen',
name: 'authScreen',
redirect: (context, state) async {

View File

@@ -304,7 +304,7 @@ class CreateDietCubit extends Cubit<CreateDietState> {
}
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<CreateDietState> {
}
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;

View File

@@ -25,9 +25,28 @@ class DevToolsScreenContent extends StatefulWidget {
class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
String baseUrlForTest = '';
final List<TestCases> testCases = [Test850task(), GetChatRoomPreviews(), GetChatRoom(), SendMessage(), GetChatMessages(), GetChatRoomIdByUserSlug()];
final List<TestCases> testCases = [
Test850task(),
GetChatRoomPreviews(),
GetChatRoom(),
SendMessage(),
GetChatMessages(),
GetChatRoomIdByUserSlug(),
GetBlogPostList(),
GetRecipes(),
GetDiets(),
GetPlans(),
GetBlogPostComments(),
SendBlogPostComment(),
GetDietsComments(),
SendDietsComment(),
GetPlansComments(),
SendPlansComment(),
GetRecipesComments(),
];
final controller = TextEditingController();
late final List<TestState> testsStates;
@override
void initState() {
super.initState();
@@ -40,7 +59,6 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
@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<DevToolsScreenContent> {
),
],
),
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())),
),
},
],
),
],
),
),
),
],

View File

@@ -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<bool> run({required String baseUrl}) async {
try {
try {
final dio = sl<Dio>();
final chatService = ChatService(dio, baseUrl: baseUrl);
@@ -159,7 +161,7 @@ class SendMessage implements TestCases {
@override
Future<bool> run({required String baseUrl}) async {
try {
try {
final dio = sl<Dio>();
final chatService = ChatService(dio, baseUrl: baseUrl);
@@ -178,7 +180,7 @@ class GetChatMessages implements TestCases {
@override
Future<bool> run({required String baseUrl}) async {
try {
try {
final dio = sl<Dio>();
final chatService = ChatService(dio, baseUrl: baseUrl);
@@ -197,7 +199,7 @@ class GetChatRoomIdByUserSlug implements TestCases {
@override
Future<bool> run({required String baseUrl}) async {
try {
try {
final dio = sl<Dio>();
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<bool> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
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<bool> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
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<bool> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
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<bool> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
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<bool> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
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<bool> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
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<bool> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
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<bool> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
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<bool> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
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<bool> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
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<bool> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
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;
}
}
}

View File

@@ -11,14 +11,14 @@ class Recipes with _$Recipes {
required String image,
required String state,
@JsonKey(name: 'cooking_time') required int cookingTime,
required List<Ingredient> ingredients,
required List<Ingredient>? ingredients,
required String description,
required List<Step> steps,
@JsonKey(name: 'video_url') required String videoUrl,
required List<Step>? 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,

View File

@@ -25,17 +25,17 @@ mixin _$Recipes {
String get state => throw _privateConstructorUsedError;
@JsonKey(name: 'cooking_time')
int get cookingTime => throw _privateConstructorUsedError;
List<Ingredient> get ingredients => throw _privateConstructorUsedError;
List<Ingredient>? get ingredients => throw _privateConstructorUsedError;
String get description => throw _privateConstructorUsedError;
List<Step> get steps => throw _privateConstructorUsedError;
List<Step>? 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<Ingredient> ingredients,
List<Ingredient>? ingredients,
String description,
List<Step> steps,
@JsonKey(name: 'video_url') String videoUrl,
List<Step>? 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<Ingredient>,
as List<Ingredient>?,
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<Step>,
videoUrl: null == videoUrl
as List<Step>?,
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<Ingredient> ingredients,
List<Ingredient>? ingredients,
String description,
List<Step> steps,
@JsonKey(name: 'video_url') String videoUrl,
List<Step>? 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<Ingredient>,
as List<Ingredient>?,
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<Step>,
videoUrl: null == videoUrl
as List<Step>?,
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<Ingredient> ingredients,
required final List<Ingredient>? ingredients,
required this.description,
required final List<Step> steps,
required final List<Step>? 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<Ingredient> _ingredients;
final List<Ingredient>? _ingredients;
@override
List<Ingredient> get ingredients {
List<Ingredient>? 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<Step> _steps;
final List<Step>? _steps;
@override
List<Step> get steps {
List<Step>? 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<Ingredient> ingredients,
required final List<Ingredient>? ingredients,
required final String description,
required final List<Step> steps,
@JsonKey(name: 'video_url') required final String videoUrl,
required final List<Step>? 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<Ingredient> get ingredients;
List<Ingredient>? get ingredients;
@override
String get description;
@override
List<Step> get steps;
List<Step>? 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

View File

@@ -12,18 +12,18 @@ _$RecipesImpl _$$RecipesImplFromJson(Map<String, dynamic> json) =>
image: json['image'] as String,
state: json['state'] as String,
cookingTime: (json['cooking_time'] as num).toInt(),
ingredients: (json['ingredients'] as List<dynamic>)
.map((e) => Ingredient.fromJson(e as Map<String, dynamic>))
ingredients: (json['ingredients'] as List<dynamic>?)
?.map((e) => Ingredient.fromJson(e as Map<String, dynamic>))
.toList(),
description: json['description'] as String,
steps: (json['steps'] as List<dynamic>)
.map((e) => Step.fromJson(e as Map<String, dynamic>))
steps: (json['steps'] as List<dynamic>?)
?.map((e) => Step.fromJson(e as Map<String, dynamic>))
.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,

View File

@@ -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<RecipesState> {
final FeedRepository feedRepository;
@@ -38,7 +39,8 @@ class RecipesCubit extends Cubit<RecipesState> {
),
);
lastPage = currentPage;
} catch (e) {
} catch (e, st) {
log(e.toString(), stackTrace: st);
emit(RecipesState.error(errorMessage: e.toString()));
}
}

View File

@@ -47,4 +47,4 @@ class Recipes extends StatelessWidget {
),
);
}
}
}

View File

@@ -61,7 +61,7 @@ class _RecipesWidgetState extends State<RecipesWidget> {
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<RecipesWidget> {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Text(
widget.recipes.title,
widget.recipes.title.toString(),
style: TextStyle(
fontWeight: FontWeight.w600,
color: Colors.black87,

View File

@@ -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<InternetConnectionCheckerScreen> createState() => _InternetConnectionCheckerScreenState();
}
class _InternetConnectionCheckerScreenState extends State<InternetConnectionCheckerScreen> {
bool _hasInternet = true;
final Connectivity _connectivity = Connectivity();
late StreamSubscription<List<ConnectivityResult>> _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<void> _checkConnection() async {
late List<ConnectivityResult> result;
try {
result = await _connectivity.checkConnectivity();
} on PlatformException catch (e) {
log('Couldn\'t check connectivity status', error: e);
return;
}
return _updateAccessStatus(result);
}
Future<void> _updateAccessStatus(List<ConnectivityResult> result) async {
final hasInternet = await _checkInternetAccess(result);
setState(() {
_hasInternet = hasInternet;
});
}
Future<bool> _checkInternetAccess(List<ConnectivityResult> 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<void> 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(),
],
),
);
}
}

View File

@@ -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<PlansState> {
try {
emit(PlansState.loaded(plansList: await plansRepository.getPlansBySlug(slug)));
} catch (e) {
print(e);
log(e.toString());
emit(PlansState.error(errorMessage: e.toString()));
}
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -12,5 +12,5 @@ class Ingredient with _$Ingredient {
required String portion,
}) = _Ingredient;
factory Ingredient.fromJson(Map<String, Object?> json) => _$IngredientFromJson(json);
factory Ingredient.fromJson(Map<String, Object?> json) => _$IngredientFromJson(json);
}

View File

@@ -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<CreateRecipeState> {
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<CreateRecipeState> {
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<CreateRecipeState> {
removeStep(StepEntity step) {
List<StepEntity> 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<CreateRecipeState> {
List<Ingredient> 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<Ingredient> 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<CreateRecipeState> {
List<String> 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<StepEntity> 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<StepEntity> 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<void> publishRecipe() async {
Future<bool> 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;
}
}
}

View File

@@ -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;
}

View File

@@ -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.

View File

@@ -212,7 +212,7 @@ class _AboutRecipesScreenState extends State<AboutRecipesScreen> {
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<AboutRecipesScreen> {
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<AboutRecipesScreen> {
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<AboutRecipesScreen> {
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<AboutRecipesScreen> {
),
),
),
itemCount: widget.recipes.steps.length,
itemCount: widget.recipes.steps?.length ?? 0,
),
SizedBox(height: 8),
Padding(

View File

@@ -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<RecipeCreateScreen> {
InfoAboutRecipeWidget(),
IngredientsWidget(),
StepsWidget(),
Builder(
builder: (context) {
return InkWell(
onTap: () {
context.read<CreateRecipeCubit>().publishRecipe();
},
borderRadius: BorderRadius.circular(20),
child: Ink(
width: MediaQuery.of(context).size.width - 32,
decoration: BoxDecoration(
color: appTheme.blueA400,
BlocBuilder<CreateRecipeCubit, CreateRecipeState>(
builder: (context, state) {
return Builder(
builder: (context) {
return InkWell(
onTap: () async {
if (context.read<CreateRecipeCubit>().validateAll()) {
final isSuccess = await context.read<CreateRecipeCubit>().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,
),
),
),
),
),
),
);
},
);
},
),

View File

@@ -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<ImageWidget> createState() => _ImageWidgetState();
}
class _ImageWidgetState extends State<ImageWidget> {
XFile? image;
late final CreateRecipeCubit createRecipeCubit;
@override
@@ -30,7 +29,7 @@ class _ImageWidgetState extends State<ImageWidget> {
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<ImageWidget> {
ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Image.file(
File(image!.path),
File(widget.image!.path),
),
),
Positioned(
@@ -48,9 +47,6 @@ class _ImageWidgetState extends State<ImageWidget> {
top: 16,
child: GestureDetector(
onTap: () {
setState(() {
image = null;
});
createRecipeCubit.replaceImage(null);
},
child: Container(
@@ -76,10 +72,11 @@ class _ImageWidgetState extends State<ImageWidget> {
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<ImageWidget> {
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();
},

View File

@@ -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<InfoAboutRecipeWidget> {
children: [
Text('Название'),
SizedBox(height: 4),
CustomTextFormField(
hintText: 'Например: тар-тар из говядины',
onChanged: (value) {
createRecipeCubit.replaceTitle(value);
BlocBuilder<CreateRecipeCubit, CreateRecipeState>(
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<InfoAboutRecipeWidget> {
const SizedBox(height: 12),
Text('Описание'),
SizedBox(height: 4),
CustomTextFormField(
hintText: 'Расскажите, почему вы выбрали именно этот рецепт? Опишите, каким будет готовое блюдо и в чём его достоинства.',
minLines: 5,
maxLines: 5,
onChanged: (value) {
createRecipeCubit.replaceDescription(value);
BlocBuilder<CreateRecipeCubit, CreateRecipeState>(
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<CreateRecipeCubit, CreateRecipeState>(
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),

View File

@@ -69,7 +69,7 @@ class _IngredientsWidgetState extends State<IngredientsWidget> {
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<IngredientsWidget> {
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();

View File

@@ -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<PortionWidget> {
late final CreateRecipeCubit createRecipeCubit;
@override
void initState() {
super.initState();
@@ -21,49 +22,86 @@ class _PortionWidgetState extends State<PortionWidget> {
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
return BlocBuilder<CreateRecipeCubit, CreateRecipeState>(
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!,
),
],
],
),
],
),
],
),
),
],
),
],
),
],
);
},
);
}
}

View File

@@ -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<TimeOfCookWidget> {
@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<CreateRecipeCubit, CreateRecipeState>(
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')}');
}
},
),
),
],
),
],
);
},
);
}
}

View File

@@ -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<RecipeModel?> publishRecipe(RecipeModel recipe) async {
Future<void> 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;
}

View File

@@ -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<Map<String, List<Product>>> getInputProduct(@Query('name') String name);
@POST('/api/recipes/create')
@MultiPart()
Future<RecipeModel> publishRecipe({
@Part() required String type,
@Part() required String state,
@Part() String? title,
@Part() required String description,
@Part() String? video_url,
@Part() List<Map<String, Object>>? ingredients,
@Part() List<Map<String, Object>>? steps,
@Part() List<String>? tags,
@Part() int? cooking_time,
@Part() int? active_time,
@Part() required int servings,
@Part() required int energy_per_serving,
@Part() File? image,
Future<void> publishRecipe({
@Body() required FormData formData,
});
@POST('/api/recipes/update/{id}')
@@ -44,4 +27,4 @@ abstract class RecipeService {
@GET('/api/recipes/tags')
Future<RecipeTagModel> getTags();
}
}

View File

@@ -92,91 +92,12 @@ class _RecipeService implements RecipeService {
}
@override
Future<RecipeModel> publishRecipe({
required String type,
required String state,
String? title,
required String description,
String? video_url,
List<Map<String, Object>>? ingredients,
List<Map<String, Object>>? steps,
List<String>? tags,
int? cooking_time,
int? active_time,
required int servings,
required int energy_per_serving,
File? image,
}) async {
Future<void> publishRecipe({required FormData formData}) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
queryParameters.removeWhere((k, v) => v == null);
final _headers = <String, dynamic>{};
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<RecipeModel>(Options(
final _data = formData;
final _options = _setStreamType<void>(Options(
method: 'POST',
headers: _headers,
extra: _extra,
@@ -193,15 +114,7 @@ class _RecipeService implements RecipeService {
_dio.options.baseUrl,
baseUrl,
)));
final _result = await _dio.fetch<Map<String, dynamic>>(_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<void>(_options);
}
@override

View File

@@ -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<NewMessagesCubit>(create: (context) => sl<NewMessagesCubit>()),
BlocProvider<NotificationsCubit>(create: (context) => sl<NotificationsCubit>()),
BlocProvider<FeedCubit>(create: (context) => sl<FeedCubit>()..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,

View File

@@ -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