diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index 2d390f02..3abc813c 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -3,7 +3,7 @@ name: Eatvio App Android Test on: [ pull_request ] env: - flutter_version: "3.29.1" + flutter_version: "3.32.0" java_version: "21.x" flutter_channel: "stable" diff --git a/.gitea/workflows/docker-run-tests.yml b/.gitea/workflows/docker-run-tests.yml index cf14247c..85d14070 100644 --- a/.gitea/workflows/docker-run-tests.yml +++ b/.gitea/workflows/docker-run-tests.yml @@ -3,7 +3,7 @@ name: Eatvio App Android Test on custom docker on: [ pull_request ] env: - flutter_version: "3.29.1" + flutter_version: "3.32.0" java_version: "21.x" flutter_channel: "stable" my_local_cache_dir: "/opt/hostedtoolcache/" diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 80825645..70ff32b8 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -3,7 +3,7 @@ name: Eatvio App Android Test on: [ pull_request ] env: - flutter_version: "3.29.3" + flutter_version: "3.32.0" java_version: "21.x" flutter_channel: "stable" my_local_cache_dir: "/opt/hostedtoolcache/" diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html index f2f60fc9..f21ab9cc 100644 --- a/android/build/reports/problems/problems-report.html +++ b/android/build/reports/problems/problems-report.html @@ -650,7 +650,7 @@ code + .copy-button { diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index efdcc4ac..f587a472 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-all.zip diff --git a/android/settings.gradle b/android/settings.gradle index 849ba91f..833c476b 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -20,7 +20,7 @@ plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version "8.7.3" apply false // START: FlutterFire Configuration - id "com.google.gms.google-services" version "4.3.15" apply false + id "com.google.gms.google-services" version "4.4.2" apply false // END: FlutterFire Configuration id "org.jetbrains.kotlin.android" version "2.0.21" apply false } diff --git a/assets/lottie_animations/loading.json b/assets/lottie_animations/loading.json new file mode 100644 index 00000000..a0ac541f --- /dev/null +++ b/assets/lottie_animations/loading.json @@ -0,0 +1 @@ +{"nm":"Main Scene","ddd":0,"h":500,"w":500,"meta":{"g":"@lottiefiles/creator 1.46.0"},"layers":[{"ty":0,"nm":"Nested Scene 1","sr":1,"st":0,"op":31,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"w":500,"h":500,"refId":"precomp_newScene_abc3f55b-16ae-4191-8057-2b89484cea7e","ind":1},{"ty":4,"nm":"Слой_x0020_11","sr":1,"st":0,"op":31,"ip":0,"hd":false,"ln":"Слой_x0020_1","ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[2116.5,2116.5]},"s":{"a":0,"k":[10.7995,10.7995]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[249.892,323.7606]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"gr","bm":0,"ln":"_2456927096976","hd":false,"nm":"_2456927096976","it":[{"ty":"gr","bm":0,"hd":false,"nm":"Group 2","it":[{"ty":"sh","bm":0,"hd":false,"nm":"Path 11","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[194,83],[145,154],[-70,66],[-67,-70],[-150,-64],[-164,0],[-150,65],[-112,119],[-71,-67],[67,-70],[194,-84],[211,0]],"o":[[-212,0],[-194,-84],[-67,-70],[71,-67],[112,119],[150,65],[163,0],[150,-64],[67,-70],[70,66],[-145,154],[-194,83],[0,0]],"v":[[2118,1902],[1504,1776],[990,1415],[997,1167],[1246,1174],[1643,1452],[2118,1550],[2592,1452],[2989,1174],[3238,1167],[3245,1415],[2731,1776],[2118,1902]]}}},{"ty":"fl","bm":0,"hd":false,"nm":"Fill","c":{"a":0,"k":[1,1,1]},"r":2,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[2117.50004629844,1510.5843947960868]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[2117.50004629844,1510.5843947960868]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]},{"ty":"gr","bm":0,"hd":true,"nm":"Group 3","it":[{"ty":"sh","bm":0,"hd":false,"nm":"Path 12","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,-882],[881,0],[0,881],[-882,0]],"o":[[881,0],[0,881],[-882,0],[0,-882],[0,0]],"v":[[2118,1],[3713,1270],[2118,2866],[522,1270],[2118,1]]}}},{"ty":"fl","bm":0,"hd":false,"nm":"Fill","c":{"a":0,"k":[0.2627,0.651,0.949]},"r":2,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[2117.5,1433.5000000000002]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[2117.5,1433.5000000000002]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]},{"ty":"tr","a":{"a":0,"k":[2116.5000000000005,2116.5]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[2116.5000000000005,2116.5]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]}],"ind":2},{"ty":4,"nm":"Слой_x0020_1","sr":1,"st":0,"op":31,"ip":0,"hd":false,"ln":"Слой_x0020_1","ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[2117.5,1433.4999999999998]},"s":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[8.78118,8.78118],"t":0},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[12,12],"t":15},{"s":[8.7812,8.7812],"t":30}]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[249.99999499999998,250.00001499999996]},"r":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":0},{"s":[360],"t":30}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"gr","bm":0,"ln":"_2456927096976","hd":false,"nm":"_2456927096976","it":[{"ty":"gr","bm":0,"hd":true,"nm":"Group 2","it":[{"ty":"sh","bm":0,"hd":false,"nm":"Path 11","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[194,83],[145,154],[-70,66],[-67,-70],[-150,-64],[-164,0],[-150,65],[-112,119],[-71,-67],[67,-70],[194,-84],[211,0]],"o":[[-212,0],[-194,-84],[-67,-70],[71,-67],[112,119],[150,65],[163,0],[150,-64],[67,-70],[70,66],[-145,154],[-194,83],[0,0]],"v":[[2118,1902],[1504,1776],[990,1415],[997,1167],[1246,1174],[1643,1452],[2118,1550],[2592,1452],[2989,1174],[3238,1167],[3245,1415],[2731,1776],[2118,1902]]}}},{"ty":"fl","bm":0,"hd":false,"nm":"Fill","c":{"a":0,"k":[1,1,1]},"r":2,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[2117.50004629844,1510.5843947960868]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[2117.50004629844,1510.5843947960868]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]},{"ty":"gr","bm":0,"hd":false,"nm":"Group 3","it":[{"ty":"sh","bm":0,"hd":false,"nm":"Path 12","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,-882],[881,0],[0,881],[-882,0]],"o":[[881,0],[0,881],[-882,0],[0,-882],[0,0]],"v":[[2118,1],[3713,1270],[2118,2866],[522,1270],[2118,1]]}}},{"ty":"fl","bm":0,"hd":false,"nm":"Fill","c":{"a":0,"k":[0.2627,0.651,0.949]},"r":2,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[2117.5,1433.5000000000002]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[2117.5,1433.5000000000002]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]},{"ty":"tr","a":{"a":0,"k":[2117.5,1433.5000000000002]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[2117.5,1433.5000000000002]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]}],"ind":3}],"v":"5.7.0","fr":30,"op":30,"ip":0,"assets":[{"nm":"Nested Scene 1","id":"precomp_newScene_abc3f55b-16ae-4191-8057-2b89484cea7e","layers":[]}]} \ No newline at end of file diff --git a/assets/rive_animations/loading.riv b/assets/rive_animations/loading.riv deleted file mode 100644 index d328f26d..00000000 Binary files a/assets/rive_animations/loading.riv and /dev/null differ diff --git a/ios/Flutter/ephemeral/flutter_lldb_helper.py b/ios/Flutter/ephemeral/flutter_lldb_helper.py new file mode 100644 index 00000000..a88caf99 --- /dev/null +++ b/ios/Flutter/ephemeral/flutter_lldb_helper.py @@ -0,0 +1,32 @@ +# +# Generated file, do not edit. +# + +import lldb + +def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict): + """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages.""" + base = frame.register["x0"].GetValueAsAddress() + page_len = frame.register["x1"].GetValueAsUnsigned() + + # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the + # first page to see if handled it correctly. This makes diagnosing + # misconfiguration (e.g. missing breakpoint) easier. + data = bytearray(page_len) + data[0:8] = b'IHELPED!' + + error = lldb.SBError() + frame.GetThread().GetProcess().WriteMemory(base, data, error) + if not error.Success(): + print(f'Failed to write into {base}[+{page_len}]', error) + return + +def __lldb_init_module(debugger: lldb.SBDebugger, _): + target = debugger.GetDummyTarget() + # Caveat: must use BreakpointCreateByRegEx here and not + # BreakpointCreateByName. For some reasons callback function does not + # get carried over from dummy target for the later. + bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$") + bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__)) + bp.SetAutoContinue(True) + print("-- LLDB integration loaded --") diff --git a/ios/Flutter/ephemeral/flutter_lldbinit b/ios/Flutter/ephemeral/flutter_lldbinit new file mode 100644 index 00000000..e3ba6fbe --- /dev/null +++ b/ios/Flutter/ephemeral/flutter_lldbinit @@ -0,0 +1,5 @@ +# +# Generated file, do not edit. +# + +command script import --relative-to-command-file flutter_lldb_helper.py diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 2566b21b..31db8575 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 = 16; + CURRENT_PROJECT_VERSION = 18; 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 = 16; + CURRENT_PROJECT_VERSION = 18; 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 = 16; + CURRENT_PROJECT_VERSION = 18; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 24f859fd..83164ead 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 16 + 18 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index dbcea1bc..d667c3b5 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -40,8 +40,9 @@ import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; import 'package:eatvio_app/features/plans/presentation/pages/about_plan_screen.dart'; import 'package:eatvio_app/features/profile/presentation/pages/profile_screen.dart'; import 'package:eatvio_app/features/profile/presentation/pages/subs_screen.dart'; -import 'package:eatvio_app/features/recipe/presentation/about_recipe_screen.dart'; +import 'package:eatvio_app/features/recipe/presentation/pages/about_recipe_screen.dart'; import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart'; +import 'package:eatvio_app/features/recipe/presentation/pages/my_recipes_screen.dart'; import 'package:eatvio_app/features/registration/presentation/pages/registration_screen.dart'; import 'package:eatvio_app/features/registration/presentation/pages/verify_screen.dart'; import 'package:eatvio_app/features/settings/entities/settings_entity.dart'; @@ -76,8 +77,11 @@ class AppRouter { name: 'authScreen', builder: (context, state) => const AuthScreen(), routes: [ - // GoRoute(path: 'webView-registration-screen', name: 'webViewRegistrationScreen', builder: (context, state) => const WebViewRegistrationScreen()), - GoRoute(path: 'registration-screen', name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()), + GoRoute( + path: 'registration-screen', + name: 'registrationScreen', + builder: (context, state) => const RegistrationScreen(), + ), ], ), ShellRoute( @@ -355,6 +359,11 @@ class AppRouter { name: 'verifyScreen', builder: (context, state) => VerifyScreen(passKey: state.extra as String), ), + GoRoute( + path: MyRecipesScreen.path, + name: MyRecipesScreen.name, + builder: (context, state) => const MyRecipesScreen(), + ), ], ); } diff --git a/lib/core/widgets/custom_popup.dart b/lib/core/widgets/custom_popup.dart index 67d87da5..a23c03c6 100644 --- a/lib/core/widgets/custom_popup.dart +++ b/lib/core/widgets/custom_popup.dart @@ -120,14 +120,14 @@ class CustomPopup extends StatelessWidget { if (complainedContentType == ComplainedContent.diet) { final result = await sl().unpublishDiet(id!); if (!result) { - showErrorDialog(context, title: S.current.could_not_delete_blog_post); + showErrorDialog(context, title: "Не удалось снять диету с публикации"); return; } showSuccessToast('Диета перенесена в архив, обновите ленту'); } else if (complainedContentType == ComplainedContent.plan) { final result = await sl().unpublish(slug!); if (!result) { - showErrorDialog(context, title: S.current.could_not_delete_blog_post); + showErrorDialog(context, title: "Не удалось снять план с публикации"); return; } showSuccessToast('План перенесен в архив, обновите ленту'); diff --git a/lib/core/widgets/loader.dart b/lib/core/widgets/loader.dart index dd4727dc..ae69e10f 100644 --- a/lib/core/widgets/loader.dart +++ b/lib/core/widgets/loader.dart @@ -1,17 +1,19 @@ import 'package:flutter/material.dart'; -import 'package:rive/rive.dart'; +import 'package:lottie/lottie.dart'; class Loader extends StatelessWidget { const Loader({super.key}); @override Widget build(BuildContext context) { - return const Center( + return Center( child: SizedBox( - height: 32, - width: 32, - child: RiveAnimation.asset( - 'assets/rive_animations/loading.riv', + height: 42, + width: 42, + child: Lottie.asset( + 'assets/lottie_animations/loading.json', + width: 42, + height: 42, ), ), ); diff --git a/lib/features/consultant/become_consultant/services/consultant_service.dart b/lib/features/consultant/become_consultant/services/consultant_service.dart index cc284993..fbf7541d 100644 --- a/lib/features/consultant/become_consultant/services/consultant_service.dart +++ b/lib/features/consultant/become_consultant/services/consultant_service.dart @@ -20,7 +20,7 @@ abstract class ConsultantService { @Part(name: 'photos[]', contentType: 'multipart/form-data') List? photos, }); - @GET('/api/consultant_request/{slug}/status') + @GET('/api/profile/consultant/{slug}/status') Future getConsultantRequestStatus({ @Path() required String slug, }); diff --git a/lib/features/menu/presentation/pages/menu_screen.dart b/lib/features/menu/presentation/pages/menu_screen.dart index 255c8510..6ccb0973 100644 --- a/lib/features/menu/presentation/pages/menu_screen.dart +++ b/lib/features/menu/presentation/pages/menu_screen.dart @@ -8,6 +8,7 @@ import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart'; import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart'; import 'package:eatvio_app/features/profile/models/profile_model.dart'; +import 'package:eatvio_app/features/recipe/presentation/pages/my_recipes_screen.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -28,7 +29,7 @@ class MenuScreen extends StatelessWidget { _tile(title: 'Настройки профиля', leading: ImageConstant.icnProfileSettings, onTap: () => GoRouter.of(context).push('/settings-screen')), if (profileModel.roles.contains('consultant')) _divider(context), if (profileModel.roles.contains('consultant')) - _tile(leading: ImageConstant.icnRecipes, title: 'Мои рецепты', onTap: () => GoRouter.of(context).push('/recipe-select-screen')), + _tile(leading: ImageConstant.icnRecipes, title: 'Мои рецепты', onTap: () => GoRouter.of(context).push(MyRecipesScreen.path)), if (profileModel.roles.contains('consultant')) _divider(context), if (profileModel.roles.contains('consultant')) _tile(leading: 'assets/icons/diets.svg', title: 'Мои диеты', onTap: () => GoRouter.of(context).push(MyDietsScreen.path)), diff --git a/lib/features/my_plans/services/my_plans_service.dart b/lib/features/my_plans/services/my_plans_service.dart index 9c5cbf0c..bc24aec8 100644 --- a/lib/features/my_plans/services/my_plans_service.dart +++ b/lib/features/my_plans/services/my_plans_service.dart @@ -8,7 +8,7 @@ part 'my_plans_service.g.dart'; abstract class MyPlansService { factory MyPlansService(Dio dio, {String baseUrl}) = _MyPlansService; - @GET('/api/plans/index') + @GET('/api/profile/my_plans') Future getPlans({ @Query('page') required int page, @Query('filters[name][\$startsWith]') String? query, diff --git a/lib/features/recipe/presentation/about_recipe_screen.dart b/lib/features/recipe/presentation/pages/about_recipe_screen.dart similarity index 100% rename from lib/features/recipe/presentation/about_recipe_screen.dart rename to lib/features/recipe/presentation/pages/about_recipe_screen.dart diff --git a/lib/features/recipe/presentation/pages/my_recipes_screen.dart b/lib/features/recipe/presentation/pages/my_recipes_screen.dart new file mode 100644 index 00000000..ad30f146 --- /dev/null +++ b/lib/features/recipe/presentation/pages/my_recipes_screen.dart @@ -0,0 +1,196 @@ +import 'package:easy_debounce/easy_debounce.dart'; +import 'package:eatvio_app/core/locator_service.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/add_item_button.dart'; +import 'package:eatvio_app/core/widgets/custom_search_bar.dart'; +import 'package:eatvio_app/core/widgets/data_error_widget.dart'; +import 'package:eatvio_app/core/widgets/loader.dart'; +import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart'; +import 'package:eatvio_app/features/feed/presentation/widgets/diet_filter_bottom_sheet.dart'; +import 'package:eatvio_app/features/feed/presentation/widgets/filter_badge.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_state.dart'; +import 'package:eatvio_app/features/my_diets/presentation/widgets/my_diet_widget.dart'; +import 'package:eatvio_app/features/recipe/presentation/widgets/my_recipes_tab.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +class MyRecipesScreen extends StatelessWidget { + const MyRecipesScreen({super.key}); + static const name = 'myRecipesScreen'; + static const path = '/my-recipes-screen'; + + @override + Widget build(BuildContext context) { + return BlocProvider(create: (context) => sl()..loadAllDiets(), child: MyRecipesScreenContent()); + } +} + +class MyRecipesScreenContent extends StatelessWidget { + MyRecipesScreenContent({super.key}); + + final activeDietsScrollController = ScrollController(); + final draftDietsScrollController = ScrollController(); + + final activeDietsSearchController = SearchController(); + final draftDietsSearchController = SearchController(); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: appTheme.gray100, + appBar: AppBar( + centerTitle: true, + title: Text( + 'Мои рецепты', + maxLines: 2, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + softWrap: true, + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + ), + body: BlocBuilder( + buildWhen: (previous, current) => previous.selectedStateIndex != current.selectedStateIndex, + builder: (context, state) { + return NestedScrollView( + controller: switch (state.selectedStateIndex) { + 1 => draftDietsScrollController, + _ => activeDietsScrollController + }, + headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { + return [ + SliverAppBar( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(bottom: Radius.circular(12)), + ), + backgroundColor: appTheme.gray100, + foregroundColor: appTheme.gray100, + automaticallyImplyLeading: false, + stretchTriggerOffset: 1, + floating: true, + primary: false, + snap: true, + scrolledUnderElevation: 0, + bottom: PreferredSize( + preferredSize: Size( + MediaQuery.sizeOf(context).width, + 64, + ), + child: Padding( + padding: const EdgeInsets.only(bottom: 0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 8), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 4), + child: MyRecipesTab(), + ), + const SizedBox(height: 8), + Row( + children: [ + const SizedBox(width: 4), + Expanded( + child: CustomSearchBar( + enableIcon: false, + backgroundColor: appTheme.whiteA700, + borderRadius: 100, + searchController: switch (state.selectedStateIndex) { + 1 => draftDietsSearchController, + _ => activeDietsSearchController + }, + onChanged: (query) { + EasyDebounce.debounce( + 'searchDiets', + const Duration(milliseconds: 500), + () { + context.read().loadCurrentDiets(query: query == '' ? null : query); + }, + ); + }, + ), + ), + const SizedBox(width: 12), + AddItemButton( + onTap: () => GoRouter.of(context).pushNamed(CreateDietScreen.name), + ), + const SizedBox(width: 4), + ], + ), + const SizedBox(height: 16), + ], + ), + ), + ), + ), + ]; + }, + body: RefreshIndicator( + onRefresh: () => context.read().loadCurrentDiets(), + child: BlocBuilder( + builder: (BuildContext context, MyDietsState state) { + if (state.isLoading) { + return Scaffold( + backgroundColor: Colors.grey.shade200, + body: const Loader(), + ); + } else if (state.error != null) { + return DataErrorWidget(onReload: () => context.read().loadAllDiets()); + } else { + final gotDietLoadingError = state.activeDietsError != null && state.selectedStateIndex == 0 || + state.draftDietsError != null && state.selectedStateIndex == 1; + + if (gotDietLoadingError) { + return DataErrorWidget(onReload: () => context.read().loadCurrentDiets()); + } + + if (state.isLoading) { + return const Loader(); + } + + final diets = switch (state.selectedStateIndex) { + 1 => state.draftDiets, + _ => state.activeDiets, + }; + + return Stack( + children: [ + const SizedBox.expand(), + ListView.separated( + shrinkWrap: true, + physics: const BouncingScrollPhysics(), + itemBuilder: (context, index) { + return MyDietWidget( + diet: diets[index], + key: ValueKey(diets[index].id), + ); + }, + separatorBuilder: (context, index) { + return const SizedBox(height: 20); + }, + itemCount: diets.length, + ), + Positioned( + right: 0, + bottom: 74, + child: FilterBadge( + filterBottomSheet: DietFilterBottomSheet( + myDietsCubit: context.read(), + ), + ), + ), + ], + ); + } + }, + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/features/recipe/presentation/widgets/my_recipes_tab.dart b/lib/features/recipe/presentation/widgets/my_recipes_tab.dart new file mode 100644 index 00000000..f6de8669 --- /dev/null +++ b/lib/features/recipe/presentation/widgets/my_recipes_tab.dart @@ -0,0 +1,68 @@ +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/animated_option_button.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/widgets.dart'; + +class MyRecipesTab extends StatefulWidget { + const MyRecipesTab({super.key, this.additionalOnTap}); + + final void Function()? additionalOnTap; + + @override + State createState() => _MyRecipesTabState(); +} + +class _MyRecipesTabState extends State { + final List tabTitles = []; + + @override + void didChangeDependencies() { + final s = S.of(context); + if (tabTitles.isEmpty) { + tabTitles.add(s.published); + tabTitles.add(s.drafted); + } + super.didChangeDependencies(); + } + + @override + Widget build(BuildContext context) { + return Align( + alignment: Alignment.centerLeft, + child: Row( + children: [ + Expanded( + child: Container( + height: 48, + decoration: BoxDecoration( + color: appTheme.whiteA700, + borderRadius: BorderRadius.circular(100), + ), + padding: const EdgeInsets.all(4), + child: Row( + children: [ + ...List.generate( + 2, + (index) => + Expanded( + child: AnimatedOptionButton( + textStyle: TextStyles.textSMedium, + isSelected: false, // state.selectedStateIndex == index, + text: tabTitles[index], + onTap: () { + widget.additionalOnTap?.call(); + // context.read().changeSelectedStateIndex(index); + }, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/registration/presentation/pages/verify_email_screen.dart b/lib/features/registration/presentation/pages/verify_email_screen.dart new file mode 100644 index 00000000..9f0da694 --- /dev/null +++ b/lib/features/registration/presentation/pages/verify_email_screen.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; + +class VerifyEmailScreen extends StatefulWidget { + const VerifyEmailScreen({super.key}); + + @override + State createState() => _VerifyEmailScreenState(); +} + +class _VerifyEmailScreenState extends State { + final List _focusNodes = List.generate(4, (_) => FocusNode()); + final List _controllers = List.generate(4, (_) => TextEditingController()); + + @override + void dispose() { + for (var controller in _controllers) { + controller.dispose(); + } + for (var node in _focusNodes) { + node.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text("Подтвердите почту")), + body: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text("Код отправлен на почту\nnazvanie@mail.com", textAlign: TextAlign.center), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate(4, (index) => _buildCodeField(index)), + ), + const SizedBox(height: 20), + const Text("Запросить код повторно", style: TextStyle(color: Colors.grey)), + TextButton( + onPressed: () { + // логика запроса нового кода + }, + child: const Text("Не приходит код?"), + ) + ], + ), + ), + ); + } + + Widget _buildCodeField(int index) { + return SizedBox( + width: 50, + child: TextField( + controller: _controllers[index], + focusNode: _focusNodes[index], + keyboardType: TextInputType.number, + textAlign: TextAlign.center, + maxLength: 1, + style: const TextStyle(fontSize: 24), + decoration: InputDecoration( + counterText: "", + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.0), + ), + ), + onChanged: (value) { + if (value.isNotEmpty && index < 3) { + FocusScope.of(context).requestFocus(_focusNodes[index + 1]); + } + if (value.isEmpty && index > 0) { + FocusScope.of(context).requestFocus(_focusNodes[index - 1]); + } + }, + ), + ); + } +} diff --git a/lib/features/registration/presentation/pages/verify_screen.dart b/lib/features/registration/presentation/pages/verify_screen.dart index 69fa009c..6059a0b5 100644 --- a/lib/features/registration/presentation/pages/verify_screen.dart +++ b/lib/features/registration/presentation/pages/verify_screen.dart @@ -1,5 +1,8 @@ import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:lottie/lottie.dart'; import 'package:webview_flutter/webview_flutter.dart'; class VerifyScreen extends StatefulWidget { @@ -22,6 +25,81 @@ class _VerifyScreenState extends State { super.initState(); _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate( + NavigationDelegate( + onNavigationRequest: (NavigationRequest request) async { + if (request.url.contains('user_detailz') && !request.url.contains('change-phone-verify') || request.url.contains('email/verify')) { + GoRouter.of(context).pop(); + await showModalBottomSheet( + useRootNavigator: true, + showDragHandle: true, + context: context, + builder: (context) => Container( + padding: const EdgeInsets.all(16), + width: MediaQuery.of(context).size.width, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Номер телефона\nуспешно привязан', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 20, + height: 1, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 16), + Lottie.asset( + 'assets/lottie_animations/success.json', + width: 160, + height: 160, + ), + const SizedBox(height: 16), + const Text( + 'Теперь Вам доступен весь функционал!', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + height: 1, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 16), + InkWell( + onTap: () { + Navigator.of(context).pop(); + GoRouter.of(context).pop(); + }, + child: Ink( + width: MediaQuery.of(context).size.width - 32, + height: 52, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: appTheme.blueA400, + ), + child: const Center( + child: Text( + 'Закрыть', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + ], + ), + ), + ); + return NavigationDecision.prevent; + } + return NavigationDecision.navigate; + }, + ), + ) ..loadRequest(Uri.parse('$baseUrl/login_with_pass_key?id=${widget.passKey}')); } diff --git a/pubspec.yaml b/pubspec.yaml index 5400a274..3b1a20b4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -47,7 +47,6 @@ dependencies: file_picker: ^10.1.9 audioplayers: ^6.4.0 intl: ^0.20.2 - rive: ^0.13.20 mask_text_input_formatter: ^2.9.0 lottie: ^3.3.1 flutter_portal: ^1.1.4 @@ -71,9 +70,6 @@ dependencies: version: ^3.0.2 package_info_plus: ^8.3.0 -dependency_overrides: - intl: ^0.19.0 - dev_dependencies: build_runner: ^2.4.15 freezed: ^3.0.6 @@ -93,7 +89,6 @@ flutter: - assets/audio/ - assets/icons/ - assets/splash/ - - assets/rive_animations/ - assets/lottie_animations/ flutter_intl: