Compare commits

...

252 Commits

Author SHA1 Message Date
misha
a492c2d964 Merge remote-tracking branch 'upstream/main' into issue-914 2026-03-04 09:21:45 +03:00
60753a13d2 Merge pull request 'Рецепты и мелкие правки' (#920) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#920
2026-03-04 06:07:24 +00:00
mso7nueh
50deaeddd1 feat: Add Android CI workflow for testing and building APKs 2026-02-27 12:23:32 +03:00
mso7nueh
56fb2f1bac feat: Add Android CI workflow for testing and building APKs 2026-02-27 12:10:43 +03:00
mso7nueh
24185cc875 feat: Implement Gitea Actions CI/CD workflows for Android app testing, building, and deployment. 2026-02-26 16:34:45 +03:00
mso7nueh
9e7bf258d0 feat: Add Gitea workflow for Android app testing and APK build. 2026-02-26 16:18:56 +03:00
mso7nueh
098633b3ac Merge branch 'main' of http://gitea.office.local:3000/mso7nueh/eatvio_app 2026-02-26 16:15:32 +03:00
mso7nueh
c3aa77b02d feat: Add GitHub Actions workflow for Android app testing, coverage analysis, and release APK building. 2026-02-26 16:14:55 +03:00
Username1548
2d5384f2ec fix: ci/cd 2026-02-26 14:10:41 +03:00
Username1548
1c7e494f90 Merge branch 'main' of http://gitea.office.local:3000/mso7nueh/eatvio_app 2026-02-26 14:00:52 +03:00
Username1548
8bbe2613d2 fix: ci/cd 2026-02-26 14:00:14 +03:00
mso7nueh
5f6ceab6a1 feat: Add Gitea CI/CD workflows for Android build, test, and coverage analysis. 2026-02-26 13:59:19 +03:00
Username1548
250a3f5d73 fix: CI/CD 2026-02-26 13:46:16 +03:00
mso7nueh
3e0d8b6c27 feat: Add GitHub Actions workflow for Android app testing and APK build. 2026-02-26 13:15:55 +03:00
mso7nueh
9ed3b37272 feat: Add Gitea Actions workflow for Flutter Android testing and building with coverage analysis. 2026-02-26 13:12:07 +03:00
mso7nueh
ae628a6ecc feat: Add Gitea workflows for Android APK build and comprehensive testing. 2026-02-26 13:04:20 +03:00
mso7nueh
88d8ec52d9 feat: add Android CI workflow for testing, coverage analysis, and APK build 2026-02-26 13:02:26 +03:00
mso7nueh
937921e26f feat: Add Gitea Actions workflow for Android app testing, building, and artifact upload. 2026-02-26 12:27:14 +03:00
mso7nueh
d324bd6540 feat: Add Gitea workflow files for Android testing and building. 2026-02-26 11:57:29 +03:00
mso7nueh
5766e216c8 feat: Add Gitea CI/CD workflows for Android app building and comprehensive testing with coverage. 2026-02-26 11:38:59 +03:00
mso7nueh
ee9b97b694 feat: Add Gitea Actions workflow for Flutter Android testing and release APK building. 2026-02-26 11:35:23 +03:00
mso7nueh
31eec6c9d7 feat: Implement Gitea Actions CI/CD workflows for Flutter Android app testing, building, and TestFlight deployment. 2026-02-26 11:21:48 +03:00
mso7nueh
5fae3eb5e8 feat: Add CI/CD workflows for building, testing, and deploying Android and iOS applications. 2026-02-26 11:18:51 +03:00
mso7nueh
b0a167b9e9 feat: Add MyRecipesScreen to display user's recipes, manage active/drafted states, and navigate to recipe creation. 2026-02-26 03:00:32 +03:00
mso7nueh
0b2e026d1d feat: Introduce user-generated content features, including recipe creation and management, and blog post creation. 2026-02-26 02:52:08 +03:00
mso7nueh
0641513ebe feat: Add user recipe management and creation functionality, including models, widgets, cubits, repositories, and tests. 2026-02-25 13:10:07 +03:00
misha
be0a99b164 fix: ci/cd 2026-02-25 13:02:28 +03:00
misha
6dab0d159b feat(consultant_registration): added new consultant registration flow 2026-02-25 07:08:05 +03:00
mso7nueh
5167766dbf merge upstream 2026-02-24 09:22:15 +00:00
cc79e40df9 Merge pull request 'feat(my_diets): draft diets fixes' (#901) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#901
Reviewed-by: mso7nueh <mso7nueh@mail.ru>
2026-02-24 07:52:31 +00:00
misha
cdc5936d3c analizer: fixed analizer issues 2026-02-20 13:22:16 +03:00
misha
67e770f5d0 review: fixed review issues 2026-02-20 13:20:17 +03:00
mso7nueh
6c4104e9ea feat: Issue-919 2026-02-18 18:08:00 +03:00
mso7nueh
6d1bcb85fb feat(plans): Issue-918 2026-02-18 18:05:59 +03:00
mso7nueh
9753644570 feat(recipe): drafts 2026-02-18 18:04:01 +03:00
mso7nueh
75983818d1 chore: Remove temporary test and analysis output files. 2026-02-16 18:20:00 +03:00
mso7nueh
5b149e2fd2 refactor: Corrected typo in MyRecipesCubit filename and updated related tests, widgets, services, and DI registrations. 2026-02-16 18:16:26 +03:00
misha
699148b202 feat(my_diets): fixed navigation to diet creation screen 2026-02-16 15:33:15 +03:00
mso7nueh
6e7894b911 build: Remove eatvio/ from .gitignore. 2026-02-16 12:49:09 +03:00
mso7nueh
d4f41fe6cf feat: implement recipe drafting functionality and refactor MyRecipesTab to be stateless 2026-02-16 12:49:09 +03:00
misha
b7804db49d feat(my_diets): merge conflict fix 2026-02-11 16:54:08 +03:00
misha
e90a466a5b Merge remote-tracking branch 'upstream/main' 2026-02-11 16:26:08 +03:00
misha
5f30aaeb2a feat(my_diets): merge conflict fixes 2026-02-11 16:23:48 +03:00
mso7nueh
516dfa4158 Merge pull request 'Улучшение валидации и пользовательского опыта в регистрациях и создании диет' (#910) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#910
Reviewed-by: misha <misha@noreply.localhost>
2026-02-11 09:45:19 +00:00
mso7nueh
ae07851396 test: update OAuth verification mock to use anyNamed arguments for email and phone. 2026-02-10 18:47:53 +03:00
mso7nueh
34eccb7db5 feat: Enhance OAuth error state to include specific registration validation errors and pass email in the verification request. 2026-02-10 18:36:18 +03:00
misha
042cc0ea20 Merge remote-tracking branch 'upstream/main' 2026-02-10 12:38:08 +03:00
misha
f81aebe9d8 feat(tests): added new tests and updated fixtures 2026-02-10 08:02:50 +03:00
mso7nueh
d70aaf955f feat: Implement auto-scrolling to the first validation error in the create diet form and refine image validation logic. 2026-02-09 23:15:12 +03:00
misha
c91e83adaf feat(my_diets): fixed draft, active and personal diets creation, updating 2026-02-09 10:39:51 +03:00
mso7nueh
531f732b1a Merge pull request 'feat: enhance BottomNavigationBarWidget with DEV logo display and refactor page list initialization' (#907) from mso7nueh/eatvio_app:feat_add_dev_marker_logo into main
Reviewed-on: eatvio/eatvio_app#907
Reviewed-by: misha <misha@noreply.localhost>
2026-02-06 09:54:34 +00:00
mso7nueh
d10f2f1356 Merge pull request 'feat: update add product modal to reflect selection state with icons' (#906) from mso7nueh/eatvio_app:feat_add_product_modal into main
Reviewed-on: eatvio/eatvio_app#906
Reviewed-by: misha <misha@noreply.localhost>
2026-02-06 09:54:25 +00:00
mso7nueh
bf110b70c1 feat: enhance BottomNavigationBarWidget with DEV logo display and refactor page list initialization 2026-02-05 14:41:25 +03:00
mso7nueh
97418e11d4 feat: update add product modal to reflect selection state with icons 2026-02-05 14:40:55 +03:00
mso7nueh
225077907f Merge pull request 'build: bump version to 1.3.17+97' (#903) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#903
2026-02-04 12:50:52 +00:00
mso7nueh
296adc8229 build: bump version to 1.3.17+97 2026-02-04 15:50:28 +03:00
mso7nueh
7dd7e3b488 Merge pull request 'feat: Integrate Huawei Mobile Services (HMS) by updating configuration, adding manifest metadata, and differentiating Huawei devices for token submission.' (#902) from mso7nueh/eatvio_app:fix_hms_push_token into main
Reviewed-on: eatvio/eatvio_app#902
Reviewed-by: misha <misha@noreply.localhost>
2026-02-04 12:49:13 +00:00
mso7nueh
a496edaf4c feat: Integrate Huawei Mobile Services (HMS) by updating configuration, adding manifest metadata, and differentiating Huawei devices for token submission. 2026-02-04 13:23:58 +03:00
misha
400cc004ef fix: analizer fixed 2026-02-04 12:21:09 +03:00
misha
beda152df0 fix: temporary removed tests from ci/cd 2026-02-04 11:46:47 +03:00
misha
42ffde820d feat(my_diets): draft diets fixes 2026-02-04 11:38:53 +03:00
mso7nueh
adb7b0bd74 Merge pull request 'chore: update project version' (#898) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#898
2026-02-03 12:12:43 +00:00
mso7nueh
a768eefed0 chore: update project version 2026-02-03 15:12:07 +03:00
mso7nueh
0ac50d6d18 Merge pull request 'feat: Add local Mulish font family and remove Google Fonts dependency.' (#897) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#897
2026-02-03 11:50:27 +00:00
mso7nueh
a0d8fda50d feat: Add local Mulish font family and remove Google Fonts dependency. 2026-02-03 14:49:49 +03:00
mso7nueh
1d286fa8d9 Merge pull request 'feat: introduce My Diets and Create Diet features with associated UI, state management, and data handling.' (#889) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#889
Reviewed-by: misha <misha@noreply.localhost>
2026-02-03 08:16:56 +00:00
mso7nueh
c2a5e7305e feat: add a customizable FeedIconButton widget that supports text, IconData, or SvgPicture. 2026-02-02 15:32:10 +03:00
mso7nueh
bbb1b28692 merge upstream 2026-02-02 12:12:12 +00:00
mso7nueh
21a65caea9 Обновить .gitea/workflows/build.yml 2026-02-02 12:09:13 +00:00
mso7nueh
eac366e9ae Обновить .gitea/workflows/test.yml 2026-02-02 12:09:00 +00:00
mso7nueh
278f4d7e10 feat: Add numerous repository implementations, cubits, and core utilities across various features, and update dependencies. 2026-02-02 14:24:17 +03:00
mso7nueh
ac5504a514 feat: Implement FeedScreen with dynamic content display, search functionality, and filtering options. 2026-01-30 14:05:45 +03:00
mso7nueh
843e1ce4dd test: add unit tests for ValidationUtils covering email, phone, URL, password, username, text, title, and description length validations, along with phone normalization and error message generation. 2026-01-29 16:01:18 +03:00
mso7nueh
db6db89567 feat: add unit tests for TokenInterceptor's request and error handling logic. 2026-01-29 15:57:11 +03:00
mso7nueh
044d86e710 fix mask formatter in phone 2026-01-29 15:49:13 +03:00
mso7nueh
4ff880e079 feat: add register via social screen 2026-01-29 15:42:02 +03:00
mso7nueh
05b9ca17c7 feat: Introduce token interceptor for authentication, WebSocket manager for real-time updates, and a new menu screen. 2026-01-29 15:00:04 +03:00
mso7nueh
a1c0c2694b test: Add basic widget tests for MyDietsTab and MyDietWidget, and integration tests for create_diet. 2026-01-28 18:16:24 +03:00
mso7nueh
2d7407ce99 feat: introduce My Diets and Create Diet features with associated UI, state management, and data handling. 2026-01-28 17:32:29 +03:00
62fe5ebfa0 Merge pull request 'feat(consultant)' (#888) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#888
Reviewed-by: mso7nueh <mso7nueh@mail.ru>
2026-01-28 13:31:42 +00:00
misha
3dc36ab52d feat(consultants): fixed infinite loading after verifying phone 2026-01-28 12:56:04 +03:00
190e212eb4 Merge pull request 'feat(tests)' (#876) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#876
2026-01-28 09:44:50 +00:00
misha
cf3d1a945c Merge remote-tracking branch 'upstream/main' 2026-01-28 11:52:38 +03:00
mso7nueh
c54915f45d Обновить .gitea/workflows/test-flight-cd.yaml 2026-01-27 15:17:51 +00:00
mso7nueh
b188570882 Обновить .gitea/workflows/test-flight-cd.yaml 2026-01-27 15:14:00 +00:00
mso7nueh
d0794670f6 Обновить .gitea/workflows/test-flight-cd.yaml 2026-01-27 15:07:38 +00:00
mso7nueh
3b53ed6f7d Merge pull request 'Some fixes' (#873) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#873
Reviewed-by: misha <misha@noreply.localhost>
2026-01-27 14:57:23 +00:00
mso7nueh
a003814265 chore: update Huawei dependencies in build.gradle to latest versions for improved functionality 2026-01-27 16:56:22 +03:00
mso7nueh
e99465a550 chore: add kotlin-parcelize plugin to build.gradle for improved parceling support 2026-01-27 16:29:17 +03:00
mso7nueh
a01f3914c4 fix: update file_picker dependency version format in pubspec.yaml 2026-01-27 15:50:11 +03:00
mso7nueh
15f9149cfd feat: Introduce Gitea workflows for Android testing, coverage analysis, and build environment setup. 2026-01-27 15:14:38 +03:00
mso7nueh
79df8c5f9a feat: introduce DiaryCubit for managing user diary and diet plans, and add new unit tests for various features. 2026-01-27 14:33:21 +03:00
mso7nueh
a93da1b505 Merge branch 'main' of http://gitea.office.local:3000/mso7nueh/eatvio_app 2026-01-27 13:44:39 +03:00
mso7nueh
8144013747 feat: diary add modal and error dialogs 2026-01-27 13:44:35 +03:00
misha
7bac2a13ac Merge remote-tracking branch 'upstream/main' 2026-01-26 16:55:08 +03:00
misha
29589ef6fd feat(tests): test coverage is 70.5 2026-01-26 16:38:15 +03:00
mso7nueh
97a6fe57e9 merge upstream 2026-01-26 12:30:55 +00:00
mso7nueh
8309498d9e feat: Increment app version and add tests for glossary moderation request modal widget. 2026-01-26 14:51:22 +03:00
mso7nueh
aa098b31bc feat: add glossary moderation request modal widget to handle new product submissions. 2026-01-26 14:42:43 +03:00
mso7nueh
7a85c65918 feat: add smoke tests for the DietPreviewScreen to verify rendering and information display 2026-01-26 14:39:24 +03:00
mso7nueh
6ff35fd162 feat: Implement DietPreviewScreen to display comprehensive diet details including schedule and nutritional information. 2026-01-26 14:34:38 +03:00
mso7nueh
6811b325be feat: Implement core authentication logic with AuthCubit and introduce a comprehensive email verification flow. 2026-01-26 14:31:39 +03:00
misha
92205c09d6 feat(tests): test coverage is 70.4 2026-01-23 12:40:17 +03:00
008dba9fed Merge pull request 'feat(tests)' (#867) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#867
Reviewed-by: mso7nueh <mso7nueh@mail.ru>
2026-01-23 09:20:37 +00:00
mso7nueh
55ca558bfb fix: analyzer 2026-01-22 12:26:57 +03:00
mso7nueh
91499ae207 feat: implement diary repository and cubit for managing daily food entries, nutritional data, and user interactions. 2026-01-22 00:58:56 +03:00
mso7nueh
4bacdf5e76 feat: implement UI for managing recipe ingredients with glossary search, nutritional calculation, and moderation request functionality. 2026-01-22 00:50:20 +03:00
mso7nueh
e76c1aed20 feat: implement recipe creation feature including state management, ingredient/step management, image handling, and validation. 2026-01-22 00:42:30 +03:00
mso7nueh
de8f879f59 feat: Implement social authentication facade for VK, OK.ru, Mail.ru, and Yandex. 2026-01-21 23:54:08 +03:00
misha
f6809a06b3 feat(tests): test coverage is 69.2 2026-01-21 09:19:41 +03:00
misha
775d4fb80f Merge remote-tracking branch 'upstream/main' 2026-01-20 12:00:44 +03:00
mso7nueh
64e9df69f7 Merge pull request 'feat: Implement glossary moderation, notifications, product modals, social authentication, and internationalization.' (#868) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#868
Reviewed-by: misha <misha@noreply.localhost>
2026-01-19 18:46:05 +00:00
mso7nueh
6ccfcdedda feat: create AddProductModal for adding and editing diary product portions with glossary search functionality. 2026-01-19 16:06:19 +03:00
mso7nueh
d3c8034b8f feat: Implement glossary moderation, notifications, product modals, social authentication, and internationalization. 2026-01-19 16:00:30 +03:00
misha
44ede73ceb feat(tests): tests coverage 67.2 2026-01-19 12:32:11 +03:00
2c63990afc Merge pull request 'feat(tests)' (#864) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#864
Reviewed-by: mso7nueh <mso7nueh@mail.ru>
2026-01-19 09:12:46 +00:00
mso7nueh
eb69b253bc Merge pull request 'chore: enhance markdown processing and UI adjustments in comments input' (#852) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#852
Reviewed-by: misha <misha@noreply.localhost>
2026-01-15 10:28:12 +00:00
mso7nueh
f26886a7c5 Merge pull request 'fix: splash screen stuck' (#861) from mso7nueh/eatvio_app:fix_splash_stuck into main
Reviewed-on: eatvio/eatvio_app#861
Reviewed-by: misha <misha@noreply.localhost>
2026-01-15 10:27:57 +00:00
misha
13ccf56df0 feat(tests): updated tests coverage to 66.5% 2026-01-14 18:47:11 +03:00
mso7nueh
c0032f7a95 feat: enhance Firebase messaging error logging and simplify BottomNavigationBarWidget by removing profileModel dependency 2026-01-14 16:09:07 +03:00
mso7nueh
4852d2cf10 refactor: replace HTML rendering with MarkdownHtmlWidget across various components for consistent markdown processing 2026-01-14 14:48:32 +03:00
mso7nueh
41e96c63c6 feat(token_storage): add clearCache method for testing and ensure fresh state in tests 2026-01-14 13:02:24 +03:00
mso7nueh
c786821275 fix: handle null context in MarkdownHtmlWidget and improve test stability with pumpAndSettle calls 2026-01-13 13:03:58 +03:00
mso7nueh
7b425e56f1 chore: remove unused markdown_utils import from expandable_text.dart 2026-01-13 12:37:10 +03:00
mso7nueh
bf2ba1fbda feat: integrate MarkdownHtmlWidget for improved markdown rendering in blog and diet components 2026-01-13 12:36:12 +03:00
mso7nueh
d206e7ec12 refactor: implement lazy initialization for global BlocProviders and enhance Firebase initialization with timeout handling 2026-01-13 12:22:22 +03:00
mso7nueh
a778257215 chore: enhance markdown processing and UI adjustments in comments input 2026-01-12 16:31:22 +03:00
mso7nueh
497ff19cfa Merge pull request 'fix(registration): update navigation to verify email screen and streamline error message display' (#845) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#845
2025-12-26 13:26:04 +00:00
mso7nueh
bb59552e3e fix(registration): update navigation to verify email screen and streamline error message display 2025-12-26 16:17:34 +03:00
mso7nueh
55b2bb0436 Merge pull request 'refactor(tests): update password and sex widget tests to use const constructors for RegistrationStateV2' (#841) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#841
2025-12-26 10:54:42 +00:00
mso7nueh
90746d0be7 refactor(tests): update password and sex widget tests to use const constructors for RegistrationStateV2 2025-12-26 13:54:10 +03:00
mso7nueh
d70d172c69 chore: update version to 1.3.11+91 in pubspec.yaml; clean up whitespace in various files 2025-12-26 13:51:44 +03:00
mso7nueh
4e23992d22 chore: bump version number to 1.3.11+91 in pubspec.yaml 2025-12-26 13:37:41 +03:00
mso7nueh
7a846ed218 chore: add blank lines for improved readability in multiple files 2025-12-26 13:35:08 +03:00
mso7nueh
fcc2936c22 Merge pull request 'feat(tests), feat(registration), feat(diary), feat(oAuth)' (#835) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#835
2025-12-25 14:19:38 +00:00
misha
b11cf2c0b0 Merge remote-tracking branch 'upstream/main' 2025-12-25 16:36:23 +03:00
misha
0ead14bca5 feat(registration): added email verification, feat(diary): fixed moderation request, feat(oauth): fixed email and phone verification 2025-12-25 16:31:52 +03:00
mso7nueh
08e56d9777 Merge pull request 'chore: update Flutter version in CI workflow from 3.38.1 to 3.38.5' (#831) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#831
2025-12-25 12:55:49 +00:00
misha
99659f7ea1 feat(tests): increased tests coverage 2025-12-25 12:56:38 +03:00
mso7nueh
e158d812e4 refactor: clean up imports and improve code readability across various files 2025-12-24 17:54:35 +03:00
mso7nueh
3d774267ca chore: update pubspec.yaml dependencies and Flutter SDK version; add blank lines in various files for improved readability 2025-12-24 14:52:51 +03:00
mso7nueh
aeb89a9aba merge upstream 2025-12-23 13:01:33 +00:00
mso7nueh
a4f5bfdb4d Merge pull request 'feat(tests)' (#832) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#832
2025-12-23 13:01:15 +00:00
misha
5b705dfb72 feat(tests): test coverage 60% 2025-12-23 10:59:56 +03:00
mso7nueh
a792dcd163 chore: update dependencies in pubspec.yaml for retrofit and development tools 2025-12-23 10:37:41 +03:00
mso7nueh
9c7ca14017 chore: update Flutter version in CI workflow from 3.38.1 to 3.38.5 2025-12-23 10:21:15 +03:00
mso7nueh
38118055a5 Merge pull request 'fix: ui overflow nav-bar, name files in chat, qr-code scanner in dev-tools, message field size, keyboard type' (#830) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#830
2025-12-22 13:53:25 +00:00
mso7nueh
5d6e25858b test(chat): update chat service test to validate message response and ensure correct message model handling 2025-12-22 16:52:40 +03:00
mso7nueh
42b443c3ba fix: ui overflow nav-bar, name files in chat, qr-code scanner in dev-tools, message field size, keyboard type 2025-12-22 16:48:13 +03:00
mso7nueh
21f7c40cde Merge pull request 'feat(chat): enhance chat message model and repository to support improved message handling and parsing' (#816) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#816
2025-12-19 12:36:00 +00:00
mso7nueh
f56092d88f feat(chat): enhance chat message model and repository to support improved message handling and parsing 2025-12-19 15:35:37 +03:00
mso7nueh
2b97c0f57d Merge pull request 'feat(router): enhance deep link handling with logging and support for various content types' (#815) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#815
2025-12-19 12:16:12 +00:00
mso7nueh
a1304e4612 feat(router): enhance deep link handling with logging and support for various content types 2025-12-19 15:15:54 +03:00
mso7nueh
7b534cf891 Merge pull request 'feat(chat): add plan order message widget and enhance chat bubble to support message types' (#814) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#814
2025-12-19 12:15:42 +00:00
mso7nueh
22b2e5f249 feat(chat): add plan order message widget and enhance chat bubble to support message types 2025-12-19 15:08:11 +03:00
mso7nueh
5dfdbe4543 Merge pull request 'feat(ios): read and set Flutter build number in Fastfile for app store deployment' (#813) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#813
2025-12-17 12:52:57 +00:00
mso7nueh
5c9a9c22ba feat(ios): read and set Flutter build number in Fastfile for app store deployment 2025-12-17 15:52:34 +03:00
mso7nueh
4f920359c8 Merge pull request 'implement OAuth verification request and confirmation flow with error handling and UI updates' (#811) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#811
2025-12-17 12:42:26 +00:00
mso7nueh
2ab3bc1cf0 feat(auth): implement OAuth verification request and confirmation flow with error handling and UI updates 2025-12-17 15:41:38 +03:00
mso7nueh
91bd8f9d68 merge upstream 2025-12-17 07:48:37 +00:00
9c6f6bdfb1 Merge pull request 'feat(diary): meal summary fix' (#810) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#810
2025-12-17 07:33:36 +00:00
misha
fe1e81e455 feat(diary): meal summary fix 2025-12-17 08:38:13 +03:00
mso7nueh
b7e064a560 test(tests): add whitespace to multiple test files for consistency and improved readability 2025-12-16 14:18:25 +03:00
mso7nueh
f1b7a24044 feat(coverage): enhance coverage analysis script to filter generated files and generate detailed markdown report for files with less than 100% coverage 2025-12-15 23:37:17 +03:00
mso7nueh
cb441b4027 Merge branch 'mso7nueh-main' 2025-12-12 11:38:22 +03:00
7282fea59a Merge pull request 'changed version' (#807) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#807
2025-12-12 07:52:00 +00:00
misha
4adec4dbbc changed version 2025-12-12 10:50:37 +03:00
mso7nueh
d5b0e972a5 chore(version): update app version to 1.3.7+87 and adjust related iOS project settings 2025-12-12 10:43:26 +03:00
mso7nueh
391efeb8dd fix(chat): initialize startDate to current date when personal diet is not enabled and streamline widget layout for improved readability 2025-12-12 10:38:53 +03:00
d115af6f8e Merge pull request 'feat(tests)' (#805) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#805
2025-12-12 07:15:24 +00:00
mso7nueh
2408443a26 test(tests): add whitespace to multiple test files for consistency and improved readability 2025-12-11 21:43:36 +03:00
mso7nueh
c378c7fc16 test(tests): add extensive widget tests for MarkdownEditor and CommentsActionsBottomSheet, improving test coverage and functionality validation 2025-12-11 21:19:41 +03:00
misha
070a3e39e7 feat(tests): added services tests and fixtures 2025-12-11 15:53:35 +03:00
mso7nueh
d1753e99f8 Merge pull request 'tests' (#804) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#804
2025-12-11 11:13:28 +00:00
mso7nueh
960afbd6ed chore(dependencies): add webview_flutter_platform_interface package to pubspec.yaml and clean up test files by removing unnecessary whitespace 2025-12-11 14:12:41 +03:00
mso7nueh
70e4a42e98 test(tests): add comprehensive unit tests for various models and repositories, enhancing test coverage and ensuring correct functionality 2025-12-09 16:19:48 +03:00
mso7nueh
04061cb4c1 Merge pull request 'chore(version): update app version to 1.3.6+86 and filter active plans in PlansRepository' (#802) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#802
2025-12-08 09:11:29 +00:00
mso7nueh
edd064d20d chore(version): update app version to 1.3.6+86 and filter active plans in PlansRepository 2025-12-08 12:10:19 +03:00
mso7nueh
cdb7378265 Merge pull request 'chore(version): update app version to 1.3.5+85 and improve test coverage reporting in CI' (#801) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#801
2025-12-08 08:55:58 +00:00
mso7nueh
746d366c18 refactor(tests): replace 'final' with 'const' for improved performance in various test files 2025-12-08 11:32:40 +03:00
mso7nueh
c2a506cdbf chore(version): update app version to 1.3.5+85 and improve test coverage reporting in CI 2025-12-08 11:19:08 +03:00
mso7nueh
abe4985f0e Merge pull request 'main' (#800) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#800
2025-12-04 13:14:34 +00:00
mso7nueh
ca06b2bd0f chore(tests): remove unused tests and clean up test files for MyDietsRepository and MyPlansCubit 2025-12-04 16:13:36 +03:00
mso7nueh
04c932646d Merge resolve-conflicts into main 2025-12-04 16:13:30 +03:00
mso7nueh
3d4363185b Resolve merge conflicts: add network_image_mock to pubspec.yaml 2025-12-04 15:50:53 +03:00
mso7nueh
953230b387 feat(dependencies): add rxdart package and improve string extension methods for better handling of empty strings and gender parsing 2025-12-04 15:45:54 +03:00
mso7nueh
9d845e1264 chore(dependencies): add gpassword package to pubspec.yaml 2025-12-03 18:42:48 +03:00
mso7nueh
32de60742e Merge mso7nueh-main into main: resolve conflicts in pubspec.yaml and feed_repository_test.dart 2025-12-03 18:38:37 +03:00
mso7nueh
3923b61c67 Merge mso7nueh-main into main: resolve conflicts 2025-12-03 18:34:25 +03:00
mso7nueh
0cd8e662d9 feat(blog): enhance markdown processing in blog, diet, plan, recipe, and my diet widgets to auto-convert URLs to markdown links and improve link handling 2025-12-03 18:20:44 +03:00
89ea73fc59 Merge pull request 'feat(tests): widget and unit tests for chat' (#796) from misha/eatvio_app:issue-795 into main
Reviewed-on: eatvio/eatvio_app#796
2025-12-03 15:18:49 +00:00
misha
9e33063c30 feat(tests): widget and unit tests for chat 2025-12-03 18:12:17 +03:00
mso7nueh
f3d4478c65 refactor(tests): update model instantiation to use const for improved performance 2025-12-03 06:16:34 +03:00
673aa86370 Merge pull request 'feat(tests): widget and unit tests for diets, plans and recipes' (#794) from misha/eatvio_app:issue-793 into main
Reviewed-on: eatvio/eatvio_app#794
2025-12-02 12:15:07 +00:00
misha
1738d27b82 added widget and unit tests for plans, diets, recipes 2025-12-02 12:34:13 +03:00
mso7nueh
d0becb9e11 feat(test): enhance internet connection tests with dotenv integration and async handling 2025-12-01 23:54:41 +03:00
mso7nueh
965ae2f02b feat(test): update oauth handling and improve preferences management 2025-12-01 23:51:40 +03:00
mso7nueh
9fcfac87d4 feat(test): oauth, prefs, credentials 2025-11-28 14:22:05 +03:00
mso7nueh
2a25be5aa4 feat(test): comments, auth, feed 2025-11-28 14:13:05 +03:00
b420d11996 Merge pull request 'feat(tests)' (#792) from misha/eatvio_app:issue-791 into main
Reviewed-on: eatvio/eatvio_app#792
2025-11-28 07:55:06 +00:00
misha
cbeb91a9f0 fix: analyzer issue 2025-11-28 01:00:56 +03:00
misha
9ca09e8137 Remove unwanted file 2025-11-28 00:49:38 +03:00
misha
8917f54e7b feat(tests): fixed feed tests failing and added tests for my_diets, my_plans and recipes 2025-11-28 00:46:25 +03:00
misha
626f74d20d Merge remote-tracking branch 'upstream/main' into issue-791 2025-11-27 23:50:39 +03:00
mso7nueh
7df32dabd2 feat(tests): menu, settings, remove icov, remove generated files from % tests 2025-11-27 08:18:23 +03:00
mso7nueh
2e57cd9519 feat(consultant): Issue-420 + Tests 2025-11-25 15:52:41 +03:00
misha
c6c42b0525 feat(test): repostory, cubit and widget tests for feed 2025-11-25 11:13:37 +03:00
mso7nueh
58bb766721 feat(tests): profile tests 2025-11-21 23:31:13 +03:00
mso7nueh
0be7392a39 feat(tests): add fixtures 2025-11-21 23:24:49 +03:00
mso7nueh
4deda92c5e feat(tests): add fixtures 2025-11-20 16:58:56 +03:00
misha
b83d91e7f2 feat(tests): feed service tests 2025-11-20 09:54:12 +03:00
e5029f581d Merge pull request 'Updated app version' (#780) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#780
2025-11-19 13:57:51 +00:00
misha
c69fe7f0a6 Updated app version 2025-11-19 16:57:17 +03:00
fc0e9b1063 Merge pull request 'Update proj version' (#778) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#778
2025-11-19 13:48:20 +00:00
misha
453f119317 Merge remote-tracking branch 'upstream/main' 2025-11-19 16:47:43 +03:00
misha
6d0bee4da8 set build type to prod 2025-11-19 16:46:18 +03:00
misha
04d4be38d4 Update proj version 2025-11-19 16:45:08 +03:00
fa22cf5d36 Merge pull request 'feat(personalDiets)' (#760) from misha/eatvio_app:issue-756 into main
Reviewed-on: eatvio/eatvio_app#760
2025-11-19 09:58:07 +00:00
91e8348015 Merge pull request 'feat(registration): fixed registration on android devices, feat(leaders): fixed diet creation loader' (#776) from misha/eatvio_app:issue-738 into main
Reviewed-on: eatvio/eatvio_app#776
2025-11-19 08:17:36 +00:00
misha
1b948b99e3 Merge remote-tracking branch 'upstream/main' into issue-738 2025-11-19 11:16:45 +03:00
misha
b6eac3c46f feat(registration): fixed registration on android devices, feat(leaders): fixed diet creation loader 2025-11-19 11:07:16 +03:00
mso7nueh
6fa944b3df Merge branch 'main' of http://gitea.office.local:3000/mso7nueh/eatvio_app 2025-11-19 04:49:05 +03:00
mso7nueh
c31411c4a2 fix: analyzer 2025-11-19 04:48:35 +03:00
mso7nueh
6214d19a14 feat(auth): tests Issue-761 2025-11-19 04:47:30 +03:00
mso7nueh
db5dddff86 feat(barcode): add scanner, upd flutter version in ci 2025-11-17 20:25:52 +03:00
mso7nueh
39775cd7a8 Merge pull request 'updated app version' (#774) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#774
2025-11-17 10:22:48 +00:00
Username1548
36e2f12e3f updated app version 2025-11-17 12:01:31 +03:00
mso7nueh
66d2533add Merge pull request 'fix: Issue-752' (#769) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#769
2025-11-14 10:34:42 +00:00
mso7nueh
425f16bfc9 fix(consultation): Issue-752 2025-11-13 19:17:28 +03:00
mso7nueh
c5e30d8f00 Merge branch 'main' of http://gitea.office.local:3000/mso7nueh/eatvio_app 2025-11-13 18:59:48 +03:00
mso7nueh
d0935fbaae feat: upd flutter 2025-11-13 18:58:56 +03:00
dda0aa7440 Merge pull request 'fix: tests fix' (#768) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#768
2025-11-13 08:21:48 +00:00
misha
c81327b895 fix: tests fix 2025-11-13 11:21:02 +03:00
732d667ac1 Merge pull request 'improvements: added loaders' (#766) from misha/eatvio_app:issue-580 into main
Reviewed-on: eatvio/eatvio_app#766
2025-11-13 07:13:00 +00:00
misha
ba9f4a3584 improvements: added loaders 2025-11-13 10:12:11 +03:00
mso7nueh
f63d901a55 Merge pull request 'feat(ci/cd): add coverage test' (#764) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#764
2025-11-12 11:03:23 +00:00
mso7nueh
9148a4c668 feat(ci/cd): add coverage test 2025-11-12 13:34:55 +03:00
mso7nueh
a3f0aa3e67 Merge pull request 'feat(oauth): Issue-691+180' (#762) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#762
2025-11-11 14:40:09 +00:00
mso7nueh
8a390c2a9b Merge branch 'main' of http://gitea.office.local:3000/mso7nueh/eatvio_app 2025-11-11 17:38:23 +03:00
mso7nueh
92b37e77b1 fix(socail,diets): Issue-180+691 2025-11-11 17:38:15 +03:00
101c1e15af Merge pull request 'feat(registration)' (#739) from misha/eatvio_app:issue-738 into main
Reviewed-on: eatvio/eatvio_app#739
2025-11-11 07:24:37 +00:00
mso7nueh
172c05ac69 merge upstream 2025-11-10 21:16:17 +00:00
misha
4eecc458f8 fix: formatting 2025-11-10 18:59:03 +03:00
misha
4b781adb79 feat(registration): added api integration for short registration 2025-11-10 18:55:55 +03:00
mso7nueh
d1160ba35f feat(oauth): Issue-691 2025-11-07 01:38:00 +03:00
mso7nueh
a46ac5bb4c Merge pull request 'feat(diary_test): Issue-755' (#759) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#759
2025-11-06 19:05:03 +00:00
mso7nueh
2a06a5476a feat(diary_test): Issue-755 2025-11-06 00:38:08 +03:00
mso7nueh
f3853b58d9 Merge pull request 'fix: Issue-757' (#758) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#758
2025-11-05 21:17:36 +00:00
mso7nueh
bbc99ec9f4 fix(settings): Issue-757 2025-11-05 23:55:48 +03:00
mso7nueh
12f51d49ea fix(profile): subs screen and blog post padding 2025-10-31 12:59:02 +03:00
misha
e147f6e182 feat(registration): finished layout for new registration screen 2025-10-22 15:27:02 +03:00
misha
2440204294 feat(registration): added base layout for new registration page, and a bit of state managment 2025-10-20 17:55:56 +03:00
873 changed files with 97441 additions and 32675 deletions

View File

@@ -3,19 +3,19 @@ name: Eatvio App Android Test
on: [ pull_request ]
env:
flutter_version: "3.32.0"
flutter_version: "3.41.2"
java_version: "21.x"
flutter_channel: "stable"
jobs:
build:
runs-on: [ dev-office ]
runs-on: [ ubuntu-latest ]
steps:
- uses: actions/checkout@v3
- uses: https://github.com/actions/checkout@v3
- name: Set up JDK
uses: actions/setup-java@v3
uses: https://github.com/actions/setup-java@v3
with:
distribution: "zulu"
java-version: ${{ env.java_version }}
@@ -24,7 +24,7 @@ jobs:
- name: Install Android SDK
run: |
apt-get update
apt-get install -y openjdk-11-jdk wget
apt-get install -y openjdk-11-jdk wget xz-utils tar
wget https://dl.google.com/android/repository/commandlinetools-linux-7583922_latest.zip -O commandlinetools.zip
unzip commandlinetools.zip
mkdir -p $HOME/Android/Sdk/cmdline-tools/latest
@@ -44,7 +44,7 @@ jobs:
apt-get install -y jq
- id: flutter-action
uses: actions/flutter-action@v2
uses: https://github.com/subosito/flutter-action@v2
with:
channel: ${{ env.flutter_channel }}
flutter-version: ${{ env.flutter_version }}
@@ -79,7 +79,7 @@ jobs:
echo "APK_NAME=$APK_NAME" >> $GITHUB_ENV
- name: Upload APK artifact
uses: actions/upload-artifact@v3
uses: https://github.com/actions/upload-artifact@v3
with:
name: ${{ env.APK_NAME }}
path: ${{ env.APK_NAME }}

View File

@@ -3,7 +3,7 @@ name: Eatvio App Android Test on custom docker
on: [ pull_request ]
env:
flutter_version: "3.32.0"
flutter_version: "3.41.2"
java_version: "21.x"
flutter_channel: "stable"
my_local_cache_dir: "/opt/hostedtoolcache/"
@@ -15,7 +15,7 @@ jobs:
runs-on: [ dev-office-flutter ]
steps:
- uses: actions/checkout@v3
- uses: https://github.com/actions/checkout@v3
- name: Set up Android environment variables
id: "android-env-setup"
@@ -52,7 +52,7 @@ jobs:
# echo "APK_NAME=$APK_NAME" >> $GITHUB_ENV
# - name: Upload APK artifact
# uses: actions/upload-artifact@v3
# uses: https://github.com/actions/upload-artifact@v3
# with:
# name: ${{ env.APK_NAME }}
# path: ${{ env.APK_NAME }}

View File

@@ -9,10 +9,10 @@ jobs:
runs-on: [macos-arm64]
steps:
- name: Checkout
uses: actions/checkout@v2
uses: https://github.com/actions/checkout@v2
- name: Checkout env repository
uses: actions/checkout@v2
uses: https://github.com/actions/checkout@v2
with:
repository: mso7nueh/eatvio_app_env
path: env_repo
@@ -54,11 +54,13 @@ jobs:
- name: Flutter build
run: |
flutter upgrade
flutter clean
flutter pub get
flutter pub upgrade
flutter pub run build_runner build --delete-conflicting-outputs
# Configure Flutter for release mode
pod repo update
flutter build ios --config-only --release
cd ios
pod update

View File

@@ -3,7 +3,7 @@ name: Eatvio App Android Test
on: [ pull_request ]
env:
flutter_version: "3.35.1"
flutter_version: "3.41.2"
java_version: "21.x"
flutter_channel: "stable"
my_local_cache_dir: "/opt/hostedtoolcache/"
@@ -11,13 +11,23 @@ env:
jobs:
build:
runs-on: [ dev-office ]
runs-on: [ ubuntu-latest ]
steps:
- uses: actions/checkout@v3
- uses: https://github.com/actions/checkout@v3
- name: Set proxy server
env:
PROXY_USER: ${{ secrets.PROXY_USER }}
PROXY_PASSWORD: ${{ secrets.PROXY_PASSWORD }}
run: |
echo "Setting up proxy..."
cat <<EOF >/etc/apt/apt.conf.d/proxy.conf
Acquire::http::Proxy "socks5h://${PROXY_USER}:${PROXY_PASSWORD}@10.55.5.202:19843/";
EOF
- name: Checkout env repository
uses: actions/checkout@v3
uses: https://github.com/actions/checkout@v3
with:
repository: mso7nueh/eatvio_app_env
path: env_repo
@@ -56,13 +66,13 @@ jobs:
- name: Cache Android SDK
id: cache-sdk
uses: "https://gitea.com/actions/cache@v4"
uses: "https://github.com/actions/cache@v4"
with:
path: ${{ env.my_local_cache_dir }}
key: ${{ runner.os }}-cache-sdk
- name: Set up JDK
uses: actions/setup-java@v3
uses: https://github.com/actions/setup-java@v3
with:
distribution: "zulu"
java-version: ${{ env.java_version }}
@@ -71,7 +81,7 @@ jobs:
- name: Install utils for Android SDK
run: |
apt-get update
apt-get install -y openjdk-11-jdk wget jq clang cmake ninja-build libgtk-3-dev
apt-get install -y openjdk-17-jdk wget jq clang cmake ninja-build libgtk-3-dev
- name: Install Android SDK
if: steps.cache-sdk.outputs.cache-hit != 'true'
@@ -99,16 +109,23 @@ jobs:
echo "PATH=$PATH:${{ env.my_local_cache_dir }}Android/Sdk/cmdline-tools/latest/bin:${{ env.my_local_cache_dir }}Android/Sdk/platform-tools:${{ env.my_local_cache_dir }}Android/Sdk/build-tools/30.0.3:${{ env.my_local_cache_dir }}Android/Sdk/build-tools/34.0.0" >> $GITHUB_ENV
echo "ANDROID_HOME=${{ env.my_local_cache_dir }}Android/Sdk" >> $GITHUB_ENV
- id: flutter-action
uses: actions/flutter-action@v2
with:
channel: ${{ env.flutter_channel }}
flutter-version: ${{ env.flutter_version }}
cache: true
cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:" # optional, change this to force refresh cache
cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:" # optional, change this to specify the cache path
pub-cache-key: "flutter-pub:os:-:channel:-:version:-:arch:-:hash:" # optional, change this to force refresh cache of dart pub get dependencies
pub-cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:" # optional, change this to specify the cache path
- name: Set up Flutter
run: |
FLUTTER_DIR="${{ env.my_local_cache_dir }}flutter"
if [ ! -f "$FLUTTER_DIR/bin/flutter" ]; then
echo "Downloading Flutter ${{ env.flutter_version }}..."
curl -fsSL -o /tmp/flutter.tar.xz "https://storage.googleapis.com/flutter_infra_release/releases/${{ env.flutter_channel }}/linux/flutter_linux_${{ env.flutter_version }}-${{ env.flutter_channel }}.tar.xz"
echo "Extracting Flutter..."
mkdir -p "${{ env.my_local_cache_dir }}"
tar xf /tmp/flutter.tar.xz -C "${{ env.my_local_cache_dir }}"
rm /tmp/flutter.tar.xz
else
echo "Flutter already cached at $FLUTTER_DIR"
fi
echo "$FLUTTER_DIR/bin" >> $GITHUB_PATH
echo "$FLUTTER_DIR/bin/cache/dart-sdk/bin" >> $GITHUB_PATH
echo "$HOME/.pub-cache/bin" >> $GITHUB_PATH
# - name: Print flutter-action outputs
# shell: bash
@@ -126,7 +143,9 @@ jobs:
- name: Fix dir permissions
run: |
git config --global --add safe.directory /opt/hostedtoolcache/flutter/${{ env.flutter_channel }}-${{ env.flutter_version }}-${{ steps.flutter-action.outputs.ARCHITECTURE }}
ARCH=$(uname -m)
if [ "$ARCH" = "x86_64" ]; then ARCH="x64"; fi
git config --global --add safe.directory "${{ env.my_local_cache_dir }}flutter"
flutter config --android-sdk="$ANDROID_HOME"
flutter doctor -v
@@ -140,7 +159,69 @@ jobs:
run: flutter analyze
- name: Test
run: flutter test
run: flutter test --coverage
- name: Test coverage
run: |
echo "📊 Analyzing test coverage..."
echo ""
awk -F: '
/^SF:/ {
file = $0
sub(/^SF:/, "", file)
sub(/^[[:space:]]+/, "", file)
is_generated = 0
if (file ~ /\.(g|freezed|gr|mocks)\.dart$/) {
is_generated = 1
generated_files++
} else if (file ~ /\/(build|\.dart_tool)\//) {
is_generated = 1
generated_files++
} else {
source_files++
}
}
/^LF:/ {
if (!is_generated) {
total += $2
} else {
generated_lines += $2
}
}
/^LH:/ {
if (!is_generated) {
covered += $2
} else {
generated_covered += $2
}
}
END {
print "📁 Files:"
printf " Source files: %d\n", source_files
printf " Generated files: %d (excluded)\n", generated_files
print ""
print "📝 Lines:"
printf " Total lines: %d\n", total
printf " Covered lines: %d\n", covered
printf " Uncovered lines: %d\n", total - covered
print ""
if (total == 0) {
print "⚠️ Coverage: 0%"
exit 1
} else {
percentage = (covered / total) * 100
printf "✅ Coverage: %.2f%%\n", percentage
print ""
if (percentage < 60) {
printf "⚠️ Warning: Coverage is below 60%%\n"
} else if (percentage < 80) {
printf "💛 Good: Coverage is above 60%%\n"
} else {
printf "💚 Excellent: Coverage is above 80%%\n"
}
}
}
' coverage/lcov.info
- name: Build apk
run: |
@@ -158,7 +239,7 @@ jobs:
echo "APK_NAME=$APK_NAME" >> $GITHUB_ENV
- name: Upload APK artifact
uses: actions/upload-artifact@v3
uses: https://github.com/actions/upload-artifact@v3
with:
name: ${{ env.APK_NAME }}
path: ${{ env.APK_NAME }}

6
.gitignore vendored
View File

@@ -19,4 +19,8 @@ ios/XCBuildData/
env/.env
.env
android/key.properties
app/upload-keystore.jks
app/upload-keystore.jks
coverage/
.fvm/
.fvmrc
/eatvio

View File

@@ -7,4 +7,7 @@ run-analyse:
run-checks: run-analyse run-tests
build-generated-files:
dart run build_runner build --delete-conflicting-outputs
dart run build_runner build --delete-conflicting-outputs
clean-coverage:
@./scripts/remove_generated_from_coverage.sh

55
analyze_coverage.py Normal file
View File

@@ -0,0 +1,55 @@
#!/usr/bin/env python3
import re
def analyze_coverage():
files = {}
current_file = None
with open('coverage/lcov.info', 'r', encoding='utf-8') as f:
lines = f.readlines()
i = 0
while i < len(lines):
line = lines[i].strip()
if line.startswith('SF:'):
current_file = line[3:]
files[current_file] = {'total': 0, 'covered': 0}
i += 1
elif line.startswith('LF:') and current_file:
files[current_file]['total'] = int(line[3:])
i += 1
elif line.startswith('LH:') and current_file:
files[current_file]['covered'] = int(line[3:])
i += 1
else:
i += 1
# Filter out generated files
filtered = []
for f, d in files.items():
if not f.endswith('.g.dart') and not 'generated' in f.lower() and not f.endswith('l10n.dart'):
filtered.append((f, d))
# Find files with less than 50% coverage
low_coverage = []
for f, d in filtered:
if d['total'] > 0:
pct = d['covered']/d['total']*100
if pct < 50:
low_coverage.append((f, d))
# Sort by coverage percentage (lowest first)
sorted_files = sorted(
low_coverage,
key=lambda x: (x[1]['covered']/x[1]['total']*100 if x[1]['total'] > 0 else 0)
)
print('Files with less than 50% coverage:')
print('=' * 80)
for f, d in sorted_files[:50]:
pct = d['covered']/d['total']*100
print(f'{pct:6.1f}% ({d["covered"]:4d}/{d["total"]:4d}) - {f}')
if __name__ == '__main__':
analyze_coverage()

View File

@@ -0,0 +1,52 @@
#!/usr/bin/env python3
import re
def analyze_coverage_by_uncovered():
files = {}
current_file = None
with open('coverage/lcov.info', 'r', encoding='utf-8') as f:
lines = f.readlines()
i = 0
while i < len(lines):
line = lines[i].strip()
if line.startswith('SF:'):
current_file = line[3:]
files[current_file] = {'total': 0, 'covered': 0}
i += 1
elif line.startswith('LF:') and current_file:
files[current_file]['total'] = int(line[3:])
i += 1
elif line.startswith('LH:') and current_file:
files[current_file]['covered'] = int(line[3:])
i += 1
else:
i += 1
# Filter out generated files and UI screens/widgets (hard to test)
filtered = []
exclude_patterns = ['.g.dart', 'generated', 'l10n.dart', '/pages/', '/widgets/', '_screen.dart', '_widget.dart']
for f, d in files.items():
if d['total'] > 0:
# Skip UI files
should_exclude = any(pattern in f for pattern in exclude_patterns)
if not should_exclude:
uncovered = d['total'] - d['covered']
pct = d['covered']/d['total']*100
filtered.append((f, d, uncovered, pct))
# Sort by uncovered lines (most uncovered first)
sorted_files = sorted(filtered, key=lambda x: x[2], reverse=True)
print('Files sorted by uncovered lines (excluding UI screens/widgets):')
print('=' * 100)
print(f'{"Uncovered":>10} {"Covered":>8} {"Total":>8} {"%":>7} {"File":<60}')
print('=' * 100)
for f, d, uncovered, pct in sorted_files[:30]:
print(f'{uncovered:>10} {d["covered"]:>8} {d["total"]:>8} {pct:>6.1f}% {f}')
if __name__ == '__main__':
analyze_coverage_by_uncovered()

52
analyze_output.txt Normal file
View File

@@ -0,0 +1,52 @@
Files with less than 50% coverage:
================================================================================
0.0% ( 0/ 37) - lib/core/firebase_api.dart
0.0% ( 0/ 1) - lib/features/auth/models/oauth_verification_confirm_request.dart
0.0% ( 0/ 1) - lib/features/auth/models/oauth_verification_confirm_response.dart
0.0% ( 0/ 1) - lib/features/auth/models/oauth_verification_request_response.dart
0.0% ( 0/ 342) - lib/features/dev_tools/test_cases.dart
0.9% ( 2/ 220) - lib/features/dev_tools/presentation/dev_tools_screen.dart
2.4% ( 1/ 42) - lib/features/auth/presentation/pages/scan_qr_code_screen.dart
2.9% ( 1/ 34) - lib/features/diary/presentation/pages/barcode_scanner_screen.dart
3.6% ( 1/ 28) - lib/features/personal_diet_suggestion/presentation/personal_diet_suggestion_screen.dart
3.8% ( 1/ 26) - lib/features/update_screen/presentation/update_screen.dart
4.4% ( 4/ 90) - lib/core/widgets/custom_popup.dart
5.9% ( 1/ 17) - lib/features/recipe/presentation/pages/my_recipes_screen.dart
6.2% ( 2/ 32) - lib/features/consultations/presentation/pages/consultations_screen.dart
7.1% ( 1/ 14) - lib/features/menu/presentation/pages/blocked_users_screen.dart
7.7% ( 1/ 13) - lib/features/registration_v2/presentation/pages/registration_v2_page.dart
8.3% ( 18/ 217) - lib/features/blog_post/presentation/pages/blog_post_screen.dart
11.1% ( 1/ 9) - lib/features/registration_v2/presentation/widgets/step_progress.dart
11.8% ( 14/ 119) - lib/features/auth/sdk/social_auth_facade.dart
13.0% ( 3/ 23) - lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart
14.5% ( 11/ 76) - lib/core/app_router.dart
19.3% ( 11/ 57) - lib/features/create_recipe/presentation/widgets/image_widget.dart
19.6% ( 10/ 51) - lib/features/create_recipe/entities/recipe_create_entity.dart
19.7% ( 12/ 61) - lib/features/create_recipe/presentation/widgets/step_image_widget.dart
20.0% ( 6/ 30) - lib/core/image_compressing/image_compress_utility.dart
20.0% ( 57/ 285) - lib/features/complaining/presentation/complaining_bottom_sheet.dart
21.9% ( 7/ 32) - lib/core/routing/content_routes.dart
25.6% ( 46/ 180) - lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart
26.8% ( 11/ 41) - lib/features/create_recipe/reporitories/recipe_repository.dart
27.4% ( 96/ 351) - lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart
28.0% ( 28/ 100) - lib/core/utils/image_constant.dart
28.4% ( 23/ 81) - lib/core/websockets/ws_manager.dart
29.0% ( 157/ 541) - lib/features/profile/presentation/pages/profile_screen.dart
29.3% ( 137/ 468) - lib/features/notifications/models/notification_model.dart
33.1% ( 47/ 142) - lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart
33.3% ( 9/ 27) - lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart
35.2% ( 58/ 165) - lib/features/registration/presentation/pages/registration_screen.dart
35.6% ( 36/ 101) - lib/features/chat/presentation/pages/empty_chat_screen.dart
36.0% ( 9/ 25) - lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart
36.7% ( 11/ 30) - lib/core/routing/main_navigation_routes.dart
37.5% ( 3/ 8) - lib/core/permission_handler/permission_handler.dart
38.6% ( 22/ 57) - lib/features/registration/presentation/pages/date_of_birth_screen.dart
38.9% ( 35/ 90) - lib/core/routing/other_routes.dart
39.0% ( 30/ 77) - lib/features/create_diet/presentation/widgets/diet_image_picker_widget.dart
39.3% ( 119/ 303) - lib/features/settings/presentation/pages/settings_screen.dart
39.3% ( 22/ 56) - lib/features/consultant/become_consultant/presentation/pages/photo_or_scans_screen.dart
40.0% ( 4/ 10) - lib/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_state.dart
40.0% ( 4/ 10) - lib/features/registration/services/registration_analytics_service.dart
41.5% ( 95/ 229) - lib/features/create_diet/presentation/cubit/create_diet_cubit.dart
41.9% ( 31/ 74) - lib/features/create_plan/presentation/widgets/plan_image_picker_widget.dart
42.7% ( 67/ 157) - lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart

View File

@@ -1 +1,94 @@
{"agcgw":{"url":"connect-drru.hispace.dbankcloud.ru","backurl":"connect-drru.hispace.dbankcloud.cn","websocketurl":"connect-ws-drru.hispace.dbankcloud.ru","websocketbackurl":"connect-ws-drru.hispace.dbankcloud.cn"},"agcgw_all":{"SG":"connect-dra.dbankcloud.cn","SG_back":"connect-dra.hispace.hicloud.com","CN":"connect-drcn.dbankcloud.cn","CN_back":"connect-drcn.hispace.hicloud.com","RU":"connect-drru.hispace.dbankcloud.ru","RU_back":"connect-drru.hispace.dbankcloud.cn","DE":"connect-dre.dbankcloud.cn","DE_back":"connect-dre.hispace.hicloud.com"},"websocketgw_all":{"SG":"connect-ws-dra.hispace.dbankcloud.cn","SG_back":"connect-ws-dra.hispace.dbankcloud.com","CN":"connect-ws-drcn.hispace.dbankcloud.cn","CN_back":"connect-ws-drcn.hispace.dbankcloud.com","RU":"connect-ws-drru.hispace.dbankcloud.ru","RU_back":"connect-ws-drru.hispace.dbankcloud.cn","DE":"connect-ws-dre.hispace.dbankcloud.cn","DE_back":"connect-ws-dre.hispace.dbankcloud.com"},"client":{"cp_id":"20007000036693451","product_id":"461323198429446894","client_id":"1595824543740483264","client_secret":"2A1727B0D31AA9BE1F5D9E8A5CDA1A67B9EF084AC9FB68D6C633A3CCB08ECDEE","project_id":"461323198429446894","app_id":"113238569","api_key":"DQEDAJNRITp9M3uc1rPHyT8PzLc7bnVcKJAeavZZk0TQNMx9ifvBebr+JOmZe9tEvjnLwv7c1hAHXmPGR/gWG86iABDrCVotq+NGnw==","package_name":"com.eatvioapp.app"},"oauth_client":{"client_id":"113238569","client_type":1},"app_info":{"app_id":"113238569","package_name":"com.eatvioapp.app"},"service":{"analytics":{"collector_url":"datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com","collector_url_cn":"datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn","collector_url_de":"datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn","collector_url_ru":"datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com","collector_url_sg":"datacollector-dra.dt.hicloud.com,datacollector-dra.dt.dbankcloud.cn","resource_id":"p1","channel_id":""},"ml":{"mlservice_url":"ml-api-drru.ai.dbankcloud.ru,ml-api-drru.ai.dbankcloud.cn"},"cloudstorage":{"storage_url":"https://agc-storage-drcn.platform.dbankcloud.cn","storage_url_ru":"https://agc-storage-drru.cloud.huawei.ru","storage_url_sg":"https://ops-dra.agcstorage.link","storage_url_de":"https://ops-dre.agcstorage.link","storage_url_cn":"https://agc-storage-drcn.platform.dbankcloud.cn","storage_url_ru_back":"https://agc-storage-drru.cloud.huawei.ru","storage_url_sg_back":"https://agc-storage-dra.cloud.huawei.asia","storage_url_de_back":"https://agc-storage-dre.cloud.huawei.eu","storage_url_cn_back":"https://agc-storage-drcn.cloud.huawei.com.cn"},"search":{"url":"https://search-drru.cloud.huawei.ru"},"edukit":{"edu_url":"edukit.cloud.huawei.com.cn","dh_url":"edukit.cloud.huawei.com.cn"}},"region":"RU","configuration_version":"3.0","appInfos":[{"package_name":"com.eatvioapp.app","client":{"app_id":"113238569"},"app_info":{"package_name":"com.eatvioapp.app","app_id":"113238569"},"oauth_client":{"client_type":1,"client_id":"113238569"}}]}
{
"agcgw" : {
"url" : "connect-drru.hispace.dbankcloud.ru",
"backurl" : "connect-drru.hispace.dbankcloud.cn",
"websocketurl" : "connect-ws-drru.hispace.dbankcloud.ru",
"websocketbackurl" : "connect-ws-drru.hispace.dbankcloud.cn"
},
"agcgw_all" : {
"SG" : "connect-dra.dbankcloud.cn",
"SG_back" : "connect-dra.hispace.hicloud.com",
"CN" : "connect-drcn.dbankcloud.cn",
"CN_back" : "connect-drcn.hispace.hicloud.com",
"RU" : "connect-drru.hispace.dbankcloud.ru",
"RU_back" : "connect-drru.hispace.dbankcloud.cn",
"DE" : "connect-dre.dbankcloud.cn",
"DE_back" : "connect-dre.hispace.hicloud.com"
},
"websocketgw_all" : {
"SG" : "connect-ws-dra.hispace.dbankcloud.cn",
"SG_back" : "connect-ws-dra.hispace.dbankcloud.com",
"CN" : "connect-ws-drcn.hispace.dbankcloud.cn",
"CN_back" : "connect-ws-drcn.hispace.dbankcloud.com",
"RU" : "connect-ws-drru.hispace.dbankcloud.ru",
"RU_back" : "connect-ws-drru.hispace.dbankcloud.cn",
"DE" : "connect-ws-dre.hispace.dbankcloud.cn",
"DE_back" : "connect-ws-dre.hispace.dbankcloud.com"
},
"client" : {
"cp_id" : "20007000036693451",
"product_id" : "461323198429446894",
"client_id" : "1595824543740483264",
"client_secret" : "2A1727B0D31AA9BE1F5D9E8A5CDA1A67B9EF084AC9FB68D6C633A3CCB08ECDEE",
"project_id" : "461323198429446894",
"app_id" : "113238569",
"api_key" : "DQEDAJNRITp9M3uc1rPHyT8PzLc7bnVcKJAeavZZk0TQNMx9ifvBebr+JOmZe9tEvjnLwv7c1hAHXmPGR/gWG86iABDrCVotq+NGnw==",
"package_name" : "com.eatvioapp.app"
},
"oauth_client" : {
"client_id" : "113238569",
"client_type" : 1
},
"app_info" : {
"app_id" : "113238569",
"package_name" : "com.eatvioapp.app"
},
"service" : {
"analytics" : {
"collector_url" : "datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com",
"collector_url_cn" : "datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn",
"collector_url_de" : "datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn",
"collector_url_ru" : "datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com",
"collector_url_sg" : "datacollector-dra.dt.hicloud.com,datacollector-dra.dt.dbankcloud.cn",
"resource_id" : "p1",
"channel_id" : ""
},
"ml" : {
"mlservice_url" : "ml-api-drru.ai.dbankcloud.ru,ml-api-drru.ai.dbankcloud.cn"
},
"cloudstorage" : {
"storage_url" : "https://agc-storage-drcn.platform.dbankcloud.cn",
"storage_url_ru" : "https://agc-storage-drru.cloud.huawei.ru",
"storage_url_sg" : "https://ops-dra.agcstorage.link",
"storage_url_de" : "https://ops-dre.agcstorage.link",
"storage_url_cn" : "https://agc-storage-drcn.platform.dbankcloud.cn",
"storage_url_ru_back" : "https://agc-storage-drru.cloud.huawei.ru",
"storage_url_sg_back" : "https://agc-storage-dra.cloud.huawei.asia",
"storage_url_de_back" : "https://agc-storage-dre.cloud.huawei.eu",
"storage_url_cn_back" : "https://agc-storage-drcn.cloud.huawei.com.cn"
},
"search" : {
"url" : "https://search-drru.cloud.huawei.ru"
},
"edukit" : {
"edu_url" : "edukit.cloud.huawei.com.cn",
"dh_url" : "edukit.cloud.huawei.com.cn"
}
},
"region" : "RU",
"configuration_version" : "3.0",
"appInfos" : [ {
"package_name" : "com.eatvioapp.app",
"client" : {
"app_id" : "113238569"
},
"app_info" : {
"package_name" : "com.eatvioapp.app",
"app_id" : "113238569"
},
"oauth_client" : {
"client_type" : 1,
"client_id" : "113238569"
}
} ]
}

View File

@@ -4,6 +4,7 @@ plugins {
id 'com.google.gms.google-services'
// END: FlutterFire Configuration
id "kotlin-android"
id "kotlin-parcelize"
id "dev.flutter.flutter-gradle-plugin"
id 'com.huawei.agconnect'
}
@@ -19,15 +20,15 @@ def yandexClientId = project.hasProperty('YANDEX_CLIENT_ID') ? project.property(
android {
namespace = "com.eatvioapp.app"
compileSdk = flutter.compileSdkVersion
ndkVersion = "27.0.12077973"
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
@@ -68,10 +69,10 @@ android {
}
dependencies {
implementation 'com.huawei.agconnect:agconnect-core:1.9.1.301'
implementation 'com.huawei.hms:push:6.12.0.300'
implementation 'com.huawei.agconnect:agconnect-core:1.9.5.300'
implementation 'com.huawei.hms:push:6.13.0.301'
// Huawei Analytics dependency to satisfy com.huawei.hianalytics.* references during R8
implementation 'com.huawei.hms:hianalytics:6.12.0.300'
implementation 'com.huawei.hms:hianalytics:6.12.0.301'
// BouncyCastle provider required by Huawei secure utils used transitively by HMS
implementation 'org.bouncycastle:bcprov-jdk15to18:1.78.1'
}

View File

@@ -21,6 +21,12 @@
<meta-data
android:name="push_kit_auto_init_enabled"
android:value="true" />
<meta-data
android:name="com.huawei.hms.client.appid"
android:value="appid=113238569" />
<meta-data
android:name="com.huawei.hms.client.cpid"
android:value="cpid=20007000036693451" />
<activity
android:name=".MainActivity"
android:exported="true"

View File

@@ -47,17 +47,21 @@ class MainActivity: FlutterActivity() {
}
if (appId.isNullOrBlank()) {
android.util.Log.e("HMS", "Missing Huawei appId. Ensure agconnect-services.json or manifest meta-data is configured.")
runOnUiThread {
result.error("HMS_TOKEN_ERROR", "Missing Huawei appId. Ensure agconnect-services.json or manifest meta-data is configured.", null)
}
return@launch
}
android.util.Log.i("HMS", "Getting token for appId: $appId")
val token = HmsInstanceId.getInstance(applicationContext)
.getToken(appId, "HCM")
android.util.Log.i("HMS", "Token retrieved successfully")
runOnUiThread {
result.success(token)
}
} catch (e: Exception) {
android.util.Log.e("HMS", "Error getting HMS token: ${e.message}", e)
runOnUiThread {
result.error("HMS_TOKEN_ERROR", e.message, null)
}

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

3
assets/icons/check.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="12" height="9" viewBox="0 0 12 9" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.16229 6.8625L10.5185 0.50625C10.6685 0.35625 10.8435 0.28125 11.0435 0.28125C11.2435 0.28125 11.4185 0.35625 11.5685 0.50625C11.7185 0.65625 11.7935 0.834375 11.7935 1.04063C11.7935 1.24688 11.7185 1.425 11.5685 1.575L4.68729 8.475C4.53729 8.625 4.36229 8.7 4.16229 8.7C3.96229 8.7 3.78729 8.625 3.63729 8.475L0.412289 5.25C0.262289 5.1 0.190414 4.92188 0.196664 4.71563C0.202914 4.50938 0.281039 4.33125 0.431039 4.18125C0.581039 4.03125 0.759164 3.95625 0.965414 3.95625C1.17166 3.95625 1.34979 4.03125 1.49979 4.18125L4.16229 6.8625Z" fill="#16A249"/>
</svg>

After

Width:  |  Height:  |  Size: 668 B

View File

@@ -0,0 +1,11 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect opacity="0.2" width="20" height="20" rx="6" fill="#16A249"/>
<g clip-path="url(#clip0_1823_10218)">
<path d="M8.16229 12.8625L14.5185 6.50625C14.6685 6.35625 14.8435 6.28125 15.0435 6.28125C15.2435 6.28125 15.4185 6.35625 15.5685 6.50625C15.7185 6.65625 15.7935 6.83438 15.7935 7.04063C15.7935 7.24688 15.7185 7.425 15.5685 7.575L8.68729 14.475C8.53729 14.625 8.36229 14.7 8.16229 14.7C7.96229 14.7 7.78729 14.625 7.63729 14.475L4.41229 11.25C4.26229 11.1 4.19041 10.9219 4.19666 10.7156C4.20291 10.5094 4.28104 10.3313 4.43104 10.1813C4.58104 10.0313 4.75916 9.95625 4.96541 9.95625C5.17166 9.95625 5.34979 10.0313 5.49979 10.1813L8.16229 12.8625Z" fill="#16A249"/>
</g>
<defs>
<clipPath id="clip0_1823_10218">
<rect width="12" height="9" fill="white" transform="translate(4 6)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 911 B

View File

@@ -0,0 +1,12 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1846_342)">
<path d="M5.33333 16C3.85185 16 2.59259 15.4815 1.55556 14.4444C0.518518 13.4074 0 12.1481 0 10.6667V5.33333C0 3.85185 0.518518 2.59259 1.55556 1.55556C2.59259 0.518518 3.85185 0 5.33333 0H10.6667C12.1481 0 13.4074 0.518518 14.4444 1.55556C15.4815 2.59259 16 3.85185 16 5.33333V10.6667C16 12.1481 15.4815 13.4074 14.4444 14.4444C13.4074 15.4815 12.1481 16 10.6667 16H5.33333ZM5.33333 14.2222H10.6667C11.6444 14.2222 12.4815 13.8741 13.1778 13.1778C13.8741 12.4815 14.2222 11.6444 14.2222 10.6667V5.33333C14.2222 4.35556 13.8741 3.51852 13.1778 2.82222C12.4815 2.12593 11.6444 1.77778 10.6667 1.77778H5.33333C4.35556 1.77778 3.51852 2.12593 2.82222 2.82222C2.12593 3.51852 1.77778 4.35556 1.77778 5.33333V10.6667C1.77778 11.6444 2.12593 12.4815 2.82222 13.1778C3.51852 13.8741 4.35556 14.2222 5.33333 14.2222Z" fill="#D22121"/>
<rect x="4.81836" y="10.9749" width="8" height="1" rx="0.5" transform="rotate(-45 4.81836 10.9749)" fill="#D22121"/>
<rect x="5.52539" y="5.31799" width="8" height="1" rx="0.5" transform="rotate(45 5.52539 5.31799)" fill="#D22121"/>
</g>
<defs>
<clipPath id="clip0_1846_342">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,3 @@
<svg width="10" height="9" viewBox="0 0 10 9" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.53669 8.89177L6.8122 7.15382C6.52317 7.26749 6.22614 7.34957 5.92111 7.40005C5.61615 7.45053 5.3063 7.47578 4.99155 7.47578C3.95998 7.47578 3.01527 7.19683 2.15742 6.63893C1.29964 6.08096 0.619788 5.34562 0.117849 4.43293C0.0770142 4.36144 0.047164 4.28862 0.0282984 4.21447C0.00943278 4.14041 0 4.06506 0 3.98842C0 3.91178 0.0102087 3.8356 0.030626 3.75987C0.0510433 3.68423 0.0816693 3.61224 0.122504 3.5439C0.317857 3.21368 0.525501 2.89417 0.745437 2.58538C0.96529 2.2765 1.22096 2.00328 1.51243 1.76572L0.406101 0.633204C0.338315 0.564287 0.303606 0.478889 0.301972 0.377008C0.300421 0.275127 0.337662 0.185536 0.413696 0.108233C0.484667 0.0360776 0.570746 0 0.671934 0C0.773041 0 0.85908 0.0360776 0.93005 0.108233L9.05304 8.3668C9.12091 8.43579 9.15689 8.52119 9.16097 8.62299C9.16505 8.72487 9.12908 8.81446 9.05304 8.89177C8.98207 8.96392 8.89599 9 8.79481 9C8.6937 9 8.60766 8.96392 8.53669 8.89177ZM4.99155 6.01955C5.10025 6.01955 5.20405 6.01299 5.30295 5.99988C5.40194 5.98676 5.49855 5.95815 5.5928 5.91406L3.09751 3.37713C3.05725 3.47295 3.02989 3.57118 3.01544 3.67181C3.00098 3.77237 2.99375 3.8779 2.99375 3.98842C2.99375 4.5537 3.18772 5.03355 3.57565 5.42795C3.96358 5.82235 4.43554 6.01955 4.99155 6.01955ZM4.99155 0.501058C6.02622 0.501058 6.97529 0.781293 7.83878 1.34176C8.70227 1.90223 9.38344 2.64201 9.88227 3.56109C9.91992 3.62436 9.94896 3.69224 9.96937 3.76473C9.98979 3.83721 10 3.91178 10 3.98842C10 4.06506 9.99106 4.13962 9.97317 4.21211C9.95529 4.28459 9.92748 4.35247 9.88975 4.41574C9.74086 4.70378 9.57442 4.97521 9.39042 5.23004C9.20634 5.48487 9.00057 5.72471 8.77312 5.94956C8.67136 6.05302 8.55074 6.10059 8.41125 6.09229C8.27184 6.08399 8.15121 6.02811 8.04937 5.92465L7.0223 4.88517C6.97142 4.83344 6.93781 4.77369 6.92147 4.70594C6.90514 4.63827 6.90608 4.56769 6.92429 4.49421C6.94749 4.40868 6.96411 4.32565 6.97415 4.24511C6.98428 4.16465 6.98934 4.07909 6.98934 3.98842C6.98934 3.42313 6.79538 2.94329 6.40745 2.54888C6.01952 2.15448 5.54755 1.95728 4.99155 1.95728C4.90236 1.95728 4.81898 1.96322 4.74139 1.97509C4.66381 1.98688 4.58447 2.00461 4.50337 2.02827C4.43117 2.04994 4.35894 2.05248 4.28666 2.03587C4.21446 2.01926 4.15293 1.9851 4.10205 1.93337L3.64584 1.46967C3.50643 1.32785 3.46012 1.16274 3.50692 0.974343C3.55372 0.785942 3.67136 0.665878 3.85985 0.614149C4.04516 0.575788 4.23174 0.547349 4.41958 0.528832C4.60742 0.510316 4.79807 0.501058 4.99155 0.501058ZM5.94046 3.04758C6.03977 3.15876 6.1216 3.28347 6.18596 3.42172C6.25032 3.55997 6.29066 3.70449 6.30699 3.85527C6.31516 3.91531 6.29303 3.95699 6.2406 3.98032C6.18817 4.00357 6.139 3.9919 6.0931 3.94532L5.04153 2.88093C4.99563 2.83427 4.98542 2.78428 5.0109 2.73097C5.0363 2.67767 5.07852 2.65101 5.13757 2.65101C5.29593 2.66438 5.44199 2.70478 5.57577 2.7722C5.70962 2.83954 5.83119 2.93133 5.94046 3.04758Z" fill="#2F80ED"/>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

3
assets/icons/sucess.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.33333 16C3.85185 16 2.59259 15.4815 1.55556 14.4444C0.518519 13.4074 0 12.1481 0 10.6667V5.33333C0 3.85185 0.518519 2.59259 1.55556 1.55556C2.59259 0.518519 3.85185 0 5.33333 0H10.6667C12.1481 0 13.4074 0.518519 14.4444 1.55556C15.4815 2.59259 16 3.85185 16 5.33333V10.6667C16 12.1481 15.4815 13.4074 14.4444 14.4444C13.4074 15.4815 12.1481 16 10.6667 16H5.33333ZM7.11111 9.06667L5.77778 7.73333C5.61481 7.57037 5.40741 7.48889 5.15556 7.48889C4.9037 7.48889 4.6963 7.57037 4.53333 7.73333C4.37037 7.8963 4.28889 8.1037 4.28889 8.35556C4.28889 8.60741 4.37037 8.81481 4.53333 8.97778L6.48889 10.9333C6.66667 11.1111 6.87407 11.2 7.11111 11.2C7.34815 11.2 7.55556 11.1111 7.73333 10.9333L11.8222 6.84444C11.9852 6.68148 12.0667 6.47407 12.0667 6.22222C12.0667 5.97037 11.9852 5.76296 11.8222 5.6C11.6593 5.43704 11.4519 5.35556 11.2 5.35556C10.9481 5.35556 10.7407 5.43704 10.5778 5.6L7.11111 9.06667ZM5.33333 14.2222H10.6667C11.6444 14.2222 12.4815 13.8741 13.1778 13.1778C13.8741 12.4815 14.2222 11.6444 14.2222 10.6667V5.33333C14.2222 4.35556 13.8741 3.51852 13.1778 2.82222C12.4815 2.12593 11.6444 1.77778 10.6667 1.77778H5.33333C4.35556 1.77778 3.51852 2.12593 2.82222 2.82222C2.12593 3.51852 1.77778 4.35556 1.77778 5.33333V10.6667C1.77778 11.6444 2.12593 12.4815 2.82222 13.1778C3.51852 13.8741 4.35556 14.2222 5.33333 14.2222Z" fill="#16A249"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

File diff suppressed because it is too large Load Diff

0
flutter Normal file
View File

View File

@@ -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 = 81;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -539,7 +539,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 81;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -578,7 +578,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 81;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (

View File

@@ -21,7 +21,7 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>82</string>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>

View File

@@ -48,8 +48,33 @@ platform :ios do
UI.message "Configuring Flutter for release mode..."
sh("cd .. && flutter build ios --config-only --release")
# Read build number from Flutter Generated.xcconfig
generated_xcconfig_path = File.join("..", "Flutter", "Generated.xcconfig")
flutter_build_number = nil
if File.exist?(generated_xcconfig_path)
File.foreach(generated_xcconfig_path) do |line|
matches = line.match(/FLUTTER_BUILD_NUMBER=(.*)/)
if matches
flutter_build_number = matches[1].strip
break
end
end
end
if flutter_build_number.nil?
UI.user_error!("Could not find FLUTTER_BUILD_NUMBER in #{generated_xcconfig_path}")
end
UI.message "Flutter build number: #{flutter_build_number}"
match(type: "appstore", readonly: true)
increment_build_number(xcodeproj: "Runner.xcodeproj")
# Set build number explicitly instead of incrementing
increment_build_number(
xcodeproj: "Runner.xcodeproj",
build_number: flutter_build_number
)
build_app(
workspace: "Runner.xcworkspace",
@@ -74,7 +99,7 @@ platform :ios do
skip_submission: true,
distribute_external: false,
notify_external_testers: false,
changelog: "TestFlight build #{get_build_number(xcodeproj: 'Runner.xcodeproj')}",
changelog: "TestFlight build #{flutter_build_number}",
verbose: true
)
rescue => ex

View File

@@ -33,15 +33,16 @@ class AppConfig {
}
/// Список глобальных BlocProvider'ов
/// Используются lazy: true для отложенной инициализации, чтобы не блокировать старт приложения
List<BlocProvider> get globalBlocProviders => [
BlocProvider<NewMessagesCubit>(create: (context) => sl<NewMessagesCubit>()),
BlocProvider<NotificationsCubit>(create: (context) => sl<NotificationsCubit>()..getNotifications()),
BlocProvider<FeedCubit>(create: (context) => sl<FeedCubit>()..loadFeedBlogPostList()),
BlocProvider<ProfileAvatarCubit>(create: (context) => sl<ProfileAvatarCubit>()..getMyProfile()),
BlocProvider<ChatsCubit>(create: (context) => sl<ChatsCubit>()..getChatRoomsWithLoading()),
BlocProvider<ViewsCubit>(create: (context) => sl<ViewsCubit>()),
BlocProvider<NewMessagesCubit>(lazy: true, create: (context) => sl<NewMessagesCubit>()),
BlocProvider<NotificationsCubit>(lazy: true, create: (context) => sl<NotificationsCubit>()),
BlocProvider<FeedCubit>(lazy: true, create: (context) => sl<FeedCubit>()),
BlocProvider<ProfileAvatarCubit>(lazy: true, create: (context) => sl<ProfileAvatarCubit>()),
BlocProvider<ChatsCubit>(lazy: true, create: (context) => sl<ChatsCubit>()),
BlocProvider<ViewsCubit>(lazy: true, create: (context) => sl<ViewsCubit>()),
BlocProvider<InternetConnectionCubit>(lazy: false, create: (context) => sl<InternetConnectionCubit>()),
BlocProvider<MarketingCubit>(create: (context) => sl<MarketingCubit>()),
BlocProvider<MarketingCubit>(lazy: true, create: (context) => sl<MarketingCubit>()),
];
/// Делегаты локализации

View File

@@ -1,3 +1,5 @@
import 'dart:async';
import 'dart:developer';
import 'dart:io' as io;
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/env_config.dart';
@@ -37,9 +39,19 @@ class AppInitializer {
/// Инициализация Firebase
static Future<void> _initializeFirebase() async {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform).timeout(
const Duration(seconds: 10),
onTimeout: () {
throw TimeoutException('Firebase initialization timeout');
},
);
if (sl<AppConfig>().buildType == AppBuildType.prod) {
await FirebaseMessaging.instance.requestPermission();
try {
await FirebaseMessaging.instance.requestPermission().timeout(const Duration(seconds: 5));
} catch (e) {
// Разрешение не критично, продолжаем работу
log('Failed to request Firebase messaging permission: $e');
}
}
}
@@ -68,6 +80,10 @@ class AppInitializer {
options.tracesSampleRate = 1.0;
options.debug = false;
}, appRunner: appRunner);
FlutterError.onError = (details) async {
if (kDebugMode) return;
await Sentry.captureException(details.exception, stackTrace: details.stack);
};
}
}

View File

@@ -1,3 +1,4 @@
import 'dart:developer' as developer;
import 'package:eatvio_app/core/routing/auth_routes.dart';
import 'package:eatvio_app/core/routing/content_routes.dart';
import 'package:eatvio_app/core/routing/main_navigation_routes.dart';
@@ -13,6 +14,9 @@ import 'package:go_router/go_router.dart';
class AppRouter {
static GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
// Домены для deep links
static const List<String> _deepLinkHosts = ['eatvio.ru', 'www.eatvio.ru'];
static GoRouter appRouter = GoRouter(
initialLocation: RouteConstants.updateScreen,
navigatorKey: navigatorKey,
@@ -27,51 +31,128 @@ class AppRouter {
);
/// Обработка редиректов для deep links
/// Возвращает null для обычной навигации, чтобы не мешать внутренним переходам
static String? _handleRedirect(BuildContext context, GoRouterState state) {
final segments = state.uri.pathSegments;
if (segments.length == 4) {
final type = segments[1];
switch (type) {
case 'diary':
FlutterNativeSplash.remove();
return '/diary-screen';
}
final uri = state.uri;
final host = uri.host.toLowerCase();
final segments = uri.pathSegments;
// Проверяем, является ли это deep link по домену
final isDeepLink = _deepLinkHosts.contains(host);
// Если это не deep link, не мешаем обычной навигации
if (!isDeepLink) {
return null;
}
if (segments.length == 3) {
// Логирование для отладки
developer.log(
'Deep link detected: host=$host, path=${uri.path}, segments=$segments',
name: 'AppRouter',
);
// Проверка на пустой путь или некорректный формат
if (segments.isEmpty) {
developer.log('Empty segments in deep link, redirecting to feed', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.feedScreen;
}
// Проверка языка (первый сегмент должен быть ru или en)
if (segments[0] != 'ru' && segments[0] != 'en') {
developer.log(
'Invalid language segment: ${segments[0]}, redirecting to feed',
name: 'AppRouter',
);
FlutterNativeSplash.remove();
return RouteConstants.feedScreen;
}
// Обработка deep links с учетом языка
// Формат: /{lang}/{type}/{identifier?}
if (segments.length == 4) {
// Формат: /{lang}/diary/{...}
final type = segments[1];
if (type == 'diary') {
developer.log('Deep link: diary, redirecting to diary-screen', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.diaryScreen;
}
developer.log(
'Unknown deep link type for 4 segments: $type, redirecting to feed',
name: 'AppRouter',
);
FlutterNativeSplash.remove();
return RouteConstants.feedScreen;
} else if (segments.length == 3) {
// Формат: /{lang}/{type}/{identifier}
final type = segments[1];
final contentIdentifier = segments[2];
switch (type) {
case 'recipe':
developer.log('Deep link: recipe/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/about-recipes-screen?slug=$contentIdentifier';
case 'blog_post':
developer.log('Deep link: blog_post/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/blog-post-screen?id=$contentIdentifier';
case 'plan':
developer.log('Deep link: plan/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/about-plan-screen?slug=$contentIdentifier';
case 'diet':
developer.log('Deep link: diet/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/about-diet-screen?slug=$contentIdentifier';
case 'chat':
developer.log('Deep link: chat/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/chat-select-screen';
return RouteConstants.chatSelectScreen;
case 'profile':
developer.log('Deep link: profile/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/profile-screen?slug=$contentIdentifier';
}
}
if (segments.length == 2) {
final type = segments[1];
switch (type) {
case 'chat':
case 'diary':
developer.log('Deep link: diary/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/chat-select-screen';
return RouteConstants.diaryScreen;
default:
developer.log(
'Unknown deep link type: $type, redirecting to feed',
name: 'AppRouter',
);
FlutterNativeSplash.remove();
return RouteConstants.feedScreen;
}
} else if (segments.length == 2) {
// Формат: /{lang}/{type} - только для chat
final type = segments[1];
if (type == 'chat') {
developer.log('Deep link: chat', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.chatSelectScreen;
} else {
developer.log(
'Unknown deep link type for 2 segments: $type, redirecting to feed',
name: 'AppRouter',
);
FlutterNativeSplash.remove();
return RouteConstants.feedScreen;
}
} else {
// Неизвестный формат deep link
developer.log(
'Unknown deep link format: ${segments.length} segments, redirecting to feed',
name: 'AppRouter',
);
FlutterNativeSplash.remove();
return RouteConstants.feedScreen;
}
return null;
}
static void navigateToRegisterViaSocial({required AuthResultEntity authResult, required String provider}) {

View File

@@ -1,5 +1,6 @@
import 'package:eatvio_app/core/comments/models/comment_update_model.dart';
import 'package:eatvio_app/core/comments/services/comments_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class CommentsRepository {
final CommentsService service;
@@ -9,7 +10,8 @@ class CommentsRepository {
Future<void> deleteComment({required int id}) async {
try {
return service.deleteComment(id);
} catch (e) {
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
}
@@ -17,7 +19,8 @@ class CommentsRepository {
Future<void> updateComment({required int id, required String body}) async {
try {
return service.updateComment(id, CommentUpdateModel(body: body));
} catch (e) {
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
}

View File

@@ -69,4 +69,9 @@ class AppConstants {
static const String usersEndpoint = '/users';
static const String postsEndpoint = '/posts';
static const String commentsEndpoint = '/comments';
// Delay of pending requests
/// 30 секунд стоит потому что запрос может идти 12-15 секунд
/// Особенно если производится отладка на сервере разработчика
static const Duration pendingDelay = Duration(seconds: 30);
}

View File

@@ -20,7 +20,7 @@ class StateUtils {
if (isError(state)) {
// Попытка извлечь сообщение об ошибке из состояния
final stateString = state.toString();
final errorMatch = RegExp(r'error[:\s]+([^,}]+)').firstMatch(stateString);
final errorMatch = RegExp(r'error[:\s]+([^,}]+)', caseSensitive: false).firstMatch(stateString);
return errorMatch?.group(1)?.trim();
}
return null;

View File

@@ -45,9 +45,10 @@ import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_
import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart';
import 'package:eatvio_app/features/profile/presentation/bloc/sub_cubit/sub_cubit.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/presentation/cubit/my_recipes_cubit/my_recipes_cubit.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_v2/cubit/registration_cubit.dart';
import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart';
import 'package:eatvio_app/features/user_details/presentation/cubit/user_details_cubit.dart';
import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart';
@@ -75,11 +76,12 @@ class CubitRegistrations {
/// Регистрация Cubit'ов как фабрик
static void _registerFactoryCubits() {
// Auth related cubits
sl.registerFactory<AuthCubit>(() => AuthCubit(authRepository: sl()));
sl.registerFactory<AuthCubit>(() => AuthCubit(authRepository: sl(), firebaseApi: sl(), prefs: sl(), profileRepository: sl()));
sl.registerFactory<RegistrationCubit>(() => RegistrationCubit(registrationRepository: sl(), prefs: sl()));
sl.registerFactory<ResetPasswordCubit>(() => ResetPasswordCubit(resetPasswordRepository: sl()));
sl.registerFactory<VerifyEmailCubit>(() => VerifyEmailCubit(profileRepository: sl()));
sl.registerFactory<VerifyEmailCubit>(() => VerifyEmailCubit(profileRepository: sl(), prefs: sl()));
sl.registerFactory<OAuthCubit>(() => OAuthCubit(authRepository: sl(), prefs: sl()));
sl.registerFactory<RegistrationCubitV2>(() => RegistrationCubitV2(registrationRepository: sl()));
// Feed related cubits
sl.registerFactory<FeedCubit>(() => FeedCubit(feedRepository: sl()));
sl.registerFactory<RecipesCubit>(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl(), aboutRecipeRepository: sl()));
@@ -112,7 +114,7 @@ class CubitRegistrations {
// User content cubits
sl.registerFactory<MyDietsCubit>(() => MyDietsCubit(myDietsRepository: sl(), createDietRepository: sl()));
sl.registerFactory<MyPlansCubit>(() => MyPlansCubit(myPlansRepository: sl()));
sl.registerFactory<MyRecipesCubit>(() => MyRecipesCubit(myRecipesRepository: sl(), aboutRecipeRepository: sl()));
sl.registerFactory<MyRecipesCubit>(() => MyRecipesCubit(myRecipesRepository: sl(), aboutRecipeRepository: sl(), recipeRepository: sl()));
// Other feature cubits
sl.registerFactory<CalendarCubit>(() => CalendarCubit(calendarRepository: sl()));

View File

@@ -1,10 +1,12 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/constants/app_constants.dart';
import 'package:eatvio_app/core/interceptors/phone_verify_error_interceptor.dart';
import 'package:eatvio_app/core/interceptors/token_interceptor.dart';
import 'package:eatvio_app/core/prefs.dart';
import 'package:get_it/get_it.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:sentry_dio/sentry_dio.dart';
/// Регистрация внешних зависимостей в системе внедрения зависимостей
class ExternalRegistrations {
@@ -19,14 +21,21 @@ class ExternalRegistrations {
/// Регистрация Dio HTTP клиента
static void _registerDio() {
sl.registerLazySingleton(
() =>
Dio(BaseOptions(baseUrl: sl<AppConfig>().baseUrl, headers: {'accept': 'application/json', 'content-type': 'application/json'}))
..interceptors.addAll([TokenInterceptor(), PhoneVerifyErrorInterceptor()]),
() => Dio(
BaseOptions(
baseUrl: sl<AppConfig>().baseUrl,
headers: {'accept': 'application/json', 'content-type': 'application/json'},
connectTimeout: AppConstants.pendingDelay,
receiveTimeout: AppConstants.pendingDelay,
sendTimeout: AppConstants.pendingDelay,
),
)..interceptors.addAll([TokenInterceptor(), PhoneVerifyErrorInterceptor()])
..addSentry(),
);
}
/// Регистрация Prefs
static void _registerPrefs() {
sl.registerSingletonAsync<Prefs>(()async => Prefs(await SharedPreferences.getInstance()));
sl.registerSingletonAsync<Prefs>(() async => Prefs(await SharedPreferences.getInstance()));
}
}

View File

@@ -17,6 +17,7 @@ import 'package:eatvio_app/features/diary/repositories/diary_repository.dart';
import 'package:eatvio_app/features/diets/repositories/diet_repository.dart';
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart';
import 'package:eatvio_app/features/barcode/repositories/barcode_repository.dart';
import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart';
import 'package:eatvio_app/features/menu/repositories/notification_settings_repository.dart';
import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart';
@@ -30,6 +31,7 @@ import 'package:eatvio_app/features/profile/repositories/profile_repository.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/registration/repositories/registration_repository.dart';
import 'package:eatvio_app/features/registration_v2/repositories/registration_repository_v2.dart';
import 'package:eatvio_app/features/settings/repositories/service_repository.dart';
import 'package:get_it/get_it.dart';
@@ -54,6 +56,7 @@ class RepositoryRegistrations {
sl.registerLazySingleton<ConsultantRepository>(() => ConsultantRepository(consultantService: sl()));
sl.registerLazySingleton<DiaryRepository>(() => DiaryRepository(diaryService: sl()));
sl.registerLazySingleton<GlossaryRepository>(() => GlossaryRepository(glossaryService: sl()));
sl.registerLazySingleton<BarcodeRepository>(() => BarcodeRepository(barcodeService: sl(), dio: sl()));
sl.registerLazySingleton<DietRepository>(() => DietRepository(dietsService: sl()));
sl.registerLazySingleton<PlansRepository>(() => PlansRepository(plansService: sl()));
sl.registerLazySingleton<PlanRepository>(() => PlanRepository(planService: sl()));
@@ -70,6 +73,7 @@ class RepositoryRegistrations {
sl.registerLazySingleton<CommentsRepository>(() => CommentsRepository(service: sl()));
sl.registerLazySingleton<NotificationSettingsRepository>(() => NotificationSettingsRepository(sl()));
sl.registerLazySingleton<PersonalDietSuggestionRepository>(() => PersonalDietSuggestionRepository(personalDietSuggestionService: sl()));
sl.registerLazySingleton<RegistrationRepositoryV2>(() => RegistrationRepositoryV2(registrationService: sl()));
// Content repositories
sl.registerLazySingleton<BlogPostRepository>(() => BlogPostRepository(blogPostService: sl()));

View File

@@ -19,6 +19,7 @@ import 'package:eatvio_app/features/diary/services/diary_service.dart';
import 'package:eatvio_app/features/diets/services/diet_service.dart';
import 'package:eatvio_app/features/feed/services/feed_service.dart';
import 'package:eatvio_app/features/glossary/services/glossary_service.dart';
import 'package:eatvio_app/features/barcode/services/barcode_service.dart';
import 'package:eatvio_app/features/menu/services/blocked_users_service.dart';
import 'package:eatvio_app/features/menu/services/notification_settings_service.dart';
import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart';
@@ -32,6 +33,7 @@ import 'package:eatvio_app/features/profile/services/profile_service.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/services/registration_service.dart';
import 'package:eatvio_app/features/registration_v2/services/registration_service_v2.dart';
import 'package:eatvio_app/features/settings/services/settings_service.dart';
import 'package:get_it/get_it.dart';
@@ -55,6 +57,7 @@ class ServiceRegistrations {
sl.registerLazySingleton<ConsultantService>(() => ConsultantService(sl()));
sl.registerLazySingleton<DiaryService>(() => DiaryService(sl()));
sl.registerLazySingleton<GlossaryService>(() => GlossaryService(sl()));
sl.registerLazySingleton<BarcodeService>(() => BarcodeService(sl()));
sl.registerLazySingleton<DietService>(() => DietService(sl()));
sl.registerLazySingleton<PlansService>(() => PlansService(sl()));
sl.registerLazySingleton<PlanService>(() => PlanService(sl()));
@@ -78,6 +81,7 @@ class ServiceRegistrations {
sl.registerLazySingleton<NotificationSettingsService>(() => NotificationSettingsService(sl()));
sl.registerLazySingleton<ViewsService>(() => ViewsService(sl()));
sl.registerLazySingleton<PersonalDietSuggestionService>(() => PersonalDietSuggestionService(sl()));
sl.registerLazySingleton<RegistrationServiceV2>(() => RegistrationServiceV2(sl()));
// Content services
sl.registerLazySingleton<CreateRecipeService>(() => CreateRecipeService(sl()));

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'package:eatvio_app/core/app_config.dart';
@@ -11,6 +12,7 @@ import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/services.dart' show MethodChannel, PlatformException;
import 'package:sentry_flutter/sentry_flutter.dart';
class FirebaseApi {
final _firebaseMessaging = FirebaseMessaging.instance;
@@ -19,12 +21,25 @@ class FirebaseApi {
String? token;
if (await _isHuaweiDevice()) {
token = await _getHuaweiPushTokenNative();
token = await _getHuaweiPushTokenNative().timeout(
const Duration(seconds: 5),
onTimeout: () => null,
);
} else {
if ((sl<AppConfig>().buildType == AppBuildType.prod)) {
await _firebaseMessaging.requestPermission();
try {
await _firebaseMessaging.requestPermission().timeout(
const Duration(seconds: 5),
);
} catch (e) {
// Разрешение не критично, продолжаем
log('Failed to request Firebase messaging permission: $e');
}
}
token = await _firebaseMessaging.getToken();
token = await _firebaseMessaging.getToken().timeout(
const Duration(seconds: 5),
onTimeout: () => null,
);
}
return token;
}
@@ -35,13 +50,20 @@ class FirebaseApi {
await sl<Dio>().post(
'/api/anonymous_token',
data: {
'platform': Platform.isIOS ? 'ios' : 'android',
'platform': Platform.isIOS ? 'ios' : (await _isHuaweiDevice()) ? 'huawei' : 'android',
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
'token': await getToken(),
},
).timeout(
const Duration(seconds: 10),
onTimeout: () {
throw TimeoutException('registerAnonymousPushToken timeout');
},
);
} catch (e, st) {
log(e.toString(), stackTrace: st);
await Sentry.captureException(e, stackTrace: st);
// Не пробрасываем ошибку дальше, так как это не критичная операция
}
}
@@ -57,9 +79,16 @@ class FirebaseApi {
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
'token': await getToken(),
},
).timeout(
const Duration(seconds: 10),
onTimeout: () {
throw TimeoutException('registerPushToken timeout');
},
);
} catch (e, st) {
log(e.toString(), stackTrace: st);
await Sentry.captureException(e, stackTrace: st);
// Не пробрасываем ошибку дальше, так как это не критичная операция
}
}
}
@@ -84,10 +113,12 @@ class FirebaseApi {
} on PlatformException catch (e, st) {
showErrorToast('$e $st');
log(e.toString(), stackTrace: st);
await Sentry.captureException(e, stackTrace: st);
return null;
} catch (e, st) {
showErrorToast('$e $st');
log(e.toString(), stackTrace: st);
await Sentry.captureException(e, stackTrace: st);
return null;
}
}

View File

@@ -1,4 +1,7 @@
import 'dart:io';
import 'package:eatvio_app/core/app_initializer.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:image/image.dart' as img;
import 'package:path/path.dart' as p;
@@ -27,6 +30,17 @@ abstract class ImageCompressUtility {
}
static Future<File> compressFile(File file) async {
// Skip compression in test environment to avoid hangs
try {
final appConfig = sl<AppConfig>();
if (appConfig.buildType == AppBuildType.tests) {
// In tests, return the original file without compression
return file;
}
} catch (_) {
// If AppConfig is not available, proceed with compression
}
final image = img.decodeImage(file.readAsBytesSync())!;
int width = image.width;
int height = image.height;

View File

@@ -1,6 +1,8 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/core/websockets/ws_manager.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:go_router/go_router.dart';
class TokenInterceptor extends Interceptor {
@@ -23,6 +25,7 @@ class TokenInterceptor extends Interceptor {
ErrorInterceptorHandler handler,
) async {
if (err.response?.statusCode == 401) {
await sl<WsManager>().clear();
await TokenStorage.removeTokenInStorage();
AppRouter.navigatorKey.currentContext?.go('/auth-screen');
}

View File

@@ -4,6 +4,7 @@ import 'package:eatvio_app/core/di/cubit_registrations.dart';
import 'package:eatvio_app/core/di/external_registrations.dart';
import 'package:eatvio_app/core/di/repository_registrations.dart';
import 'package:eatvio_app/core/di/service_registrations.dart';
import 'package:eatvio_app/core/prefs.dart';
import 'package:eatvio_app/core/websockets/ws_manager.dart';
import 'package:get_it/get_it.dart';
@@ -18,6 +19,9 @@ Future<void> init({required AppBuildType buildType}) async {
// Регистрация внешних зависимостей (Dio, etc.)
ExternalRegistrations.registerAll();
// Ожидание инициализации асинхронных зависимостей
await sl.getAsync<Prefs>();
// Регистрация сервисов
ServiceRegistrations.registerAll();

View File

@@ -1 +1 @@
enum PrefsKeys { hasAnonymousTokenBeenSent, welcomeScreenWasShowed, registrationDate, marketingBottomSheetHasBeenShown }
enum PrefsKeys { hasAnonymousTokenBeenSent, welcomeScreenWasShowed, registrationDate, marketingBottomSheetHasBeenShown, isEmailVerificationSkipped }

View File

@@ -1,5 +1,3 @@
import 'package:eatvio_app/core/firebase_api.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/routing/route_constants.dart';
import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart';
import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart';
@@ -9,6 +7,7 @@ import 'package:eatvio_app/features/auth/presentation/pages/scan_qr_code_screen.
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/registration/presentation/pages/verify_email_screen.dart';
import 'package:eatvio_app/features/registration_v2/presentation/pages/registration_v2_page.dart';
import 'package:go_router/go_router.dart';
/// Маршруты для аутентификации
@@ -20,12 +19,11 @@ class AuthRoutes {
path: RouteConstants.authScreen,
name: 'authScreen',
builder: (context, state) => const AuthScreen(),
redirect: (context, state) {
sl<FirebaseApi>().registerAnonymousPushToken();
return null;
},
// registerAnonymousPushToken вызывается в AuthCubit.initializeUser()
// и не должен блокировать навигацию
routes: [
GoRoute(path: RouteConstants.registrationScreen, name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()),
GoRoute(path: RouteConstants.registrationScreenV2, name: 'registrationScreenV2', builder: (context, state) => const RegistrationV2Page()),
GoRoute(
path: RouteConstants.forgetPasswordScreen,
name: 'forgetPasswordScreen',

View File

@@ -23,14 +23,16 @@ class MainNavigationRoutes {
return [
ShellRoute(
builder: (context, state, child) {
return BlocBuilder<ProfileAvatarCubit, ProfileAvatarState>(
builder: (BuildContext context, ProfileAvatarState state) {
if (state is ProfileAvatarStateLoaded) {
return BottomNavigationBarWidget(profileModel: state.profile, child: child);
}
return const SizedBox.shrink();
},
);
// Инициализация ProfileAvatarCubit при первом использовании ShellRoute
final profileAvatarCubit = context.read<ProfileAvatarCubit>();
if (profileAvatarCubit.state is ProfileAvatarStateLoading) {
// Вызываем getMyProfile() только если состояние loading (первый запуск)
WidgetsBinding.instance.addPostFrameCallback((_) {
profileAvatarCubit.getMyProfile();
});
}
return BottomNavigationBarWidget(child: child);
},
routes: [
GoRoute(

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/firebase_api.dart';
@@ -6,6 +7,7 @@ import 'package:eatvio_app/core/prefs.dart';
import 'package:eatvio_app/core/prefs_keys.dart';
import 'package:eatvio_app/core/routing/route_constants.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:eatvio_app/features/calendar/presentation/pages/calendar_screen.dart';
import 'package:eatvio_app/features/chat/presentation/pages/chat_screen.dart';
import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart';
@@ -58,24 +60,33 @@ class OtherRoutes {
builder: (context, state) => const WelcomeScreen(),
redirect: (context, state) async {
try {
final prefs = sl<Prefs>();
final firebaseApi = sl<FirebaseApi>();
// Prefs уже инициализирован в locator_service, но на всякий случай проверяем
final prefs = await sl.getAsync<Prefs>();
final welcomeScreenWasShowed = prefs.getBool(PrefsKeys.welcomeScreenWasShowed) ?? false;
await firebaseApi.registerPushToken();
if (!welcomeScreenWasShowed) {
return null;
} else if (await TokenStorage.haveTokenInStorage()) {
return '/feed-screen';
} else {
final hasAnonymousTokenBeenSent = prefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent) ?? false;
if (!hasAnonymousTokenBeenSent) {
await firebaseApi.registerAnonymousPushToken();
prefs.setBool(PrefsKeys.hasAnonymousTokenBeenSent, true);
}
if (await TokenStorage.haveTokenInStorage()) {
try {
final profileRepository = sl<ProfileRepository>();
final profile = await profileRepository.getMyProfile();
final firebaseApi = sl<FirebaseApi>();
await firebaseApi.registerPushToken();
if (profile.emailVerifiedAt != null || (prefs.getBool(PrefsKeys.isEmailVerificationSkipped) ?? false)) {
return RouteConstants.feedScreen;
}
return RouteConstants.verifyEmailScreen;
} catch (e) {
return '/auth-screen';
}
} else {
return '/auth-screen';
}
} catch (e) {
return '/feed-screen';
return '/auth-screen';
}
},
),
@@ -85,7 +96,27 @@ class OtherRoutes {
builder: (context, state) => const UpdateScreen(),
redirect: (context, state) async {
try {
final versions = (await sl<Dio>().get('${sl<AppConfig>().baseUrl}/api/app_versions')).data;
// Проверка версии с таймаутом, чтобы не блокировать запуск приложения
final versions = await sl<Dio>()
.get('${sl<AppConfig>().baseUrl}/api/app_versions')
.timeout(
const Duration(seconds: 5),
onTimeout: () {
// При таймауте пропускаем проверку и идем дальше
throw TimeoutException('Version check timeout');
},
)
.then((response) => response.data)
.catchError((e) {
// При любой ошибке пропускаем проверку версии
return null;
});
if (versions == null) {
// Если проверка не удалась, пропускаем экран обновления
return RouteConstants.welcomeScreen;
}
PackageInfo packageInfo = await PackageInfo.fromPlatform();
String version = packageInfo.version;
Version currentVersion = Version.parse(version);
@@ -96,6 +127,7 @@ class OtherRoutes {
return RouteConstants.welcomeScreen;
}
} catch (e) {
// При любой ошибке пропускаем экран обновления
return RouteConstants.welcomeScreen;
}
},
@@ -121,22 +153,35 @@ class OtherRoutes {
GoRoute(
path: RecipeCreateScreen.path,
name: RecipeCreateScreen.name,
builder: (context, state) => RecipeCreateScreen(recipe: state.extra != null ? state.extra as Recipe : null),
builder: (context, state) {
if (state.extra is Map<String, dynamic>) {
final data = state.extra as Map<String, dynamic>;
return RecipeCreateScreen(
recipe: data['recipe'] as Recipe?,
autoValidate: data['autoValidate'] as bool? ?? false,
);
}
return RecipeCreateScreen(recipe: state.extra != null ? state.extra as Recipe : null);
},
),
GoRoute(
path: CreateDietScreen.path,
name: CreateDietScreen.name,
builder: (context, state) {
final diet = state.extra as Diet?;
return CreateDietScreen(diet: diet);
final data = state.extra as Map<String, dynamic>?;
final diet = data?['diet'] as Diet?;
final isEditing = data?['isEditing'] as bool?;
return CreateDietScreen(diet: diet, isEditing: isEditing);
},
),
GoRoute(
path: DietPreviewScreen.path,
name: DietPreviewScreen.name,
builder: (context, state) {
final data = state.extra as Diet;
return DietPreviewScreen(diet: data);
final data = state.extra as Map<String, dynamic>;
final diet = data['diet'] as Diet;
final isImageInBase64 = data['isImageInBase64'] as bool;
return DietPreviewScreen(diet: diet, isImageInBase64: isImageInBase64);
},
),
GoRoute(path: RouteConstants.createPlanScreen, name: 'createPlanScreen', builder: (context, state) => const CreatePlanScreen()),
@@ -157,7 +202,7 @@ class OtherRoutes {
},
),
GoRoute(
path: RouteConstants.subsScreen,
path: '${RouteConstants.subsScreen}/:slug',
name: 'subsScreen',
builder: (context, state) => SubsScreen(slug: state.pathParameters['slug']!, subType: state.extra as SubType),
),
@@ -184,7 +229,12 @@ class OtherRoutes {
GoRoute(
path: RouteConstants.stepperBecomeConsultantScreen,
name: 'stepperBecomeConsultantScreen',
builder: (context, state) => StepperBecomeConsultantScreen(slug: state.extra == null ? null : state.extra as String),
builder: (context, state) {
final data = state.extra as Map<String, dynamic>;
final slug = data['slug'] as String?;
final isCanceled = data['isCanceled'] as bool?;
return StepperBecomeConsultantScreen(slug: slug, isCanceled: isCanceled,);
},
),
GoRoute(path: RouteConstants.calendarScreen, name: 'calendarScreen', builder: (context, state) => const CalendarScreen()),
GoRoute(path: RouteConstants.supportScreen, name: 'supportScreen', builder: (context, state) => const SupportScreen()),
@@ -195,7 +245,11 @@ class OtherRoutes {
),
// Other routes
GoRoute(path: RouteConstants.glossaryScreen, name: 'glossaryScreen', builder: (context, state) => GlossaryScreen(passKey: state.extra as String)),
GoRoute(
path: RouteConstants.glossaryScreen,
name: 'glossaryScreen',
builder: (context, state) => GlossaryScreen(passKey: state.extra as String),
),
GoRoute(path: RouteConstants.blockedUsersScreen, name: 'blockedUsersScreen', builder: (context, state) => const BlockedUsersScreen()),
GoRoute(
path: RouteConstants.notificationsSettingsScreen,
@@ -243,7 +297,7 @@ class OtherRoutes {
GoRoute(
path: PersonalDietSuggestionScreen.path,
name: PersonalDietSuggestionScreen.name,
builder: (context, state) => PersonalDietSuggestionScreen(dietId: state.extra as int,),
builder: (context, state) => PersonalDietSuggestionScreen(dietId: state.extra as int),
),
];
}

View File

@@ -3,6 +3,7 @@ class RouteConstants {
// Auth routes
static const String authScreen = '/auth-screen';
static const String registrationScreen = 'registration-screen';
static const String registrationScreenV2 = 'registration-screen-v2';
static const String forgetPasswordScreen = 'forget-password-screen';
static const String scanQRCodeScreen = 'scan-qr-code-screen';
static const String verifyScreen = '/verify-screen';

View File

@@ -10,161 +10,76 @@ class CustomTextStyles {
static TextStyle get headlineSmallMulish => theme.textTheme.headlineSmall!.mulish;
// Title text style
static TextStyle get titleMediumSegoeUI => theme.textTheme.titleMedium!.segoeUI.copyWith(
fontWeight: FontWeight.w600,
);
static TextStyle get titleMediumSegoeUI => theme.textTheme.titleMedium!.segoeUI.copyWith(fontWeight: FontWeight.w600);
static TextStyle get bodyMediumBlack900 => theme.textTheme.bodyMedium!.copyWith(
color: appTheme.black900,
);
static TextStyle get bodyMediumBlack900 => theme.textTheme.bodyMedium!.copyWith(color: appTheme.black900);
static TextStyle get bodyMediumBlack90013 => theme.textTheme.bodyMedium!.copyWith(
color: appTheme.black900,
fontSize: 13,
);
static TextStyle get bodyMediumBlack90013 => theme.textTheme.bodyMedium!.copyWith(color: appTheme.black900, fontSize: 13);
static TextStyle get bodyMediumBluegray800 => theme.textTheme.bodyMedium!.copyWith(
color: appTheme.black,
);
static TextStyle get bodyMediumBluegray800 => theme.textTheme.bodyMedium!.copyWith(color: appTheme.black);
static TextStyle get bodyMediumBluegray800_1 => theme.textTheme.bodyMedium!.copyWith(
color: appTheme.black,
);
static TextStyle get bodyMediumBluegray800_1 => theme.textTheme.bodyMedium!.copyWith(color: appTheme.black);
static TextStyle get bodyMediumBluegray800_2 => theme.textTheme.bodyMedium!.copyWith(
color: appTheme.black,
);
static TextStyle get bodyMediumBluegray800_2 => theme.textTheme.bodyMedium!.copyWith(color: appTheme.black);
static TextStyle get bodyMediumWhiteA700 => theme.textTheme.bodyMedium!.copyWith(
color: appTheme.whiteA700,
);
static TextStyle get bodyMediumWhiteA700 => theme.textTheme.bodyMedium!.copyWith(color: appTheme.whiteA700);
static TextStyle get bodySmallMulishBlack900 => theme.textTheme.bodySmall!.mulish.copyWith(
color: appTheme.black900,
fontSize: 12,
);
static TextStyle get bodySmallMulishBlack900 => theme.textTheme.bodySmall!.mulish.copyWith(color: appTheme.black900, fontSize: 12);
static TextStyle get bodySmallMulishBluegray800 => theme.textTheme.bodySmall!.mulish.copyWith(
color: appTheme.black,
fontSize: 12,
);
static TextStyle get bodySmallMulishBluegray800 => theme.textTheme.bodySmall!.mulish.copyWith(color: appTheme.black, fontSize: 12);
static TextStyle get bodySmallMulishBluegray80012 => theme.textTheme.bodySmall!.mulish.copyWith(
color: appTheme.black,
fontSize: 12,
);
static TextStyle get bodySmallMulishBluegray80012 => theme.textTheme.bodySmall!.mulish.copyWith(color: appTheme.black, fontSize: 12);
static TextStyle get bodySmallMulishBluegray80012_1 => theme.textTheme.bodySmall!.mulish.copyWith(
color: appTheme.black,
fontSize: 12,
);
static TextStyle get bodySmallMulishBluegray80012_1 => theme.textTheme.bodySmall!.mulish.copyWith(color: appTheme.black, fontSize: 12);
static TextStyle get bodySmallMulishBluegray900 => theme.textTheme.bodySmall!.mulish.copyWith(
color: appTheme.blueGray900,
fontSize: 12,
);
static TextStyle get bodySmallMulishBluegray900 => theme.textTheme.bodySmall!.mulish.copyWith(color: appTheme.blueGray900, fontSize: 12);
static TextStyle get bodySmallMulishGray600 => theme.textTheme.bodySmall!.mulish.copyWith(
color: appTheme.gray600,
);
static TextStyle get bodySmallMulishGray600 => theme.textTheme.bodySmall!.mulish.copyWith(color: appTheme.gray600);
static TextStyle get bodySmallMulishPrimary => theme.textTheme.bodySmall!.mulish.copyWith(
color: theme.colorScheme.primary,
fontSize: 12,
);
static TextStyle get bodySmallMulishPrimary => theme.textTheme.bodySmall!.mulish.copyWith(color: theme.colorScheme.primary, fontSize: 12);
static TextStyle get bodySmallSegoeUIBlack900 => theme.textTheme.bodySmall!.segoeUI.copyWith(
color: appTheme.black900,
fontSize: 12,
);
static TextStyle get bodySmallSegoeUIBlack900 => theme.textTheme.bodySmall!.segoeUI.copyWith(color: appTheme.black900, fontSize: 12);
// Label text style
static TextStyle get labelLargeGray60003 => theme.textTheme.labelLarge!.copyWith(
color: appTheme.gray60003,
);
static TextStyle get labelLargeGray60003 => theme.textTheme.labelLarge!.copyWith(color: appTheme.gray60003);
static TextStyle get labelLargeWhiteA700 => theme.textTheme.labelLarge!.copyWith(
color: appTheme.whiteA700,
);
static TextStyle get labelLargeWhiteA700 => theme.textTheme.labelLarge!.copyWith(color: appTheme.whiteA700);
// Title text style
static TextStyle get titleMedium16 => theme.textTheme.titleMedium!.copyWith(
fontSize: 16,
);
static TextStyle get titleMedium16 => theme.textTheme.titleMedium!.copyWith(fontSize: 16);
static TextStyle get titleMediumBlack => theme.textTheme.titleMedium!.copyWith(
fontSize: 17,
fontWeight: FontWeight.w900,
);
static TextStyle get titleMediumBlack => theme.textTheme.titleMedium!.copyWith(fontSize: 17, fontWeight: FontWeight.w900);
static TextStyle get titleMediumBlack900 => theme.textTheme.titleMedium!.copyWith(
color: appTheme.black900,
);
static TextStyle get titleMediumBlack900 => theme.textTheme.titleMedium!.copyWith(color: appTheme.black900);
static TextStyle get titleMediumSemiBold => theme.textTheme.titleMedium!.copyWith(
fontWeight: FontWeight.w600,
);
static TextStyle get titleMediumSemiBold => theme.textTheme.titleMedium!.copyWith(fontWeight: FontWeight.w600);
static TextStyle get titleMediumWhiteA700 => theme.textTheme.titleMedium!.copyWith(
color: appTheme.whiteA700,
fontWeight: FontWeight.w600,
);
static TextStyle get titleMediumWhiteA700 => theme.textTheme.titleMedium!.copyWith(color: appTheme.whiteA700, fontWeight: FontWeight.w600);
static TextStyle get titleSmallBlue800 => theme.textTheme.titleSmall!.copyWith(
color: appTheme.blue800,
fontWeight: FontWeight.w800,
);
static TextStyle get titleSmallBlue800 => theme.textTheme.titleSmall!.copyWith(color: appTheme.blue800, fontWeight: FontWeight.w800);
static TextStyle get titleSmallBluegray800 => theme.textTheme.titleSmall!.copyWith(
color: appTheme.black,
);
static TextStyle get titleSmallBluegray800 => theme.textTheme.titleSmall!.copyWith(color: appTheme.black);
static TextStyle get titleSmallBluegray800Bold => theme.textTheme.titleSmall!.copyWith(
color: appTheme.black,
fontWeight: FontWeight.w700,
);
static TextStyle get titleSmallBluegray800Bold => theme.textTheme.titleSmall!.copyWith(color: appTheme.black, fontWeight: FontWeight.w700);
static TextStyle get titleSmallBluegray800ExtraBold => theme.textTheme.titleSmall!.copyWith(
color: appTheme.black,
fontWeight: FontWeight.w800,
);
static TextStyle get titleSmallBluegray800ExtraBold => theme.textTheme.titleSmall!.copyWith(color: appTheme.black, fontWeight: FontWeight.w800);
static TextStyle get titleSmallBold => theme.textTheme.titleSmall!.copyWith(
fontSize: 15,
fontWeight: FontWeight.w700,
);
static TextStyle get titleSmallBold => theme.textTheme.titleSmall!.copyWith(fontSize: 15, fontWeight: FontWeight.w700);
static TextStyle get titleSmallGreenA70001 => theme.textTheme.titleSmall!.copyWith(
color: appTheme.greenA70001,
);
static TextStyle get titleSmallGreenA70001 => theme.textTheme.titleSmall!.copyWith(color: appTheme.greenA70001);
static TextStyle get titleSmallGreenA70001ExtraBold => theme.textTheme.titleSmall!.copyWith(
color: appTheme.greenA70001,
fontWeight: FontWeight.w800,
);
static TextStyle get titleSmallGreenA70001ExtraBold => theme.textTheme.titleSmall!.copyWith(color: appTheme.greenA70001, fontWeight: FontWeight.w800);
static TextStyle get titleSmallLightgreen300 => theme.textTheme.titleSmall!.copyWith(
color: appTheme.lightGreen300,
fontWeight: FontWeight.w800,
);
static TextStyle get titleSmallLightgreen300 => theme.textTheme.titleSmall!.copyWith(color: appTheme.lightGreen300, fontWeight: FontWeight.w800);
static TextStyle get titleSmallRed200 => theme.textTheme.titleSmall!.copyWith(
color: appTheme.red200,
fontWeight: FontWeight.w800,
);
static TextStyle get titleSmallRed200 => theme.textTheme.titleSmall!.copyWith(color: appTheme.red200, fontWeight: FontWeight.w800);
static TextStyle get titleSmallSecondaryContainer => theme.textTheme.titleSmall!.copyWith(
color: theme.colorScheme.secondaryContainer,
fontWeight: FontWeight.w800,
);
static TextStyle get titleSmallSecondaryContainer =>
theme.textTheme.titleSmall!.copyWith(color: theme.colorScheme.secondaryContainer, fontWeight: FontWeight.w800);
static TextStyle get titleSmallSecondaryContainer_1 => theme.textTheme.titleSmall!.copyWith(
color: theme.colorScheme.secondaryContainer,
);
static TextStyle get titleSmallSecondaryContainer_1 => theme.textTheme.titleSmall!.copyWith(color: theme.colorScheme.secondaryContainer);
static TextStyle get titleSmallSegoeUIOnPrimary => theme.textTheme.titleSmall!.segoeUI.copyWith(
color: theme.colorScheme.onPrimary,
);
static TextStyle get titleSmallSegoeUIOnPrimary => theme.textTheme.titleSmall!.segoeUI.copyWith(color: theme.colorScheme.onPrimary);
}
extension on TextStyle {
@@ -178,81 +93,32 @@ extension on TextStyle {
}
abstract class TextStyles {
static const headingXLSemibold = TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
height: 1.255,
);
static const headingXLSemibold = TextStyle(fontSize: 20, fontWeight: FontWeight.w600, height: 1.255);
static const headingFS40FW600 = TextStyle(
fontSize: 40,
fontWeight: FontWeight.w600,
height: 1.255,
);
static const headingFS40FW600 = TextStyle(fontSize: 40, fontWeight: FontWeight.w600, height: 1.255);
static const textLSemibold = TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
height: 1.255,
);
static const textLSemibold = TextStyle(fontSize: 18, fontWeight: FontWeight.w600, height: 1.255);
static const textLMedium = TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
height: 1.255,
);
static const textMSemibold = TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
height: 1.255,
);
static const textLMedium = TextStyle(fontSize: 18, fontWeight: FontWeight.w500, height: 1.255);
static const textMMedium = TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1,
);
static const textMSemibold = TextStyle(fontSize: 16, fontWeight: FontWeight.w600, height: 1.255);
static const textMMediumAuto = TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.255,
);
static const textMMedium = TextStyle(fontSize: 16, fontWeight: FontWeight.w500, height: 1);
static const textSSemibold = TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
height: 1.255,
);
static const textMMediumAuto = TextStyle(fontSize: 16, fontWeight: FontWeight.w500, height: 1.255);
static const textSMedium = TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
height: 1.255,
);
static const textSSemibold = TextStyle(fontSize: 14, fontWeight: FontWeight.w600, height: 1.255);
static const textFS13FW400 = TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.255,
);
static const textSMedium = TextStyle(fontSize: 14, fontWeight: FontWeight.w500, height: 1.255);
static const textXSSemibold = TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
height: 1.255,
);
static const textFS13FW400 = TextStyle(fontSize: 14, fontWeight: FontWeight.w400, height: 1.255);
static const textFS10FW500 = TextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
height: 1.255,
);
static const textXSSemibold = TextStyle(fontSize: 12, fontWeight: FontWeight.w500, height: 1.255);
static const textFS10FW500 = TextStyle(fontSize: 10, fontWeight: FontWeight.w500, height: 1.255);
static const textFS24FW700 = TextStyle(fontSize: 24, fontWeight: FontWeight.w700, height: 1.255);
static const textFS22FW600 = TextStyle(fontSize: 22, fontWeight: FontWeight.w600, height: 1.255);
static const textFS24FW700 = TextStyle(
fontSize: 24,
fontWeight: FontWeight.w700,
height: 1.255,
);
}

View File

@@ -1,29 +1,23 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart';
/// Helper class for managing themes and colors.
class ThemeHelper {
// The current app theme
final _appTheme = 'primary';
// A map of custom color themes supported by the app
final Map<String, PrimaryColors> _supportedCustomColor = {
'primary': PrimaryColors(),
};
// A map of custom color themes supported by the app
final Map<String, PrimaryColors> _supportedCustomColor = {'primary': PrimaryColors()};
// A map of color schemes supported by the app
final Map<String, ColorScheme> _supportedColorScheme = {
'primary': ColorSchemes.primaryColorScheme,
};
// A map of color schemes supported by the app
final Map<String, ColorScheme> _supportedColorScheme = {'primary': ColorSchemes.primaryColorScheme};
/// Returns the primary colors for the current theme.
PrimaryColors _getThemeColors() {
//throw exception to notify given theme is not found or not generated by the generator
if (!_supportedCustomColor.containsKey(_appTheme)) {
throw Exception(
"$_appTheme is not found.Make sure you have added this theme class in JSON Try running flutter pub run build_runner",
);
throw Exception("$_appTheme is not found.Make sure you have added this theme class in JSON Try running flutter pub run build_runner");
}
//return theme from map
@@ -34,15 +28,14 @@ class ThemeHelper {
ThemeData _getThemeData() {
//throw exception to notify given theme is not found or not generated by the generator
if (!_supportedColorScheme.containsKey(_appTheme)) {
throw Exception(
"$_appTheme is not found.Make sure you have added this theme class in JSON Try running flutter pub run build_runner",
);
throw Exception("$_appTheme is not found.Make sure you have added this theme class in JSON Try running flutter pub run build_runner");
}
//return theme from map
var colorScheme = _supportedColorScheme[_appTheme] ?? ColorSchemes.primaryColorScheme;
return ThemeData(
textTheme: GoogleFonts.mulishTextTheme(),
fontFamily: 'Mulish-Local',
textTheme: ThemeData.light().textTheme.apply(fontFamily: 'Mulish-Local'),
visualDensity: VisualDensity.standard,
colorScheme: colorScheme,
scaffoldBackgroundColor: colorScheme.onPrimary,
@@ -60,17 +53,9 @@ class ThemeHelper {
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
backgroundColor: Colors.transparent,
side: BorderSide(
color: colorScheme.primary,
width: 1,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
visualDensity: const VisualDensity(
vertical: -4,
horizontal: -4,
),
side: BorderSide(color: colorScheme.primary, width: 1),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
visualDensity: const VisualDensity(vertical: -4, horizontal: -4),
padding: EdgeInsets.zero,
),
),
@@ -149,6 +134,7 @@ class PrimaryColors {
Color get greenA70001 => const Color(0XFF00BE13);
Color get green => const Color(0XFF0BB450);
Color get darkGreen => const Color(0XFF16A249);
Color get transparentGreen => const Color.fromARGB(12, 1, 154, 63);
@@ -184,6 +170,8 @@ class PrimaryColors {
Color get lightGray0 => const Color(0xFFF6F6F6);
Color get darkGrey => const Color(0xFF5B656F);
Color get lightGray00 => const Color(0xFFE9E9EA);
Color get dottedBorder => const Color(0xFFE6E6E6);
@@ -210,6 +198,8 @@ class PrimaryColors {
Color get cardNotificationsBG => const Color(0xFFA4A4A4);
Color get grey7B => const Color(0xFF7B7B7B);
Color get greyD9 => const Color(0xFFD9D9D9);
}
PrimaryColors get appTheme => ThemeHelper().themeColor();

View File

@@ -1,9 +1,24 @@
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
abstract class TokenStorage {
static SharedPreferences? _cachedPrefs;
static Future<SharedPreferences> _getPrefs() async {
_cachedPrefs ??= await SharedPreferences.getInstance();
return _cachedPrefs!;
}
/// Clears the cached SharedPreferences instance.
/// This is primarily used for testing to ensure fresh state between tests.
@visibleForTesting
static void clearCache() {
_cachedPrefs = null;
}
static Future<bool> haveTokenInStorage() async {
final sharedPreferences = await SharedPreferences.getInstance();
try {
final sharedPreferences = await _getPrefs();
if (sharedPreferences.getString('token') == null) {
return false;
} else {
@@ -15,25 +30,25 @@ abstract class TokenStorage {
}
static Future<String?> getTokenFromStorage() async {
final sharedPreferences = await SharedPreferences.getInstance();
final sharedPreferences = await _getPrefs();
return sharedPreferences.getString('token');
}
static Future<void> saveTokenInStorage(String token) async {
final sharedPreferences = await SharedPreferences.getInstance();
final sharedPreferences = await _getPrefs();
await sharedPreferences.setString('token', token);
}
static Future<void> removeTokenInStorage() async {
final sharedPreferences = await SharedPreferences.getInstance();
final sharedPreferences = await _getPrefs();
await sharedPreferences.remove('token');
}
}
abstract class SocketIdStorage {
static Future<bool> haveSocketIdInStorage() async {
final sharedPreferences = await SharedPreferences.getInstance();
try {
final sharedPreferences = await TokenStorage._getPrefs();
if (sharedPreferences.getString('socketId') == null) {
return false;
} else {
@@ -45,17 +60,17 @@ abstract class SocketIdStorage {
}
static Future<String?> getSocketIdFromStorage() async {
final sharedPreferences = await SharedPreferences.getInstance();
final sharedPreferences = await TokenStorage._getPrefs();
return sharedPreferences.getString('socketId');
}
static Future<void> saveSocketIdInStorage(String socketId) async {
final sharedPreferences = await SharedPreferences.getInstance();
final sharedPreferences = await TokenStorage._getPrefs();
await sharedPreferences.setString('socketId', socketId);
}
static Future<void> removeSocketIdInStorage() async {
final sharedPreferences = await SharedPreferences.getInstance();
final sharedPreferences = await TokenStorage._getPrefs();
await sharedPreferences.remove('socketId');
}
}

View File

@@ -0,0 +1,67 @@
/// Утилиты для обработки markdown текста
class MarkdownUtils {
/// Обрабатывает markdown текст:
/// - Исправляет неправильный синтаксис markdown ссылок
/// - Автоматически преобразует URL в markdown ссылки
/// - Преобразует **текст** в <b>текст</b> для надежного отображения жирного текста
static String processMarkdown(String text) {
// Сначала исправляем неправильный синтаксис markdown ссылок
// Например: [http://example.com](url) -> [http://example.com](http://example.com)
final wrongLinkRegex = RegExp(r'\[(https?://[^\]]+|www\.[^\]]+)\]\(([^\)]+)\)');
text = text.replaceAllMapped(wrongLinkRegex, (match) {
final urlInText = match.group(1)!;
if (!urlInText.startsWith('http://') && !urlInText.startsWith('https://') && !urlInText.startsWith('www.')) {
return match.group(0)!;
}
final fullUrl = urlInText.startsWith('www.') ? 'https://$urlInText' : urlInText;
return '[$urlInText]($fullUrl)';
});
// Находим все существующие markdown ссылки и их позиции
final linkRegex = RegExp(r'\[([^\]]+)\]\(([^\)]+)\)');
final existingLinks = <_LinkRange>[];
for (final linkMatch in linkRegex.allMatches(text)) {
existingLinks.add(_LinkRange(linkMatch.start, linkMatch.end));
}
// Регулярное выражение для поиска URL (http://, https://, www.)
final urlRegex = RegExp(r'(https?://[^\s\)]+|www\.[^\s\)]+)', caseSensitive: false);
// Заменяем URL на markdown ссылки, если они еще не являются ссылками
String result = text;
int offset = 0;
for (final urlMatch in urlRegex.allMatches(text)) {
// Проверяем, не находится ли URL внутри существующей markdown ссылки
bool isInsideLink = existingLinks.any((link) =>
urlMatch.start >= link.start && urlMatch.end <= link.end);
if (!isInsideLink) {
final url = urlMatch.group(0)!;
final fullUrl = url.startsWith('www.') ? 'https://$url' : url;
final replacement = '[$url]($fullUrl)';
result = result.replaceRange(
urlMatch.start + offset,
urlMatch.end + offset,
replacement,
);
offset += replacement.length - url.length;
}
}
// Гарантируем, что жирный текст будет распознан
// Заменяем **текст** на <b>текст</b> вручную для надежности
final boldRegex = RegExp(r'\*\*([^*]+)\*\*');
result = result.replaceAllMapped(boldRegex, (match) {
return '<b>${match.group(1)}</b>';
});
return result;
}
}
/// Вспомогательный класс для хранения диапазона позиций ссылки
class _LinkRange {
final int start;
final int end;
_LinkRange(this.start, this.end);
}

View File

@@ -1,11 +1,12 @@
extension Capitalization on String {
String capitalize() {
if (isEmpty) return '';
return "${this[0].toUpperCase()}${substring(1).toLowerCase()}";
}
String parseGender() {
if (this == 'male') {
return 'Мужской';
return 'Мужской';
} else if (this == 'female') {
return 'Женский';
}

View File

@@ -16,6 +16,21 @@ class ValidationUtils {
return regex.hasMatch(phone);
}
/// Нормализация номера телефона (+79991234455)
static String normalizePhone(String phone) {
if (phone.isEmpty) return '';
final digits = phone.replaceAll(RegExp(r'[^\d]'), '');
if (digits.isEmpty) return '';
String normalized = digits;
if (digits.startsWith('8')) {
normalized = '7${digits.substring(1)}';
} else if (digits.length == 10) {
normalized = '7$digits';
}
return '+$normalized';
}
/// Валидация URL
static bool isValidUrl(String url) {
if (url.isEmpty) return false;

View File

@@ -1,5 +1,6 @@
import 'package:eatvio_app/core/views/service/views_service.dart';
import 'package:eatvio_app/features/feed/models/view_model.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class ViewsRepository {
final ViewsService viewsService;
@@ -9,7 +10,8 @@ class ViewsRepository {
Future<void> viewContent({required ViewModel viewModel}) async {
try {
await viewsService.viewContent(viewModel: viewModel);
} catch (e) {
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
}

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
@@ -8,6 +9,7 @@ import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/websockets/ws_service.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
class WsManager {
@@ -22,6 +24,10 @@ class WsManager {
PrivateChannel? _privateMessagesChannel;
PrivateChannel? _privateDirectChatChannel;
StreamSubscription? _lifecycleSubscription;
StreamSubscription? _eventSubscription;
StreamSubscription? _connectionEstablishedSubscription;
Stream<ChannelReadEvent>? get newMessagesStream => _privateMessagesChannel?.bind('App\\Events\\ChatMessageUpdated');
Stream<ChannelReadEvent>? get newNotificationsStream => _privateNotificationChannel?.bind('Illuminate\\Notifications\\Events\\BroadcastNotificationCreated');
@@ -33,7 +39,7 @@ class WsManager {
Future<void> initialize() async {
if (client == null) {
try {
await TokenStorage.haveTokenInStorage();
if (!await TokenStorage.haveTokenInStorage()) return;
final wsUrl = await wsService.getWebsocket();
final options = PusherChannelsOptions.fromHost(
@@ -71,7 +77,7 @@ class WsManager {
}
}
client?.eventStream.listen((event) async {
_connectionEstablishedSubscription = client?.eventStream.listen((event) async {
if (event.name == 'pusher:connection_established') {
final data = event.data;
final socketId = extractSocketId(data);
@@ -87,21 +93,26 @@ class WsManager {
await _initPrivateNotificationsChannel();
await _initPrivateMessagesChannel();
client?.lifecycleStream.listen((data) {
_lifecycleSubscription = client?.lifecycleStream.listen((data) {
if (data == PusherChannelsClientLifeCycleState.establishedConnection) {
_unsubscribe();
_subscribe();
}
});
client?.eventStream.listen((event) {
EasyDebounce.debounce(
'searchChatRoom',
const Duration(milliseconds: 300),
() => sl<ChatsCubit>().getChatRooms(),
);
_eventSubscription = client?.eventStream.listen((event) {
log('WsManager received event: ${event.name}');
// Только если это не системные сообщения Pusher, обновляем список чатов
if (!event.name.startsWith('pusher:')) {
EasyDebounce.debounce(
'searchChatRoom',
const Duration(milliseconds: 300),
() => sl<ChatsCubit>().getChatRooms(),
);
}
});
_subscribe();
} catch (e, st) {
await Sentry.captureException(e, stackTrace: st);
log(e.toString(), stackTrace: st);
}
}
@@ -166,6 +177,12 @@ class WsManager {
Future<void> clear() async {
await client?.disconnect();
await _lifecycleSubscription?.cancel();
await _eventSubscription?.cancel();
await _connectionEstablishedSubscription?.cancel();
_lifecycleSubscription = null;
_eventSubscription = null;
_connectionEstablishedSubscription = null;
client = null;
userId = null;
_privateDirectChatChannel?.unsubscribe();

View File

@@ -3,16 +3,28 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
class CustomDropDownButton<T> extends StatelessWidget {
const CustomDropDownButton({super.key, required this.items, required this.onChanged, this.value, this.hint, this.hasError = false});
const CustomDropDownButton({
super.key,
required this.items,
required this.onChanged,
this.value,
this.hint,
this.selectedItemBuilder,
this.hasError = false,
this.isExpanded = false,
});
final T? value;
final List<DropdownMenuItem<T>> items;
final void Function(T?)? onChanged;
final Widget? hint;
final bool hasError;
final List<Widget> Function(BuildContext)? selectedItemBuilder;
final bool isExpanded;
@override
Widget build(BuildContext context) {
return DropdownButtonFormField(
hint: hint,
isExpanded: isExpanded,
icon: SvgPicture.asset('assets/icons/arrow_down2.svg', width: 12, height: 8),
borderRadius: BorderRadius.circular(20),
dropdownColor: appTheme.lightGray0,
@@ -35,6 +47,7 @@ class CustomDropDownButton<T> extends StatelessWidget {
initialValue: value,
items: items,
onChanged: onChanged,
selectedItemBuilder: selectedItemBuilder,
);
}
}

View File

@@ -35,7 +35,7 @@ class CustomOutlinedButton extends BaseButton {
margin: margin,
decoration: decoration,
child: OutlinedButton(
style: buttonStyle!,
style: buttonStyle ?? OutlinedButton.styleFrom(),
onPressed: isDisabled ?? false ? null : onPressed ?? () {},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
@@ -59,7 +59,7 @@ class CustomOutlinedButton extends BaseButton {
margin: margin,
decoration: decoration,
child: OutlinedButton(
style: buttonStyle!,
style: buttonStyle ?? OutlinedButton.styleFrom(),
onPressed: isDisabled ?? false ? null : onPressed ?? () {},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,

View File

@@ -27,7 +27,7 @@ class FeedIconButton extends StatelessWidget {
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (svgIcon != null) svgIcon!,
?svgIcon,
if (icon != null)
Icon(
icon,

View File

@@ -12,15 +12,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
class InfoRow extends StatelessWidget {
const InfoRow({
required this.type,
super.key,
this.diet,
this.itemsBackgroundColor,
this.disableDescriptionOfTypes = false,
this.plan,
this.recipe,
});
const InfoRow({required this.type, super.key, this.diet, this.itemsBackgroundColor, this.disableDescriptionOfTypes = false, this.plan, this.recipe});
final ProductType type;
final Color? itemsBackgroundColor;
final Diet? diet;
@@ -50,44 +42,13 @@ class InfoRow extends StatelessWidget {
InfoBouble(
backgroundColor: itemsBackgroundColor,
children: [
SvgPicture.asset(
'assets/icons/time2.svg',
width: 13.3,
height: 13.3,
),
SvgPicture.asset('assets/icons/time2.svg', width: 13.3, height: 13.3),
const SizedBox(width: 4),
Text(
switch (type) {
ProductType.diet => '${diet?.duration} ${getDayForm(diet?.duration ?? 7, s)}',
ProductType.plan => '${plan?.duration} ${getDayForm(plan?.duration ?? 7, s)}',
ProductType.recipe => timeTitle ?? '',
},
style: TextStyles.textXSSemibold.copyWith(color: appTheme.black),
),
],
),
const SizedBox(width: 12),
InfoBouble(
backgroundColor: itemsBackgroundColor,
children: [
SvgPicture.asset(
switch (type) {
ProductType.diet => 'assets/icons/people.svg',
ProductType.plan => 'assets/icons/people.svg',
ProductType.recipe => 'assets/icons/calories.svg',
},
width: 13.3,
height: 13.3,
),
const SizedBox(width: 4),
Text(
switch (type) {
ProductType.diet => '80 человек прошли',
ProductType.plan => '80 человек прошли',
ProductType.recipe => '${recipe?.calories.toStringAsFixed(0) ?? '0'} ккал',
},
style: TextStyles.textXSSemibold.copyWith(color: appTheme.black),
),
Text(switch (type) {
ProductType.diet => '${diet?.duration} ${getDayForm(diet?.duration ?? 7, s)}',
ProductType.plan => '${plan?.duration} ${getDayForm(plan?.duration ?? 7, s)}',
ProductType.recipe => timeTitle ?? '',
}, style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)),
],
),
const SizedBox(width: 12),
@@ -95,11 +56,17 @@ class InfoRow extends StatelessWidget {
InfoBouble(
backgroundColor: itemsBackgroundColor,
children: [
SvgPicture.asset(
'assets/icons/steps.svg',
width: 13.3,
height: 13.3,
),
SvgPicture.asset('assets/icons/calories.svg', width: 13.3, height: 13.3),
const SizedBox(width: 4),
Text('${(recipe?.calories ?? 0).toStringAsFixed(0)} ккал', style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)),
],
),
const SizedBox(width: 12),
if (type == ProductType.recipe)
InfoBouble(
backgroundColor: itemsBackgroundColor,
children: [
SvgPicture.asset('assets/icons/steps.svg', width: 13.3, height: 13.3),
const SizedBox(width: 4),
Text(
'${recipe?.steps?.length ?? 0} ${getStepForm(recipe?.steps?.length ?? 0)}',
@@ -110,13 +77,7 @@ class InfoRow extends StatelessWidget {
if (plan?.type.hasAtLeastOneType() == true || diet?.type.hasAtLeastOneType() == true)
InfoBouble(
backgroundColor: itemsBackgroundColor,
children: [
TypeBadges(
type: diet?.type,
planType: plan?.type,
disableDescriptionOfTypes: disableDescriptionOfTypes,
),
],
children: [TypeBadges(type: diet?.type, planType: plan?.type, disableDescriptionOfTypes: disableDescriptionOfTypes)],
),
],
),

View File

@@ -0,0 +1,124 @@
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:url_launcher/url_launcher.dart';
import 'package:eatvio_app/core/utils/markdown_utils.dart';
/// Переиспользуемый виджет для отображения HTML из markdown текста
class MarkdownHtmlWidget extends StatelessWidget {
/// Markdown текст для отображения
final String markdownText;
/// Ширина изображений (по умолчанию ширина экрана минус 32)
final double? imageWidth;
/// Цвет текста для body и параграфов
final Color? textColor;
/// Размер шрифта (по умолчанию 16)
final double? fontSize;
/// Callback для обработки нажатий на ссылки
final OnTap? onLinkTap;
const MarkdownHtmlWidget({
super.key,
required this.markdownText,
this.imageWidth,
this.textColor,
this.fontSize,
this.onLinkTap,
});
@override
Widget build(BuildContext context) {
// Обрабатываем markdown текст
final processedText = MarkdownUtils.processMarkdown(markdownText);
// Конвертируем markdown в HTML
final htmlData = md.markdownToHtml(
processedText,
extensionSet: md.ExtensionSet.gitHubFlavored,
);
final screenWidth = MediaQuery.maybeOf(context)?.size.width ?? 400.0;
final defaultImageWidth = imageWidth ?? (screenWidth - 32);
final defaultTextColor = textColor ?? Colors.black87;
final defaultFontSize = fontSize ?? 16.0;
return Html(
data: htmlData,
style: {
"body": Style(
margin: Margins.zero,
padding: HtmlPaddings.zero,
fontSize: FontSize(defaultFontSize),
lineHeight: const LineHeight(1.5),
color: defaultTextColor,
),
"p": Style(
margin: Margins.only(bottom: 12),
color: defaultTextColor,
),
"b, strong": Style(
fontWeight: FontWeight.bold,
color: Colors.black,
),
"h1, h2, h3, h4, h5, h6": Style(
margin: Margins.only(top: 16, bottom: 8),
color: Colors.black,
fontWeight: FontWeight.bold,
),
"h1": Style(fontSize: FontSize(24)),
"h2": Style(fontSize: FontSize(20)),
"h3": Style(fontSize: FontSize(18)),
"ul, ol": Style(
margin: Margins.only(bottom: 12),
padding: HtmlPaddings.only(left: 16),
),
"li": Style(
margin: Margins.only(bottom: 4),
color: defaultTextColor,
),
"strong, b": Style(
fontWeight: FontWeight.bold,
color: Colors.black,
),
"em, i": Style(fontStyle: FontStyle.italic),
"code": Style(
backgroundColor: Colors.grey.shade100,
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
fontFamily: 'monospace',
),
"pre": Style(
backgroundColor: Colors.grey.shade100,
padding: HtmlPaddings.all(12),
margin: Margins.only(bottom: 12),
),
"blockquote": Style(
padding: HtmlPaddings.only(left: 16),
margin: Margins.only(left: 8, bottom: 12),
fontStyle: FontStyle.italic,
color: Colors.grey.shade700,
),
"a": Style(
color: Colors.blue,
textDecoration: TextDecoration.underline,
),
"img": Style(width: Width(defaultImageWidth)),
},
onLinkTap: onLinkTap ?? ((url, attributes, element) async {
if (url != null) {
try {
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
} catch (e) {
debugPrint('Error launching URL: $e');
}
}
}) as OnTap,
);
}
}

View File

@@ -1,9 +1,7 @@
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:permission_handler/permission_handler.dart';
Future<void> showErrorDialog(BuildContext context, {String? title, String? description, String? buttonTitle, bool? needToOpenSettings}) async {
@@ -45,7 +43,7 @@ Future<void> showErrorDialog(BuildContext context, {String? title, String? descr
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () {
AppRouter.navigatorKey.currentContext?.pop();
Navigator.of(context).pop();
if (needToOpenSettings ?? false) {
openAppSettings();
}

View File

@@ -8,32 +8,33 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class SuccessPublishedDietOrPlanBottomSheet extends StatelessWidget {
const SuccessPublishedDietOrPlanBottomSheet({super.key, required this.title, this.isPlan = false});
const SuccessPublishedDietOrPlanBottomSheet({super.key, required this.title, this.isPlan = false, this.isDrafted, this.isUpdated});
final String title;
final bool isPlan;
final bool? isDrafted;
final bool? isUpdated;
@override
Widget build(BuildContext context) {
final s = S.of(context);
final String text;
if (isDrafted == true) {
text = 'cохранена в черновиках';
} else if (isUpdated == true) {
text = 'успешно обновлена';
} else if (isPlan == true) {
text = 'опубликован';
} else {
text = 'опубликованна';
}
return Column(
children: [
const SizedBox(
height: 20,
),
const SizedBox(height: 20),
const BottomSheetHandlerBar(),
const SizedBox(
height: 20,
),
Text(
title,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
Text(
isPlan ? 'опубликован' : 'опубликованна',
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
const SizedBox(
height: 20,
),
const SizedBox(height: 20),
Text(title, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
Text(text, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: CustomOutlinedButton(
@@ -43,15 +44,10 @@ class SuccessPublishedDietOrPlanBottomSheet extends StatelessWidget {
text: s.ok,
height: 52,
buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
),
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
),
),
const SizedBox(
height: 20,
),
const SizedBox(height: 20),
],
);
}

View File

@@ -0,0 +1,27 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'oauth_verification_confirm_request.freezed.dart';
part 'oauth_verification_confirm_request.g.dart';
@freezed
abstract class OAuthVerificationConfirmRequest with _$OAuthVerificationConfirmRequest {
const factory OAuthVerificationConfirmRequest({
@JsonKey(name: 'access_token') required String accessToken,
@JsonKey(name: 'refresh_token') required String refreshToken,
@JsonKey(name: 'client_id') required String clientId,
required String provider,
required String id,
String? email,
String? phone,
required String code,
@JsonKey(name: 'verification_type') required String verificationType,
@JsonKey(name: 'first_name') required String firstName,
@JsonKey(name: 'last_name') required String lastName,
@JsonKey(name: 'date_of_birth') required String dateOfBirth,
required String gender,
required int weight,
required int height,
}) = _OAuthVerificationConfirmRequest;
factory OAuthVerificationConfirmRequest.fromJson(Map<String, dynamic> json) => _$OAuthVerificationConfirmRequestFromJson(json);
}

View File

@@ -0,0 +1,36 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'oauth_verification_confirm_response.freezed.dart';
part 'oauth_verification_confirm_response.g.dart';
@freezed
abstract class OAuthVerificationConfirmResponse with _$OAuthVerificationConfirmResponse {
const factory OAuthVerificationConfirmResponse({
required bool success,
required String message,
required String token,
required Map<String, dynamic> user,
@JsonKey(name: 'is_new_user') required bool isNewUser,
}) = _OAuthVerificationConfirmResponse;
factory OAuthVerificationConfirmResponse.fromJson(Map<String, dynamic> json) => _$OAuthVerificationConfirmResponseFromJson(json);
}

View File

@@ -0,0 +1,30 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'oauth_verification_request_response.freezed.dart';
part 'oauth_verification_request_response.g.dart';
@freezed
abstract class OAuthVerificationRequestResponse with _$OAuthVerificationRequestResponse {
const factory OAuthVerificationRequestResponse({required String email, required String status}) = _OAuthVerificationRequestResponse;
factory OAuthVerificationRequestResponse.fromJson(Map<String, dynamic> json) => _$OAuthVerificationRequestResponseFromJson(json);
}

View File

@@ -1,15 +1,23 @@
import 'dart:developer';
import 'package:eatvio_app/core/firebase_api.dart';
import 'package:eatvio_app/core/prefs.dart';
import 'package:eatvio_app/core/prefs_keys.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state.dart';
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
class AuthCubit extends Cubit<AuthState> {
final AuthRepository authRepository;
final ProfileRepository profileRepository;
final FirebaseApi firebaseApi;
final Prefs prefs;
AuthCubit({required this.authRepository}) : super(const AuthState());
AuthCubit({required this.authRepository, required this.prefs, required this.profileRepository, required this.firebaseApi}) : super(const AuthState());
Future<bool> getTokenByCredentials(String email, String password) async {
emit(const AuthState.loading());
@@ -25,4 +33,26 @@ class AuthCubit extends Cubit<AuthState> {
return false;
}
}
Future<void> initializeUser() async {
try {
await registerAnonymousPushToken();
FlutterNativeSplash.remove();
} catch (e) {
log('Error in initializeUser: $e');
FlutterNativeSplash.remove();
}
}
Future<void> registerAnonymousPushToken() async {
final hasAnonymousTokenBeenSent = prefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent) ?? false;
if (!hasAnonymousTokenBeenSent) {
await firebaseApi.registerAnonymousPushToken();
prefs.setBool(PrefsKeys.hasAnonymousTokenBeenSent, true);
}
}
Future<void> registerPushToken() async {
await firebaseApi.registerPushToken();
}
}

View File

@@ -3,6 +3,7 @@ import 'package:dio/dio.dart';
import 'package:eatvio_app/core/mixins/registration_date_cache_mixin.dart';
import 'package:eatvio_app/core/prefs.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/features/auth/models/oauth_verification_request_response.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart';
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -28,6 +29,39 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
log('OAuthCubit: Got token model: ${tokenModel.token}');
TokenStorage.saveTokenInStorage(tokenModel.token);
emit(OAuthState.loaded(token: tokenModel.token));
} on DioException catch (e) {
log('OAuthCubit: Error occurred: $e');
// Проверяем, является ли ошибка ошибкой валидации email/phone
final errors = e.response?.data['errors'] as Map<String, dynamic>?;
final emailErrors = errors?['email'] as List<dynamic>?;
final phoneErrors = errors?['phone'] as List<dynamic>?;
final errorsList = <RegistrationValidationErrors>[];
if (emailErrors != null && emailErrors.isNotEmpty) {
for (final error in emailErrors) {
final errorStr = error.toString();
if (errorStr.contains('Некорректный формат email')) {
errorsList.add(RegistrationValidationErrors.incorrectEmailFormat);
}
if (errorStr.contains('почта уже существует') || errorStr.contains('Эл. почта уже существует')) {
errorsList.add(RegistrationValidationErrors.emailExist);
}
}
}
if (phoneErrors != null && phoneErrors.isNotEmpty) {
for (final error in phoneErrors) {
final errorStr = error.toString();
if (errorStr.contains('phone уже существует') || errorStr.contains('телефон уже существует')) {
errorsList.add(RegistrationValidationErrors.phoneExist);
}
}
}
if (errorsList.isNotEmpty) {
emit(OAuthState.error(errorMessage: 'validation_error', registrationValidationErrors: errorsList));
} else {
emit(OAuthState.error(errorMessage: e.toString()));
}
} catch (e) {
log('OAuthCubit: Error occurred: $e');
emit(OAuthState.error(errorMessage: e.toString()));
@@ -77,16 +111,22 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
final dateOfBirth = errors?['date_of_birth'] as List<dynamic>?;
final errorsList = <RegistrationValidationErrors>[];
if (emailErrors != null && emailErrors.isNotEmpty) {
if (emailErrors.contains('Некорректный формат email')) {
errorsList.add(RegistrationValidationErrors.incorrectEmailFormat);
}
if (emailErrors.contains('Такой Эл. почта уже существует.')) {
errorsList.add(RegistrationValidationErrors.emailExist);
for (final error in emailErrors) {
final errorStr = error.toString();
if (errorStr.contains('Некорректный формат email')) {
errorsList.add(RegistrationValidationErrors.incorrectEmailFormat);
}
if (errorStr.contains('почта уже существует') || errorStr.contains('Эл. почта уже существует')) {
errorsList.add(RegistrationValidationErrors.emailExist);
}
}
}
if (phoneErrors != null && phoneErrors.isNotEmpty) {
if (phoneErrors.contains('Такой phone уже существует.')) {
errorsList.add(RegistrationValidationErrors.phoneExist);
for (final error in phoneErrors) {
final errorStr = error.toString();
if (errorStr.contains('phone уже существует') || errorStr.contains('телефон уже существует')) {
errorsList.add(RegistrationValidationErrors.phoneExist);
}
}
}
if (dateOfBirth != null && dateOfBirth.isNotEmpty) {
@@ -101,6 +141,70 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
return null;
}
Future<OAuthVerificationRequestResponse?> requestOAuthVerification({String? email, String? phone}) async {
try {
emit(const OAuthState.loading());
log('OAuthCubit: Requesting verification code for email: $email, phone: $phone');
final response = await authRepository.requestOAuthVerification(email: email, phone: phone);
if (response != null) {
log('OAuthCubit: Verification code requested successfully, masked email: ${response.email}');
}
// Сбрасываем состояние после успешного запроса кода
emit(const OAuthState());
return response;
} catch (e) {
log('OAuthCubit: Error requesting verification code: $e');
emit(OAuthState.error(errorMessage: e.toString()));
return null;
}
}
Future<void> confirmOAuthVerification({
required String provider,
required String accessToken,
required String refreshToken,
required String clientId,
required String id,
String? email,
String? phone,
required String code,
required String verificationType,
required String firstName,
required String lastName,
required String dateOfBirth,
required String gender,
required int weight,
required int height,
}) async {
try {
emit(const OAuthState.loading());
log('OAuthCubit: Confirming OAuth verification - provider: $provider, verificationType: $verificationType, code: $code');
final response = await authRepository.confirmOAuthVerification(
provider: provider,
accessToken: accessToken,
refreshToken: refreshToken,
clientId: clientId,
id: id,
email: email,
phone: phone,
code: code,
verificationType: verificationType,
firstName: firstName,
lastName: lastName,
dateOfBirth: dateOfBirth,
gender: gender,
weight: weight,
height: height,
);
log('OAuthCubit: OAuth verification confirmed successfully, token received');
TokenStorage.saveTokenInStorage(response.token);
emit(OAuthState.loaded(token: response.token));
} catch (e) {
log('OAuthCubit: Error confirming OAuth verification: $e');
emit(OAuthState.error(errorMessage: e.toString()));
rethrow; // Пробрасываем ошибку дальше, чтобы UI мог её обработать
}
}
}
enum RegistrationValidationErrors { incorrectEmailFormat, emailExist, phoneExist, incorrectDateOfBirthFormat }

View File

@@ -10,5 +10,10 @@ abstract class OAuthState with _$OAuthState {
const factory OAuthState.loaded({required String token}) = OAuthStateLoaded;
const factory OAuthState.error({required String errorMessage}) = OAuthStateError;
const factory OAuthState.error({
required String errorMessage,
List<RegistrationValidationErrors>? registrationValidationErrors,
}) = OAuthStateError;
}
enum RegistrationValidationErrors { incorrectEmailFormat, emailExist, phoneExist, incorrectDateOfBirthFormat }

View File

@@ -28,7 +28,6 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -232,9 +231,8 @@ class _AuthScreenState extends State<AuthScreen> {
final bool useMobileLayout = shortestSide < 600;
if (!useMobileLayout) isFocused = true;
final s = S.of(context);
FlutterNativeSplash.remove();
return BlocProvider<AuthCubit>(
create: (BuildContext context) => authCubit,
create: (BuildContext context) => authCubit..initializeUser(),
child: BlocBuilder<AuthCubit, AuthState>(
builder: (BuildContext context, AuthState state) {
return Scaffold(
@@ -337,7 +335,7 @@ class _AuthScreenState extends State<AuthScreen> {
children: [
const Text('Нет аккаунта? '),
GestureDetector(
onTap: () => GoRouter.of(context).goNamed('registrationScreen'),
onTap: () => GoRouter.of(context).pushNamed('registrationScreenV2'),
child: Text(
'Зарегистрироваться',
style: TextStyle(color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline),
@@ -411,7 +409,7 @@ class _AuthScreenState extends State<AuthScreen> {
_isLoading = false;
});
},
label: state is AuthStateLoading
label: state is AuthStateLoading || _isLoading
? const CupertinoActivityIndicator()
: Text('Войти', style: theme.textTheme.titleSmall!.copyWith(color: Colors.white)),
),
@@ -426,8 +424,8 @@ class _AuthScreenState extends State<AuthScreen> {
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400,
onPressed: state is AuthStateLoading
backgroundColor: state is AuthStateLoading || _isLoading ? Colors.black.withAlpha(50) : appTheme.blueA400,
onPressed: state is AuthStateLoading || _isLoading
? null
: () async {
await showDialog(
@@ -487,7 +485,7 @@ class _AuthScreenState extends State<AuthScreen> {
},
);
},
label: state is AuthStateLoading
label: state is AuthStateLoading || _isLoading
? const CupertinoActivityIndicator()
: const Icon(Icons.qr_code_scanner_rounded, color: Colors.white),
),

View File

@@ -1,166 +1,124 @@
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class ForgetPasswordScreen extends StatefulWidget {
final bool isAuth;
const ForgetPasswordScreen({super.key, required this.isAuth});
@override
State<ForgetPasswordScreen> createState() => _ForgetPasswordScreenState();
}
class _ForgetPasswordScreenState extends State<ForgetPasswordScreen> {
final emailController = TextEditingController();
final codeController = TextEditingController();
final passwordController = TextEditingController();
final passwordConfirmController = TextEditingController();
@override
Widget build(BuildContext context) {
return BlocProvider<ResetPasswordCubit>(
create: (BuildContext context) => sl<ResetPasswordCubit>(),
child: BlocConsumer<ResetPasswordCubit, ResetPasswordState>(
builder: (BuildContext context, ResetPasswordState state) {
return Scaffold(
appBar: AppBar(
title: widget.isAuth ? const Text('Смена пароля') : const Text('Восстановление пароля'),
centerTitle: false,
),
body: state is ResetPasswordStateLoadingResetPassword || state is ResetPasswordStateLoadingSendEmailCode
? const Center(
child: CircularProgressIndicator(),
)
: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (state is ResetPasswordStateCodeSendError || state is ResetPasswordStateEmpty) ...[
Text(
'${widget.isAuth ? '' : 'Забыли пароль? '}Укажите свой email,\nи мы пришлём код для сброса.',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 24),
const Text(
'E-mail',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
CustomTextFormField(
controller: emailController,
hintText: 'Введите e-mail',
),
],
if (state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError) ...[
const Text(
'Код',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
CustomTextFormField(
controller: codeController,
hintText: 'Введите код',
),
const SizedBox(height: 12),
const Text(
'Пароль',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
CustomTextFormField(
controller: passwordController,
hintText: 'Введите новый пароль',
),
const SizedBox(height: 12),
const Text(
'Подтвердите пароль',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
CustomTextFormField(
controller: passwordConfirmController,
hintText: 'Введите новый пароль',
),
],
const Spacer(),
InkWell(
onTap: () {
if (state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError) {
context.read<ResetPasswordCubit>().tryResetPassword(
emailController.text,
codeController.text,
passwordController.text,
passwordConfirmController.text,
);
} else {
context.read<ResetPasswordCubit>().trySendEmailCode(emailController.text);
}
},
borderRadius: BorderRadius.circular(20),
child: Ink(
width: MediaQuery.of(context).size.width - 32,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: appTheme.blueA400,
),
child: Center(
child: Text(
state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError
? 'Сменить пароль'
: 'Сбросить пароль',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
),
],
),
),
);
},
listener: (BuildContext context, ResetPasswordState state) {
if (state is ResetPasswordStateCodeSendSuccess) {
showSuccessToast('Код на почту успешно отправлен');
}
if (state is ResetPasswordStateCodeSendError) {
showErrorToast('Ошибка отправки кода. Проверьте правильность ввёдной почты');
}
if (state is ResetPasswordStateResetPasswordError) {
showErrorToast('Ошибка смены пароля. Проверьте правильность ввёднного кода и совпадение пароля');
}
if (state is ResetPasswordStateResetPassowrdSuccess) {
GoRouter.of(context).pop();
showSuccessToast('Пароль успешно сменён. ${widget.isAuth ? '' : 'Войдите в аккаунт'}');
}
},
),
);
}
}
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_state.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class ForgetPasswordScreen extends StatefulWidget {
final bool isAuth;
const ForgetPasswordScreen({super.key, required this.isAuth});
@override
State<ForgetPasswordScreen> createState() => _ForgetPasswordScreenState();
}
class _ForgetPasswordScreenState extends State<ForgetPasswordScreen> {
final emailController = TextEditingController();
final codeController = TextEditingController();
final passwordController = TextEditingController();
final passwordConfirmController = TextEditingController();
@override
Widget build(BuildContext context) {
return BlocProvider<ResetPasswordCubit>(
create: (BuildContext context) => sl<ResetPasswordCubit>(),
child: BlocConsumer<ResetPasswordCubit, ResetPasswordState>(
builder: (BuildContext context, ResetPasswordState state) {
return Scaffold(
appBar: AppBar(title: widget.isAuth ? const Text('Смена пароля') : const Text('Восстановление пароля'), centerTitle: false),
body: state is ResetPasswordStateLoadingResetPassword || state is ResetPasswordStateLoadingSendEmailCode
? const Center(child: CircularProgressIndicator())
: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (state is ResetPasswordStateCodeSendError || state is ResetPasswordStateEmpty) ...[
Text(
'${widget.isAuth ? '' : 'Забыли пароль? '}Укажите свой email,\nи мы пришлём код для сброса.',
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
const SizedBox(height: 24),
const Text('E-mail', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
const SizedBox(height: 4),
CustomTextFormField(controller: emailController, hintText: 'Введите e-mail'),
],
if (state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError) ...[
const Text('Код', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
const SizedBox(height: 4),
CustomTextFormField(controller: codeController, hintText: 'Введите код'),
const SizedBox(height: 12),
const Text('Пароль', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
const SizedBox(height: 4),
CustomTextFormField(
controller: passwordController,
hintText: 'Введите новый пароль',
inputFormatters: [FilteringTextInputFormatter.deny(RegExp(r'\s'))],
),
const SizedBox(height: 12),
const Text('Подтвердите пароль', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
const SizedBox(height: 4),
CustomTextFormField(
controller: passwordConfirmController,
hintText: 'Введите новый пароль',
inputFormatters: [FilteringTextInputFormatter.deny(RegExp(r'\s'))],
),
],
const Spacer(),
InkWell(
onTap: () {
if (state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError) {
context.read<ResetPasswordCubit>().tryResetPassword(
emailController.text,
codeController.text,
passwordController.text,
passwordConfirmController.text,
);
} else {
context.read<ResetPasswordCubit>().trySendEmailCode(emailController.text);
}
},
borderRadius: BorderRadius.circular(20),
child: Ink(
width: MediaQuery.of(context).size.width - 32,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
child: Center(
child: Text(
state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError
? 'Сменить пароль'
: 'Сбросить пароль',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white),
),
),
),
),
],
),
),
);
},
listener: (BuildContext context, ResetPasswordState state) {
if (state is ResetPasswordStateCodeSendSuccess) {
showSuccessToast('Код на почту успешно отправлен');
}
if (state is ResetPasswordStateCodeSendError) {
showErrorToast('Ошибка отправки кода. Проверьте правильность ввёдной почты');
}
if (state is ResetPasswordStateResetPasswordError) {
showErrorToast('Ошибка смены пароля. Проверьте правильность ввёднного кода и совпадение пароля');
}
if (state is ResetPasswordStateResetPassowrdSuccess) {
GoRouter.of(context).pop();
showSuccessToast('Пароль успешно сменён. ${widget.isAuth ? '' : 'Войдите в аккаунт'}');
}
},
),
);
}
}

View File

@@ -1,3 +1,4 @@
import 'dart:developer';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
@@ -15,6 +16,7 @@ import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widge
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
import 'package:eatvio_app/core/firebase_api.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/core/utils/validation_utils.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -49,6 +51,8 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
bool weightError = false;
bool heightError = false;
bool incorrectEmailFormatError = false;
bool showVerificationButton = false;
String? verificationType; // 'email' or 'phone'
// Состояние для выбора пола
bool isMaleSelected = true;
@@ -221,7 +225,28 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
hasError: emailExistError || incorrectEmailFormatError,
onChanged: (newValue) {},
),
if (emailExistError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Такая почта уже существует.')],
if (emailExistError) ...[
const SizedBox(height: 4),
const ValidationErrorWidget(error: 'Такая почта уже существует.'),
if (showVerificationButton && verificationType == 'email') ...[
const SizedBox(height: 12),
InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () => _handleVerificationRequest(context, 'email'),
child: Ink(
width: MediaQuery.of(context).size.width - 32,
height: 52,
decoration: BoxDecoration(color: appTheme.blueA400, borderRadius: BorderRadius.circular(20)),
child: const Center(
child: Text(
'Да, это мой аккаунт, подтвердить',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 16, color: Colors.white),
),
),
),
),
],
],
if (incorrectEmailFormatError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Некорректный формат email')],
const SizedBox(height: 12),
const Text('Номер телефона'),
@@ -235,7 +260,28 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
hasError: phoneExistError,
onChanged: (newValue) {},
),
if (phoneExistError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Такой номер телефона уже существует.')],
if (phoneExistError) ...[
const SizedBox(height: 4),
const ValidationErrorWidget(error: 'Такой номер телефона уже существует.'),
if (showVerificationButton && verificationType == 'phone') ...[
const SizedBox(height: 12),
InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () => _handleVerificationRequest(context, 'phone'),
child: Ink(
width: MediaQuery.of(context).size.width - 32,
height: 52,
decoration: BoxDecoration(color: appTheme.blueA400, borderRadius: BorderRadius.circular(20)),
child: const Center(
child: Text(
'Да, это мой аккаунт, подтвердить',
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 16, color: Colors.white),
),
),
),
),
],
],
const SizedBox(height: 12),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -289,12 +335,17 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
width: MediaQuery.of(context).size.width / 2 - 22,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text('Вес'),
const Text('Рост'),
const SizedBox(height: 4),
CustomTextFormField(controller: weightController, hintText: 'Введите вес', onChanged: (newValue) {}, hasError: weightError),
if (weightError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Вес должен быть от 30 до 300 кг')],
CustomTextFormField(
controller: heightController,
hintText: 'Введите рост',
keyboardType: TextInputType.number,
onChanged: (newValue) {},
hasError: heightError,
),
if (heightError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Рост должен быть от 100 до 250 см')],
],
),
),
@@ -303,11 +354,18 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
width: MediaQuery.of(context).size.width / 2 - 22,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text('Рост'),
const Text('Вес'),
const SizedBox(height: 4),
CustomTextFormField(controller: heightController, hintText: 'Введите рост', onChanged: (newValue) {}, hasError: heightError),
if (heightError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Рост должен быть от 100 до 250 см')],
CustomTextFormField(
controller: weightController,
hintText: 'Введите вес',
keyboardType: TextInputType.number,
onChanged: (newValue) {},
hasError: weightError,
),
if (weightError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Вес должен быть от 30 до 300 кг')],
],
),
),
@@ -481,7 +539,7 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
if (hasErrorWeightOrHeightError) return;
// Очищаем номер телефона от маски для отправки
final cleanPhone = phoneController.text.replaceAll(RegExp(r'[^\d+]'), '');
final cleanPhone = ValidationUtils.normalizePhone(phoneController.text);
if (clientID == null) return;
// Вызываем OAuth регистрацию
@@ -501,6 +559,10 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
clientId: clientID,
);
if (errors != null) {
bool hasEmailExist = false;
bool hasPhoneExist = false;
// Сначала определяем, какие ошибки присутствуют
for (final error in errors) {
switch (error) {
case RegistrationValidationErrors.incorrectEmailFormat:
@@ -509,14 +571,10 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
});
break;
case RegistrationValidationErrors.emailExist:
setState(() {
emailExistError = true;
});
hasEmailExist = true;
break;
case RegistrationValidationErrors.phoneExist:
setState(() {
phoneExistError = true;
});
hasPhoneExist = true;
break;
case RegistrationValidationErrors.incorrectDateOfBirthFormat:
setState(() {
@@ -525,8 +583,236 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
break;
}
}
// Устанавливаем verificationType согласно правилам:
// - Если оба (email и phone) существуют -> 'email'
// - Если только email -> 'email'
// - Если только phone -> 'phone'
if (hasEmailExist || hasPhoneExist) {
setState(() {
emailExistError = hasEmailExist;
phoneExistError = hasPhoneExist;
showVerificationButton = true;
if (hasEmailExist && hasPhoneExist) {
verificationType = 'email'; // Если оба существуют, используем email
} else if (hasEmailExist) {
verificationType = 'email'; // Только email
} else {
verificationType = 'phone'; // Только phone
}
});
}
// Если есть ошибки, возвращаемся и не показываем toast об успехе
return;
}
// Регистрация успешна только если нет ошибок
showSuccessToast('Регистрация успешна');
}
Future<void> _handleVerificationRequest(BuildContext context, String type) async {
try {
// Используем service locator для доступа к OAuthCubit
final oauthCubit = CubitRegistrations.sl<OAuthCubit>();
// Всегда передаем и email и phone
final String email = emailController.text;
final String phone = ValidationUtils.normalizePhone(phoneController.text);
final response = await oauthCubit.requestOAuthVerification(email: email, phone: phone);
if (response != null && context.mounted) {
// Показываем диалог для ввода кода
final displayText = response.email;
_showVerificationCodeDialog(context, type, displayText);
} else if ((emailController.text.isEmpty || phoneController.text.isEmpty) && context.mounted) {
showErrorToast('Пожалуйста, заполните все поля');
} else if (context.mounted) {
showErrorToast('Не удалось отправить код верификации');
}
} catch (e) {
if (context.mounted) {
showErrorToast('Ошибка: ${e.toString()}');
}
}
}
void _showVerificationCodeDialog(BuildContext context, String verificationType, String displayText) {
final List<FocusNode> focusNodes = List.generate(4, (_) => FocusNode());
final List<TextEditingController> controllers = List.generate(4, (_) => TextEditingController());
showDialog(
context: context,
barrierDismissible: false,
builder: (dialogContext) => AlertDialog(
title: const Text('Подтверждение аккаунта'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Код отправлен на $displayText'),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(4, (index) {
return SizedBox(
width: 50,
child: TextField(
controller: controllers[index],
focusNode: focusNodes[index],
textAlign: TextAlign.center,
keyboardType: TextInputType.number,
maxLength: 1,
decoration: const InputDecoration(counterText: '', border: OutlineInputBorder()),
onChanged: (value) {
if (value.isNotEmpty && index < 3) {
focusNodes[index + 1].requestFocus();
}
if (value.isEmpty && index > 0) {
focusNodes[index - 1].requestFocus();
}
// Проверяем, заполнен ли весь код
final code = controllers.map((c) => c.text).join();
if (code.length == 4) {
// Небольшая задержка перед отправкой, чтобы пользователь увидел последний символ
Future.delayed(const Duration(milliseconds: 100), () async {
if (dialogContext.mounted && context.mounted) {
await _handleVerificationConfirm(context, dialogContext, verificationType, code, controllers, focusNodes);
}
});
}
},
),
);
}),
),
],
),
),
actions: [
TextButton(
onPressed: () {
for (var controller in controllers) {
controller.dispose();
}
for (var node in focusNodes) {
node.dispose();
}
Navigator.of(dialogContext).pop();
},
child: const Text('Отмена'),
),
],
),
);
}
Future<void> _handleVerificationConfirm(
BuildContext widgetContext,
BuildContext dialogContext,
String verificationType,
String code,
List<TextEditingController> controllers,
List<FocusNode> focusNodes,
) async {
try {
// Используем service locator для доступа к OAuthCubit, так как контекст из диалога может не иметь доступа к провайдеру
final oauthCubit = CubitRegistrations.sl<OAuthCubit>();
final authResult = widget.authResult;
if (authResult == null) {
showErrorToast('Ошибка: данные авторизации не найдены');
return;
}
// Всегда передаем и email и phone (полный набор данных от провайдера)
// Используем данные из формы, если они есть, иначе из authResult
final String email = emailController.text.isNotEmpty ? emailController.text : (authResult.email ?? '');
final String phone = phoneController.text.isNotEmpty ? ValidationUtils.normalizePhone(phoneController.text) : ValidationUtils.normalizePhone(authResult.phone ?? '');
// Проверяем, что данные есть
if (email.isEmpty && phone.isEmpty) {
log('OAuth Verification Confirm: Both email and phone are empty');
showErrorToast('Ошибка: не указан email или телефон');
return;
}
// Проверяем, что все необходимые данные от провайдера есть
if (authResult.accessToken.isEmpty || authResult.clientId.isEmpty || authResult.userId.isEmpty) {
log('OAuth Verification Confirm: Missing required OAuth data');
showErrorToast('Ошибка: неполные данные авторизации');
return;
}
// Проверяем, что код не пустой
if (code.isEmpty || code.length != 4) {
showErrorToast('Введите полный код');
return;
}
// Получаем данные профиля из формы
final String firstName = firstNameController.text.isNotEmpty ? firstNameController.text : (authResult.firstName ?? '');
final String lastName = lastNameController.text.isNotEmpty ? lastNameController.text : (authResult.lastName ?? '');
final String dateOfBirth = dateOfBirthController.text.isNotEmpty ? dateOfBirthController.text : (authResult.birthDate ?? '');
final String gender = isMaleSelected ? 'male' : 'female';
final int weight = weightController.text.isNotEmpty ? (int.tryParse(weightController.text) ?? 0) : 0;
final int height = heightController.text.isNotEmpty ? (int.tryParse(heightController.text) ?? 0) : 0;
// Проверяем, что все обязательные поля заполнены
if (firstName.isEmpty || lastName.isEmpty || dateOfBirth.isEmpty || weight == 0 || height == 0) {
log('OAuth Verification Confirm: Missing required profile data');
showErrorToast('Ошибка: заполните все обязательные поля');
return;
}
// Логируем перед отправкой запроса
log('OAuth Verification Confirm: provider=${widget.provider}, verificationType=$verificationType, code=$code, email=$email, phone=$phone');
// Отправляем запрос на подтверждение с полным набором данных от провайдера
await oauthCubit.confirmOAuthVerification(
provider: widget.provider ?? 'social',
accessToken: authResult.accessToken,
refreshToken: authResult.refreshToken ?? '',
clientId: authResult.clientId,
id: authResult.userId,
email: email,
phone: phone,
code: code,
verificationType: verificationType,
firstName: firstName,
lastName: lastName,
dateOfBirth: dateOfBirth,
gender: gender,
weight: weight,
height: height,
);
// Закрываем диалог
for (var controller in controllers) {
controller.dispose();
}
for (var node in focusNodes) {
node.dispose();
}
if (dialogContext.mounted) {
Navigator.of(dialogContext).pop();
}
// Перезагружаем веб-сокет и переходим на главный экран
if (widgetContext.mounted) {
_reconnectWebSocket(widgetContext);
}
} catch (e, stackTrace) {
log('OAuth Verification Confirm Error: $e', error: e, stackTrace: stackTrace);
if (widgetContext.mounted) {
showErrorToast('Неверный код. Попробуйте снова.');
}
// Очищаем поля
for (var controller in controllers) {
controller.clear();
}
if (focusNodes.isNotEmpty) {
focusNodes[0].requestFocus();
}
}
}
}

View File

@@ -1,8 +1,13 @@
import 'dart:developer';
import 'package:eatvio_app/features/auth/models/credentials_model.dart';
import 'package:eatvio_app/features/auth/models/oauth_model.dart';
import 'package:eatvio_app/features/auth/models/oauth_login_request.dart';
import 'package:eatvio_app/features/auth/models/oauth_verification_confirm_request.dart';
import 'package:eatvio_app/features/auth/models/oauth_verification_confirm_response.dart';
import 'package:eatvio_app/features/auth/models/oauth_verification_request_response.dart';
import 'package:eatvio_app/features/auth/models/token_model.dart';
import 'package:eatvio_app/features/auth/services/auth_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class AuthRepository {
final AuthService authService;
@@ -14,12 +19,12 @@ class AuthRepository {
return await authService.getTokenByCredentials(
credentials: CredentialsModel(email: email, password: password),
);
} catch (e) {
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
}
Future<TokenModel> signInViaSocialMedia({
required String provider,
required String accessToken,
@@ -30,7 +35,8 @@ class AuthRepository {
return await authService.signInViaSocialMedia(
body: OAuthLoginRequest(provider: provider, accessToken: accessToken, refreshToken: refreshToken, clientId: clientId),
);
} catch (e) {
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
}
@@ -69,7 +75,73 @@ class AuthRepository {
),
))['token']!
as String;
} catch (e) {
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
}
Future<OAuthVerificationRequestResponse?> requestOAuthVerification({String? email, String? phone}) async {
try {
Map<String, dynamic>? body;
if (email != null) {
body = {'email': email};
} else if (phone != null) {
body = {'phone': phone};
}
if (body != null) {
return await authService.requestOAuthVerification(body: body);
}
return null;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
}
Future<OAuthVerificationConfirmResponse> confirmOAuthVerification({
required String provider,
required String accessToken,
required String refreshToken,
required String clientId,
required String id,
String? email,
String? phone,
required String code,
required String verificationType,
required String firstName,
required String lastName,
required String dateOfBirth,
required String gender,
required int weight,
required int height,
}) async {
try {
final request = OAuthVerificationConfirmRequest(
provider: provider,
accessToken: accessToken,
refreshToken: refreshToken,
clientId: clientId,
id: id,
email: email,
phone: phone,
code: code,
verificationType: verificationType,
firstName: firstName,
lastName: lastName,
dateOfBirth: dateOfBirth,
gender: gender,
weight: weight,
height: height,
);
// Логируем запрос перед отправкой
log(
'AuthRepository: Sending OAuth verification confirm request: provider=$provider, verificationType=$verificationType, code=$code, email=$email, phone=$phone',
);
return await authService.confirmOAuthVerification(body: request);
} catch (e, s) {
log('AuthRepository: Error in confirmOAuthVerification: $e');
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
}

View File

@@ -1,4 +1,5 @@
import 'package:eatvio_app/features/auth/services/reset_password_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class ResetPasswordRepository {
final ResetPasswordService resetPasswordService;
@@ -20,7 +21,8 @@ class ResetPasswordRepository {
} else {
return false;
}
} catch (e) {
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
}
@@ -46,7 +48,8 @@ class ResetPasswordRepository {
} else {
return false;
}
} catch (e) {
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
}

View File

@@ -8,8 +8,10 @@ import 'package:eatvio_app/features/auth/models/yandex_profile_model.dart';
import 'package:flutter/material.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/env_config.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:http/http.dart' as http;
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:vk_id/vk_id.dart';
import 'package:webview_flutter/webview_flutter.dart';
@@ -40,7 +42,8 @@ class SocialAuthFacade {
return null;
}
return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier, clientId: clientIdStr);
} catch (e) {
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
return null;
}
}
@@ -156,8 +159,9 @@ class SocialAuthFacade {
birthDate: yandexProfileData.birthday,
clientId: clientId,
);
} catch (e) {
} catch (e, s) {
log(e.toString());
await Sentry.captureException(e, stackTrace: s);
}
return null;
}
@@ -204,7 +208,8 @@ class SocialAuthFacade {
if (code == null || deviceId == null) return null;
return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier, clientId: clientIdStr);
} catch (e) {
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
return null;
}
}
@@ -255,8 +260,9 @@ class SocialAuthFacade {
if (!context.mounted) return null;
return showDialog<Map<String, String>>(
context: context,
barrierDismissible: false,
builder: (_) {
barrierDismissible: true,
builder: (dialogContext) {
final s = S.of(dialogContext);
const borderRadius = BorderRadius.all(Radius.circular(16));
return Dialog(
insetPadding: const EdgeInsets.all(16),
@@ -264,7 +270,21 @@ class SocialAuthFacade {
elevation: 0,
child: ClipRRect(
borderRadius: borderRadius,
child: SizedBox(height: 560, child: WebViewWidget(controller: controller)),
child: SizedBox(
height: MediaQuery.of(context).size.height * 0.7,
child: Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Navigator.of(dialogContext).pop();
},
),
title: Text(s.authorization),
),
body: WebViewWidget(controller: controller),
),
),
),
);
},

View File

@@ -2,6 +2,9 @@ import 'package:dio/dio.dart';
import 'package:eatvio_app/features/auth/models/credentials_model.dart';
import 'package:eatvio_app/features/auth/models/oauth_model.dart';
import 'package:eatvio_app/features/auth/models/oauth_login_request.dart';
import 'package:eatvio_app/features/auth/models/oauth_verification_confirm_request.dart';
import 'package:eatvio_app/features/auth/models/oauth_verification_confirm_response.dart';
import 'package:eatvio_app/features/auth/models/oauth_verification_request_response.dart';
import 'package:eatvio_app/features/auth/models/token_model.dart';
import 'package:retrofit/retrofit.dart';
@@ -19,4 +22,10 @@ abstract class AuthService {
@POST('/api/oauth/login')
Future<TokenModel> signInViaSocialMedia({@Body() required OAuthLoginRequest body});
@POST('/api/oauth/verification/request')
Future<OAuthVerificationRequestResponse> requestOAuthVerification({@Body() required Map<String, dynamic> body});
@POST('/api/oauth/verification/confirm')
Future<OAuthVerificationConfirmResponse> confirmOAuthVerification({@Body() required OAuthVerificationConfirmRequest body});
}

View File

@@ -0,0 +1,29 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'barcode_product_model.freezed.dart';
part 'barcode_product_model.g.dart';
@freezed
abstract class BarcodeProductModel with _$BarcodeProductModel {
const factory BarcodeProductModel({
// ignore: non_constant_identifier_names
required String GTIN,
@JsonKey(name: 'Товарный знак (ru)') String? brandName,
@JsonKey(name: 'Описание (ru)') String? description,
@JsonKey(name: 'Количество/вес') String? quantity,
@JsonKey(name: 'Рынок сбыта') String? market,
@JsonKey(name: 'Калорийность') double? calories,
@JsonKey(name: 'Белки') double? proteins,
@JsonKey(name: 'Жиры') double? fats,
@JsonKey(name: 'Углеводы') double? carbohydrates,
}) = _BarcodeProductModel;
factory BarcodeProductModel.fromJson(Map<String, Object?> json) => _$BarcodeProductModelFromJson(json);
}
@freezed
abstract class BarcodeTaskStatusModel with _$BarcodeTaskStatusModel {
const factory BarcodeTaskStatusModel({String? status, @JsonKey(name: 'task_id') String? taskId, BarcodeProductModel? data}) = _BarcodeTaskStatusModel;
factory BarcodeTaskStatusModel.fromJson(Map<String, Object?> json) => _$BarcodeTaskStatusModelFromJson(json);
}

View File

@@ -0,0 +1,112 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/barcode/models/barcode_product_model.dart';
import 'package:eatvio_app/features/barcode/services/barcode_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class BarcodeRepository {
final BarcodeService barcodeService;
final Dio dio;
BarcodeRepository({
required this.barcodeService,
required this.dio,
});
/// Получить информацию о продукте по штрихкоду
/// Возвращает либо данные продукта, либо статус задачи обработки
Future<BarcodeResult> getProductByBarcode(String barcode) async {
try {
final response = await dio.get('/api/gsone/barcode/$barcode');
if (response.statusCode == 200) {
// Данные найдены в БД
final product = BarcodeProductModel.fromJson(response.data as Map<String, dynamic>);
return BarcodeResult.success(product);
} else if (response.statusCode == 202) {
// Запущена задача обработки
final taskStatus = BarcodeTaskStatusModel.fromJson(response.data as Map<String, dynamic>);
return BarcodeResult.processing(taskStatus.taskId ?? '');
}
throw Exception('Unexpected status code: ${response.statusCode}');
} on DioException catch (e, s) {
if (e.response?.statusCode == 202) {
// Если получен статус 202, значит запущена задача обработки
final taskStatus = BarcodeTaskStatusModel.fromJson(e.response!.data as Map<String, dynamic>);
return BarcodeResult.processing(taskStatus.taskId ?? '');
}
await Sentry.captureException(e, stackTrace: s);
rethrow;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
}
/// Получить статус задачи обработки
Future<BarcodeTaskStatus> getTaskStatus(String taskId) async {
try {
final statusModel = await barcodeService.getTaskStatus(taskId: taskId);
if (statusModel.status == 'completed' && statusModel.data != null) {
return BarcodeTaskStatus.completed(statusModel.data!);
} else if (statusModel.status == 'failed') {
return BarcodeTaskStatus.failed();
} else {
return BarcodeTaskStatus.processing();
}
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
}
}
/// Результат запроса информации о продукте по штрихкоду
class BarcodeResult {
final BarcodeResultType type;
final BarcodeProductModel? product;
final String? taskId;
BarcodeResult._({required this.type, this.product, this.taskId});
factory BarcodeResult.success(BarcodeProductModel product) {
return BarcodeResult._(type: BarcodeResultType.success, product: product);
}
factory BarcodeResult.processing(String taskId) {
return BarcodeResult._(type: BarcodeResultType.processing, taskId: taskId);
}
}
enum BarcodeResultType {
success,
processing,
}
/// Статус задачи обработки
class BarcodeTaskStatus {
final BarcodeTaskStatusType type;
final BarcodeProductModel? product;
BarcodeTaskStatus._({required this.type, this.product});
factory BarcodeTaskStatus.completed(BarcodeProductModel product) {
return BarcodeTaskStatus._(type: BarcodeTaskStatusType.completed, product: product);
}
factory BarcodeTaskStatus.processing() {
return BarcodeTaskStatus._(type: BarcodeTaskStatusType.processing);
}
factory BarcodeTaskStatus.failed() {
return BarcodeTaskStatus._(type: BarcodeTaskStatusType.failed);
}
}
enum BarcodeTaskStatusType {
completed,
processing,
failed,
}

View File

@@ -0,0 +1,24 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/barcode/models/barcode_product_model.dart';
import 'package:retrofit/retrofit.dart';
part 'barcode_service.g.dart';
@RestApi()
abstract class BarcodeService {
factory BarcodeService(Dio dio, {String baseUrl}) = _BarcodeService;
@GET('/api/gsone/barcode/{barcode}')
Future<BarcodeProductModel> getProductByBarcode({@Path('barcode') required String barcode});
@GET('/api/gsone/barcode/status/{task_id}')
Future<BarcodeTaskStatusModel> getTaskStatus({@Path('task_id') required String taskId});
}

View File

@@ -1,10 +1,10 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/enums/complained_content.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/core/widgets/custom_popup.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/core/widgets/markdown_html_widget.dart';
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_cubit.dart';
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_state.dart';
import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart';
@@ -13,12 +13,11 @@ import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit
import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.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:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
import 'package:markdown/markdown.dart' as md;
class BlogPostScreen extends StatefulWidget {
final int id;
@@ -78,96 +77,100 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
return Scaffold(
resizeToAvoidBottomInset: true,
bottomSheet: Container(
padding: const EdgeInsets.symmetric(vertical: 12) + (Platform.isIOS ? const EdgeInsets.only(bottom: 16) : EdgeInsets.zero),
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom + 8,
),
decoration: BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (replyId != null)
GestureDetector(
onTap: () {
context.read<CommentsCubit>().setReplyId(null);
setState(() {
replyId = null;
textEditingController.clear();
});
FocusManager.instance.primaryFocus?.unfocus();
},
child: Container(
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
height: 32,
width: 32,
child: const Icon(Icons.close_rounded, size: 18, color: Colors.white),
),
),
if (replyId != null) const SizedBox(width: 8),
SizedBox(
width: MediaQuery.of(context).size.width - (replyId == null ? 64 : 104),
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 52),
child: TextFormField(
textCapitalization: TextCapitalization.sentences,
onChanged: (_) {
setState(() {});
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (replyId != null)
GestureDetector(
onTap: () {
context.read<CommentsCubit>().setReplyId(null);
setState(() {
replyId = null;
textEditingController.clear();
});
FocusManager.instance.primaryFocus?.unfocus();
},
controller: textEditingController,
maxLines: 5,
minLines: 1,
keyboardType: TextInputType.multiline,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1.2),
decoration: InputDecoration(
isDense: false,
// убираем true, чтобы padding учитывался корректно
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
// вертикальный паддинг увеличен
hintText: 'Комментарий',
hintStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, height: 1.2, color: Color(0xFFAEAEB2)),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
child: Container(
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
height: 32,
width: 32,
child: const Icon(Icons.close_rounded, size: 18, color: Colors.white),
),
),
if (replyId != null) const SizedBox(width: 8),
Expanded(
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 52),
child: TextFormField(
textCapitalization: TextCapitalization.sentences,
onChanged: (_) {
setState(() {});
},
controller: textEditingController,
maxLines: 5,
minLines: 1,
keyboardType: TextInputType.multiline,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1.2),
decoration: InputDecoration(
isDense: false,
// убираем true, чтобы padding учитывался корректно
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
// вертикальный паддинг увеличен
hintText: S.of(context).comment_hint,
hintStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, height: 1.2, color: Color(0xFFAEAEB2)),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
),
),
),
),
),
const SizedBox(width: 8),
GestureDetector(
onTap: () {
if (textEditingController.text.length > 2) {
commentsCubit.sendComment(
id: widget.id.toString(),
commentText: textEditingController.text,
parentCommentId: replyId,
commentSourceType: CommentSourceType.blogPost,
);
context.read<CommentsCubit>().setReplyId(null);
setState(() {
textEditingController.clear();
replyId = null;
});
FocusManager.instance.primaryFocus?.unfocus();
}
},
child: Container(
decoration: BoxDecoration(color: textEditingController.text.length > 2 ? appTheme.blueA400 : Colors.grey, shape: BoxShape.circle),
height: 32,
width: 32,
child: const Icon(Icons.send_rounded, size: 18, color: Colors.white),
const SizedBox(width: 8),
GestureDetector(
onTap: () {
if (textEditingController.text.length > 2) {
commentsCubit.sendComment(
id: widget.id.toString(),
commentText: textEditingController.text,
parentCommentId: replyId,
commentSourceType: CommentSourceType.blogPost,
);
context.read<CommentsCubit>().setReplyId(null);
setState(() {
textEditingController.clear();
replyId = null;
});
FocusManager.instance.primaryFocus?.unfocus();
}
},
child: Container(
decoration: BoxDecoration(color: textEditingController.text.length > 2 ? appTheme.blueA400 : Colors.grey, shape: BoxShape.circle),
height: 32,
width: 32,
child: const Icon(Icons.send_rounded, size: 18, color: Colors.white),
),
),
),
],
],
),
),
),
appBar: AppBar(
@@ -194,7 +197,7 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 80),
padding: EdgeInsets.only(bottom: 160 + MediaQuery.of(context).padding.bottom), // поле комментария (52-200) + padding (12+12+56) + safe area
physics: const BouncingScrollPhysics(),
child: Column(
children: [
@@ -233,34 +236,8 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
if (blogPostState.blogPostModel?.description.isNotEmpty ?? false)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Html(
data: md.markdownToHtml(blogPostState.blogPostModel!.description.replaceAll('\n', '<br>')),
style: {
"body": Style(margin: Margins.zero, padding: HtmlPaddings.zero, fontSize: FontSize(16), lineHeight: const LineHeight(1.5)),
"p": Style(margin: Margins.only(bottom: 12), color: Colors.black87),
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: Colors.black, fontWeight: FontWeight.bold),
"h1": Style(fontSize: FontSize(24)),
"h2": Style(fontSize: FontSize(20)),
"h3": Style(fontSize: FontSize(18)),
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
"li": Style(margin: Margins.only(bottom: 4), color: Colors.black87),
"strong, b": Style(fontWeight: FontWeight.bold, color: Colors.black),
"em, i": Style(fontStyle: FontStyle.italic),
"code": Style(
backgroundColor: Colors.grey.shade100,
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
fontFamily: 'monospace',
),
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
"blockquote": Style(
padding: HtmlPaddings.only(left: 16),
margin: Margins.only(left: 8, bottom: 12),
fontStyle: FontStyle.italic,
color: Colors.grey.shade700,
),
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
},
child: MarkdownHtmlWidget(
markdownText: blogPostState.blogPostModel!.description,
),
),
const SizedBox(height: 8),

View File

@@ -5,6 +5,7 @@ import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/permission_handler/permission_handler.dart';
import 'package:eatvio_app/core/theme/app_decoration.dart';
import 'package:eatvio_app/core/widgets/loader.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/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_state.dart';
@@ -567,6 +568,7 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
CustomTextFormField(key: AppKeys.blogPostCreateScreenVideoLinkTextField, controller: videoUrlController),
const SizedBox(height: 80),
_createBlogPostButton(),
const SizedBox(height: 80),
],
),
),
@@ -598,7 +600,9 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
showErrorDialog(context, title: S.current.could_not_update_blog_post);
} else {
if (!mounted) return;
context.pop();
if (context.canPop()) {
context.pop();
}
}
} else {
final result = await blogPostCubit.createBlogPost(
@@ -615,21 +619,23 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
showErrorDialog(context, title: S.current.could_not_publish_blog_post);
} else {
if (!mounted) return;
context.pop();
if (context.canPop()) {
context.pop();
}
}
}
},
borderRadius: BorderRadius.circular(16),
child: Ink(
width: MediaQuery.of(context).size.width - 32,
height: 52,
decoration: BoxDecoration(borderRadius: BorderRadius.circular(16), color: appTheme.blueA400),
child: isLoading
? const Center(child: CircularProgressIndicator())
: const Center(
child: isLoading
? const Center(child: Loader())
: Ink(
width: MediaQuery.of(context).size.width - 32,
height: 52,
decoration: BoxDecoration(borderRadius: BorderRadius.circular(16), color: appTheme.blueA400),
child: const Center(
child: Text('Создать', style: TextStyle(color: Colors.white)),
),
),
),
);
}

View File

@@ -8,6 +8,7 @@ import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
import 'package:eatvio_app/features/blog_post_create/services/blog_post_service.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class BlogPostRepository {
final BlogPostService blogPostService;
@@ -29,9 +30,14 @@ class BlogPostRepository {
if (image != null) 'image': await MultipartFile.fromFile((await ImageCompressUtility.compressFile(File(image.path))).path, filename: image.name),
});
return await blogPostService.createBlogPost(
formData: formData,
);
try {
return await blogPostService.createBlogPost(
formData: formData,
);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
}
Future<XFile?> downloadImage(String imageUrl) async {
@@ -54,8 +60,9 @@ class BlogPostRepository {
log('Ошибка загрузки изображения: ${response.statusCode}');
return null;
}
} catch (e) {
} catch (e, s) {
log('Произошла ошибка: $e');
await Sentry.captureException(e, stackTrace: s);
return null;
}
}
@@ -79,11 +86,23 @@ class BlogPostRepository {
: null,
});
return await blogPostService.updateBlogPost(
id: id,
formData: formData,
);
try {
return await blogPostService.updateBlogPost(
id: id,
formData: formData,
);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
}
Future<void> deleteBlogPost(int id) async => await blogPostService.deleteBlogPost(id: id);
Future<void> deleteBlogPost(int id) async {
try {
await blogPostService.deleteBlogPost(id: id);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
}
}

View File

@@ -36,20 +36,24 @@ class ProfileAvatarCubit extends Cubit<ProfileAvatarState> {
Future<void> getMyProfile() async {
emit(const ProfileAvatarState.loading());
final profile = await profileRepository.getMyProfile();
final profileDetails = await profileRepository.getMyProfileDetails();
try {
final profile = await profileRepository.getMyProfile();
final profileDetails = await profileRepository.getMyProfileDetails();
emit(ProfileAvatarState.loaded(profile: profile));
final notificationStatus = await Permission.notification.status;
final isNotificationEnabled = notificationStatus.isGranted;
AppMetricaUserProfile userProfile = AppMetricaUserProfile([
AppMetricaNameAttribute.withValue(profile.name),
AppMetricaGenderAttribute.withValue(profileDetails.gender == 'male' ? AppMetricaGender.male : AppMetricaGender.female),
AppMetricaBirthDateAttribute.withAge(_calculateAge(profileDetails.birthdate)),
AppMetricaNotificationEnabledAttribute.withValue(isNotificationEnabled),
]);
AppMetrica.setUserProfileID(profile.slug);
AppMetrica.reportUserProfile(userProfile);
try {
final notificationStatus = await Permission.notification.status;
final isNotificationEnabled = notificationStatus.isGranted;
AppMetricaUserProfile userProfile = AppMetricaUserProfile([
AppMetricaNameAttribute.withValue(profile.name),
AppMetricaGenderAttribute.withValue(profileDetails.gender == 'male' ? AppMetricaGender.male : AppMetricaGender.female),
AppMetricaBirthDateAttribute.withAge(_calculateAge(profileDetails.birthdate)),
AppMetricaNotificationEnabledAttribute.withValue(isNotificationEnabled),
]);
AppMetrica.setUserProfileID(profile.slug);
AppMetrica.reportUserProfile(userProfile);
} catch (e) {
// Игнорируем ошибки плагинов, профиль уже загружен
}
} catch (e) {
AppRouter.navigatorKey.currentContext?.go('/auth-screen');
emit(ProfileAvatarState.error(errorMessage: e.toString()));

View File

@@ -1,11 +1,13 @@
import 'package:badges/badges.dart' as badges;
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/env_config.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/utils/image_constant.dart';
import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.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';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_state.dart';
import 'package:eatvio_app/generated/l10n.dart';
@@ -16,14 +18,9 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
class BottomNavigationBarWidget extends StatefulWidget {
final ProfileModel profileModel;
final Widget child;
const BottomNavigationBarWidget({
super.key,
required this.child,
required this.profileModel,
});
const BottomNavigationBarWidget({super.key, required this.child});
@override
State<BottomNavigationBarWidget> createState() => _BottomNavigationBarWidgetState();
@@ -31,37 +28,20 @@ class BottomNavigationBarWidget extends StatefulWidget {
class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
int currentBottomNavBarIndex = 1;
late final List<String> pages;
List<String>? pages;
int tapsCount = 0;
@override
void initState() {
super.initState();
if (widget.profileModel.roles.contains('consultant')) {
pages = [
'/my-profile-screen',
'/feed-screen',
'/diary-screen',
'/consultations-screen',
'/chat-select-screen',
'/notifications-screen',
'/menu-screen',
];
List<String> _getPages(bool isConsultant) {
if (isConsultant) {
return ['/my-profile-screen', '/feed-screen', '/diary-screen', '/consultations-screen', '/chat-select-screen', '/notifications-screen', '/menu-screen'];
} else {
pages = [
'/my-profile-screen',
'/feed-screen',
'/diary-screen',
'/chat-select-screen',
'/notifications-screen',
'/menu-screen',
];
return ['/my-profile-screen', '/feed-screen', '/diary-screen', '/chat-select-screen', '/notifications-screen', '/menu-screen'];
}
}
void _onItemTapped(int index) async {
if (pages.length == 7) {
if (pages == null) return;
if (pages!.length == 7) {
if (index == 6) {
tapsCount++;
if (tapsCount == 7) {
@@ -82,184 +62,189 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
setState(() {
currentBottomNavBarIndex = index;
});
GoRouter.of(context).replace(pages[currentBottomNavBarIndex]);
GoRouter.of(context).replace(pages![currentBottomNavBarIndex]);
}
}
static const Color _devAccentColor = Color(0xFFF59E0B); // amber-500
Widget _buildLogoIcon() {
final isProduction = sl<AppConfig>().baseUrl == EnvConfig.baseUrl;
final logo = SvgPicture.asset(
ImageConstant.imgLogo,
height: 32,
colorFilter: isProduction ? null : const ColorFilter.mode(_devAccentColor, BlendMode.srcIn),
);
if (isProduction) return logo;
return Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: _devAccentColor.withValues(alpha: 0.6), width: 1.5),
color: _devAccentColor.withValues(alpha: 0.08),
),
child: logo,
),
Positioned(
top: -6,
right: -4,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
decoration: BoxDecoration(
color: _devAccentColor,
borderRadius: BorderRadius.circular(4),
boxShadow: [BoxShadow(color: _devAccentColor.withValues(alpha: 0.4), blurRadius: 4, offset: const Offset(0, 1))],
),
child: const Text(
'DEV',
style: TextStyle(fontSize: 8, fontWeight: FontWeight.w700, color: Colors.white, letterSpacing: 0.3, height: 1.0),
),
),
),
],
);
}
@override
Widget build(BuildContext context) {
final s = S.of(context);
return Scaffold(
body: widget.child,
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
onTap: _onItemTapped,
selectedFontSize: 10,
unselectedFontSize: 10,
selectedItemColor: appTheme.blueA400,
currentIndex: currentBottomNavBarIndex,
items: [
BottomNavigationBarItem(
label: '',
icon: CircleAvatar(
foregroundImage: NetworkImage(
widget.profileModel.profilePhotoUrl.contains('https') ? widget.profileModel.profilePhotoUrl : sl<AppConfig>().baseUrl + widget.profileModel.profilePhotoUrl,
),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
radius: 16,
),
),
BottomNavigationBarItem(
label: 'Лента',
icon: Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(
colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
width: 20,
height: 20,
ImageConstant.icnFeed,
),
),
activeIcon: Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(
ImageConstant.icnFeed,
width: 20,
height: 20,
colorFilter: ColorFilter.mode(
appTheme.blueA400,
BlendMode.srcIn,
),
),
),
),
BottomNavigationBarItem(
label: s.diary,
icon: Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(
colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
ImageConstant.icnDiary,
width: 20,
height: 20,
),
),
activeIcon: Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(
ImageConstant.icnDiary,
width: 20,
height: 20,
colorFilter: ColorFilter.mode(
appTheme.blueA400,
BlendMode.srcIn,
),
),
),
),
if (widget.profileModel.roles.contains('consultant'))
BottomNavigationBarItem(
label: 'Консультации',
icon: Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(
colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
'assets/icons/consultations.svg',
width: 20,
height: 20,
),
),
activeIcon: Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(
'assets/icons/consultations.svg',
width: 20,
height: 20,
colorFilter: ColorFilter.mode(
appTheme.blueA400,
BlendMode.srcIn,
),
),
),
),
BottomNavigationBarItem(
label: s.chat,
icon: BlocBuilder<ChatsCubit, ChatsState>(
builder: (BuildContext context, ChatsState state) {
final hasUnreadMessages = state.chatsWithUnreadMessagesCount > 0;
return badges.Badge(
position: badges.BadgePosition.topEnd(),
badgeAnimation: const badges.BadgeAnimation.fade(),
badgeStyle: badges.BadgeStyle(
badgeColor: hasUnreadMessages ? Colors.red : Colors.transparent,
),
badgeContent: Text(
hasUnreadMessages ? state.chatsWithUnreadMessagesCount.toString() : '',
style: TextStyle(
color: hasUnreadMessages ? Colors.white : Colors.transparent,
fontSize: 8,
return BlocBuilder<ProfileAvatarCubit, ProfileAvatarState>(
builder: (BuildContext context, ProfileAvatarState state) {
if (state is ProfileAvatarStateLoaded) {
final profileModel = state.profile;
final isConsultant = profileModel.roles.contains('consultant');
// Инициализируем pages на основе profileModel
pages ??= _getPages(isConsultant);
return Scaffold(
body: widget.child,
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
onTap: _onItemTapped,
selectedFontSize: 10,
unselectedFontSize: 10,
selectedItemColor: appTheme.blueA400,
currentIndex: currentBottomNavBarIndex,
items: [
BottomNavigationBarItem(
label: '',
icon: CircleAvatar(
foregroundImage: NetworkImage(
profileModel.profilePhotoUrl.contains('https') ? profileModel.profilePhotoUrl : sl<AppConfig>().baseUrl + profileModel.profilePhotoUrl,
),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
radius: 16,
),
child: Padding(
),
BottomNavigationBarItem(
label: 'Лента',
icon: Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(
ImageConstant.icnChat,
width: 20,
height: 20,
colorFilter: pages[currentBottomNavBarIndex] == '/chat-select-screen'
? ColorFilter.mode(
appTheme.blueA400,
BlendMode.srcIn,
)
: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
),
child: SvgPicture.asset(colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn), width: 20, height: 20, ImageConstant.icnFeed),
),
);
},
),
),
BottomNavigationBarItem(
label: 'Уведомления',
icon: BlocBuilder<NotificationsCubit, NotificationsState>(
builder: (BuildContext context, NotificationsState state) {
return badges.Badge(
position: badges.BadgePosition.topEnd(),
badgeAnimation: const badges.BadgeAnimation.fade(),
showBadge: state is NotificationsStateLoaded && state.hasUnreadNotifications,
badgeContent: const Text(
'',
style: TextStyle(
color: Colors.white,
fontSize: 8,
),
),
child: Padding(
activeIcon: Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(
ImageConstant.icnBell,
colorFilter: pages[currentBottomNavBarIndex] == '/notifications-screen'
? ColorFilter.mode(
appTheme.blueA400,
BlendMode.srcIn,
)
: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
width: 20,
height: 20,
child: SvgPicture.asset(ImageConstant.icnFeed, width: 20, height: 20, colorFilter: ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn)),
),
),
BottomNavigationBarItem(
label: s.diary,
icon: Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn), ImageConstant.icnDiary, width: 20, height: 20),
),
activeIcon: Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(ImageConstant.icnDiary, width: 20, height: 20, colorFilter: ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn)),
),
),
if (profileModel.roles.contains('consultant'))
BottomNavigationBarItem(
label: 'Консультации',
icon: Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(
colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
'assets/icons/consultations.svg',
width: 20,
height: 20,
),
),
activeIcon: Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(
'assets/icons/consultations.svg',
width: 20,
height: 20,
colorFilter: ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn),
),
),
),
);
},
BottomNavigationBarItem(
label: s.chat,
icon: BlocBuilder<ChatsCubit, ChatsState>(
builder: (BuildContext context, ChatsState state) {
final hasUnreadMessages = state.chatsWithUnreadMessagesCount > 0;
return badges.Badge(
position: badges.BadgePosition.topEnd(),
badgeAnimation: const badges.BadgeAnimation.fade(),
badgeStyle: badges.BadgeStyle(badgeColor: hasUnreadMessages ? Colors.red : Colors.transparent),
badgeContent: Text(
hasUnreadMessages ? state.chatsWithUnreadMessagesCount.toString() : '',
style: TextStyle(color: hasUnreadMessages ? Colors.white : Colors.transparent, fontSize: 8),
),
child: Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(
ImageConstant.icnChat,
width: 20,
height: 20,
colorFilter: pages![currentBottomNavBarIndex] == '/chat-select-screen'
? ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn)
: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
),
),
);
},
),
),
BottomNavigationBarItem(
label: 'Уведомления',
icon: BlocBuilder<NotificationsCubit, NotificationsState>(
builder: (BuildContext context, NotificationsState state) {
return badges.Badge(
position: badges.BadgePosition.topEnd(),
badgeAnimation: const badges.BadgeAnimation.fade(),
showBadge: state is NotificationsStateLoaded && state.hasUnreadNotifications,
badgeContent: const Text('', style: TextStyle(color: Colors.white, fontSize: 8)),
child: Padding(
padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset(
ImageConstant.icnBell,
colorFilter: pages![currentBottomNavBarIndex] == '/notifications-screen'
? ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn)
: const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
width: 20,
height: 20,
),
),
);
},
),
),
BottomNavigationBarItem(label: '', icon: _buildLogoIcon()),
],
),
),
BottomNavigationBarItem(
label: '',
icon: SvgPicture.asset(
ImageConstant.imgLogo,
height: 32,
),
),
],
),
);
}
// Показываем child даже когда профиль еще загружается, чтобы экраны могли инициализироваться
return Scaffold(body: widget.child);
},
);
}
}

View File

@@ -1,5 +1,6 @@
import 'package:eatvio_app/features/calendar/models/calendar_model.dart';
import 'package:eatvio_app/features/calendar/services/calendar_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class CalendarRepository {
final CalendarService calendarService;
@@ -9,7 +10,8 @@ class CalendarRepository {
Future<List<CalendarModel>> getCalendar() async {
try {
return await calendarService.getCalendar();
} catch (e) {
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
}

View File

@@ -1,5 +1,6 @@
import 'package:eatvio_app/features/cancel_plan_bottom_sheet/models/cancelation_model.dart';
import 'package:eatvio_app/features/cancel_plan_bottom_sheet/service/cancel_plan_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class CancelPlanRepository {
final CancelPlanService cancelService;
@@ -10,7 +11,8 @@ class CancelPlanRepository {
}) async {
try {
await cancelService.cancelChosenPlan(id: id, body: body);
} catch (e) {
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
}

View File

@@ -4,17 +4,36 @@ import 'package:freezed_annotation/freezed_annotation.dart';
part 'chat_message_model.freezed.dart';
part 'chat_message_model.g.dart';
int _parseAuthorId(dynamic json) {
if (json == null) return 0;
if (json is num) return json.toInt();
return 0;
}
DateTime _parseUpdatedAt(dynamic json) {
if (json == null) return DateTime.now();
if (json is String) return parseDate(json);
return DateTime.now();
}
String _parseData(dynamic json) {
if (json == null) return '';
return json.toString();
}
@freezed
abstract class ChatMessageModel with _$ChatMessageModel {
const factory ChatMessageModel({
required int id,
required String data,
@JsonKey(name: 'data', fromJson: _parseData) required String data,
@JsonKey(name: 'media_data') required List<dynamic>? mediaData,
@JsonKey(name: 'author_id') required int authorId,
@JsonKey(name: 'updated_at', fromJson: parseDate) required DateTime updatedAt,
@JsonKey(name: 'urls_media_info') List<Map<String, dynamic>>? urlsMediaInfo,
@JsonKey(name: 'author_id', fromJson: _parseAuthorId) required int authorId,
@JsonKey(name: 'updated_at', fromJson: _parseUpdatedAt) required DateTime updatedAt,
@JsonKey(name: 'read_by_to_user', defaultValue: false) required bool isRead,
@JsonKey(name: 'is_author_message_auth_user') required bool? isCurrentUserAuthor,
@JsonKey(name: 'room_id') required String? roomId,
@JsonKey(name: 'type') String? type,
@Default(MessageState.sent) @JsonKey(includeToJson: false, includeFromJson: false) MessageState messageState,
@Default(null) @JsonKey(includeToJson: false, includeFromJson: false) String? uuid,
}) = _ChatMessageModel;

View File

@@ -31,18 +31,18 @@ abstract class ChatRoom with _$ChatRoom {
}
ChatMessageModel? parseLastMessage(dynamic json) {
if (json is List) {
if (json == null || json is List) {
return null;
}
return ChatMessageModel.fromJson(json);
if (json is Map<String, dynamic>) {
return ChatMessageModel.fromJson(json);
}
return null;
}
@freezed
abstract class Users with _$Users {
const factory Users({
@JsonKey(name: "from_chat_user") required ChatUser fromChatUser,
@JsonKey(name: "to_chat_user") required ChatUser toChatUser,
}) = _Users;
const factory Users({@JsonKey(name: "from_chat_user") required ChatUser fromChatUser, @JsonKey(name: "to_chat_user") required ChatUser toChatUser}) = _Users;
factory Users.fromJson(Map<String, dynamic> json) => _$UsersFromJson(json);
}

View File

@@ -164,9 +164,15 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
messagesSubscription = wsManager.directChatStream?.listen((event) {
AudioPlayer().play((AssetSource('audio/recive_message.wav')));
final message = ChatMessageModel.fromJson(jsonDecode(event.data));
List<ChatMessageModel> messagesList = [message, ...(state as ChatRoomStateLoaded).chatMessages];
final currentState = state as ChatRoomStateLoaded;
// Сообщения от текущего пользователя не приходят через WebSocket,
// они обрабатываются через ответ API в sendMessage
// Здесь обрабатываем только входящие сообщения от других пользователей
List<ChatMessageModel> messagesList = [message, ...currentState.chatMessages];
chatRepository.emitNewMessageToChats(message);
emit((state as ChatRoomStateLoaded).copyWith(chatMessages: messagesList, isLoading: isLoading));
emit(currentState.copyWith(chatMessages: messagesList, isLoading: isLoading));
EasyDebounce.debounce('searchChatRoom', const Duration(milliseconds: 300), () => sl<ChatsCubit>().getChatRooms());
});
@@ -185,6 +191,8 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
final messageUuid = uuid.v4();
final compressedFiles = files != null ? await ImageCompressUtility.compressFilesList(List<File>.from(files)) : null;
AudioPlayer().play((AssetSource('audio/send_message.wav')));
// Создаем временное сообщение с состоянием loading
emit(
(state as ChatRoomStateLoaded).copyWith(
chatMessages: [
@@ -206,12 +214,25 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
);
try {
await chatRepository.sendMessage(data: data, files: compressedFiles, slug: userSlug);
// Отправляем сообщение и получаем ответ от сервера с правильными данными
final serverMessage = await chatRepository.sendMessage(data: data, files: compressedFiles, slug: userSlug);
// Заменяем локальное сообщение данными с сервера (с правильными URL файлов)
emit(
(state as ChatRoomStateLoaded).copyWith(
chatMessages: (state as ChatRoomStateLoaded).chatMessages.map((elem) {
if (elem.uuid == messageUuid) {
return elem.copyWith(messageState: MessageState.sent);
// Сохраняем UUID, состояние и обновляем данные из ответа сервера
// Используем значения из локального сообщения для полей, которые могут отсутствовать в ответе
return serverMessage.copyWith(
uuid: messageUuid,
messageState: MessageState.sent,
isCurrentUserAuthor: true,
isRead: elem.isRead,
roomId: elem.roomId ?? serverMessage.roomId,
authorId: serverMessage.authorId,
updatedAt: serverMessage.updatedAt,
);
}
return elem;
}).toList(),
@@ -320,6 +341,18 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
if (message != null) {
try {
AudioPlayer().play((AssetSource('audio/send_message.wav')));
// Определяем, есть ли файлы для повторной отправки
List<File>? filesToResend;
if (message.mediaData != null && message.mediaData!.isNotEmpty) {
// Если mediaData содержит File объекты, используем их
// Если это строки (URL), то файлы уже на сервере и повторная отправка не нужна
final firstItem = message.mediaData!.first;
if (firstItem is File) {
filesToResend = message.mediaData!.cast<File>();
}
}
emit(
(state as ChatRoomStateLoaded).copyWith(
chatMessages: [
@@ -341,12 +374,27 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
);
try {
await chatRepository.sendMessage(data: message.data, files: message.mediaData as List<File>?, slug: userSlug);
// Отправляем сообщение и получаем ответ от сервера
final serverMessage = await chatRepository.sendMessage(
data: message.data,
files: filesToResend,
slug: userSlug,
);
// Заменяем локальное сообщение данными с сервера
emit(
(state as ChatRoomStateLoaded).copyWith(
chatMessages: (state as ChatRoomStateLoaded).chatMessages.map((elem) {
if (elem.uuid == uuid) {
return elem.copyWith(messageState: MessageState.sent);
return serverMessage.copyWith(
uuid: uuid,
messageState: MessageState.sent,
isCurrentUserAuthor: true,
isRead: elem.isRead,
roomId: elem.roomId ?? serverMessage.roomId,
authorId: serverMessage.authorId,
updatedAt: serverMessage.updatedAt,
);
}
return elem;
}).toList(),

Some files were not shown because too many files have changed in this diff Show More