From 1e1005a09c8fedc8997b2b7da9c2c933dc1867a5 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Fri, 23 May 2025 16:52:50 +0300 Subject: [PATCH 1/9] feat(email) add email verify screen, add my recipe screen, plans/index -> my_plans --- .gitea/workflows/build.yml | 2 +- .gitea/workflows/docker-run-tests.yml | 2 +- .gitea/workflows/test.yml | 2 +- ios/Flutter/ephemeral/flutter_lldb_helper.py | 32 +++ ios/Flutter/ephemeral/flutter_lldbinit | 5 + ios/Runner.xcodeproj/project.pbxproj | 6 +- ios/Runner/Info.plist | 2 +- lib/core/app_router.dart | 14 +- .../menu/presentation/pages/menu_screen.dart | 3 +- .../my_plans/services/my_plans_service.dart | 2 +- .../{ => pages}/about_recipe_screen.dart | 0 .../presentation/pages/my_recipes_screen.dart | 196 ++++++++++++++++++ .../presentation/widgets/my_recipes_tab.dart | 68 ++++++ .../pages/verify_email_screen.dart | 81 ++++++++ pubspec.yaml | 3 - 15 files changed, 404 insertions(+), 14 deletions(-) create mode 100644 ios/Flutter/ephemeral/flutter_lldb_helper.py create mode 100644 ios/Flutter/ephemeral/flutter_lldbinit rename lib/features/recipe/presentation/{ => pages}/about_recipe_screen.dart (100%) create mode 100644 lib/features/recipe/presentation/pages/my_recipes_screen.dart create mode 100644 lib/features/recipe/presentation/widgets/my_recipes_tab.dart create mode 100644 lib/features/registration/presentation/pages/verify_email_screen.dart 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/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..ff6e8a89 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 = 17; 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 = 17; 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 = 17; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 24f859fd..90850ad3 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 16 + 17 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index dbcea1bc..d8805f73 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'; @@ -77,7 +78,11 @@ class AppRouter { 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 +360,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/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..d801b778 --- /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: Text("Подтвердите почту")), + body: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text("Код отправлен на почту\nnazvanie@mail.com", textAlign: TextAlign.center), + SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate(4, (index) => _buildCodeField(index)), + ), + SizedBox(height: 20), + Text("Запросить код повторно", style: TextStyle(color: Colors.grey)), + TextButton( + onPressed: () { + // логика запроса нового кода + }, + child: 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: 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/pubspec.yaml b/pubspec.yaml index 5400a274..dff61dd4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -71,9 +71,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 From 29b3de325c0bea2081f880013abeb4544fada2d9 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Sat, 24 May 2025 16:52:23 +0300 Subject: [PATCH 2/9] fix: analyzer --- .../kotlin-compiler-3965398408914680734.salive | 0 .../presentation/pages/verify_email_screen.dart | 14 +++++++------- 2 files changed, 7 insertions(+), 7 deletions(-) create mode 100644 android/.kotlin/sessions/kotlin-compiler-3965398408914680734.salive diff --git a/android/.kotlin/sessions/kotlin-compiler-3965398408914680734.salive b/android/.kotlin/sessions/kotlin-compiler-3965398408914680734.salive new file mode 100644 index 00000000..e69de29b diff --git a/lib/features/registration/presentation/pages/verify_email_screen.dart b/lib/features/registration/presentation/pages/verify_email_screen.dart index d801b778..9f0da694 100644 --- a/lib/features/registration/presentation/pages/verify_email_screen.dart +++ b/lib/features/registration/presentation/pages/verify_email_screen.dart @@ -25,25 +25,25 @@ class _VerifyEmailScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: Text("Подтвердите почту")), + appBar: AppBar(title: const Text("Подтвердите почту")), body: Padding( padding: const EdgeInsets.all(24.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text("Код отправлен на почту\nnazvanie@mail.com", textAlign: TextAlign.center), - SizedBox(height: 20), + const Text("Код отправлен на почту\nnazvanie@mail.com", textAlign: TextAlign.center), + const SizedBox(height: 20), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: List.generate(4, (index) => _buildCodeField(index)), ), - SizedBox(height: 20), - Text("Запросить код повторно", style: TextStyle(color: Colors.grey)), + const SizedBox(height: 20), + const Text("Запросить код повторно", style: TextStyle(color: Colors.grey)), TextButton( onPressed: () { // логика запроса нового кода }, - child: Text("Не приходит код?"), + child: const Text("Не приходит код?"), ) ], ), @@ -60,7 +60,7 @@ class _VerifyEmailScreenState extends State { keyboardType: TextInputType.number, textAlign: TextAlign.center, maxLength: 1, - style: TextStyle(fontSize: 24), + style: const TextStyle(fontSize: 24), decoration: InputDecoration( counterText: "", border: OutlineInputBorder( From cf5b4597567303708d5d81bda06ae41da7b0027d Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 26 May 2025 13:37:33 +0300 Subject: [PATCH 3/9] fix: phone-verify redirect --- ...kotlin-compiler-3965398408914680734.salive | 0 .../reports/problems/problems-report.html | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- android/settings.gradle | 2 +- assets/lottie_animations/loading.json | 1 + assets/rive_animations/loading.riv | Bin 969 -> 0 bytes lib/core/const.dart | 4 +- lib/core/widgets/custom_popup.dart | 4 +- lib/core/widgets/loader.dart | 14 ++-- .../presentation/pages/verify_screen.dart | 78 ++++++++++++++++++ pubspec.yaml | 2 - 11 files changed, 94 insertions(+), 15 deletions(-) delete mode 100644 android/.kotlin/sessions/kotlin-compiler-3965398408914680734.salive create mode 100644 assets/lottie_animations/loading.json delete mode 100644 assets/rive_animations/loading.riv diff --git a/android/.kotlin/sessions/kotlin-compiler-3965398408914680734.salive b/android/.kotlin/sessions/kotlin-compiler-3965398408914680734.salive deleted file mode 100644 index e69de29b..00000000 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 d328f26d4951ef0b71a5d1e83ba1f03527d176f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 969 zcma)*TSyd97{|Y}dp2buFKcYhX1YXz2&JouS*vq)T!qbgshK%M5NxF-ropzM)nLT!)r{>|nob&ts=lg&EnZq8n zBnGrk`@(1fEucl?B%Z?4cm~hnIXsUS@Dh$-`1jcgXfr@!aU8&!6h;sO@Is0O=$00Z z!U6&u>+<pm-LuUhVd~*GA_p%I6e;1CVWQf`vVt^X@8iV#LaYfX^R}S;=6ac^ zOdimP+VaF2q@9U5oVZ(M)Px6>){y*eSs=>VE(cNFRn7oy3g>d7wdPewI3^{9S{}wNIPt|IMLO9JS5y}>L}QOxXi)nDNWP`7o%%hf8Q z#J#-!Dg>49{kcSa>T!y6EXTq7s4c=0pT~|`Pe6HWW?nAtwi0^hpvH;%*=84?>6iOM zy~^{QeMJ3{noVdV9RACTo`$SDP^OpJtSzQ(>Fey#BV<{|YAiiVG`%FA9zdqXz0>hw z7x(_CW7x%VOiI?j03lS+9%RH&Krz!1J?SBlR@p{45uG2?kocW?CI1$5wO<X1TJu{A^`Sx!tk1uApu|0wLD|w!C;qbt6bEu5+W(iH1SKwi4Kup(M2w zph09O&ge@;MVJ0k_vgfxm-v5RUVi)$zt>-GKjb}mCfWfMG@+P--uiN%-2ntdaNR}* RD1;*LH6iwMO&|p4;U`uuCL#a; diff --git a/lib/core/const.dart b/lib/core/const.dart index 31c47e9c..4f38a54e 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -1,5 +1,5 @@ -String baseUrl = 'https://eatvio.ru'; -// String baseUrl = 'https://dev2.eatvio.com'; +// String baseUrl = 'https://eatvio.ru'; +String baseUrl = 'https://dev2.eatvio.com'; const List durationValues = [7, 14, 21]; 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/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 dff61dd4..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 @@ -90,7 +89,6 @@ flutter: - assets/audio/ - assets/icons/ - assets/splash/ - - assets/rive_animations/ - assets/lottie_animations/ flutter_intl: From f38eead280db2fdcd27224ff79384a47a806d88f Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 26 May 2025 13:48:55 +0300 Subject: [PATCH 4/9] fix: consultant profile --- ios/Runner.xcodeproj/project.pbxproj | 6 +++--- ios/Runner/Info.plist | 2 +- lib/core/app_router.dart | 1 - lib/core/const.dart | 4 ++-- .../become_consultant/services/consultant_service.dart | 2 +- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index ff6e8a89..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 = 17; + 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 = 17; + 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 = 17; + 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 90850ad3..83164ead 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 17 + 18 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index d8805f73..d667c3b5 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -77,7 +77,6 @@ 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', diff --git a/lib/core/const.dart b/lib/core/const.dart index 4f38a54e..31c47e9c 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -1,5 +1,5 @@ -// String baseUrl = 'https://eatvio.ru'; -String baseUrl = 'https://dev2.eatvio.com'; +String baseUrl = 'https://eatvio.ru'; +// String baseUrl = 'https://dev2.eatvio.com'; const List durationValues = [7, 14, 21]; 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, }); From 51d3c1ad2f346adb9baf971f32fc833d3ff826a9 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 26 May 2025 14:26:12 +0300 Subject: [PATCH 5/9] fix: avatars --- .../blog_post/presentation/pages/blog_post_screen.dart | 2 +- .../bloc/profile_avatar_cubit/profile_avatar_cubit.dart | 6 +++++- .../presentation/widgets/bottom_navigation_bar_widget.dart | 3 ++- lib/features/chat/presentation/pages/chat_screen.dart | 3 ++- .../chat/presentation/widgets/chat_entry_widget.dart | 3 ++- .../feed/presentation/widgets/blog_post_widget.dart | 6 ++++-- .../feed/presentation/widgets/consultant_widget.dart | 2 +- lib/features/feed/presentation/widgets/plan_widget.dart | 2 +- lib/features/profile/presentation/pages/profile_screen.dart | 2 +- .../presentation/widgets/profile_blog_post_widget.dart | 2 +- lib/features/profile/presentation/widgets/sub_widget.dart | 3 ++- 11 files changed, 22 insertions(+), 12 deletions(-) diff --git a/lib/features/blog_post/presentation/pages/blog_post_screen.dart b/lib/features/blog_post/presentation/pages/blog_post_screen.dart index fe3e7bda..ad85dcc7 100644 --- a/lib/features/blog_post/presentation/pages/blog_post_screen.dart +++ b/lib/features/blog_post/presentation/pages/blog_post_screen.dart @@ -186,7 +186,7 @@ class _BlogPostScreenState extends State { CircleAvatar( radius: 16, foregroundImage: NetworkImage( - widget.blogPost.author.authorProfileIcon, + baseUrl + widget.blogPost.author.authorProfileIcon, ), backgroundImage: const AssetImage('assets/images/no_avatar.png'), ), diff --git a/lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart b/lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart index 4655d87d..ae45a6ca 100644 --- a/lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart +++ b/lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart @@ -12,7 +12,11 @@ class ProfileAvatarCubit extends Cubit { getMyProfile() async { emit(const ProfileAvatarState.loading()); try { - emit(ProfileAvatarState.loaded(profile: await profileRepository.getMyProfile())); + emit( + ProfileAvatarState.loaded( + profile: await profileRepository.getMyProfile(), + ), + ); } catch (e) { emit(ProfileAvatarState.error(errorMessage: 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 52b9b838..9584c009 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 @@ -1,4 +1,5 @@ import 'package:badges/badges.dart' as badges; +import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/utils/image_constant.dart'; import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_state.dart'; @@ -81,7 +82,7 @@ class _BottomNavigationBarWidgetState extends State { BottomNavigationBarItem( label: '', icon: CircleAvatar( - foregroundImage: NetworkImage(widget.profileModel.profilePhotoUrl), + foregroundImage: NetworkImage(baseUrl + widget.profileModel.profilePhotoUrl), backgroundImage: const AssetImage('assets/images/no_avatar.png'), radius: 16, ), diff --git a/lib/features/chat/presentation/pages/chat_screen.dart b/lib/features/chat/presentation/pages/chat_screen.dart index b20646b9..537aa9e9 100644 --- a/lib/features/chat/presentation/pages/chat_screen.dart +++ b/lib/features/chat/presentation/pages/chat_screen.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'dart:math'; import 'package:easy_debounce/easy_debounce.dart'; +import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; import 'package:eatvio_app/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; @@ -141,7 +142,7 @@ class _ChatScreenState extends State { if (state.chatRoom.users.receiver.profilePhotoUrl != null) CircleAvatar( radius: 20, - foregroundImage: NetworkImage(state.chatRoom.users.receiver.profilePhotoUrl!), + foregroundImage: NetworkImage(baseUrl + state.chatRoom.users.receiver.profilePhotoUrl!), backgroundImage: const AssetImage('assets/images/no_avatar.png'), ), if (state.chatRoom.users.receiver.profilePhotoUrl == null) diff --git a/lib/features/chat/presentation/widgets/chat_entry_widget.dart b/lib/features/chat/presentation/widgets/chat_entry_widget.dart index 1338c60b..a6d5b3bb 100644 --- a/lib/features/chat/presentation/widgets/chat_entry_widget.dart +++ b/lib/features/chat/presentation/widgets/chat_entry_widget.dart @@ -1,3 +1,4 @@ +import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart'; import 'package:eatvio_app/features/chat/models/chat_room_pagination_model.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; @@ -41,7 +42,7 @@ class _ChatEntryWidgetState extends State { CircleAvatar( radius: 24, foregroundImage: NetworkImage( - widget.chatRoom.users.toChatUser.profilePhotoUrl, + baseUrl + widget.chatRoom.users.toChatUser.profilePhotoUrl, ), backgroundImage: const AssetImage('assets/images/no_avatar.png'), backgroundColor: Colors.white, diff --git a/lib/features/feed/presentation/widgets/blog_post_widget.dart b/lib/features/feed/presentation/widgets/blog_post_widget.dart index c5f6ff5f..542fe274 100644 --- a/lib/features/feed/presentation/widgets/blog_post_widget.dart +++ b/lib/features/feed/presentation/widgets/blog_post_widget.dart @@ -78,7 +78,7 @@ class _BlogPostWidgetState extends State { radius: 20, backgroundColor: Colors.black12, foregroundImage: NetworkImage( - state.feedBlogPostList[widget.index].author.authorProfileIcon, + baseUrl + state.feedBlogPostList[widget.index].author.authorProfileIcon, ), backgroundImage: const AssetImage('assets/images/no_avatar.png'), ), @@ -245,7 +245,9 @@ class _BlogPostWidgetState extends State { final box = context.findRenderObject() as RenderBox?; SharePlus.instance.share( ShareParams( - uri: Uri.parse('$baseUrl/ru/blog_post/${state.feedBlogPostList[widget.index].id}',), + uri: Uri.parse( + '$baseUrl/ru/blog_post/${state.feedBlogPostList[widget.index].id}', + ), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); diff --git a/lib/features/feed/presentation/widgets/consultant_widget.dart b/lib/features/feed/presentation/widgets/consultant_widget.dart index 5246d348..6e8562f3 100644 --- a/lib/features/feed/presentation/widgets/consultant_widget.dart +++ b/lib/features/feed/presentation/widgets/consultant_widget.dart @@ -66,7 +66,7 @@ class ConsultantWidget extends StatelessWidget { ), const SizedBox(width: 8), CircleAvatar( - foregroundImage: NetworkImage(consultant.photo), + foregroundImage: NetworkImage(baseUrl + consultant.photo), backgroundImage: const AssetImage('assets/images/no_avatar.png'), ), const SizedBox(width: 8), diff --git a/lib/features/feed/presentation/widgets/plan_widget.dart b/lib/features/feed/presentation/widgets/plan_widget.dart index 849549e8..e47ca1db 100644 --- a/lib/features/feed/presentation/widgets/plan_widget.dart +++ b/lib/features/feed/presentation/widgets/plan_widget.dart @@ -185,7 +185,7 @@ class _PlanWidgetState extends State { decoration: BoxDecoration( shape: BoxShape.circle, image: DecorationImage( - image: NetworkImage(widget.plan.author.authorProfileIcon), + image: NetworkImage(baseUrl + widget.plan.author.authorProfileIcon), fit: BoxFit.cover, ), ), diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index 79c41c54..64c7dda7 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -1044,7 +1044,7 @@ class _ProfileScreenState extends State { tag: state.profile.profilePhotoUrl, child: CircleAvatar( radius: 32, - foregroundImage: NetworkImage(state.profile.profilePhotoUrl), + foregroundImage: NetworkImage(baseUrl + state.profile.profilePhotoUrl), backgroundImage: const AssetImage('assets/images/no_avatar.png'), ), ), diff --git a/lib/features/profile/presentation/widgets/profile_blog_post_widget.dart b/lib/features/profile/presentation/widgets/profile_blog_post_widget.dart index 43ade18c..10afdd8c 100644 --- a/lib/features/profile/presentation/widgets/profile_blog_post_widget.dart +++ b/lib/features/profile/presentation/widgets/profile_blog_post_widget.dart @@ -66,7 +66,7 @@ class _ProfileBlogPostWidgetState extends State { radius: 20, backgroundColor: Colors.black12, foregroundImage: NetworkImage( - state.blogPosts[widget.index].author.authorProfileIcon, + baseUrl + state.blogPosts[widget.index].author.authorProfileIcon, ), backgroundImage: const AssetImage('assets/images/no_avatar.png'), ), diff --git a/lib/features/profile/presentation/widgets/sub_widget.dart b/lib/features/profile/presentation/widgets/sub_widget.dart index c8342077..aa17433d 100644 --- a/lib/features/profile/presentation/widgets/sub_widget.dart +++ b/lib/features/profile/presentation/widgets/sub_widget.dart @@ -1,3 +1,4 @@ +import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:eatvio_app/features/profile/models/sub_model.dart'; import 'package:eatvio_app/features/profile/presentation/widgets/sub_action_button_widget.dart'; @@ -35,7 +36,7 @@ class _SubWidgetState extends State { child: Row( children: [ CircleAvatar( - foregroundImage: NetworkImage(widget.subModel.profilePhotoUrl), + foregroundImage: NetworkImage(baseUrl + widget.subModel.profilePhotoUrl), backgroundImage: const AssetImage('assets/images/no_avatar.png'), radius: 28, ), From 67bf7b05100b75633b523dcad57e9c37c910447a Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Mon, 26 May 2025 17:56:29 +0300 Subject: [PATCH 6/9] feat(email): verify screen --- devtools_options.yaml | 3 +- lib/core/app_router.dart | 6 ++ lib/core/locator_service.dart | 2 + .../presentation/widgets/author_card.dart | 3 +- .../presentation/widgets/comments_widget.dart | 3 +- .../menu/presentation/pages/menu_screen.dart | 1 + .../repositories/profile_repository.dart | 12 ++- .../verify_email_cubit.dart | 35 ++++++++ .../verify_email_state.dart | 24 +++++ .../pages/verify_email_screen.dart | 88 +++++++++++++------ .../presentation/pages/verify_screen.dart | 1 - pubspec.yaml | 1 + 12 files changed, 146 insertions(+), 33 deletions(-) create mode 100644 lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart create mode 100644 lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart diff --git a/devtools_options.yaml b/devtools_options.yaml index 5c27c3e3..70935407 100644 --- a/devtools_options.yaml +++ b/devtools_options.yaml @@ -1,2 +1,3 @@ extensions: - - provider: true \ No newline at end of file + - provider: true + - shared_preferences: true \ No newline at end of file diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index d667c3b5..c5e2b77a 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -44,6 +44,7 @@ import 'package:eatvio_app/features/recipe/presentation/pages/about_recipe_scree 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_email_screen.dart'; import 'package:eatvio_app/features/registration/presentation/pages/verify_screen.dart'; import 'package:eatvio_app/features/settings/entities/settings_entity.dart'; import 'package:eatvio_app/features/settings/presentation/pages/change_email_screen.dart'; @@ -359,6 +360,11 @@ class AppRouter { name: 'verifyScreen', builder: (context, state) => VerifyScreen(passKey: state.extra as String), ), + GoRoute( + path: '/verify-email-screen', + name: 'verifyEmailScreen', + builder: (context, state) => const VerifyEmailScreen(), + ), GoRoute( path: MyRecipesScreen.path, name: MyRecipesScreen.name, diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index fe28d2ec..a606b5bc 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -84,6 +84,7 @@ import 'package:eatvio_app/features/recipe/presentation/cubit/about_recipe_cubit import 'package:eatvio_app/features/recipe/repositories/about_recipe_repository.dart'; import 'package:eatvio_app/features/recipe/services/about_recipe_service.dart'; import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart'; +import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart'; import 'package:eatvio_app/features/registration/repositories/registration_repository.dart'; import 'package:eatvio_app/features/registration/services/registration_service.dart'; import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart'; @@ -138,6 +139,7 @@ Future init() async { sl.registerFactory(() => ConsultationsCubit(consultationsRepository: sl())); sl.registerFactory(() => ConsultantCommentsCubit(diaryRepository: sl())); sl.registerFactory(() => CancelPlanCubit(cancelPlanRepository: sl())); + sl.registerFactory(() => VerifyEmailCubit(profileRepository: sl())); // Repository sl.registerLazySingleton(() => ChatRepository(chatService: sl())); diff --git a/lib/features/diets/presentation/widgets/author_card.dart b/lib/features/diets/presentation/widgets/author_card.dart index c1113f93..6fff0b93 100644 --- a/lib/features/diets/presentation/widgets/author_card.dart +++ b/lib/features/diets/presentation/widgets/author_card.dart @@ -1,3 +1,4 @@ +import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/features/diets/presentation/widgets/info_bouble.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; @@ -36,7 +37,7 @@ class AuthorCard extends StatelessWidget { decoration: BoxDecoration( shape: BoxShape.circle, image: DecorationImage( - image: NetworkImage(author.authorProfileIcon), + image: NetworkImage(baseUrl + author.authorProfileIcon), fit: BoxFit.cover, ), ), diff --git a/lib/features/feed/presentation/widgets/comments_widget.dart b/lib/features/feed/presentation/widgets/comments_widget.dart index b12b691e..16f52f90 100644 --- a/lib/features/feed/presentation/widgets/comments_widget.dart +++ b/lib/features/feed/presentation/widgets/comments_widget.dart @@ -1,3 +1,4 @@ +import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; @@ -77,7 +78,7 @@ class _CommentsWidgetState extends State { ); }, child: CircleAvatar( - foregroundImage: NetworkImage(comment.authorProfileIcon), + foregroundImage: NetworkImage(baseUrl + comment.authorProfileIcon), backgroundImage: const AssetImage('assets/images/no_avatar.png'), radius: radius, ), diff --git a/lib/features/menu/presentation/pages/menu_screen.dart b/lib/features/menu/presentation/pages/menu_screen.dart index 6ccb0973..72532065 100644 --- a/lib/features/menu/presentation/pages/menu_screen.dart +++ b/lib/features/menu/presentation/pages/menu_screen.dart @@ -58,6 +58,7 @@ class MenuScreen extends StatelessWidget { ), _divider(context), _tile(leading: ImageConstant.icnExit, title: 'Выход', onTap: _onTapLogout(context)), + _tile(leading: ImageConstant.icnExit, title: 'Verify email', onTap: () => GoRouter.of(context).pushNamed('verifyEmailScreen')), ], ), ), diff --git a/lib/features/profile/repositories/profile_repository.dart b/lib/features/profile/repositories/profile_repository.dart index 46b3bcf6..75879d15 100644 --- a/lib/features/profile/repositories/profile_repository.dart +++ b/lib/features/profile/repositories/profile_repository.dart @@ -183,7 +183,8 @@ class ProfileRepository { } Future getPassKey(String phone) async { - return (jsonDecode((await profileService.getPassKey(json: '{"route_name": "user_detailz.change-phone-verify", "route_params": {"phone": "$phone"}}'))))['key_is']!; + return (jsonDecode( + (await profileService.getPassKey(json: '{"route_name": "user_detailz.change-phone-verify", "route_params": {"phone": "$phone"}}'))))['key_is']!; } Future getMyPhone() async { @@ -194,7 +195,12 @@ class ProfileRepository { return await profileService.sendEmailCode(); } - Future tryVerifyEmailByCode(String code) async { - return await profileService.tryVerifyEmailByCode(code: "{\"code\": \"$code\"}"); + Future tryVerifyEmailByCode(String code) async { + try { + await profileService.tryVerifyEmailByCode(code: "{\"code\": \"$code\"}"); + return true; + } catch (e) { + return false; + } } } diff --git a/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart b/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart new file mode 100644 index 00000000..864951ae --- /dev/null +++ b/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart @@ -0,0 +1,35 @@ +import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; +import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class VerifyEmailCubit extends Cubit { + final ProfileRepository profileRepository; + + VerifyEmailCubit({ + required this.profileRepository, + }) : super(const VerifyEmailState.loading()); + + sendEmailCode() async { + try { + final profile = await profileRepository.getMyProfile(); + await profileRepository.sendEmailCode(); + emit(VerifyEmailState.loaded(email: profile.email)); + } catch (e) { + emit(const VerifyEmailState.error(errorMessage: 'Ошибка отправки кода.\nПовторите попытку позже')); + } + } + + tryVerify(String code) async { + try { + emit(const VerifyEmailState.verifyLoading()); + final result = await profileRepository.tryVerifyEmailByCode(code); + if (result == true) { + emit(const VerifyEmailState.verifySuccess()); + } else { + emit(const VerifyEmailState.verifyError()); + } + } catch (e) { + emit(const VerifyEmailState.verifyError()); + } + } +} diff --git a/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart b/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart new file mode 100644 index 00000000..718b2cce --- /dev/null +++ b/lib/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart @@ -0,0 +1,24 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'verify_email_state.freezed.dart'; + +@freezed +abstract class VerifyEmailState with _$VerifyEmailState { + const factory VerifyEmailState.loading() = VerifyEmailStateLoading; + + const factory VerifyEmailState.loaded({ + required String email, + }) = VerifyEmailStateLoaded; + + const factory VerifyEmailState.error({ + required String errorMessage, + }) = VerifyEmailStateError; + + const factory VerifyEmailState() = _VerifyEmailState; + + const factory VerifyEmailState.verifyLoading() = VerifyEmailStateVerifyLoading; + + const factory VerifyEmailState.verifySuccess() = VerifyEmailStateVerifySuccess; + + const factory VerifyEmailState.verifyError() = VerifyEmailStateVerifyError; +} diff --git a/lib/features/registration/presentation/pages/verify_email_screen.dart b/lib/features/registration/presentation/pages/verify_email_screen.dart index 9f0da694..75f38712 100644 --- a/lib/features/registration/presentation/pages/verify_email_screen.dart +++ b/lib/features/registration/presentation/pages/verify_email_screen.dart @@ -1,4 +1,10 @@ +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/utils/toasts.dart'; +import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart'; +import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; class VerifyEmailScreen extends StatefulWidget { const VerifyEmailScreen({super.key}); @@ -10,6 +16,7 @@ class VerifyEmailScreen extends StatefulWidget { class _VerifyEmailScreenState extends State { final List _focusNodes = List.generate(4, (_) => FocusNode()); final List _controllers = List.generate(4, (_) => TextEditingController()); + String email = ''; @override void dispose() { @@ -22,36 +29,66 @@ class _VerifyEmailScreenState extends State { super.dispose(); } + void _checkAndSubmitCode(BuildContext context) { + final code = _controllers.map((c) => c.text).join(); + if (code.length == 4) { + context.read().tryVerify(code); + } + } + @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)), + return BlocProvider( + create: (_) => sl()..sendEmailCode(), + child: BlocConsumer( + listener: (context, state) { + if (state is VerifyEmailStateLoaded) { + setState(() { + email = state.email; + }); + } else if (state is VerifyEmailStateVerifySuccess) { + showSuccessToast('Почта успешно подтверждена'); + GoRouter.of(context).pop(); + } else if (state is VerifyEmailStateVerifyError) { + showErrorToast("Неверный код. Попробуйте снова."); + for (var controller in _controllers) { + controller.clear(); + } + FocusScope.of(context).requestFocus(_focusNodes[0]); + } + }, + builder: (context, state) { + return Scaffold( + appBar: AppBar(title: const Text("Подтвердите почту")), + body: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text("Код отправлен на почту\n$email", textAlign: TextAlign.center), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate(4, (index) => _buildCodeField(context, index)), + ), + const SizedBox(height: 20), + const Text("Запросить код повторно", style: TextStyle(color: Colors.grey)), + TextButton( + onPressed: () { + context.read().sendEmailCode(); + }, + child: const Text("Не приходит код?"), + ), + ], + ), ), - const SizedBox(height: 20), - const Text("Запросить код повторно", style: TextStyle(color: Colors.grey)), - TextButton( - onPressed: () { - // логика запроса нового кода - }, - child: const Text("Не приходит код?"), - ) - ], - ), + ); + }, ), ); } - Widget _buildCodeField(int index) { + Widget _buildCodeField(BuildContext context, int index) { return SizedBox( width: 50, child: TextField( @@ -63,9 +100,7 @@ class _VerifyEmailScreenState extends State { style: const TextStyle(fontSize: 24), decoration: InputDecoration( counterText: "", - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8.0), - ), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)), ), onChanged: (value) { if (value.isNotEmpty && index < 3) { @@ -74,6 +109,7 @@ class _VerifyEmailScreenState extends State { if (value.isEmpty && index > 0) { FocusScope.of(context).requestFocus(_focusNodes[index - 1]); } + _checkAndSubmitCode(context); }, ), ); diff --git a/lib/features/registration/presentation/pages/verify_screen.dart b/lib/features/registration/presentation/pages/verify_screen.dart index 6059a0b5..7f8aede5 100644 --- a/lib/features/registration/presentation/pages/verify_screen.dart +++ b/lib/features/registration/presentation/pages/verify_screen.dart @@ -69,7 +69,6 @@ class _VerifyScreenState extends State { InkWell( onTap: () { Navigator.of(context).pop(); - GoRouter.of(context).pop(); }, child: Ink( width: MediaQuery.of(context).size.width - 32, diff --git a/pubspec.yaml b/pubspec.yaml index 3b1a20b4..9821d31a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -79,6 +79,7 @@ dev_dependencies: flutter_test: sdk: flutter + flutter_lints: ^5.0.0 flutter: From 242cb474ccb67c5d80570fea1bff0920ecc2667b Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Tue, 27 May 2025 16:50:36 +0300 Subject: [PATCH 7/9] feat(email): add alert for phone verify and email verify after registration --- lib/core/dio/phone_verify_error_interceptor.dart | 3 +++ lib/features/menu/presentation/pages/menu_screen.dart | 1 - .../bloc/registration_cubit/registration_cubit.dart | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/core/dio/phone_verify_error_interceptor.dart b/lib/core/dio/phone_verify_error_interceptor.dart index 5d764cff..ab6188bf 100644 --- a/lib/core/dio/phone_verify_error_interceptor.dart +++ b/lib/core/dio/phone_verify_error_interceptor.dart @@ -1,6 +1,7 @@ import 'package:dio/dio.dart'; import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/widgets/show_error_dialog.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:go_router/go_router.dart'; @@ -13,6 +14,8 @@ class PhoneVerifyErrorInterceptor extends Interceptor { if (data is Map && data['type'] == 'unverificated_phone') { final String phone = await sl().getMyPhone(); final String passKey = await sl().getPassKey(phone); + await showErrorDialog(AppRouter.navigatorKey.currentContext!, + title: 'Привяжите номер телефона', description: 'Для того, что бы выполнить это действие необходимо привязать номер телефона'); AppRouter.navigatorKey.currentContext?.pushNamed('verifyScreen', extra: passKey); return handler.reject( DioException( diff --git a/lib/features/menu/presentation/pages/menu_screen.dart b/lib/features/menu/presentation/pages/menu_screen.dart index 72532065..6ccb0973 100644 --- a/lib/features/menu/presentation/pages/menu_screen.dart +++ b/lib/features/menu/presentation/pages/menu_screen.dart @@ -58,7 +58,6 @@ class MenuScreen extends StatelessWidget { ), _divider(context), _tile(leading: ImageConstant.icnExit, title: 'Выход', onTap: _onTapLogout(context)), - _tile(leading: ImageConstant.icnExit, title: 'Verify email', onTap: () => GoRouter.of(context).pushNamed('verifyEmailScreen')), ], ), ), 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 7b371f22..5956f579 100644 --- a/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart +++ b/lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart @@ -59,6 +59,7 @@ class RegistrationCubit extends Cubit { ); await TokenStorage.saveTokenInStorage(token); AppRouter.navigatorKey.currentContext?.go('/feed-screen'); + AppRouter.navigatorKey.currentContext?.goNamed('verifyEmailScreen'); } catch (e) { log(e.toString()); } From 566a96b18eed6bcd1b1436f9b8e9495579054e92 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Tue, 27 May 2025 17:38:46 +0300 Subject: [PATCH 8/9] feat(recipe): my recipes screen --- lib/core/locator_service.dart | 6 + lib/features/feed/models/recipes_model.dart | 4 +- .../presentation/widgets/my_diet_widget.dart | 1 + .../recipe/models/my_recipe_model.dart | 36 +++ .../my_recipes_cubit/my_recieps_cubit.dart | 21 ++ .../my_recipes_cubit/my_recipes_state.dart | 17 ++ .../presentation/pages/my_recipes_screen.dart | 184 ++----------- .../widgets/my_recipe_widget.dart | 242 ++++++++++++++++++ .../repositories/my_recipes_repository.dart | 16 ++ .../recipe/services/my_recipes_service.dart | 13 + 10 files changed, 372 insertions(+), 168 deletions(-) create mode 100644 lib/features/recipe/models/my_recipe_model.dart create mode 100644 lib/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart create mode 100644 lib/features/recipe/presentation/cubit/my_recipes_cubit/my_recipes_state.dart create mode 100644 lib/features/recipe/presentation/widgets/my_recipe_widget.dart create mode 100644 lib/features/recipe/repositories/my_recipes_repository.dart create mode 100644 lib/features/recipe/services/my_recipes_service.dart diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index a606b5bc..14af4917 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -81,8 +81,11 @@ import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recip import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart'; import 'package:eatvio_app/features/create_recipe/services/recipe_service.dart'; import 'package:eatvio_app/features/recipe/presentation/cubit/about_recipe_cubit.dart'; +import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart'; import 'package:eatvio_app/features/recipe/repositories/about_recipe_repository.dart'; +import 'package:eatvio_app/features/recipe/repositories/my_recipes_repository.dart'; import 'package:eatvio_app/features/recipe/services/about_recipe_service.dart'; +import 'package:eatvio_app/features/recipe/services/my_recipes_service.dart'; import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart'; import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart'; import 'package:eatvio_app/features/registration/repositories/registration_repository.dart'; @@ -140,6 +143,7 @@ Future init() async { sl.registerFactory(() => ConsultantCommentsCubit(diaryRepository: sl())); sl.registerFactory(() => CancelPlanCubit(cancelPlanRepository: sl())); sl.registerFactory(() => VerifyEmailCubit(profileRepository: sl())); + sl.registerFactory(() => MyRecipesCubit(myRecipesRepository: sl())); // Repository sl.registerLazySingleton(() => ChatRepository(chatService: sl())); @@ -167,6 +171,7 @@ Future init() async { sl.registerLazySingleton(() => CancelPlanRepository(cancelService: sl())); sl.registerLazySingleton(() => CreatePlanRepository(createPlanService: sl())); sl.registerLazySingleton(() => NotificationsRepository(notificationsService: sl())); + sl.registerFactory(() => MyRecipesRepository(myRecipesService: sl())); // Services sl.registerLazySingleton(() => ChatService(sl())); @@ -195,6 +200,7 @@ Future init() async { sl.registerLazySingleton(() => CancelPlanService(sl())); sl.registerLazySingleton(() => CreatePlanService(sl())); sl.registerLazySingleton(() => NotificationsService(sl())); + sl.registerLazySingleton(() => MyRecipesService(sl())); // External sl.registerLazySingleton( diff --git a/lib/features/feed/models/recipes_model.dart b/lib/features/feed/models/recipes_model.dart index 262b9ecc..dcb4d2ea 100644 --- a/lib/features/feed/models/recipes_model.dart +++ b/lib/features/feed/models/recipes_model.dart @@ -16,8 +16,8 @@ abstract class Recipe with _$Recipe { @JsonKey(name: 'description_md') required String description, required List? steps, @JsonKey(name: 'video_url') required String? videoUrl, - @JsonKey(name: 'created_at') required DateTime createdAt, - @JsonKey(name: 'updated_at') required DateTime updatedAt, + @JsonKey(name: 'created_at') DateTime? createdAt, + @JsonKey(name: 'updated_at') DateTime? updatedAt, required String? title, required String? type, required String slug, diff --git a/lib/features/my_diets/presentation/widgets/my_diet_widget.dart b/lib/features/my_diets/presentation/widgets/my_diet_widget.dart index 7eb7f15c..587e05ca 100644 --- a/lib/features/my_diets/presentation/widgets/my_diet_widget.dart +++ b/lib/features/my_diets/presentation/widgets/my_diet_widget.dart @@ -26,6 +26,7 @@ import 'package:share_plus/share_plus.dart'; class MyDietWidget extends StatefulWidget { final Diet diet; + const MyDietWidget({ super.key, required this.diet, diff --git a/lib/features/recipe/models/my_recipe_model.dart b/lib/features/recipe/models/my_recipe_model.dart new file mode 100644 index 00000000..f7068938 --- /dev/null +++ b/lib/features/recipe/models/my_recipe_model.dart @@ -0,0 +1,36 @@ +import 'package:eatvio_app/features/feed/models/recipes_model.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'my_recipe_model.g.dart'; + +part 'my_recipe_model.freezed.dart'; + +@freezed +abstract class MyRecipePaginationModel with _$MyRecipePaginationModel { + const factory MyRecipePaginationModel({ + required List data, + }) = _MyRecipePaginationModel; + + factory MyRecipePaginationModel.fromJson(Map json) => _$MyRecipePaginationModelFromJson(json); +} + +@freezed +abstract class MyRecipeModel with _$MyRecipeModel { + const factory MyRecipeModel({ + required int id, + required String image, + required String state, + @JsonKey(name: 'cooking_time') required int cookingTime, + @JsonKey(name: 'active_time') required int activeCookingTime, + @JsonKey(name: 'description_md') required String description, + required List? steps, + @JsonKey(name: 'video_url') required String? videoUrl, + required String? title, + required String slug, + @JsonKey(name: 'energy_per_serving') required double calories, + @JsonKey(name: 'total_likes') required int likesCount, + @JsonKey(name: 'total_views') required int viewsCount, + }) = _MyRecipeModel; + + factory MyRecipeModel.fromJson(Map json) => _$MyRecipeModelFromJson(json); +} diff --git a/lib/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart b/lib/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart new file mode 100644 index 00000000..43d3a2c4 --- /dev/null +++ b/lib/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart @@ -0,0 +1,21 @@ +import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recipes_state.dart'; +import 'package:eatvio_app/features/recipe/repositories/my_recipes_repository.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class MyRecipesCubit extends Cubit { + final MyRecipesRepository myRecipesRepository; + + MyRecipesCubit({ + required this.myRecipesRepository, + }) : super(const MyRecipesState.loading()); + + getMyRecipes() async { + emit(const MyRecipesState.loading()); + try { + final myRecipes = await myRecipesRepository.getMyRecipes(); + emit(MyRecipesState.loaded(recipes: myRecipes)); + } catch (e) { + emit(const MyRecipesState.error()); + } + } +} diff --git a/lib/features/recipe/presentation/cubit/my_recipes_cubit/my_recipes_state.dart b/lib/features/recipe/presentation/cubit/my_recipes_cubit/my_recipes_state.dart new file mode 100644 index 00000000..1a7d3d73 --- /dev/null +++ b/lib/features/recipe/presentation/cubit/my_recipes_cubit/my_recipes_state.dart @@ -0,0 +1,17 @@ +import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'my_recipes_state.freezed.dart'; + +@freezed +abstract class MyRecipesState with _$MyRecipesState { + const factory MyRecipesState() = _MyRecipesState; + + const factory MyRecipesState.loading() = MyRecipesStateLoading; + + const factory MyRecipesState.loaded({ + required List recipes, + }) = MyRecipesStateLoaded; + + const factory MyRecipesState.error() = MyRecipesStateError; +} diff --git a/lib/features/recipe/presentation/pages/my_recipes_screen.dart b/lib/features/recipe/presentation/pages/my_recipes_screen.dart index ad30f146..a9e50b27 100644 --- a/lib/features/recipe/presentation/pages/my_recipes_screen.dart +++ b/lib/features/recipe/presentation/pages/my_recipes_screen.dart @@ -1,42 +1,18 @@ -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:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart'; +import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recipes_state.dart'; +import 'package:eatvio_app/features/recipe/presentation/widgets/my_recipe_widget.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( @@ -52,144 +28,20 @@ class MyRecipesScreenContent extends StatelessWidget { 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(), - ), - ), - ), - ], - ); - } - }, - ), - ), - ); - }, + body: BlocProvider( + create: (BuildContext context) => sl()..getMyRecipes(), + child: BlocBuilder( + builder: (BuildContext context, MyRecipesState state) { + if (state is MyRecipesStateLoaded) { + return ListView.separated( + itemBuilder: (context, index) => MyRecipeWidget(recipe: state.recipes[index]), + separatorBuilder: (context, index) => const SizedBox(height: 12), + itemCount: state.recipes.length, + ); + } + return Container(); + }, + ), ), ); } diff --git a/lib/features/recipe/presentation/widgets/my_recipe_widget.dart b/lib/features/recipe/presentation/widgets/my_recipe_widget.dart new file mode 100644 index 00000000..8acae4c1 --- /dev/null +++ b/lib/features/recipe/presentation/widgets/my_recipe_widget.dart @@ -0,0 +1,242 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; +import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart'; +import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart'; +import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recipes_state.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:share_plus/share_plus.dart'; + +class MyRecipeWidget extends StatelessWidget { + final MyRecipeModel recipe; + + const MyRecipeWidget({ + super.key, + required this.recipe, + }); + + @override + Widget build(BuildContext context) { + // final s = S.of(context); + return DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Colors.white, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + InkWell( + // onTap: () => GoRouter.of(context).pushNamed('aboutDietScreen', extra: {'diet': widget.diet, 'navigateToComments': false}), + child: ClipRRect( + borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), + child: CachedNetworkImage( + imageUrl: '$baseUrl/storage/${recipe.image}', + fit: BoxFit.fitWidth, + width: MediaQuery.of(context).size.width, + ), + ), + ), + // Positioned( + // bottom: 16, + // child: SizedBox( + // width: MediaQuery.sizeOf(context).width, + // child: InfoRow(diet: widget.diet, itemsBackgroundColor: appTheme.whiteA700, disableDescriptionOfTypes: true, type: ProductType.diet), + // ), + // ), + ], + ), + const SizedBox(height: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + recipe.title!, + style: TextStyles.textMSemibold.copyWith(color: appTheme.black), + ), + ), + if (recipe.description.isNotEmpty) ...[ + const SizedBox( + height: 12, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Html( + data: recipe.description.length > 200 ? '${recipe.description.substring(0, 200)}...' : recipe.description, + style: { + "*": Style( + margin: Margins.zero, + color: appTheme.black, + fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14), + fontWeight: TextStyles.textSMedium.fontWeight ?? FontWeight.w400, + ), + }, + ), + ), + ], + ], + ), + const SizedBox(height: 20), + BlocBuilder( + builder: (BuildContext context, MyRecipesState state) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: CustomOutlinedButton( + onPressed: () async { + // if (state.selectedStateIndex == 1) { + // final result = await context.read().publishDiet(widget.diet.id); + // if (result) { + // if (!context.mounted) return; + // showBottomSheetWithBlur( + // context: context, + // child: SuccessPublishedDietOrPlanBottomSheet(title: widget.diet.name), + // ); + // await context.read().loadAllDiets(); + // } else { + // showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); + // } + // } else if (state.selectedStateIndex == 2) { + // } else { + // GoRouter.of(context).pushNamed(CreateDietScreen.name, extra: widget.diet); + // } + }, + text: '', + // text: switch (state.selectedStateIndex) { + // 1 => s.publish, + // 2 => s.toOffer, + // _ => s.edit, + // }, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + side: const WidgetStatePropertyAll(BorderSide.none), + ), + ), + ), + // if (state.selectedStateIndex == 0) ...[ + const SizedBox(width: 6), + CustomOutlinedButton( + onPressed: () async { + final box = context.findRenderObject() as RenderBox?; + await SharePlus.instance.share( + ShareParams( + uri: Uri.parse('$baseUrl/ru/recipe/${recipe.id}'), + sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, + ), + ); + }, + height: 52, + width: 52, + leftIcon: SvgPicture.asset( + 'assets/icons/share2.svg', + width: 20, + height: 20, + colorFilter: ColorFilter.mode(appTheme.whiteA700, BlendMode.srcIn), + ), + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.red), + buttonStyle: ButtonStyle( + shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + side: const WidgetStatePropertyAll(BorderSide.none), + ), + text: '', + ), + ], + // ], + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: CustomOutlinedButton( + onPressed: () async { + // if (state.selectedStateIndex == 0) { + // final result = await context.read().unpublishDiet(widget.diet.id); + // if (result) { + // if (!context.mounted) return; + // showBottomSheetWithBlur( + // context: context, + // height: 250, + // child: DietDraftedBottomSheet(title: widget.diet.name), + // ); + // await context.read().loadAllDiets(); + // } else { + // showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); + // } + // } else { + // await showErrorDialog(context, title: 'Предупреждение', description: 'Вы можете создать новую диету на основе существующей.'); + // AppRouter.navigatorKey.currentContext?.pushNamed(CreateDietScreen.name, extra: widget.diet); + // } + }, + text: '', + // text: switch (state.selectedStateIndex) { + // 0 => s.removeFromActive, + // _ => s.edit, + // }, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.black), + buttonStyle: ButtonStyle( + shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), + backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0), + side: const WidgetStatePropertyAll(BorderSide.none), + ), + ), + ), + const SizedBox(width: 6), + CustomOutlinedButton( + onPressed: () async { + // final result = await context.read().deleteDiet(widget.diet.id); + // if (result) { + // if (!context.mounted) return; + // showBottomSheetWithBlur( + // context: context, + // child: DietDeletedBottomSheet(title: widget.diet.name), + // ); + // await context.read().loadAllDiets(); + // } + }, + height: 52, + width: 52, + leftIcon: SvgPicture.asset( + 'assets/icons/trash_can.svg', + width: 20, + height: 20, + ), + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.red), + buttonStyle: ButtonStyle( + shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), + backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0), + side: const WidgetStatePropertyAll(BorderSide.none), + ), + text: '', + ), + ], + ), + const SizedBox(height: 8), + ], + ), + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/features/recipe/repositories/my_recipes_repository.dart b/lib/features/recipe/repositories/my_recipes_repository.dart new file mode 100644 index 00000000..73dc105c --- /dev/null +++ b/lib/features/recipe/repositories/my_recipes_repository.dart @@ -0,0 +1,16 @@ +import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart'; +import 'package:eatvio_app/features/recipe/services/my_recipes_service.dart'; + +class MyRecipesRepository { + final MyRecipesService myRecipesService; + + MyRecipesRepository({required this.myRecipesService}); + + Future> getMyRecipes() async { + try { + return (await myRecipesService.getMyRecipes()).data; + } catch (e) { + rethrow; + } + } +} \ No newline at end of file diff --git a/lib/features/recipe/services/my_recipes_service.dart b/lib/features/recipe/services/my_recipes_service.dart new file mode 100644 index 00000000..7500edcb --- /dev/null +++ b/lib/features/recipe/services/my_recipes_service.dart @@ -0,0 +1,13 @@ +import 'package:dio/dio.dart'; +import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart'; +import 'package:retrofit/retrofit.dart'; + +part 'my_recipes_service.g.dart'; + +@RestApi() +abstract class MyRecipesService { + factory MyRecipesService(Dio dio, {String baseUrl}) = _MyRecipesService; + + @GET('/api/recipes/my') + Future getMyRecipes(); +} From 38031c89c2bd61083461fe5858eb2717277728bc Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 28 May 2025 15:56:40 +0300 Subject: [PATCH 9/9] feat(recipe): my recipes screen complete --- lib/core/app_router.dart | 6 +- lib/core/locator_service.dart | 2 +- lib/core/widgets/custom_popup.dart | 45 +-- .../my_recipes_cubit/my_recieps_cubit.dart | 37 ++- .../my_recipes_cubit/my_recipes_state.dart | 1 + .../presentation/pages/my_recipes_screen.dart | 1 + .../widgets/my_recipe_widget.dart | 314 +++++++++--------- 7 files changed, 210 insertions(+), 196 deletions(-) diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index c5e2b77a..172d840d 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -206,7 +206,11 @@ class AppRouter { name: 'aboutRecipesScreen', builder: (context, state) { final data = state.extra as Map; - return AboutRecipeScreen(recipe: data['recipe'] as Recipe, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); + return AboutRecipeScreen( + recipe: data['recipe'] as Recipe, + index: data['index'] as int?, + navigateToComments: data['navigateToComments'] as bool, + ); }, ), GoRoute( diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 14af4917..80a98f3e 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -143,7 +143,7 @@ Future init() async { sl.registerFactory(() => ConsultantCommentsCubit(diaryRepository: sl())); sl.registerFactory(() => CancelPlanCubit(cancelPlanRepository: sl())); sl.registerFactory(() => VerifyEmailCubit(profileRepository: sl())); - sl.registerFactory(() => MyRecipesCubit(myRecipesRepository: sl())); + sl.registerFactory(() => MyRecipesCubit(myRecipesRepository: sl(), aboutRecipeRepository: sl())); // Repository sl.registerLazySingleton(() => ChatRepository(chatService: sl())); diff --git a/lib/core/widgets/custom_popup.dart b/lib/core/widgets/custom_popup.dart index a23c03c6..47081135 100644 --- a/lib/core/widgets/custom_popup.dart +++ b/lib/core/widgets/custom_popup.dart @@ -6,14 +6,12 @@ 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_diet/presentation/create_diet_screen.dart'; import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.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/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'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; @@ -100,37 +98,12 @@ class CustomPopup extends StatelessWidget { ), ); }); - addItem('assets/icons/edit_paint.svg', 'Редактировать', () async { - 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 if (complainedContentType == ComplainedContent.diet) { - await showErrorDialog(context, - title: 'Предупреждение', description: 'Диета будет перенесена в архив. Вы создаете новую диету на основе существующей.'); - await sl().unpublishDiet(id!); - GoRouter.of(context).pushNamed(CreateDietScreen.name, extra: diet); - } else { - showInfoToast('В разработке'); - return; - } - }); - if (complainedContentType == ComplainedContent.plan || complainedContentType == ComplainedContent.diet) { - addItem('assets/icons/remove_from_public.svg', 'Снять с публикации', () async { - if (complainedContentType == ComplainedContent.diet) { - final result = await sl().unpublishDiet(id!); - if (!result) { - showErrorDialog(context, title: "Не удалось снять диету с публикации"); - return; - } - showSuccessToast('Диета перенесена в архив, обновите ленту'); - } else if (complainedContentType == ComplainedContent.plan) { - final result = await sl().unpublish(slug!); - if (!result) { - showErrorDialog(context, title: "Не удалось снять план с публикации"); - return; - } - showSuccessToast('План перенесен в архив, обновите ленту'); + if (complainedContentType == ComplainedContent.posts || complainedContentType == ComplainedContent.recipe) { + addItem('assets/icons/edit_paint.svg', 'Редактировать', () async { + if (complainedContentType == ComplainedContent.posts) { + GoRouter.of(context).pushNamed('blogPostCreateScreen', extra: feedBlogPostModel); + } else if (complainedContentType == ComplainedContent.recipe) { + GoRouter.of(context).pushNamed(RecipeCreateScreen.name, extra: recipe); } }); } @@ -145,21 +118,21 @@ class CustomPopup extends StatelessWidget { } else if (complainedContentType == ComplainedContent.recipe) { final result = await sl().deleteRecipe(slug!); if (!result) { - showErrorDialog(context, title: S.current.could_not_delete_blog_post); + showErrorDialog(context, title: 'Не удалось удалить рецепт'); return; } showSuccessToast('Рецепт удален, обновите ленту'); } else if (complainedContentType == ComplainedContent.diet) { final result = await sl().deleteDiet(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().deletePlan(slug!); if (!result) { - showErrorDialog(context, title: S.current.could_not_delete_blog_post); + showErrorDialog(context, title: 'Не удалось удалить план'); return; } showSuccessToast('План удален, обновите ленту'); diff --git a/lib/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart b/lib/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart index 43d3a2c4..df454594 100644 --- a/lib/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart +++ b/lib/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart @@ -1,21 +1,56 @@ +import 'package:eatvio_app/features/feed/models/recipes_model.dart'; +import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart'; import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recipes_state.dart'; +import 'package:eatvio_app/features/recipe/repositories/about_recipe_repository.dart'; import 'package:eatvio_app/features/recipe/repositories/my_recipes_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MyRecipesCubit extends Cubit { final MyRecipesRepository myRecipesRepository; + final AboutRecipeRepository aboutRecipeRepository; + + static const String active = 'active'; + static const String archived = 'archived'; MyRecipesCubit({ required this.myRecipesRepository, + required this.aboutRecipeRepository, }) : super(const MyRecipesState.loading()); getMyRecipes() async { emit(const MyRecipesState.loading()); try { final myRecipes = await myRecipesRepository.getMyRecipes(); - emit(MyRecipesState.loaded(recipes: myRecipes)); + final List activeRecipes = myRecipes.where((recipe) => recipe.state == active).toList(); + emit(MyRecipesState.loaded(recipes: activeRecipes, selectedState: active)); } catch (e) { emit(const MyRecipesState.error()); } } + + setActiveState() async { + emit(const MyRecipesState.loading()); + try { + final myRecipes = await myRecipesRepository.getMyRecipes(); + final List activeRecipes = myRecipes.where((recipe) => recipe.state == active).toList(); + emit(MyRecipesState.loaded(recipes: activeRecipes, selectedState: active)); + } catch (e) { + emit(const MyRecipesState.error()); + } + } + + setArchivedState() async { + emit(const MyRecipesState.loading()); + try { + final myRecipes = await myRecipesRepository.getMyRecipes(); + final List archivedRecipes = myRecipes.where((recipe) => recipe.state == archived).toList(); + emit(MyRecipesState.loaded(recipes: archivedRecipes, selectedState: archived)); + } catch (e) { + emit(const MyRecipesState.error()); + } + } + + Future getRecipeById(String slug) async { + return await aboutRecipeRepository.getRecipe(slug); + } } diff --git a/lib/features/recipe/presentation/cubit/my_recipes_cubit/my_recipes_state.dart b/lib/features/recipe/presentation/cubit/my_recipes_cubit/my_recipes_state.dart index 1a7d3d73..39a6fe2c 100644 --- a/lib/features/recipe/presentation/cubit/my_recipes_cubit/my_recipes_state.dart +++ b/lib/features/recipe/presentation/cubit/my_recipes_cubit/my_recipes_state.dart @@ -11,6 +11,7 @@ abstract class MyRecipesState with _$MyRecipesState { const factory MyRecipesState.loaded({ required List recipes, + required String selectedState, }) = MyRecipesStateLoaded; const factory MyRecipesState.error() = MyRecipesStateError; diff --git a/lib/features/recipe/presentation/pages/my_recipes_screen.dart b/lib/features/recipe/presentation/pages/my_recipes_screen.dart index a9e50b27..c3e29db5 100644 --- a/lib/features/recipe/presentation/pages/my_recipes_screen.dart +++ b/lib/features/recipe/presentation/pages/my_recipes_screen.dart @@ -34,6 +34,7 @@ class MyRecipesScreen extends StatelessWidget { builder: (BuildContext context, MyRecipesState state) { if (state is MyRecipesStateLoaded) { return ListView.separated( + padding: const EdgeInsets.symmetric(vertical: 16), itemBuilder: (context, index) => MyRecipeWidget(recipe: state.recipes[index]), separatorBuilder: (context, index) => const SizedBox(height: 12), itemCount: state.recipes.length, diff --git a/lib/features/recipe/presentation/widgets/my_recipe_widget.dart b/lib/features/recipe/presentation/widgets/my_recipe_widget.dart index 8acae4c1..1deb983f 100644 --- a/lib/features/recipe/presentation/widgets/my_recipe_widget.dart +++ b/lib/features/recipe/presentation/widgets/my_recipe_widget.dart @@ -1,18 +1,24 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:eatvio_app/core/const.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/utils/localization_utils.dart'; +import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart'; import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart'; import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart'; -import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recipes_state.dart'; +import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:go_router/go_router.dart'; import 'package:share_plus/share_plus.dart'; -class MyRecipeWidget extends StatelessWidget { +class MyRecipeWidget extends StatefulWidget { final MyRecipeModel recipe; const MyRecipeWidget({ @@ -20,9 +26,30 @@ class MyRecipeWidget extends StatelessWidget { required this.recipe, }); + @override + State createState() => _MyRecipeWidgetState(); +} + +class _MyRecipeWidgetState extends State { + late final int hours; + late final int minutes; + late final String timeTitle; + + @override + void initState() { + super.initState(); + hours = widget.recipe.cookingTime ~/ 60; + minutes = widget.recipe.cookingTime % 60; + if (hours > 0) { + timeTitle = '$hours ${getHourForm(hours)} $minutes ${getMinuteForm(minutes)}'; + } else { + timeTitle = '$minutes ${getMinuteForm(minutes)}'; + } + } + @override Widget build(BuildContext context) { - // final s = S.of(context); + final s = S.of(context); return DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), @@ -35,23 +62,36 @@ class MyRecipeWidget extends StatelessWidget { Stack( children: [ InkWell( - // onTap: () => GoRouter.of(context).pushNamed('aboutDietScreen', extra: {'diet': widget.diet, 'navigateToComments': false}), + onTap: () async => GoRouter.of(context).pushNamed( + 'aboutRecipesScreen', + extra: { + 'recipe': await context.read().getRecipeById(widget.recipe.slug), + 'navigateToComments': false, + }, + ), child: ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), child: CachedNetworkImage( - imageUrl: '$baseUrl/storage/${recipe.image}', + imageUrl: '$baseUrl/storage/${widget.recipe.image}', fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width, ), ), ), - // Positioned( - // bottom: 16, - // child: SizedBox( - // width: MediaQuery.sizeOf(context).width, - // child: InfoRow(diet: widget.diet, itemsBackgroundColor: appTheme.whiteA700, disableDescriptionOfTypes: true, type: ProductType.diet), - // ), - // ), + Positioned( + left: 8, + bottom: 8, + child: Wrap( + runSpacing: 8, + children: [ + _iconButton('assets/icons/clock.svg', timeTitle), + const SizedBox(width: 8), + _iconButton('assets/icons/steps.svg', '${widget.recipe.steps?.length ?? 0} ${getStepForm(widget.recipe.steps?.length ?? 0)}'), + const SizedBox(width: 8), + _iconButton('assets/icons/calories.svg', '${widget.recipe.calories.toStringAsFixed(0)} ккал'), + ], + ), + ), ], ), const SizedBox(height: 12), @@ -61,18 +101,18 @@ class MyRecipeWidget extends StatelessWidget { Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( - recipe.title!, + widget.recipe.title!, style: TextStyles.textMSemibold.copyWith(color: appTheme.black), ), ), - if (recipe.description.isNotEmpty) ...[ + if (widget.recipe.description.isNotEmpty) ...[ const SizedBox( height: 12, ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Html( - data: recipe.description.length > 200 ? '${recipe.description.substring(0, 200)}...' : recipe.description, + data: widget.recipe.description.length > 200 ? '${widget.recipe.description.substring(0, 200)}...' : widget.recipe.description, style: { "*": Style( margin: Margins.zero, @@ -87,156 +127,116 @@ class MyRecipeWidget extends StatelessWidget { ], ), const SizedBox(height: 20), - BlocBuilder( - builder: (BuildContext context, MyRecipesState state) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + Row( children: [ - Row( - children: [ - Expanded( - child: CustomOutlinedButton( - onPressed: () async { - // if (state.selectedStateIndex == 1) { - // final result = await context.read().publishDiet(widget.diet.id); - // if (result) { - // if (!context.mounted) return; - // showBottomSheetWithBlur( - // context: context, - // child: SuccessPublishedDietOrPlanBottomSheet(title: widget.diet.name), - // ); - // await context.read().loadAllDiets(); - // } else { - // showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); - // } - // } else if (state.selectedStateIndex == 2) { - // } else { - // GoRouter.of(context).pushNamed(CreateDietScreen.name, extra: widget.diet); - // } - }, - text: '', - // text: switch (state.selectedStateIndex) { - // 1 => s.publish, - // 2 => s.toOffer, - // _ => s.edit, - // }, - height: 52, - buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), - buttonStyle: ButtonStyle( - shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), - backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), - side: const WidgetStatePropertyAll(BorderSide.none), - ), - ), + Expanded( + child: CustomOutlinedButton( + onPressed: () async { + GoRouter.of(context).pushNamed( + RecipeCreateScreen.name, + extra: await context.read().getRecipeById(widget.recipe.slug), + ); + }, + text: s.edit, + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + buttonStyle: ButtonStyle( + shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + side: const WidgetStatePropertyAll(BorderSide.none), ), - // if (state.selectedStateIndex == 0) ...[ - const SizedBox(width: 6), - CustomOutlinedButton( - onPressed: () async { - final box = context.findRenderObject() as RenderBox?; - await SharePlus.instance.share( - ShareParams( - uri: Uri.parse('$baseUrl/ru/recipe/${recipe.id}'), - sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, - ), - ); - }, - height: 52, - width: 52, - leftIcon: SvgPicture.asset( - 'assets/icons/share2.svg', - width: 20, - height: 20, - colorFilter: ColorFilter.mode(appTheme.whiteA700, BlendMode.srcIn), - ), - buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.red), - buttonStyle: ButtonStyle( - shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), - backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), - side: const WidgetStatePropertyAll(BorderSide.none), - ), - text: '', - ), - ], - // ], + ), ), - const SizedBox(height: 8), - Row( - children: [ - Expanded( - child: CustomOutlinedButton( - onPressed: () async { - // if (state.selectedStateIndex == 0) { - // final result = await context.read().unpublishDiet(widget.diet.id); - // if (result) { - // if (!context.mounted) return; - // showBottomSheetWithBlur( - // context: context, - // height: 250, - // child: DietDraftedBottomSheet(title: widget.diet.name), - // ); - // await context.read().loadAllDiets(); - // } else { - // showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); - // } - // } else { - // await showErrorDialog(context, title: 'Предупреждение', description: 'Вы можете создать новую диету на основе существующей.'); - // AppRouter.navigatorKey.currentContext?.pushNamed(CreateDietScreen.name, extra: widget.diet); - // } - }, - text: '', - // text: switch (state.selectedStateIndex) { - // 0 => s.removeFromActive, - // _ => s.edit, - // }, - height: 52, - buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.black), - buttonStyle: ButtonStyle( - shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), - backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0), - side: const WidgetStatePropertyAll(BorderSide.none), - ), - ), - ), - const SizedBox(width: 6), - CustomOutlinedButton( - onPressed: () async { - // final result = await context.read().deleteDiet(widget.diet.id); - // if (result) { - // if (!context.mounted) return; - // showBottomSheetWithBlur( - // context: context, - // child: DietDeletedBottomSheet(title: widget.diet.name), - // ); - // await context.read().loadAllDiets(); - // } - }, - height: 52, - width: 52, - leftIcon: SvgPicture.asset( - 'assets/icons/trash_can.svg', - width: 20, - height: 20, - ), - buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.red), - buttonStyle: ButtonStyle( - shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), - backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0), - side: const WidgetStatePropertyAll(BorderSide.none), - ), - text: '', - ), - ], - ), - const SizedBox(height: 8), ], ), - ); - }, + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: CustomOutlinedButton( + onPressed: () async { + final result = await sl().deleteRecipe(widget.recipe.slug); + if (!result) { + showErrorToast('Не удалось удалить рецепт'); + return; + } + showSuccessToast('Рецепт удален'); + if (context.mounted) context.read().getMyRecipes(); + }, + text: 'Удалить', + height: 52, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.black), + buttonStyle: ButtonStyle( + shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), + backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0), + side: const WidgetStatePropertyAll(BorderSide.none), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: CustomOutlinedButton( + onPressed: () async { + final box = context.findRenderObject() as RenderBox?; + await SharePlus.instance.share( + ShareParams( + uri: Uri.parse('$baseUrl/ru/recipe/${widget.recipe.slug}'), + sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, + ), + ); + }, + height: 52, + width: 52, + buttonStyle: ButtonStyle( + shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))), + backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), + side: const WidgetStatePropertyAll(BorderSide.none), + ), + text: s.share, + buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), + ), + ), + ], + ), + const SizedBox(height: 8), + ], + ), ), ], ), ); } + + _iconButton(String iconPath, [String? text]) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + decoration: BoxDecoration( + color: appTheme.whiteA700, + borderRadius: BorderRadius.circular(16), + ), + height: 32, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset( + iconPath, + ), + if (text != null) const SizedBox(width: 8), + if (text != null) + Text( + text, + style: const TextStyle( + fontSize: 12, + color: Colors.black, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } }