diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index 30333964..9d810d8e 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -16,7 +16,7 @@ jobs: run: | security unlock-keychain -p "$KEYCHAIN_PASSWORD" /Users/gitea_runner_ci/Library/Keychains/login.keychain-db env: - KEYCHAIN_PASSWORD: EeChahboo9Ei + KEYCHAIN_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }} - name: Flutter build run: | @@ -30,8 +30,7 @@ jobs: env: LANG: en_US.UTF-8 LC_ALL: en_US.UTF-8 - MATCH_PASSWORD: EeChahboo9Ei + MATCH_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }} MATCH_GIT_URL: http://gitea.office.local:3000/mso7nueh/fastlane FASTLANE_USER: eatvio2023@icloud.com - FASTLANE_PASSWORD: EeChahboo9Ei - FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: iqsm-pfxm-bpmg-nkry \ No newline at end of file + FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ vars.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }} \ No newline at end of file diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 33e7cf8d..80825645 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -16,6 +16,20 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Create key.properties file + run: | + cat > android/key.properties < android/app/upload-keystore.jks + - name: Cache Android SDK id: cache-sdk uses: "https://gitea.com/actions/cache@v4" @@ -104,19 +118,6 @@ jobs: - name: Test run: flutter test - - name: Create key.properties file - run: | - cat > android/key.properties < android/app/my-release-key.jks - - name: Build apk run: flutter build apk --release diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 2ebee285..485b5cb9 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -388,7 +388,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 15; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -538,7 +538,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 15; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -576,7 +576,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 15; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index eada9fab..93558f26 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 9 + 15 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/ios/build/ios/Pods.build/Release-iphonesimulator/FMDB.build/dgph b/ios/build/ios/Pods.build/Release-iphonesimulator/FMDB.build/dgph deleted file mode 100644 index 01c56549..00000000 Binary files a/ios/build/ios/Pods.build/Release-iphonesimulator/FMDB.build/dgph and /dev/null differ diff --git a/ios/build/ios/Pods.build/Release-iphonesimulator/Flutter.build/dgph b/ios/build/ios/Pods.build/Release-iphonesimulator/Flutter.build/dgph deleted file mode 100644 index 01c56549..00000000 Binary files a/ios/build/ios/Pods.build/Release-iphonesimulator/Flutter.build/dgph and /dev/null differ diff --git a/ios/build/ios/Pods.build/Release-iphonesimulator/Pods-Runner.build/dgph b/ios/build/ios/Pods.build/Release-iphonesimulator/Pods-Runner.build/dgph deleted file mode 100644 index 01c56549..00000000 Binary files a/ios/build/ios/Pods.build/Release-iphonesimulator/Pods-Runner.build/dgph and /dev/null differ diff --git a/ios/build/ios/Pods.build/Release-iphonesimulator/flutter_keyboard_visibility.build/dgph b/ios/build/ios/Pods.build/Release-iphonesimulator/flutter_keyboard_visibility.build/dgph deleted file mode 100644 index 01c56549..00000000 Binary files a/ios/build/ios/Pods.build/Release-iphonesimulator/flutter_keyboard_visibility.build/dgph and /dev/null differ diff --git a/ios/build/ios/Pods.build/Release-iphonesimulator/image_picker_ios-image_picker_ios_privacy.build/dgph b/ios/build/ios/Pods.build/Release-iphonesimulator/image_picker_ios-image_picker_ios_privacy.build/dgph deleted file mode 100644 index 01c56549..00000000 Binary files a/ios/build/ios/Pods.build/Release-iphonesimulator/image_picker_ios-image_picker_ios_privacy.build/dgph and /dev/null differ diff --git a/ios/build/ios/Pods.build/Release-iphonesimulator/image_picker_ios.build/dgph b/ios/build/ios/Pods.build/Release-iphonesimulator/image_picker_ios.build/dgph deleted file mode 100644 index 01c56549..00000000 Binary files a/ios/build/ios/Pods.build/Release-iphonesimulator/image_picker_ios.build/dgph and /dev/null differ diff --git a/ios/build/ios/Pods.build/Release-iphonesimulator/isar_flutter_libs.build/dgph b/ios/build/ios/Pods.build/Release-iphonesimulator/isar_flutter_libs.build/dgph deleted file mode 100644 index 01c56549..00000000 Binary files a/ios/build/ios/Pods.build/Release-iphonesimulator/isar_flutter_libs.build/dgph and /dev/null differ diff --git a/ios/build/ios/Pods.build/Release-iphonesimulator/location.build/dgph b/ios/build/ios/Pods.build/Release-iphonesimulator/location.build/dgph deleted file mode 100644 index 01c56549..00000000 Binary files a/ios/build/ios/Pods.build/Release-iphonesimulator/location.build/dgph and /dev/null differ diff --git a/ios/build/ios/Pods.build/Release-iphonesimulator/path_provider_foundation-path_provider_foundation_privacy.build/dgph b/ios/build/ios/Pods.build/Release-iphonesimulator/path_provider_foundation-path_provider_foundation_privacy.build/dgph deleted file mode 100644 index 01c56549..00000000 Binary files a/ios/build/ios/Pods.build/Release-iphonesimulator/path_provider_foundation-path_provider_foundation_privacy.build/dgph and /dev/null differ diff --git a/ios/build/ios/Pods.build/Release-iphonesimulator/path_provider_foundation.build/dgph b/ios/build/ios/Pods.build/Release-iphonesimulator/path_provider_foundation.build/dgph deleted file mode 100644 index 01c56549..00000000 Binary files a/ios/build/ios/Pods.build/Release-iphonesimulator/path_provider_foundation.build/dgph and /dev/null differ diff --git a/ios/build/ios/Pods.build/Release-iphonesimulator/pointer_interceptor_ios-pointer_interceptor_ios_privacy.build/dgph b/ios/build/ios/Pods.build/Release-iphonesimulator/pointer_interceptor_ios-pointer_interceptor_ios_privacy.build/dgph deleted file mode 100644 index 01c56549..00000000 Binary files a/ios/build/ios/Pods.build/Release-iphonesimulator/pointer_interceptor_ios-pointer_interceptor_ios_privacy.build/dgph and /dev/null differ diff --git a/ios/build/ios/Pods.build/Release-iphonesimulator/pointer_interceptor_ios.build/dgph b/ios/build/ios/Pods.build/Release-iphonesimulator/pointer_interceptor_ios.build/dgph deleted file mode 100644 index 01c56549..00000000 Binary files a/ios/build/ios/Pods.build/Release-iphonesimulator/pointer_interceptor_ios.build/dgph and /dev/null differ diff --git a/ios/build/ios/Pods.build/Release-iphonesimulator/shared_preferences_foundation-shared_preferences_foundation_privacy.build/dgph b/ios/build/ios/Pods.build/Release-iphonesimulator/shared_preferences_foundation-shared_preferences_foundation_privacy.build/dgph deleted file mode 100644 index 01c56549..00000000 Binary files a/ios/build/ios/Pods.build/Release-iphonesimulator/shared_preferences_foundation-shared_preferences_foundation_privacy.build/dgph and /dev/null differ diff --git a/ios/build/ios/Pods.build/Release-iphonesimulator/shared_preferences_foundation.build/dgph b/ios/build/ios/Pods.build/Release-iphonesimulator/shared_preferences_foundation.build/dgph deleted file mode 100644 index 01c56549..00000000 Binary files a/ios/build/ios/Pods.build/Release-iphonesimulator/shared_preferences_foundation.build/dgph and /dev/null differ diff --git a/ios/build/ios/Pods.build/Release-iphonesimulator/sqflite-sqflite_ios_privacy.build/dgph b/ios/build/ios/Pods.build/Release-iphonesimulator/sqflite-sqflite_ios_privacy.build/dgph deleted file mode 100644 index 01c56549..00000000 Binary files a/ios/build/ios/Pods.build/Release-iphonesimulator/sqflite-sqflite_ios_privacy.build/dgph and /dev/null differ diff --git a/ios/build/ios/Pods.build/Release-iphonesimulator/sqflite.build/dgph b/ios/build/ios/Pods.build/Release-iphonesimulator/sqflite.build/dgph deleted file mode 100644 index 01c56549..00000000 Binary files a/ios/build/ios/Pods.build/Release-iphonesimulator/sqflite.build/dgph and /dev/null differ diff --git a/ios/build/ios/Pods.build/Release-iphonesimulator/url_launcher_ios-url_launcher_ios_privacy.build/dgph b/ios/build/ios/Pods.build/Release-iphonesimulator/url_launcher_ios-url_launcher_ios_privacy.build/dgph deleted file mode 100644 index 01c56549..00000000 Binary files a/ios/build/ios/Pods.build/Release-iphonesimulator/url_launcher_ios-url_launcher_ios_privacy.build/dgph and /dev/null differ diff --git a/ios/build/ios/Pods.build/Release-iphonesimulator/url_launcher_ios.build/dgph b/ios/build/ios/Pods.build/Release-iphonesimulator/url_launcher_ios.build/dgph deleted file mode 100644 index 01c56549..00000000 Binary files a/ios/build/ios/Pods.build/Release-iphonesimulator/url_launcher_ios.build/dgph and /dev/null differ diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 63ec1094..1c7210ac 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -138,7 +138,7 @@ class AppRouter { final sharedPreferences = await SharedPreferences.getInstance(); final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed'); if (welcomeScreenWasShowed == true) { - return '/my-profile-screen'; + return '/feed-screen'; } else { return null; } @@ -196,7 +196,13 @@ class AppRouter { return AboutRecipeScreen(recipe: data['recipe'] as Recipe, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); }, ), - GoRoute(path: RecipeCreateScreen.path, name: RecipeCreateScreen.name, builder: (context, state) => const RecipeCreateScreen()), + GoRoute( + path: RecipeCreateScreen.path, + name: RecipeCreateScreen.name, + builder: (context, state) => RecipeCreateScreen( + recipe: state.extra != null ? state.extra as Recipe : null, + ), + ), GoRoute(path: '/profile-screen', name: 'profileScreen', builder: (context, state) => ProfileScreen(author: state.extra as Author)), GoRoute( path: '/subs-screen/:slug', @@ -224,7 +230,10 @@ class AppRouter { final data = state.extra as Map; final chatId = data['chatId'] as String; final sendInitialMessage = data['sendInitialMessage'] as bool?; - return ChatScreen(chatId: chatId, sendInitialMessage: sendInitialMessage,); + return ChatScreen( + chatId: chatId, + sendInitialMessage: sendInitialMessage, + ); }, ), GoRoute( diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 49f8e6db..15bb5270 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -107,7 +107,7 @@ Future init() async { sl.registerFactory(() => ChatRoomCubit(cardsRepository: sl(), chatRepository: sl(), wsManager: sl())); sl.registerFactory(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl(), aboutRecipeRepository: sl())); sl.registerFactory(() => CalendarCubit(calendarRepository: sl())); - sl.registerFactory(() => ProfileCubit(profileRepository: sl(), chatRepository: sl())); + sl.registerFactory(() => ProfileCubit(profileRepository: sl(), chatRepository: sl(), consultantRepository: sl())); sl.registerFactory(() => profile_plans.PlansCubit(plansRepository: sl())); sl.registerFactory(() => DietsCubit(feedRepository: sl(), dietsRepository: sl())); sl.registerFactory(() => CommentsCubit(feedRepository: sl())); diff --git a/lib/core/widgets/custom_popup.dart b/lib/core/widgets/custom_popup.dart index a4167f11..837c165b 100644 --- a/lib/core/widgets/custom_popup.dart +++ b/lib/core/widgets/custom_popup.dart @@ -6,9 +6,10 @@ import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/core/widgets/show_error_dialog.dart'; import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart'; import 'package:eatvio_app/features/complaining/presentation/complaining_bottom_sheet.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart'; import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; +import 'package:eatvio_app/features/feed/models/recipes_model.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart'; import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart'; import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart'; @@ -26,6 +27,7 @@ class CustomPopup extends StatelessWidget { final String? slug; final int? id; final FeedBlogPostModel? feedBlogPostModel; + final Recipe? recipe; const CustomPopup({ super.key, @@ -35,6 +37,7 @@ class CustomPopup extends StatelessWidget { this.id, this.slug, this.feedBlogPostModel, + this.recipe, }); @override @@ -94,6 +97,8 @@ class CustomPopup extends StatelessWidget { addItem('assets/icons/edit_paint.svg', 'Редактировать', () { if (complainedContentType == ComplainedContent.posts) { GoRouter.of(context).pushNamed('blogPostCreateScreen', extra: feedBlogPostModel); + } else if (complainedContentType == ComplainedContent.recipe) { + GoRouter.of(context).pushNamed(RecipeCreateScreen.name, extra: recipe); } else { showInfoToast('В разработке'); return; @@ -103,8 +108,6 @@ class CustomPopup extends StatelessWidget { addItem('assets/icons/remove_from_public.svg', 'Снять с публикации', () async { if (complainedContentType == ComplainedContent.diet) { final result = await sl().unpublishDiet(id!); - if (!context.mounted) return; - context.read().loadFeedBlogPostList(); if (!result) { showErrorDialog(context, title: S.current.could_not_delete_blog_post); return; @@ -112,8 +115,6 @@ class CustomPopup extends StatelessWidget { showSuccessToast('Диета перенесена в архив, обновите ленту'); } else if (complainedContentType == ComplainedContent.plan) { final result = await sl().unpublish(slug!); - if (!context.mounted) return; - context.read().loadFeedBlogPostList(); if (!result) { showErrorDialog(context, title: S.current.could_not_delete_blog_post); return; @@ -125,8 +126,6 @@ class CustomPopup extends StatelessWidget { addItem('assets/icons/red_trash.svg', 'Удалить', () async { if (complainedContentType == ComplainedContent.posts) { final result = await sl().deleteBlogPost(id!); - if (!context.mounted) return; - context.read().loadFeedBlogPostList(); if (!result) { showErrorDialog(context, title: S.current.could_not_delete_blog_post); return; @@ -134,8 +133,6 @@ class CustomPopup extends StatelessWidget { showSuccessToast('Пост удален, обновите ленту'); } else if (complainedContentType == ComplainedContent.recipe) { final result = await sl().deleteRecipe(slug!); - if (!context.mounted) return; - context.read().loadFeedBlogPostList(); if (!result) { showErrorDialog(context, title: S.current.could_not_delete_blog_post); return; @@ -143,8 +140,6 @@ class CustomPopup extends StatelessWidget { showSuccessToast('Рецепт удален, обновите ленту'); } else if (complainedContentType == ComplainedContent.diet) { final result = await sl().deleteDiet(id!); - if (!context.mounted) return; - context.read().loadFeedBlogPostList(); if (!result) { showErrorDialog(context, title: S.current.could_not_delete_blog_post); return; @@ -152,8 +147,6 @@ class CustomPopup extends StatelessWidget { showSuccessToast('Диета удалена, обновите ленту'); } else if (complainedContentType == ComplainedContent.plan) { final result = await sl().deletePlan(slug!); - if (!context.mounted) return; - context.read().loadFeedBlogPostList(); if (!result) { showErrorDialog(context, title: S.current.could_not_delete_blog_post); return; diff --git a/lib/features/auth/presentation/bloc/auth_cubit.dart b/lib/features/auth/presentation/bloc/auth_cubit.dart index f96f0261..03d3c896 100644 --- a/lib/features/auth/presentation/bloc/auth_cubit.dart +++ b/lib/features/auth/presentation/bloc/auth_cubit.dart @@ -30,7 +30,7 @@ class AuthCubit extends Cubit { ); showSuccessToast('Успешный вход'); await TokenStorage.saveTokenInStorage(token); - AppRouter.navigatorKey.currentContext?.go('/my-profile-screen'); + AppRouter.navigatorKey.currentContext?.go('/feed-screen'); return true; } catch (e) { log(e.toString()); diff --git a/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart b/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart index c0b5e16a..52b9b838 100644 --- a/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart +++ b/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart @@ -27,7 +27,7 @@ class BottomNavigationBarWidget extends StatefulWidget { } class _BottomNavigationBarWidgetState extends State { - int currentBottomNavBarIndex = 0; + int currentBottomNavBarIndex = 1; late final List pages; @override diff --git a/lib/features/consultant/become_consultant/models/consultation_request_model.dart b/lib/features/consultant/become_consultant/models/consultation_request_model.dart new file mode 100644 index 00000000..aa7377b1 --- /dev/null +++ b/lib/features/consultant/become_consultant/models/consultation_request_model.dart @@ -0,0 +1,26 @@ +import 'package:eatvio_app/features/profile/models/profile_model.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'consultation_request_model.freezed.dart'; + +part 'consultation_request_model.g.dart'; + +@freezed +class ConsultationRequestModel with _$ConsultationRequestModel { + const factory ConsultationRequestModel({ + @JsonKey(name: 'rating_place') required int ratingPlace, + @JsonKey(name: 'education_resource') required EducationModel educationModel, + @JsonKey(name: 'cert_photos') required List photos, + }) = _ConsultationRequestModel; + + factory ConsultationRequestModel.fromJson(Map json) => _$ConsultationRequestModelFromJson(json); +} + +@freezed +class CertPhotosModel with _$CertPhotosModel { + const factory CertPhotosModel({ + required String url, + }) = _CertPhotosModel; + + factory CertPhotosModel.fromJson(Map json) => _$CertPhotosModelFromJson(json); +} diff --git a/lib/features/consultant/become_consultant/repositories/consultant_repository.dart b/lib/features/consultant/become_consultant/repositories/consultant_repository.dart index 3abdc799..6af88cb5 100644 --- a/lib/features/consultant/become_consultant/repositories/consultant_repository.dart +++ b/lib/features/consultant/become_consultant/repositories/consultant_repository.dart @@ -1,5 +1,6 @@ import 'dart:io'; import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_profile_model.dart'; +import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart'; import 'package:eatvio_app/features/consultant/become_consultant/services/consultant_service.dart'; import 'package:http_parser/http_parser.dart'; import 'package:dio/dio.dart'; @@ -33,7 +34,9 @@ class ConsultantRepository { final fileBytes = await file.readAsBytes(); final multipartFile = MultipartFile.fromBytes( fileBytes, - filename: file.path.split('/').last, + filename: file.path + .split('/') + .last, contentType: MediaType('application', 'octet-stream'), ); multipartFiles.add(multipartFile); @@ -46,4 +49,12 @@ class ConsultantRepository { rethrow; } } + + Future getConsultantRequestStatus(String slug) async { + try { + return await consultantService.getConsultantRequestStatus(slug: slug); + } catch (e) { + rethrow; + } + } } diff --git a/lib/features/consultant/become_consultant/services/consultant_service.dart b/lib/features/consultant/become_consultant/services/consultant_service.dart index 4479122f..cc284993 100644 --- a/lib/features/consultant/become_consultant/services/consultant_service.dart +++ b/lib/features/consultant/become_consultant/services/consultant_service.dart @@ -1,5 +1,6 @@ import 'package:dio/dio.dart'; import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_profile_model.dart'; +import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart'; import 'package:retrofit/retrofit.dart'; part 'consultant_service.g.dart'; @@ -18,4 +19,9 @@ abstract class ConsultantService { Future> uploadPhotos({ @Part(name: 'photos[]', contentType: 'multipart/form-data') List? photos, }); + + @GET('/api/consultant_request/{slug}/status') + Future getConsultantRequestStatus({ + @Path() required String slug, + }); } diff --git a/lib/features/create_recipe/entities/recipe_create_entity.dart b/lib/features/create_recipe/entities/recipe_create_entity.dart index 91fec435..69d04e8c 100644 --- a/lib/features/create_recipe/entities/recipe_create_entity.dart +++ b/lib/features/create_recipe/entities/recipe_create_entity.dart @@ -55,6 +55,27 @@ enum TypeOfRecipe { } } + static TypeOfRecipe? fromApiType(String value) { + switch (value) { + case 'first_meals': + return TypeOfRecipe.firstMeals; + case 'second_meals': + return TypeOfRecipe.secondMeals; + case 'salads': + return TypeOfRecipe.salads; + case 'snacks': + return TypeOfRecipe.snacks; + case 'sweets': + return TypeOfRecipe.sweets; + case 'drinks': + return TypeOfRecipe.drinks; + case 'other': + return TypeOfRecipe.other; + default: + return null; + } + } + static TypeOfRecipe? fromString(String value) { switch (value) { case 'Первые блюда': @@ -80,6 +101,7 @@ enum TypeOfRecipe { @freezed class RecipeCreateEntity with _$RecipeCreateEntity { const RecipeCreateEntity._(); + const factory RecipeCreateEntity({ String? title, TypeOfRecipe? typeOfRecipe, @@ -94,7 +116,7 @@ class RecipeCreateEntity with _$RecipeCreateEntity { String? videoUrl, required List ingredients, required List steps, - required List tags, // tags for search + required List tags, }) = _RecipeCreateEntity; RecipeModel toModel() => RecipeModel( @@ -118,6 +140,7 @@ class RecipeCreateEntity with _$RecipeCreateEntity { @freezed class StepEntity with _$StepEntity { const StepEntity._(); + const factory StepEntity({ String? description, File? image, diff --git a/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart b/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart index a7577d0f..43273ae6 100644 --- a/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart +++ b/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart @@ -1,13 +1,17 @@ import 'dart:developer'; import 'dart:io'; +import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/features/create_recipe/entities/recipe_create_entity.dart'; -import 'package:eatvio_app/features/create_recipe/models/ingredient.dart'; +import 'package:eatvio_app/features/create_recipe/models/ingredient.dart' as i; import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart'; import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart'; +import 'package:eatvio_app/features/feed/models/recipes_model.dart'; import 'package:eatvio_app/features/glossary/models/glossary_model.dart'; import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:http/http.dart' as http; +import 'package:path_provider/path_provider.dart'; class CreateRecipeCubit extends Cubit { final RecipeRepository recipeRepository; @@ -17,6 +21,7 @@ class CreateRecipeCubit extends Cubit { required this.glossaryRepository, }) : super( const CreateRecipeState( + loading: true, entity: RecipeCreateEntity( typeOfRecipe: TypeOfRecipe.firstMeals, ingredients: [], @@ -43,7 +48,7 @@ class CreateRecipeCubit extends Cubit { emit(state.copyWith.entity(steps: steps)); } - addIngredient(Ingredient entity) { + addIngredient(i.Ingredient entity) { emit( state.copyWith.entity( ingredients: [ @@ -55,20 +60,20 @@ class CreateRecipeCubit extends Cubit { } removeIngredient(int index) { - List ingredients = [...state.entity.ingredients]; + List ingredients = [...state.entity.ingredients]; ingredients.removeAt(index); emit(state.copyWith.entity(ingredients: ingredients)); } - void updateIngredient({required int index, required Ingredient ingredient}) { + void updateIngredient({required int index, required i.Ingredient ingredient}) { final ingredients = [...state.entity.ingredients]; ingredients[index] = ingredient; emit(state.copyWith.entity(ingredients: ingredients)); } replaceIngredientPosition(int oldIndex, int newIndex) { - List ingredients = [...state.entity.ingredients]; + List ingredients = [...state.entity.ingredients]; final item = ingredients.removeAt(oldIndex); ingredients.insert(newIndex, item); emit(state.copyWith.entity(ingredients: ingredients)); @@ -152,10 +157,10 @@ class CreateRecipeCubit extends Cubit { emit(state.copyWith.entity(steps: steps)); } - Future publishRecipe() async { + Future publishRecipe([bool update = false, String? slug]) async { try { final model = state.entity.toModel(); - await recipeRepository.publishRecipe(model); + await recipeRepository.publishRecipe(model, update, slug); return true; } catch (e, st) { log(e.toString(), stackTrace: st); @@ -163,6 +168,64 @@ class CreateRecipeCubit extends Cubit { } } + Future downloadImageAsFile(String imageUrl) async { + final response = await http.get(Uri.parse(imageUrl)); + + if (response.statusCode == 200) { + final tempDir = await getTemporaryDirectory(); + final filePath = '${tempDir.path}/${imageUrl.split('/').last}'; + final file = File(filePath); + return await file.writeAsBytes(response.bodyBytes); + } else { + throw Exception('Не удалось загрузить изображение'); + } + } + + disableLoading() { + emit(state.copyWith(loading: false)); + } + + Future recipeToEntity(Recipe recipe) async { + final file = await downloadImageAsFile('$baseUrl/storage/${recipe.image}'); + + final List steps = await Future.wait( + recipe.steps!.map( + (e) async => StepEntity( + description: e.description, + image: await downloadImageAsFile('$baseUrl/storage/${e.image}'), + ), + ), + ); + emit( + CreateRecipeState( + entity: RecipeCreateEntity( + title: recipe.title, + typeOfRecipe: TypeOfRecipe.fromApiType(recipe.type!), + countOfPortions: recipe.servings, + energyInOnePortion: recipe.calories, + totalCookHours: recipe.cookingTime ~/ 60, + totalCookMinutes: recipe.cookingTime % 60, + activeCookHours: recipe.activeCookingTime ~/ 60, + activeCookMinutes: recipe.activeCookingTime % 60, + description: recipe.description, + image: file, + videoUrl: recipe.videoUrl, + ingredients: recipe.ingredients! + .map((e) => i.Ingredient( + name: e.name, + unit: e.unit!, + portion: e.portion, + productId: e.productId, + )) + .toList(), + steps: steps, + tags: recipe.tags.map((e) => (e.name)).toList(), + ), + loading: false, + ), + ); + } + bool validateAll() { final isTitleValid = validateTitle(); final isDescValid = validateDescription(); @@ -251,6 +314,15 @@ class CreateRecipeCubit extends Cubit { } } + Future deleteRecipe(String slug) async { + try { + await recipeRepository.deleteRecipe(slug); + return true; + } catch (e) { + return false; + } + } + Future> getGlossaryListByQuery(String query) async { return await glossaryRepository.getGlossaryListByQuery(query); } diff --git a/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart b/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart index 6bf9e961..5aac99df 100644 --- a/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart +++ b/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart @@ -7,6 +7,7 @@ part 'create_recipe_state.freezed.dart'; class CreateRecipeState with _$CreateRecipeState { const factory CreateRecipeState({ required RecipeCreateEntity entity, + required bool loading, String? titleValidationError, String? descriptionValidationError, String? cookingTimeValidationError, @@ -15,5 +16,5 @@ class CreateRecipeState with _$CreateRecipeState { String? energyPerServingValidationError, String? imageValidationError, }) = _CreateRecipeState; - + } diff --git a/lib/features/create_recipe/presentation/pages/recipe_create_screen.dart b/lib/features/create_recipe/presentation/pages/recipe_create_screen.dart index 7d5ee805..76683d79 100644 --- a/lib/features/create_recipe/presentation/pages/recipe_create_screen.dart +++ b/lib/features/create_recipe/presentation/pages/recipe_create_screen.dart @@ -1,6 +1,7 @@ import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart'; +import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/core/widgets/show_error_dialog.dart'; import 'package:eatvio_app/core/widgets/success_published_recipe_bottom_sheet.dart'; import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; @@ -8,13 +9,19 @@ import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recip import 'package:eatvio_app/features/create_recipe/presentation/widgets/info_about_recipe_widget.dart'; import 'package:eatvio_app/features/create_recipe/presentation/widgets/ingredients_widget.dart'; import 'package:eatvio_app/features/create_recipe/presentation/widgets/steps_widget.dart'; +import 'package:eatvio_app/features/feed/models/recipes_model.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; class RecipeCreateScreen extends StatefulWidget { - const RecipeCreateScreen({super.key}); + final Recipe? recipe; + + const RecipeCreateScreen({ + super.key, + this.recipe, + }); static const name = 'recipeCreateScreen'; static const path = '/recipe-create-screen'; @@ -24,85 +31,115 @@ class RecipeCreateScreen extends StatefulWidget { } class _RecipeCreateScreenState extends State { + late final CreateRecipeCubit createRecipeCubit; + + @override + void initState() { + super.initState(); + createRecipeCubit = sl(); + if (widget.recipe != null) { + createRecipeCubit.recipeToEntity(widget.recipe!); + } else { + createRecipeCubit.disableLoading(); + } + } + @override Widget build(BuildContext context) { final s = S.of(context); return BlocProvider( - create: (BuildContext context) => sl(), - child: Scaffold( - backgroundColor: const Color(0xFFF0F2F5), - appBar: AppBar( - title: const Text('Добавление рецепта'), - centerTitle: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - bottom: Radius.circular(12), - ), - ), - ), - body: SingleChildScrollView( - physics: const BouncingScrollPhysics(), - padding: const EdgeInsets.only(top: 8, bottom: 32), - child: Column( - spacing: 16, - children: [ - const InfoAboutRecipeWidget(), - const IngredientsWidget(), - const StepsWidget(), - BlocBuilder( - builder: (context, state) { - return Builder( - builder: (context) { - return InkWell( - onTap: () async { - if (context.read().validateAll()) { - final isSuccess = await context.read().publishRecipe(); - if (isSuccess) { - Future.delayed(const Duration(milliseconds: 500), () { - if (!context.mounted) return; - context.pop(); - showBottomSheetWithBlur( - userRootNavigator: true, - context: context, - child: SuccessPublishedRecipeBottomSheet( - title: state.entity.title ?? 'Рецепт', - ), - ); - }); - } else { - if (!context.mounted) return; - showErrorDialog(context, title: '${s.could_not_publish_recipe} ${state.entity.title}'); - } - } - }, - borderRadius: BorderRadius.circular(20), - 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: const Text( - 'Опубликовать', - style: TextStyle( - fontSize: 16, - color: Colors.white, - fontWeight: FontWeight.w600, - ), - ), - ), - ), - ); - }, - ); - }, + create: (BuildContext context) => createRecipeCubit, + child: BlocBuilder( + builder: (BuildContext context, CreateRecipeState state) { + if (state.loading) { + return const Scaffold( + body: Loader(), + ); + } else { + return Scaffold( + backgroundColor: const Color(0xFFF0F2F5), + appBar: AppBar( + title: widget.recipe != null ? const Text('Редактирование рецепта') : const Text('Добавление рецепта'), + centerTitle: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + bottom: Radius.circular(12), + ), + ), ), - ], - ), - ), + body: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.only(top: 8, bottom: 32), + child: Column( + spacing: 16, + children: [ + InfoAboutRecipeWidget( + createRecipeCubit: createRecipeCubit, + ), + const IngredientsWidget(), + StepsWidget( + createRecipeCubit: createRecipeCubit, + ), + BlocBuilder( + builder: (context, state) { + return Builder( + builder: (context) { + return InkWell( + onTap: () async { + if (context.read().validateAll()) { + final isSuccess = await context.read().publishRecipe( + widget.recipe == null ? false : true, + widget.recipe?.slug, + ); + if (isSuccess) { + Future.delayed(const Duration(milliseconds: 500), () { + if (!context.mounted) return; + context.pop(); + showBottomSheetWithBlur( + userRootNavigator: true, + context: context, + child: SuccessPublishedRecipeBottomSheet( + title: state.entity.title ?? 'Рецепт', + ), + ); + }); + } else { + if (!context.mounted) return; + showErrorDialog(context, title: '${s.could_not_publish_recipe} ${state.entity.title}'); + } + } + }, + borderRadius: BorderRadius.circular(20), + 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( + widget.recipe != null ? 'Обновить' : 'Опубликовать', + style: const TextStyle( + fontSize: 16, + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ); + }, + ); + }, + ), + ], + ), + ), + ); + } + }, ), ); } diff --git a/lib/features/create_recipe/presentation/widgets/info_about_recipe_widget.dart b/lib/features/create_recipe/presentation/widgets/info_about_recipe_widget.dart index bc32ca54..38a4c4ee 100644 --- a/lib/features/create_recipe/presentation/widgets/info_about_recipe_widget.dart +++ b/lib/features/create_recipe/presentation/widgets/info_about_recipe_widget.dart @@ -14,7 +14,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class InfoAboutRecipeWidget extends StatefulWidget { - const InfoAboutRecipeWidget({super.key}); + final CreateRecipeCubit createRecipeCubit; + + const InfoAboutRecipeWidget({super.key, required this.createRecipeCubit}); @override State createState() => _InfoAboutRecipeWidgetState(); @@ -23,13 +25,19 @@ class InfoAboutRecipeWidget extends StatefulWidget { class _InfoAboutRecipeWidgetState extends State { final List items = TypeOfRecipe.values.map((e) => SelectionPopupModel(title: e.toString())).toList(); late SelectionPopupModel selectedType; - late final CreateRecipeCubit createRecipeCubit; @override void initState() { super.initState(); - selectedType = items.first; - createRecipeCubit = context.read(); + if (widget.createRecipeCubit.state.entity.typeOfRecipe != null) { + for (int i = 0; i < items.length; i++) { + if (items[i].title == widget.createRecipeCubit.state.entity.typeOfRecipe!.toString()) { + selectedType = items[i]; + } + } + } else { + selectedType = items.first; + } } @override @@ -44,6 +52,7 @@ class _InfoAboutRecipeWidgetState extends State { const Text('Название'), const SizedBox(height: 4), BlocBuilder( + bloc: widget.createRecipeCubit, buildWhen: (previous, current) => previous.titleValidationError != current.titleValidationError, builder: (context, state) { return Column( @@ -51,10 +60,11 @@ class _InfoAboutRecipeWidgetState extends State { CustomTextFormField( hintText: 'Например: тар-тар из говядины', hasError: state.titleValidationError != null, + initialValue: state.entity.title, onChanged: (value) { - createRecipeCubit.replaceTitle(value); + widget.createRecipeCubit.replaceTitle(value); if (state.titleValidationError != null) { - createRecipeCubit.validateTitle(); + widget.createRecipeCubit.validateTitle(); } }, ), @@ -78,7 +88,7 @@ class _InfoAboutRecipeWidgetState extends State { setState(() { selectedType = newSelectedType; }); - createRecipeCubit.changeType( + widget.createRecipeCubit.changeType( TypeOfRecipe.fromString(selectedType.title) ?? TypeOfRecipe.other, ); }, @@ -100,12 +110,13 @@ class _InfoAboutRecipeWidgetState extends State { CustomTextFormField( hintText: 'Расскажите, почему вы выбрали именно этот рецепт? Опишите, каким будет готовое блюдо и в чём его достоинства.', minLines: 5, + initialValue: state.entity.description, maxLines: 5, hasError: state.descriptionValidationError != null, onChanged: (value) { - createRecipeCubit.replaceDescription(value); + widget.createRecipeCubit.replaceDescription(value); if (state.descriptionValidationError != null) { - createRecipeCubit.validateDescription(); + widget.createRecipeCubit.validateDescription(); } }, ), @@ -148,8 +159,9 @@ class _InfoAboutRecipeWidgetState extends State { const SizedBox(height: 4), CustomTextFormField( hintText: 'Прикрепите ссылку на видео', + initialValue: widget.createRecipeCubit.state.entity.videoUrl, onChanged: (value) { - createRecipeCubit.replaceVideoUrl(value); + widget.createRecipeCubit.replaceVideoUrl(value); }, ), ], diff --git a/lib/features/create_recipe/presentation/widgets/portion_widget.dart b/lib/features/create_recipe/presentation/widgets/portion_widget.dart index 99372193..abd593c2 100644 --- a/lib/features/create_recipe/presentation/widgets/portion_widget.dart +++ b/lib/features/create_recipe/presentation/widgets/portion_widget.dart @@ -14,6 +14,7 @@ class PortionWidget extends StatefulWidget { class _PortionWidgetState extends State { late final CreateRecipeCubit createRecipeCubit; + @override void initState() { super.initState(); @@ -45,6 +46,7 @@ class _PortionWidgetState extends State { CustomTextFormField( hintText: '1', hasError: state.servingsValidationError != null, + initialValue: state.entity.countOfPortions?.toString(), textInputType: TextInputType.number, onChanged: (value) { createRecipeCubit.replacePortionsCount(int.parse(value)); @@ -77,6 +79,7 @@ class _PortionWidgetState extends State { CustomTextFormField( hintText: '100', hasError: state.energyPerServingValidationError != null, + initialValue: state.entity.energyInOnePortion?.toString(), suffixText: 'Ккал', textInputType: const TextInputType.numberWithOptions(decimal: true), onChanged: (value) { diff --git a/lib/features/create_recipe/presentation/widgets/step_image_widget.dart b/lib/features/create_recipe/presentation/widgets/step_image_widget.dart index 35f534a5..1189f0f8 100644 --- a/lib/features/create_recipe/presentation/widgets/step_image_widget.dart +++ b/lib/features/create_recipe/presentation/widgets/step_image_widget.dart @@ -3,16 +3,16 @@ import 'dart:io'; import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:image_picker/image_picker.dart'; class StepImageWidget extends StatefulWidget { + final CreateRecipeCubit createRecipeCubit; final int index; const StepImageWidget({ super.key, - required this.index, + required this.index, required this.createRecipeCubit, }); @override @@ -21,12 +21,15 @@ class StepImageWidget extends StatefulWidget { class _StepImageWidgetState extends State { XFile? image; - late final CreateRecipeCubit createRecipeCubit; @override void initState() { super.initState(); - createRecipeCubit = context.read(); + if (widget.createRecipeCubit.state.entity.steps.length > widget.index) { + if (widget.createRecipeCubit.state.entity.steps[widget.index].image != null) { + image = XFile(widget.createRecipeCubit.state.entity.steps[widget.index].image!.path); + } + } } @override @@ -56,7 +59,7 @@ class _StepImageWidgetState extends State { setState(() { image = null; }); - createRecipeCubit.replaceImageInStep(null, widget.index); + widget.createRecipeCubit.replaceImageInStep(null, widget.index); }, child: Container( padding: const EdgeInsets.all(4), @@ -84,7 +87,7 @@ class _StepImageWidgetState extends State { image = await picker.pickImage(source: ImageSource.camera); setState(() {}); if (image != null) { - createRecipeCubit.replaceImageInStep(File(image!.path), widget.index); + widget.createRecipeCubit.replaceImageInStep(File(image!.path), widget.index); } if (!context.mounted) return; Navigator.of(context).pop(); @@ -103,7 +106,7 @@ class _StepImageWidgetState extends State { image = await picker.pickImage(source: ImageSource.gallery); setState(() {}); if (image != null) { - createRecipeCubit.replaceImageInStep(File(image!.path), widget.index); + widget.createRecipeCubit.replaceImageInStep(File(image!.path), widget.index); } if (!context.mounted) return; Navigator.of(context).pop(); diff --git a/lib/features/create_recipe/presentation/widgets/steps_widget.dart b/lib/features/create_recipe/presentation/widgets/steps_widget.dart index 934510f4..07474c72 100644 --- a/lib/features/create_recipe/presentation/widgets/steps_widget.dart +++ b/lib/features/create_recipe/presentation/widgets/steps_widget.dart @@ -7,24 +7,19 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class StepsWidget extends StatefulWidget { - const StepsWidget({super.key}); + final CreateRecipeCubit createRecipeCubit; + + const StepsWidget({super.key, required this.createRecipeCubit}); @override State createState() => _StepsWidgetState(); } class _StepsWidgetState extends State { - late final CreateRecipeCubit createRecipeCubit; - - @override - void initState() { - super.initState(); - createRecipeCubit = context.read(); - } - @override Widget build(BuildContext context) { return BlocBuilder( + bloc: widget.createRecipeCubit, builder: (BuildContext context, CreateRecipeState state) { return Material( shape: RoundedRectangleBorder( @@ -83,10 +78,11 @@ class _StepsWidgetState extends State { const SizedBox(height: 4), CustomTextFormField( hintText: 'Например: подготовьте ингредиенты', + initialValue: widget.createRecipeCubit.state.entity.steps[index].description, maxLines: 5, minLines: 3, onChanged: (value) { - createRecipeCubit.replaceDescriptionInStep(value, index); + widget.createRecipeCubit.replaceDescriptionInStep(value, index); }, ), const SizedBox(height: 12), @@ -94,6 +90,7 @@ class _StepsWidgetState extends State { const SizedBox(height: 4), StepImageWidget( index: index, + createRecipeCubit: widget.createRecipeCubit, ), ], ), diff --git a/lib/features/create_recipe/presentation/widgets/tags_widget.dart b/lib/features/create_recipe/presentation/widgets/tags_widget.dart index 26b9d23b..baac0c3b 100644 --- a/lib/features/create_recipe/presentation/widgets/tags_widget.dart +++ b/lib/features/create_recipe/presentation/widgets/tags_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_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class TagsWidget extends StatefulWidget { const TagsWidget({super.key}); @@ -10,8 +12,16 @@ class TagsWidget extends StatefulWidget { } class _TagsWidgetState extends State { + late final CreateRecipeCubit createRecipeCubit; final tagController = TextEditingController(); - final List tags = []; + late final List tags; + + @override + void initState() { + super.initState(); + createRecipeCubit = context.read(); + tags = createRecipeCubit.state.entity.tags; + } @override Widget build(BuildContext context) { diff --git a/lib/features/create_recipe/presentation/widgets/time_of_cook_widget.dart b/lib/features/create_recipe/presentation/widgets/time_of_cook_widget.dart index dacd76cb..d995207d 100644 --- a/lib/features/create_recipe/presentation/widgets/time_of_cook_widget.dart +++ b/lib/features/create_recipe/presentation/widgets/time_of_cook_widget.dart @@ -71,6 +71,7 @@ class _TimeOfCookWidgetState extends State { hintText: 'Часов', hasError: state.cookingTimeValidationError != null, textInputType: TextInputType.number, + initialValue: state.entity.totalCookHours?.toString(), onChanged: (value) { final hours = int.tryParse(value); if (hours == null) return; @@ -97,6 +98,7 @@ class _TimeOfCookWidgetState extends State { hintText: 'Минут', hasError: state.cookingTimeValidationError != null, textInputType: TextInputType.number, + initialValue: state.entity.totalCookMinutes?.toString(), onChanged: (value) { final minutes = int.tryParse(value); if (minutes == null) return; @@ -156,6 +158,7 @@ class _TimeOfCookWidgetState extends State { CustomTextFormField( hintText: 'Часов', hasError: state.activeTimeValidationError != null, + initialValue: state.entity.activeCookHours?.toString(), textInputType: TextInputType.number, onChanged: (value) { final hours = int.tryParse(value); @@ -183,6 +186,7 @@ class _TimeOfCookWidgetState extends State { CustomTextFormField( hintText: 'Минут', hasError: state.activeTimeValidationError != null, + initialValue: state.entity.activeCookMinutes?.toString(), textInputType: TextInputType.number, onChanged: (value) { final minutes = int.tryParse(value); diff --git a/lib/features/create_recipe/reporitories/recipe_repository.dart b/lib/features/create_recipe/reporitories/recipe_repository.dart index a0c2a08b..f0f860e9 100644 --- a/lib/features/create_recipe/reporitories/recipe_repository.dart +++ b/lib/features/create_recipe/reporitories/recipe_repository.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:convert'; import 'dart:io'; import 'package:dio/dio.dart'; import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; @@ -11,7 +10,6 @@ import 'package:eatvio_app/features/create_recipe/models/product.dart'; class RecipeRepository { final CreateRecipeService recipeService; - RecipeRepository({required this.recipeService}); @@ -23,7 +21,7 @@ class RecipeRepository { } } - Future publishRecipe(RecipeModel recipe) async { + Future publishRecipe(RecipeModel recipe, [bool update = false, String? slug]) async { try { if (recipe.image == null || (recipe.image?.isEmpty ?? true)) { return; @@ -72,15 +70,11 @@ class RecipeRepository { }; formMap.removeWhere((k, v) => v == null || (v is String && v.isEmpty)); final formData = FormData.fromMap(formMap); - await recipeService.publishRecipe(formData: formData); - } catch (e) { - rethrow; - } - } - - Future editRecipe(RecipeModel recipe, String id) async { - try { - return await recipeService.editRecipe(jsonDecode(jsonEncode(recipe)), id); + if (update) { + await recipeService.updateRecipe(slug: slug!, formData: formData); + } else { + await recipeService.publishRecipe(formData: formData); + } } catch (e) { rethrow; } @@ -100,5 +94,13 @@ class RecipeRepository { } catch (e) { rethrow; } - } + } + + Future deleteRecipe(String slug) async { + try { + return await recipeService.deleteRecipe(slug: slug); + } catch (e) { + rethrow; + } + } } diff --git a/lib/features/create_recipe/services/recipe_service.dart b/lib/features/create_recipe/services/recipe_service.dart index 4bc9c85f..41ee957a 100644 --- a/lib/features/create_recipe/services/recipe_service.dart +++ b/lib/features/create_recipe/services/recipe_service.dart @@ -37,11 +37,18 @@ abstract class CreateRecipeService { @Body() required FormData formData, }); - @POST('/api/recipes/update/{id}') - Future editRecipe(@Part() Map recipe, String id); - @GET('/api/recipes/tags') Future getTags(); + @DELETE('/api/recipes/delete/{slug}') + Future deleteRecipe({ + @Path() required String slug, + }); -} \ No newline at end of file + @POST('/api/recipes/update/{slug}') + @MultiPart() + Future updateRecipe({ + @Path() required String slug, + @Body() required FormData formData, + }); +} diff --git a/lib/features/feed/models/recipes_model.dart b/lib/features/feed/models/recipes_model.dart index 6a5668bd..fb627ca3 100644 --- a/lib/features/feed/models/recipes_model.dart +++ b/lib/features/feed/models/recipes_model.dart @@ -25,21 +25,40 @@ class Recipe with _$Recipe { @JsonKey(name: 'was_liked_by_me') required bool wasLikedByMe, @JsonKey(name: 'is_favorite') required bool isFavorite, @JsonKey(name: 'energy_per_serving') required double calories, + int? servings, @JsonKey(name: 'total_likes') required int likesCount, @JsonKey(name: 'total_views') required int viewsCount, + required List tags, required Author author, }) = _Recipes; factory Recipe.fromJson(Map json) => _$RecipeFromJson(json); } +@freezed +class Tag with _$Tag { + const factory Tag({required String name}) = _Tag; + + factory Tag.fromJson(Map json) => _$TagFromJson(json); +} + @freezed class Ingredient with _$Ingredient { - const factory Ingredient({required String name, String? unit, required String portion}) = _Ingredient; + const factory Ingredient({ + required String name, + String? unit, + required String portion, + @JsonKey(name: 'product_nutritional_id', fromJson: _parseProductId) required int productId, + }) = _Ingredient; factory Ingredient.fromJson(Map json) => _$IngredientFromJson(json); } - +int _parseProductId(json) { + if (json is String) { + return int.parse(json); + } + return json as int; +} @freezed class Step with _$Step { const factory Step({required String image, @JsonKey(name: 'description_md') required String description}) = _Step; diff --git a/lib/features/feed/presentation/widgets/recipes_widget.dart b/lib/features/feed/presentation/widgets/recipes_widget.dart index 39ca3ee1..53022a92 100644 --- a/lib/features/feed/presentation/widgets/recipes_widget.dart +++ b/lib/features/feed/presentation/widgets/recipes_widget.dart @@ -127,6 +127,7 @@ class _RecipesWidgetState extends State { path: 'recipe/${widget.recipe.slug}', slug: widget.recipe.slug, complainedContentType: ComplainedContent.recipe, + recipe: widget.recipe, ), ), ), diff --git a/lib/features/profile/models/sub_model.dart b/lib/features/profile/models/sub_model.dart index c4a04bd7..0a00bc0d 100644 --- a/lib/features/profile/models/sub_model.dart +++ b/lib/features/profile/models/sub_model.dart @@ -26,7 +26,6 @@ class SubModel with _$SubModel { @JsonKey(name: 'is_entity_subscribed_by_me') required bool isEntitySubscribedByMe, @JsonKey(name: 'profile_photo_url') required String profilePhotoUrl, required List roles, - @JsonKey(name: 'is_public_profile') required bool isPublicProfile, }) = _SubModel; factory SubModel.fromJson(Map json) => _$SubModelFromJson(json); diff --git a/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart b/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart index 71fb1729..cdbab793 100644 --- a/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart +++ b/lib/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart @@ -2,6 +2,7 @@ import 'dart:developer'; import 'dart:io'; import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; +import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart'; import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_state.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; @@ -10,20 +11,24 @@ import 'package:flutter_bloc/flutter_bloc.dart'; class ProfileCubit extends Cubit { final ProfileRepository profileRepository; final ChatRepository chatRepository; + final ConsultantRepository consultantRepository; String _currentPeriod = '1'; ProfileCubit({ required this.profileRepository, required this.chatRepository, + required this.consultantRepository, }) : super(const ProfileState.loading()); Future getProfileBySlug(String slug) async { try { + final profile = await profileRepository.getProfileBySlug(slug); emit( ProfileState.loaded( - profile: await profileRepository.getProfileBySlug(slug), + profile: profile, profileDetails: await profileRepository.getProfileDetailsBySlug(slug), blogPosts: await profileRepository.getBlogPostsByAuthorSlug(slug: slug), + consultantInfo: profile.roles.contains('consultant') ? await consultantRepository.getConsultantRequestStatus(slug) : null, ), ); } catch (e) { @@ -44,6 +49,7 @@ class ProfileCubit extends Cubit { blogPosts: await profileRepository.getBlogPostsByAuthorSlug(slug: profile.slug), weightPoints: await profileRepository.getWeight(period: period), circumferencePoints: await profileRepository.getCircumferences(period: period), + consultantInfo: profile.roles.contains('consultant') ? await consultantRepository.getConsultantRequestStatus(profile.slug) : null, ), ); } catch (e, stackTrace) { diff --git a/lib/features/profile/presentation/bloc/profile_cubit/profile_state.dart b/lib/features/profile/presentation/bloc/profile_cubit/profile_state.dart index ec7fd13d..6e8154fc 100644 --- a/lib/features/profile/presentation/bloc/profile_cubit/profile_state.dart +++ b/lib/features/profile/presentation/bloc/profile_cubit/profile_state.dart @@ -1,3 +1,4 @@ +import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart'; import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; import 'package:eatvio_app/features/profile/models/circumference_model.dart'; import 'package:eatvio_app/features/profile/models/profile_model.dart'; @@ -18,6 +19,7 @@ abstract class ProfileState with _$ProfileState { required List blogPosts, List? weightPoints, List? circumferencePoints, + ConsultationRequestModel? consultantInfo, }) = ProfileStateLoaded; const factory ProfileState.error({ diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index df96f608..699aa021 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -93,7 +93,9 @@ class _ProfileScreenState extends State { } else if (state is ProfileStateError) { return DataErrorWidget( onReload: () async { - widget.author == null || widget.author!.authorIsMe ? await profileCubit.getMyProfile() : await profileCubit.getProfileBySlug(widget.author!.authorSlug); + widget.author == null || widget.author!.authorIsMe + ? await profileCubit.getMyProfile() + : await profileCubit.getProfileBySlug(widget.author!.authorSlug); }, ); } else { @@ -144,25 +146,24 @@ class _ProfileScreenState extends State { ), ), const SizedBox(width: 12), - if (state.profile.ratingPlace != null) - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: const Color(0xFFF6FAFF), - borderRadius: BorderRadius.circular(12), - ), - child: Text( - '${state.profile.ratingPlace!}-й в рейтинге', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: appTheme.blueA400, - ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: const Color(0xFFF6FAFF), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + '${state.consultantInfo!.ratingPlace}-й в рейтинге', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: appTheme.blueA400, ), ), + ), ], ), - if (state.profile.educationResource?.hasMedicalEducation == 1) + if (state.consultantInfo!.educationModel.hasMedicalEducation == 1) const Row( children: [ SizedBox(width: 16), @@ -221,6 +222,61 @@ class _ProfileScreenState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ _header(state), + if (state.profile.roles.contains('consultant')) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const SizedBox(width: 16), + const Text( + 'Консультант', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Color( + 0xFF323F4B, + ), + ), + ), + const SizedBox(width: 12), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: const Color(0xFFF6FAFF), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + '${state.consultantInfo!.ratingPlace}-й в рейтинге', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: appTheme.blueA400, + ), + ), + ), + ], + ), + if (state.consultantInfo!.educationModel.hasMedicalEducation == 1) + const Row( + children: [ + SizedBox(width: 16), + Icon( + Icons.done_rounded, + color: Colors.green, + ), + Text( + 'Медицинское образование', + style: TextStyle( + color: Color(0xFF323F4B), + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ], + ), if (state.profileDetails.additionalInfo.isNotEmpty) _additionalInfo(state), ], ), @@ -371,7 +427,7 @@ class _ProfileScreenState extends State { } _diplomasAndCertificates(ProfileStateLoaded state) { - if (!state.profile.roles.contains('consultant') || state.profile.diplomasAndCertificates == null || state.profile.diplomasAndCertificates!.isEmpty) { + if (!state.profile.roles.contains('consultant') || state.consultantInfo!.photos.isEmpty) { return const SizedBox.shrink(); } return Padding( @@ -416,8 +472,18 @@ class _ProfileScreenState extends State { padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ - if (state.profile.diplomasAndCertificates != null) - ...state.profile.diplomasAndCertificates!.map((d) => _diplomaImage('$baseUrl/storage/${d.path}')), + if (state.consultantInfo!.photos.isNotEmpty) + ...List.generate( + state.consultantInfo!.photos.length * 2 - 1, + (index) { + if (index.isEven) { + final photo = state.consultantInfo!.photos[index ~/ 2]; + return _diplomaImage(photo.url); + } else { + return const SizedBox(width: 8); + } + }, + ), ], ), ), @@ -456,8 +522,8 @@ class _ProfileScreenState extends State { _education(ProfileStateLoaded state) { if (!state.profile.roles.contains('consultant') || - state.profile.educationResource?.completedCourses == null || - (state.profile.educationResource?.completedCourses?.isEmpty ?? false)) { + state.consultantInfo!.educationModel.completedCourses == null || + (state.consultantInfo!.educationModel.completedCourses?.isEmpty ?? false)) { return const SizedBox.shrink(); } return Padding( @@ -495,11 +561,17 @@ class _ProfileScreenState extends State { color: Colors.white, borderRadius: BorderRadius.circular(12), child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + padding: const EdgeInsets.only(left: 16, right: 16, top: 20), child: Column( children: [ - if (state.profile.educationResource?.completedCourses != null) - ...state.profile.educationResource!.completedCourses!.map((c) => _educationBlock(c.values.first.title, c.values.first.date)), + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) => _educationBlock(state.consultantInfo!.educationModel.completedCourses![index].values.first.title, + state.consultantInfo!.educationModel.completedCourses![index].values.first.date), + separatorBuilder: (context, index) => const SizedBox(height: 8), + itemCount: state.consultantInfo!.educationModel.completedCourses!.length, + ), ], ), ), @@ -564,39 +636,39 @@ class _ProfileScreenState extends State { () { launchUrl( Uri.parse( - state.profile.educationResource?.vkLink ?? '', + state.consultantInfo!.educationModel.vkLink ?? '', ), mode: LaunchMode.externalApplication, ); }, 'assets/icons/vk.svg', - state.profile.educationResource?.vkLink == null ? true : false, + state.consultantInfo!.educationModel.vkLink!.isEmpty ? true : false, ), const SizedBox(width: 12), _socialLinkButton( () { launchUrl( Uri.parse( - state.profile.educationResource?.vkLink ?? '', + state.consultantInfo!.educationModel.tgLink ?? '', ), mode: LaunchMode.externalApplication, ); }, 'assets/icons/telegram.svg', - state.profile.educationResource?.vkLink == null ? true : false, + state.consultantInfo!.educationModel.tgLink!.isEmpty ? true : false, ), const SizedBox(width: 12), _socialLinkButton( () { launchUrl( Uri.parse( - state.profile.educationResource?.personalSiteLink ?? '', + state.consultantInfo!.educationModel.personalSiteLink ?? '', ), mode: LaunchMode.externalApplication, ); }, 'assets/icons/web.svg', - state.profile.educationResource?.personalSiteLink == null ? true : false, + state.consultantInfo!.educationModel.personalSiteLink!.isEmpty ? true : false, ), ], ), @@ -620,7 +692,7 @@ class _ProfileScreenState extends State { child: SvgPicture.asset( icon, width: 24, - colorFilter: const ColorFilter.mode(Colors.black12, BlendMode.srcIn), + colorFilter: disabled ? const ColorFilter.mode(Colors.black12, BlendMode.srcIn) : const ColorFilter.mode(Colors.black, BlendMode.srcIn), ), ), ), @@ -659,7 +731,7 @@ class _ProfileScreenState extends State { for (int i = 0; i < state.profile.badges.length; i++) ...[ state.profile.badges[i].image.contains('svg') ? Tooltip( - message: state.profile.badges[i].description, + message: state.profile.badges[i].description, triggerMode: TooltipTriggerMode.tap, textStyle: const TextStyle( color: Colors.black, diff --git a/lib/features/recipe/presentation/about_recipe_screen.dart b/lib/features/recipe/presentation/about_recipe_screen.dart index 2436ba0b..6906b3da 100644 --- a/lib/features/recipe/presentation/about_recipe_screen.dart +++ b/lib/features/recipe/presentation/about_recipe_screen.dart @@ -218,6 +218,7 @@ class _AboutRecipeScreenState extends State { path: 'recipe/${widget.recipe.slug}', complainedContentType: ComplainedContent.recipe, slug: widget.recipe.slug, + recipe: widget.recipe, ), const SizedBox(width: 16), ], @@ -401,6 +402,7 @@ class _AboutRecipeScreenState extends State { path: 'recipe/${widget.recipe.slug}', complainedContentType: ComplainedContent.recipe, slug: widget.recipe.slug, + recipe: widget.recipe, ), const SizedBox(width: 16), ], @@ -425,6 +427,7 @@ class _AboutRecipeScreenState extends State { path: 'recipe/${widget.recipe.slug}', complainedContentType: ComplainedContent.recipe, slug: widget.recipe.slug, + recipe: widget.recipe, ), const SizedBox(width: 16), ], diff --git a/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart b/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart index 9a1cb16d..33688c9c 100644 --- a/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart +++ b/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart @@ -57,7 +57,7 @@ class RegistrationCubit extends Cubit { ), ); await TokenStorage.saveTokenInStorage(token); - AppRouter.navigatorKey.currentContext?.go('/my-profile-screen'); + AppRouter.navigatorKey.currentContext?.go('/feed-screen'); } catch (e) { log(e.toString()); } diff --git a/lib/features/update_screen/presentation/update_screen.dart b/lib/features/update_screen/presentation/update_screen.dart index d516d863..99e9e880 100644 --- a/lib/features/update_screen/presentation/update_screen.dart +++ b/lib/features/update_screen/presentation/update_screen.dart @@ -25,8 +25,8 @@ class UpdateScreen extends StatelessWidget { fit: BoxFit.cover, ), ), - SafeArea( - child: Positioned.fill( + Positioned.fill( + child: SafeArea( child: Padding( padding: const EdgeInsets.only(bottom: 16), child: Column( diff --git a/lib/features/welcome/presentation/pages/welcome_screen.dart b/lib/features/welcome/presentation/pages/welcome_screen.dart index 0ae017a1..70a5904e 100644 --- a/lib/features/welcome/presentation/pages/welcome_screen.dart +++ b/lib/features/welcome/presentation/pages/welcome_screen.dart @@ -37,8 +37,8 @@ class _WelcomeScreenState extends State { fit: BoxFit.cover, ), ), - SafeArea( - child: Positioned.fill( + Positioned.fill( + child: SafeArea( child: Padding( padding: const EdgeInsets.only(bottom: 16), child: Column( @@ -80,7 +80,7 @@ class _WelcomeScreenState extends State { onTap: () async { final sharedPreferences = await SharedPreferences.getInstance(); await sharedPreferences.setBool('welcomeScreenWasShowed', true); - goRouter.go('/my-profile-screen'); + goRouter.go('/feed-screen'); }, borderRadius: BorderRadius.circular(8), child: Ink( diff --git a/test/features/profile/services/profile_service_test.dart b/test/features/profile/services/profile_service_test.dart index 2304e0c7..e735602c 100644 --- a/test/features/profile/services/profile_service_test.dart +++ b/test/features/profile/services/profile_service_test.dart @@ -35,7 +35,7 @@ void main() { test('getProfileDetailsBySlug', () async { when(mockProfileService.getProfileDetailsBySlug(slug: 'slug')).thenAnswer( - (_) async => await Future.value( + (_) async => await Future.value( const ProfileDetailsModel( firstName: 'Alexnader', lastName: 'Avanesyan', @@ -70,21 +70,21 @@ void main() { test('getBlogPostByAuthorSlug', () async { when(mockProfileService.getBlogPostByAuthorSlug(slug: '')).thenAnswer( - (_) async => [], + (_) async => [], ); expect(await mockProfileService.getBlogPostByAuthorSlug(slug: 'slug'), isA>()); }); test('getWeight', () async { when(mockProfileService.getWeight(period: '')).thenAnswer( - (_) async => [], + (_) async => [], ); expect(await mockProfileService.getWeight(period: ''), isA>()); }); test('getCircumferences', () async { when(mockProfileService.getCircumferences(period: '')).thenAnswer( - (_) async => [], + (_) async => [], ); expect(await mockProfileService.getCircumferences(period: ''), isA>()); });