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