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