Compare commits

...

167 Commits

Author SHA1 Message Date
Username1548
ef4d91a5d8 removed public offert from ui on ios 2025-08-11 20:46:09 +07:00
mso7nueh
cfbd7597fa Merge pull request 'fix(archive_of_consultatons): Issue-331' (#631) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#631
2025-08-08 10:43:27 +00:00
mso7nueh
412ac0aeeb fix(archive_of_consultatons): Issue-331 2025-08-08 13:43:04 +03:00
mso7nueh
6211332a07 Merge pull request 'fix(finances): Issue-186' (#630) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#630
2025-08-08 10:30:31 +00:00
mso7nueh
4b1d21b4af fix(finances): Issue-186 2025-08-08 13:29:32 +03:00
mso7nueh
3b89a69d3b Merge pull request 'fix(env): remove default value from config' (#629) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#629
Reviewed-by: Анатолий Полоз <t0ly.od@ya.ru>
2025-08-08 09:24:00 +00:00
mso7nueh
3e8c8c7e9f fix(env): remove default value from config 2025-08-08 12:16:20 +03:00
mso7nueh
e73463dcf7 Merge pull request 'fix(fastlane): update version' (#628) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#628
2025-08-08 08:41:03 +00:00
mso7nueh
60d30163e7 fix(fastlane): update version 2025-08-08 11:40:32 +03:00
mso7nueh
04c1b889a0 Merge pull request 'fix: fastlane' (#627) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#627
2025-08-08 08:35:04 +00:00
mso7nueh
06117ddb6a fix: fastlane 2025-08-08 11:34:41 +03:00
mso7nueh
15f0538f04 Merge pull request 'fix: fastlane' (#626) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#626
2025-08-08 08:30:12 +00:00
mso7nueh
3c56c59c9f fix: fastlane 2025-08-08 11:29:47 +03:00
mso7nueh
038e30b198 Merge pull request 'fix: fastlane' (#625) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#625
2025-08-08 08:24:17 +00:00
mso7nueh
cdb6fcb462 fix: fastlane 2025-08-08 11:23:55 +03:00
mso7nueh
e93a573a72 Merge pull request 'fix: fastlane' (#624) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#624
2025-08-08 08:18:14 +00:00
mso7nueh
d8378e3686 fix: fastlane 2025-08-08 11:17:48 +03:00
mso7nueh
f464da6f9d Merge pull request 'fix(fastlane): testflight' (#623) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#623
2025-08-08 08:15:09 +00:00
mso7nueh
73f27efadd fix(fastlane): testflight 2025-08-08 11:14:41 +03:00
mso7nueh
90cac4940f Merge pull request 'feat(profile): Issue-248' (#621) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#621
2025-08-08 08:11:24 +00:00
mso7nueh
2fac007675 feat(fastlane): Issue-622 2025-08-08 11:11:00 +03:00
mso7nueh
1cafff65dc merge upstream 2025-08-07 19:56:45 +00:00
mso7nueh
fe99890c0e fix: analyzer 2025-08-07 21:29:30 +03:00
mso7nueh
15248a65fe feat(profile): Issue-248 2025-08-07 18:05:11 +03:00
ee0d4d17af Merge pull request 'feat(fastlane): reverted changes' (#620) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#620
2025-08-07 12:27:18 +00:00
Username1548
6fa7beb070 reverted changes 2025-08-07 19:23:12 +07:00
24d0d6eedc Merge pull request 'feat(fastlane): changed fastlane build uploading' (#619) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#619
2025-08-07 12:07:01 +00:00
Username1548
905559b1f0 feat(fastlane): changed fastlane build uploading 2025-08-07 19:06:27 +07:00
6cb330752f Merge pull request 'version update' (#616) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#616
2025-08-07 11:27:02 +00:00
Username1548
69995289c5 Merge remote-tracking branch 'upstream/main' 2025-08-07 18:24:13 +07:00
Username1548
f9b63f3401 version update 2025-08-07 18:20:26 +07:00
3068d82b3a Merge pull request 'feat(env): added env loading to ci/cd' (#615) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#615
2025-08-07 11:12:12 +00:00
Username1548
1305e67817 feat(env): added env loading to ci/cd 2025-08-07 18:10:58 +07:00
mso7nueh
9be8ec7ac3 Merge pull request 'feat(diary): fixed commenting of diary' (#611) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#611
2025-08-06 14:42:15 +00:00
mso7nueh
7ad01d3d4e Merge pull request 'fix: secure storage' (#612) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#612
2025-08-06 14:42:07 +00:00
mso7nueh
832ba421e9 fix: secure storage 2025-08-06 17:41:35 +03:00
Username1548
7e7f8bfb03 fix 2025-08-06 20:36:50 +07:00
Username1548
1f714b3bc2 feat(diary): fixed commenting of diary 2025-08-06 20:29:56 +07:00
mso7nueh
187d60ffb9 Merge pull request 'fix: fastlane' (#608) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#608
2025-08-06 12:53:56 +00:00
mso7nueh
64b158d1ff fix: fastlane 2025-08-06 15:53:32 +03:00
mso7nueh
62f5cab746 Merge pull request 'main' (#606) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#606
2025-08-06 12:43:13 +00:00
mso7nueh
0d27da3ec9 Merge branch 'main' of http://gitea.office.local:3000/mso7nueh/eatvio_app 2025-08-06 15:42:37 +03:00
mso7nueh
1387e20553 fix: fastlane 2025-08-06 15:42:02 +03:00
mso7nueh
13fc59ff62 Merge pull request 'test-flight' (#604) from test-flight into main
Reviewed-on: eatvio/eatvio_app#604
2025-08-06 12:39:08 +00:00
mso7nueh
d3ed027c38 Merge pull request 'fix: fastlane' (#603) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#603
2025-08-06 12:38:45 +00:00
mso7nueh
108e067b11 fix: fastlane 2025-08-06 15:38:21 +03:00
mso7nueh
c58cc54e43 Merge pull request 'fix: fastlane' (#602) from main into test-flight
Reviewed-on: eatvio/eatvio_app#602
2025-08-06 12:29:46 +00:00
mso7nueh
6df116ef74 Merge pull request 'fix: fastlane' (#601) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#601
2025-08-06 12:29:29 +00:00
mso7nueh
b051a588e0 fix: fastlane 2025-08-06 15:29:10 +03:00
mso7nueh
721430301a Merge pull request 'build 66' (#599) from main into test-flight
Reviewed-on: eatvio/eatvio_app#599
2025-08-06 11:50:46 +00:00
mso7nueh
98d7b727a3 Merge pull request 'feat(app): changed name of the app, feat(settings): hid user purcahses and finances' (#597) from misha/eatvio_app:issue-595 into main
Reviewed-on: eatvio/eatvio_app#597
2025-08-06 11:48:39 +00:00
mso7nueh
236abdd1d2 Merge pull request 'feat(version): update' (#598) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#598
2025-08-06 11:46:42 +00:00
mso7nueh
138a81d9d1 feat(version): update 2025-08-06 14:46:06 +03:00
mso7nueh
f43059c820 Merge pull request 'Issue-191, Iussue-588: Notifications, env' (#596) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#596
2025-08-06 10:34:18 +00:00
mso7nueh
c4ff14c7b2 fix(env): repo branch 2025-08-06 13:04:30 +03:00
Username1548
33151b7dae changed lable of the app and hid user purchases and finaces 2025-08-06 16:55:42 +07:00
mso7nueh
b302be66d7 fix(env): add token 2025-08-06 12:55:21 +03:00
mso7nueh
4f0f146fd2 feat(env): from repo 2025-08-06 12:44:19 +03:00
mso7nueh
7986119054 feat(env): add .env from repo 2025-08-06 12:42:37 +03:00
mso7nueh
76582fd487 feat(notifications): Issue-191 2025-08-06 11:28:31 +03:00
mso7nueh
cd9357c050 feat(notifications): Issue-191 2025-08-06 11:28:02 +03:00
mso7nueh
d53fdd1ccb Merge pull request 'env' (#590) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#590
2025-08-04 10:18:00 +00:00
mso7nueh
e75539338b fix: env 2025-08-04 13:09:05 +03:00
mso7nueh
7267fc454b Добавить env/.env 2025-08-04 10:08:55 +00:00
mso7nueh
9e0c89c04e Удалить .env 2025-08-04 10:08:38 +00:00
mso7nueh
a9a479b2d3 fix: print 2025-08-04 12:48:51 +03:00
mso7nueh
12af348000 .env 2025-08-04 09:48:44 +00:00
mso7nueh
039c09962a fix: env 2025-08-04 12:40:21 +03:00
mso7nueh
681c11fed9 feat(env): Issue-588 2025-08-01 16:58:48 +03:00
mso7nueh
5b616a9dc1 feat(env): Issue-588 2025-08-01 16:52:53 +03:00
mso7nueh
3e5cc4f8d7 feat(env): Issue-588 2025-08-01 16:52:03 +03:00
mso7nueh
3cc7809210 fix: hidePlans to env 2025-08-01 16:31:05 +03:00
mso7nueh
be1d8c295a Merge pull request 'Build 1.2.4+54' (#571) from main into test-flight
Reviewed-on: eatvio/eatvio_app#571
2025-08-01 12:00:33 +00:00
mso7nueh
7a177f7517 Merge pull request 'upd ver' (#589) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#589
2025-08-01 11:34:14 +00:00
mso7nueh
c718875b49 upd ver 2025-08-01 14:33:52 +03:00
mso7nueh
c618a6a80d Merge pull request 'fix: reg, verify, loaders, grey screen when like, hide plans' (#583) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#583
2025-08-01 11:28:49 +00:00
mso7nueh
3e7202d65e upd ver 2025-08-01 14:28:04 +03:00
mso7nueh
f78ccd01b6 Обновить .gitea/workflows/test-flight-cd.yaml 2025-08-01 11:18:13 +00:00
mso7nueh
a4d0fd0108 Обновить ios/fastlane/Fastfile 2025-08-01 11:12:46 +00:00
mso7nueh
6f1368f3c6 Обновить .gitea/workflows/test-flight-cd.yaml 2025-08-01 11:11:21 +00:00
mso7nueh
83a39835fa Обновить ios/fastlane/Fastfile 2025-08-01 10:34:39 +00:00
mso7nueh
49b597d5ae fix: update versions 2025-08-01 11:30:58 +03:00
mso7nueh
f5d27ed056 feat(plans): hide for ios 2025-08-01 10:53:28 +03:00
mso7nueh
8a26fa0990 fix: upd ver sdk 2025-07-31 14:55:35 +03:00
mso7nueh
15a9e2b1bd merge upstream 2025-07-31 11:40:19 +00:00
mso7nueh
7192db31e4 fix: upd ver 2025-07-31 14:27:01 +03:00
mso7nueh
190e1781ee fix: recipe create 2025-07-30 15:41:43 +03:00
mso7nueh
a134c2043b upd ver pack 2025-07-30 14:33:28 +03:00
mso7nueh
09a1ec0947 fix: reg, verify, loaders, grey screen when like 2025-07-30 12:34:27 +03:00
mso7nueh
1db9c57140 Merge pull request 'fix: tf' (#579) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#579
2025-07-28 13:18:55 +00:00
mso7nueh
aacbc0ce5a fix: tf 2025-07-28 16:18:34 +03:00
mso7nueh
1bdc1c53f6 Merge pull request 'fix: testflight' (#578) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#578
2025-07-28 13:16:24 +00:00
mso7nueh
20e71a77d8 fix: testflight 2025-07-28 16:14:43 +03:00
mso7nueh
d037b6aaa7 Обновить .gitea/workflows/test-flight-cd.yaml 2025-07-28 13:10:43 +00:00
mso7nueh
43495f407e Обновить .gitea/workflows/test-flight-cd.yaml 2025-07-28 11:02:17 +00:00
mso7nueh
f0c94c4f82 fix: testflight 2025-07-28 13:58:48 +03:00
60b0b20844 Merge pull request 'feat(diary): fixed logic of review viewing, feat(complaints): added validation for complaints' (#577) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#577
2025-07-25 12:41:15 +00:00
Username1548
969f8d37af feat(diary): fixed logic of review viewing, feat(complaints): added validation for complaints 2025-07-25 19:39:02 +07:00
d1c9c1f292 Merge pull request 'feat(testFlight): updated script' (#575) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#575
2025-07-21 13:16:03 +00:00
Username1548
ec05b0c8fb feat(testFlight): updated script 2025-07-21 20:14:02 +07:00
5eaab6b213 Merge pull request 'feat(testFlight): updated script' (#574) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#574
2025-07-21 13:11:02 +00:00
Username1548
5db29ae681 feat(testFlight): updated script 2025-07-21 20:10:23 +07:00
b55a0d0590 Merge pull request 'feat(testFlight): updated script' (#573) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#573
2025-07-21 13:04:23 +00:00
Username1548
44a938ea29 feat(testFlight): updated script 2025-07-21 20:03:22 +07:00
be78fca7d0 Merge pull request 'feat(testFlight): updated uploading script' (#572) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#572
2025-07-21 12:53:44 +00:00
Username1548
955daf4933 feat(testFlight): updated uploading script 2025-07-21 19:52:33 +07:00
8bdbb9059f Merge pull request 'fix: fixed analyzer issue' (#570) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#570
2025-07-16 15:05:23 +00:00
Username1548
27a0bcd6ce fix: fixed analyzer issue 2025-07-16 22:02:53 +07:00
d5e2af2de5 Merge pull request 'Уведомления и дневник (план)' (#564) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#564
2025-07-16 14:48:18 +00:00
Username1548
5a8dcafa8e Merge remote-tracking branch 'upstream/main' 2025-07-16 21:41:14 +07:00
be5796fbfd Merge pull request 'version update' (#567) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#567
2025-07-16 12:57:39 +00:00
Username1548
02b621d85a version update 2025-07-16 19:56:54 +07:00
683a7e1066 Merge pull request 'account deleting' (#565) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#565
2025-07-16 12:37:25 +00:00
Username1548
773359e900 feat(setting): added account deleting 2025-07-16 19:30:30 +07:00
Username1548
74fa65a1ba Merge remote-tracking branch 'upstream/main' 2025-07-16 17:04:06 +07:00
mso7nueh
46f11a8e78 fix(chat): photo view 2025-07-16 12:19:29 +03:00
mso7nueh
3e2aff3ad4 fix(chat): model fields 2025-07-15 13:47:00 +03:00
mso7nueh
09c53a6492 feat(notificaion): add notificaion for last day 2025-07-14 17:08:53 +03:00
mso7nueh
452c603b95 feat: diary comments view 2025-07-14 15:59:43 +03:00
mso7nueh
6bcd540d54 fix: analyzer 2025-07-14 15:16:11 +03:00
mso7nueh
2c38be9fa4 fix(diary): current plan day 2025-07-14 14:49:33 +03:00
mso7nueh
74df7d82db feat(notifications): Issue-562 2025-07-14 14:06:07 +03:00
Username1548
c8d5fc756e Merge remote-tracking branch 'upstream/main' 2025-07-14 12:16:28 +07:00
mso7nueh
1b9534b690 Merge pull request 'build' (#557) from main into test-flight
Reviewed-on: eatvio/eatvio_app#557
2025-07-11 11:49:36 +00:00
mso7nueh
5ac5deec48 Merge branch 'mso7nueh-main'
# Conflicts:
#	ios/Runner.xcodeproj/project.pbxproj
#	pubspec.yaml
2025-07-11 14:47:58 +03:00
mso7nueh
08cf1ad370 Удалены сгенерированные файлы (freezed, g.dart, build) 2025-07-11 14:44:44 +03:00
mso7nueh
2c59fd0923 Удалены сгенерированные файлы (freezed, g.dart, build) 2025-07-11 14:43:56 +03:00
mso7nueh
958d09906b baseurl 2025-07-11 14:34:48 +03:00
mso7nueh
631efe6f9e fix: chats 2025-07-11 14:22:11 +03:00
mso7nueh
0749a53e24 fix: horizontal layout 2025-07-11 14:05:14 +03:00
0982c97d66 Merge pull request 'app version update' (#556) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#556
2025-07-10 15:08:02 +00:00
Username1548
e4168862ad app version update 2025-07-10 22:03:38 +07:00
59f26ee977 Merge pull request 'build 40' (#549) from main into test-flight
Reviewed-on: eatvio/eatvio_app#549
2025-07-10 14:58:33 +00:00
187b056413 Merge pull request 'fix(notifications): fixed wrong notifications rendring and changed requested pagination page' (#551) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#551
2025-07-10 14:39:08 +00:00
Username1548
e546cacd0c Merge remote-tracking branch 'upstream/main' 2025-07-10 21:36:43 +07:00
e1266a18f4 Merge pull request 'feat(comments)' (#550) from misha/eatvio_app:comments into main
Reviewed-on: eatvio/eatvio_app#550
2025-07-10 14:34:10 +00:00
mso7nueh
27f97d0e11 fix: permission 2025-07-09 18:49:48 +03:00
mso7nueh
ea2ec05ed6 fix(chat): websockets 2025-07-09 15:06:51 +03:00
mso7nueh
7b1574392b feat(block): Issue-523 2025-07-09 10:20:21 +03:00
Username1548
4426eef2dc fix(notifications): fixed wrong notifications rendring and changed requested pagination page 2025-07-08 21:19:33 +07:00
mso7nueh
e04c5c97f9 feat(chat): chat without delay 2025-07-08 11:35:41 +03:00
mso7nueh
987d80e4ce Merge pull request 'build 40' (#549) from main into test-flight
Reviewed-on: eatvio/eatvio_app#549
2025-07-07 13:03:13 +00:00
mso7nueh
69412ee7ea merge upstream 2025-07-07 07:54:03 +00:00
mso7nueh
478ac139dd feat(chat): WIP: Issue-1594 2025-07-04 16:27:18 +03:00
mso7nueh
058b0267be Merge pull request 'Тестовый билд для диплинков' (#532) from misha/eatvio_app:deepLinks into test-flight
Reviewed-on: eatvio/eatvio_app#532
2025-07-02 12:23:08 +00:00
mso7nueh
e1ca8c3d77 Merge pull request 'build 38' (#538) from main into test-flight
Reviewed-on: eatvio/eatvio_app#538
2025-06-30 12:35:12 +00:00
mso7nueh
0f9677d6ab Merge pull request 'Build 37' (#536) from main into test-flight
Reviewed-on: eatvio/eatvio_app#536
2025-06-27 13:49:17 +00:00
mso7nueh
7f90430000 Merge pull request 'build 36' (#534) from main into test-flight
Reviewed-on: eatvio/eatvio_app#534
2025-06-27 12:13:53 +00:00
mso7nueh
a3f0fe03af Merge pull request 'build version 34' (#531) from main into test-flight
Reviewed-on: eatvio/eatvio_app#531
2025-06-26 13:13:05 +00:00
mso7nueh
dd40689fd7 Merge pull request 'build 33' (#529) from main into test-flight
Reviewed-on: eatvio/eatvio_app#529
2025-06-25 12:38:36 +00:00
mso7nueh
a30d10c179 Merge pull request 'build 32' (#527) from main into test-flight
Reviewed-on: eatvio/eatvio_app#527
2025-06-25 09:24:58 +00:00
mso7nueh
460045108e Merge pull request 'build 31, исправлена работа чата' (#525) from main into test-flight
Reviewed-on: eatvio/eatvio_app#525
2025-06-22 15:30:48 +00:00
mso7nueh
9e22a4163a Merge pull request 'Build 30' (#522) from main into test-flight
Reviewed-on: eatvio/eatvio_app#522
2025-06-20 12:47:43 +00:00
mso7nueh
f20d645420 Merge pull request 'build 29' (#517) from mso7nueh/eatvio_app:main into test-flight
Reviewed-on: eatvio/eatvio_app#517
2025-06-19 09:21:01 +00:00
mso7nueh
b1111a461d Merge pull request 'build 28' (#511) from main into test-flight
Reviewed-on: eatvio/eatvio_app#511
2025-06-16 11:34:15 +00:00
mso7nueh
f5e2634c07 Merge pull request 'build 27' (#505) from main into test-flight
Reviewed-on: eatvio/eatvio_app#505
2025-06-16 08:23:04 +00:00
mso7nueh
9e49b3dbca Merge pull request 'main' (#501) from main into test-flight
Reviewed-on: eatvio/eatvio_app#501
2025-06-06 12:44:50 +00:00
mso7nueh
e242b27e44 Merge pull request 'build 26 + 1.1.3+33' (#496) from main into test-flight
Reviewed-on: eatvio/eatvio_app#496
2025-06-06 08:33:37 +00:00
mso7nueh
1b7a5219d3 Merge pull request 'buld 25: prod' (#491) from main into test-flight
Reviewed-on: eatvio/eatvio_app#491
2025-06-04 12:27:36 +00:00
mso7nueh
1740823e48 Merge pull request 'build 24: prod' (#489) from main into test-flight
Reviewed-on: eatvio/eatvio_app#489
2025-06-04 09:15:03 +00:00
mso7nueh
265a9b60c3 Merge pull request 'build 23' (#487) from main into test-flight
Reviewed-on: eatvio/eatvio_app#487
2025-06-04 08:29:30 +00:00
mso7nueh
f145ccb8e0 Merge pull request 'build 22' (#480) from main into test-flight
Reviewed-on: eatvio/eatvio_app#480
2025-05-30 13:10:02 +00:00
mso7nueh
44e3b89875 Merge pull request 'build 21' (#476) from main into test-flight
Reviewed-on: eatvio/eatvio_app#476
2025-05-30 09:59:40 +00:00
mso7nueh
c02585b149 Merge pull request 'testflight build ver 20' (#470) from main into test-flight
Reviewed-on: eatvio/eatvio_app#470
2025-05-28 13:57:14 +00:00
mso7nueh
69c53321d2 Merge pull request 'testflight build 18' (#464) from mso7nueh/eatvio_app:main into test-flight
Reviewed-on: eatvio/eatvio_app#464
2025-05-26 10:50:34 +00:00
mso7nueh
87393726fd Merge pull request 'main' (#461) from main into test-flight
Reviewed-on: eatvio/eatvio_app#461
2025-05-23 09:29:02 +00:00
mso7nueh
a179e5fa83 Merge pull request 'testflight' (#455) from main into test-flight
Reviewed-on: eatvio/eatvio_app#455
2025-05-19 12:48:35 +00:00
96 changed files with 2646 additions and 3527 deletions

View File

@@ -11,6 +11,30 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
- name: Checkout env repository
uses: actions/checkout@v2
with:
repository: mso7nueh/eatvio_app_env
path: env_repo
token: ${{ vars.MSO7NUEH_TOKEN }}
ref: main
- name: Copy .env file
run: |
mkdir -p env
cp env_repo/.env env/.env
- name: Load environment variables from .env
run: |
set -a
source env/.env
set +a
# Export all variables to GITHUB_ENV
while IFS= read -r line; do
if [[ $line =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then
echo "$line" >> $GITHUB_ENV
fi
done < env/.env
- name: Unlock login keychain
run: |
@@ -18,6 +42,17 @@ jobs:
env:
KEYCHAIN_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }}
- name: Setup API key
run: |
echo "${API_APPSTORE_KEY}" | tr -d '\n' | base64 --decode > ios/AuthKey.p8
chmod 600 ios/AuthKey.p8
ls -la ios/AuthKey.p8
echo "File size: $(wc -c < ios/AuthKey.p8) bytes"
echo "Contents of ios directory:"
ls -la ios/
env:
API_APPSTORE_KEY: ${{ vars.API_APPSTORE_KEY }}
- name: Flutter build
run: |
flutter clean
@@ -31,6 +66,8 @@ jobs:
LANG: en_US.UTF-8
LC_ALL: en_US.UTF-8
MATCH_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }}
FASTLANE_PASSWORD: ${{ vars.FASTLANE_PASSWORD }}
MATCH_GIT_URL: http://gitea.office.local:3000/mso7nueh/fastlane
FASTLANE_USER: eatvio2023@icloud.com
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ vars.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }}
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ vars.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }}
API_APPSTORE_KEY: ${{ vars.API_APPSTORE_KEY }}

View File

@@ -3,7 +3,7 @@ name: Eatvio App Android Test
on: [ pull_request ]
env:
flutter_version: "3.32.5"
flutter_version: "3.32.8"
java_version: "21.x"
flutter_channel: "stable"
my_local_cache_dir: "/opt/hostedtoolcache/"
@@ -16,6 +16,31 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Checkout env repository
uses: actions/checkout@v3
with:
repository: mso7nueh/eatvio_app_env
path: env_repo
token: ${{ vars.MSO7NUEH_TOKEN }}
ref: main
- name: Copy .env file
run: |
mkdir -p env
cp env_repo/.env env/.env
- name: Load environment variables from .env
run: |
set -a
source env/.env
set +a
# Export all variables to GITHUB_ENV
while IFS= read -r line; do
if [[ $line =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then
echo "$line" >> $GITHUB_ENV
fi
done < env/.env
- name: Create key.properties file
run: |
cat > android/key.properties <<EOF
@@ -119,7 +144,7 @@ jobs:
run: flutter test
- name: Build apk
run: flutter build apk --release
run: flutter build apk --release --dart-define=HIDE_PLANS=false
- name: Rename and move apk
run: |

5
.gitignore vendored
View File

@@ -12,5 +12,10 @@
/android/app/.cxx
*.g.dart
*.freezed.dart
*.gr.dart
*.mocks.dart
android/build/
ios/XCBuildData/
.env
env.example
env/.env

84
ENV_SETUP.md Normal file
View File

@@ -0,0 +1,84 @@
# Настройка переменных окружения
Этот проект использует переменные окружения для хранения секретных данных и конфигурации.
## Установка
1. Скопируйте файл `env.example` в `.env`:
```bash
cp env.example .env
```
2. Отредактируйте файл `.env` и замените значения на ваши собственные секреты.
## Переменные окружения
### API URLs
- `BASE_URL` - основной URL API (по умолчанию: https://eatvio.ru)
- `DEV_BASE_URL` - URL API для разработки (по умолчанию: https://dev2.eatvio.com)
### Sentry Configuration
- `SENTRY_DSN` - DSN для Sentry мониторинга
### Firebase Configuration
- `FIREBASE_PROJECT_ID` - ID проекта Firebase
- `FIREBASE_DATABASE_URL` - URL базы данных Firebase
- `FIREBASE_STORAGE_BUCKET` - bucket для хранения файлов
- `FIREBASE_MESSAGING_SENDER_ID` - ID отправителя сообщений
### Firebase Android Configuration
- `FIREBASE_ANDROID_API_KEY` - API ключ для Android
- `FIREBASE_ANDROID_APP_ID` - ID приложения для Android
### Firebase iOS Configuration
- `FIREBASE_IOS_API_KEY` - API ключ для iOS
- `FIREBASE_IOS_APP_ID` - ID приложения для iOS
- `FIREBASE_IOS_BUNDLE_ID` - Bundle ID для iOS
### App Store URLs
- `APP_STORE_URL` - URL App Store
- `RUSTORE_URL` - URL RuStore
### Support URLs
- `TELEGRAM_SUPPORT_URL` - URL поддержки в Telegram
- `SUPPORT_EMAIL` - email для поддержки
### Test Credentials (только для разработки)
- `TEST_EMAIL` - тестовый email
- `TEST_PASSWORD` - тестовый пароль
### Email Validation API
- `EMAIL_VALIDATION_API_URL` - URL API для валидации email
### Connection Check URLs
- `GOOGLE_CHECK_URL` - URL для проверки соединения (Google)
- `YANDEX_CHECK_URL` - URL для проверки соединения (Yandex)
## Безопасность
⚠️ **ВАЖНО**: Файл `.env` добавлен в `.gitignore` и не должен попадать в репозиторий. Все секретные данные должны храниться только в этом файле.
## Использование в коде
Для доступа к переменным окружения используйте класс `EnvConfig`:
```dart
import 'package:eatvio_app/core/env_config.dart';
String baseUrl = EnvConfig.baseUrl;
String sentryDsn = EnvConfig.sentryDsn;
```
## Миграция
Все секреты были перенесены из кода в переменные окружения:
1. **Sentry DSN** - из `main.dart`
2. **Firebase конфигурация** - из `firebase_options.dart`
3. **API URLs** - из `const.dart`
4. **URL для проверки соединения** - из `internet_connection_checker_screen.dart`
5. **URL для обновления приложения** - из `update_screen.dart`
6. **URL поддержки Telegram** - из `support_screen.dart`
7. **API для валидации email** - из `registration_repository.dart`
8. **Тестовые учетные данные** - из `test_cases.dart`
9. **Email поддержки** - из `support_screen.dart`

View File

@@ -19,17 +19,17 @@ android {
ndkVersion = "27.0.12077973"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = 21
jvmTarget = JavaVersion.VERSION_11.toString()
}
defaultConfig {
applicationId = "com.eatvioapp.app"
minSdk = flutter.minSdkVersion
minSdk = 23
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName

View File

@@ -7,7 +7,7 @@
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
<application
android:label="Eatvio"
android:label="Итвио"
android:usesCleartextTraffic="true"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +1,3 @@
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip

View File

@@ -22,7 +22,7 @@ plugins {
// START: FlutterFire Configuration
id "com.google.gms.google-services" version "4.4.2" apply false
// END: FlutterFire Configuration
id "org.jetbrains.kotlin.android" version "2.0.21" apply false
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
}
include ":app"

0
env/.env vendored Normal file
View File

View File

@@ -1,3 +1,3 @@
source "https://rubygems.org"
gem "fastlane"
gem "fastlane", "~> 2.217"

View File

@@ -213,10 +213,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
@@ -283,10 +287,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
@@ -388,7 +396,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 39;
CURRENT_PROJECT_VERSION = 67;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -406,7 +414,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
MARKETING_VERSION = 1.0.50;
MARKETING_VERSION = 1.2.3;
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
@@ -538,7 +546,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 39;
CURRENT_PROJECT_VERSION = 67;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -556,7 +564,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
MARKETING_VERSION = 1.0.50;
MARKETING_VERSION = 1.2.3;
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
@@ -576,7 +584,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 39;
CURRENT_PROJECT_VERSION = 67;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -594,7 +602,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
MARKETING_VERSION = 1.0.50;
MARKETING_VERSION = 1.2.3;
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";

View File

@@ -13,7 +13,7 @@
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>eatvio_app</string>
<string>Итвио</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
@@ -21,7 +21,7 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>39</string>
<string>67</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>

View File

@@ -18,9 +18,66 @@ default_platform(:ios)
platform :ios do
desc "Push a new beta build to TestFlight"
lane :beta do
match(type: "appstore")
UI.message "Current working directory: #{Dir.pwd}"
UI.message "Checking API key file..."
possible_paths = [
"../AuthKey.p8",
"./AuthKey.p8",
"../../AuthKey.p8",
File.expand_path("../AuthKey.p8"),
File.expand_path("./AuthKey.p8")
]
auth_key_path = nil
possible_paths.each do |path|
if File.exist?(path)
UI.message "Found AuthKey.p8 at: #{path}"
auth_key_path = File.expand_path(path)
break
end
end
if auth_key_path.nil?
UI.user_error!("API key file not found. Tried paths: #{possible_paths.join(', ')}")
end
UI.message "Using auth_key_path: #{auth_key_path}"
match(type: "appstore", readonly: true)
increment_build_number(xcodeproj: "Runner.xcodeproj")
build_app(workspace: "Runner.xcworkspace", scheme: "Runner", export_method: "app-store", codesigning_identity: "Apple Distribution")
upload_to_testflight
build_app(
workspace: "Runner.xcworkspace",
scheme: "Runner",
export_method: "app-store",
codesigning_identity: "Apple Distribution",
xcargs: "--dart-define=HIDE_PLANS=true"
)
app_store_connect_api_key(
key_id: "325JS9JCZH",
issuer_id: "dfa27d1d-2bb8-4d4d-847e-0b32f9b880b3",
key_filepath: auth_key_path,
duration: 1200,
in_house: false
)
begin
upload_to_testflight(
skip_waiting_for_build_processing: true,
skip_submission: true,
distribute_external: false,
notify_external_testers: false,
changelog: "TestFlight build #{get_build_number(xcodeproj: 'Runner.xcodeproj')}",
verbose: true
)
rescue => ex
UI.error "Upload to TestFlight failed: #{ex.message}"
UI.error "Full error: #{ex.backtrace.join("\n")}"
raise ex
end
end
end

View File

@@ -5,22 +5,24 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000206">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000227">
</testcase>
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.195115">
<testcase classname="fastlane.lanes" name="1: match" time="8.292217">
</testcase>
<testcase classname="fastlane.lanes" name="2: build_app" time="116.686863">
<testcase classname="fastlane.lanes" name="2: increment_build_number" time="0.201631">
</testcase>
<testcase classname="fastlane.lanes" name="3: upload_to_testflight" time="338.642203">
<testcase classname="fastlane.lanes" name="3: build_app" time="9.870647">
<failure message="/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/actions/actions_helper.rb:67:in &apos;Fastlane::Actions.execute_action&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/runner.rb:255:in &apos;block in Fastlane::Runner#execute_action&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/runner.rb:229:in &apos;Dir.chdir&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/runner.rb:229:in &apos;Fastlane::Runner#execute_action&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/runner.rb:157:in &apos;Fastlane::Runner#trigger_action_by_name&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/fast_file.rb:159:in &apos;Fastlane::FastFile#method_missing&apos;&#10;Fastfile:23:in &apos;block (2 levels) in Fastlane::FastFile#parsing_binding&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/lane.rb:41:in &apos;Fastlane::Lane#call&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/runner.rb:49:in &apos;block in Fastlane::Runner#execute&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/runner.rb:45:in &apos;Dir.chdir&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/runner.rb:45:in &apos;Fastlane::Runner#execute&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/lane_manager.rb:46:in &apos;Fastlane::LaneManager.cruise_lane&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/command_line_handler.rb:34:in &apos;Fastlane::CommandLineHandler.handle&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/commands_generator.rb:110:in &apos;block (2 levels) in Fastlane::CommandsGenerator#run&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/commander-4.6.0/lib/commander/command.rb:187:in &apos;Commander::Command#call&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/commander-4.6.0/lib/commander/command.rb:157:in &apos;Commander::Command#run&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/commander-4.6.0/lib/commander/runner.rb:444:in &apos;Commander::Runner#run_active_command&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb:124:in &apos;Commander::Runner#run!&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/commander-4.6.0/lib/commander/delegates.rb:18:in &apos;Commander::Delegates#run!&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/commands_generator.rb:363:in &apos;Fastlane::CommandsGenerator#run&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/commands_generator.rb:43:in &apos;Fastlane::CommandsGenerator.start&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/cli_tools_distributor.rb:123:in &apos;Fastlane::CLIToolsDistributor.take_off&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/bin/fastlane:23:in &apos;&lt;top (required)&gt;&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/bin/fastlane:25:in &apos;Kernel#load&apos;&#10;/opt/homebrew/Cellar/fastlane/2.228.0/libexec/bin/fastlane:25:in &apos;&lt;main&gt;&apos;&#10;&#10;Error building the application - see the log above" />
</testcase>

View File

@@ -1,5 +1,3 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/firebase_api.dart';
@@ -121,7 +119,9 @@ class AppRouter {
GoRoute(
path: 'forget-password-screen',
name: 'forgetPasswordScreen',
builder: (context, state) => ForgetPasswordScreen(isAuth: state.extra as bool,),
builder: (context, state) => ForgetPasswordScreen(
isAuth: state.extra as bool,
),
),
GoRoute(
path: 'scan-qr-code-screen',
@@ -205,21 +205,9 @@ class AppRouter {
builder: (context, state) => const WelcomeScreen(),
redirect: (context, state) async {
try {
if (await TokenStorage.haveTokenInStorage()) {
final firebaseApi = sl<FirebaseApi>();
final fbToken = await firebaseApi.getToken();
final deviceInfo = DeviceInfoPlugin();
await sl<Dio>().post(
'/api/profile/notification/token/register',
data: {
'platform': Platform.isIOS ? 'ios' : 'android',
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
'token': fbToken,
},
);
}
final sharedPreferences = await SharedPreferences.getInstance();
final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed') ?? false;
await sl<FirebaseApi>().registerPushToken();
if (welcomeScreenWasShowed == false) {
return null;
} else if (await TokenStorage.haveTokenInStorage()) {
@@ -228,7 +216,7 @@ class AppRouter {
return '/auth-screen';
}
} catch (e) {
rethrow;
return '/feed-screen';
}
},
),
@@ -373,10 +361,10 @@ class AppRouter {
name: 'chatScreen',
builder: (context, state) {
final data = state.extra as Map<String, dynamic>;
final chatId = data['chatId'] as String;
final uuid = data['uuid'] as String;
final sendInitialMessage = data['sendInitialMessage'] as bool?;
return ChatScreen(
chatId: chatId,
uuid: uuid,
sendInitialMessage: sendInitialMessage,
);
},

View File

@@ -23,7 +23,7 @@ void showCommentsActionsBottomSheet(
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => Container(
margin: EdgeInsets.only(left: 8, right: 8, bottom: MediaQuery.of(context).padding.bottom),
margin: EdgeInsets.only(left: 8, right: 8, bottom: MediaQuery.of(context).padding.bottom + 20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: appTheme.whiteA700,
@@ -189,11 +189,12 @@ class _ActionWidget extends StatelessWidget {
padding: const EdgeInsets.symmetric(vertical: 12),
child: Row(
children: [
Text(
text,
style: TextStyles.textLMedium.copyWith(color: appTheme.black),
Expanded(
child: Text(
text,
style: TextStyles.textLMedium.copyWith(color: appTheme.black),
),
),
const Spacer(),
SvgPicture.asset(iconPath)
],
),

View File

@@ -1,5 +1,7 @@
String baseUrl = 'https://eatvio.ru';
// String baseUrl = 'https://dev2.eatvio.com';
import 'package:eatvio_app/core/env_config.dart';
String baseUrl = EnvConfig.baseUrl;
bool hidePlans = const bool.fromEnvironment('HIDE_PLANS', defaultValue: false);
const List<int> durationValues = [7, 14, 21];

43
lib/core/env_config.dart Normal file
View File

@@ -0,0 +1,43 @@
import 'package:flutter_dotenv/flutter_dotenv.dart';
class EnvConfig {
static String get baseUrl => dotenv.env['BASE_URL'] ?? '';
static String get devBaseUrl => dotenv.env['DEV_BASE_URL'] ?? '';
// Sentry Configuration
static String get sentryDsn => dotenv.env['SENTRY_DSN'] ?? '';
// Firebase Configuration
static String get firebaseProjectId => dotenv.env['FIREBASE_PROJECT_ID'] ?? '';
static String get firebaseDatabaseUrl => dotenv.env['FIREBASE_DATABASE_URL'] ?? '';
static String get firebaseStorageBucket => dotenv.env['FIREBASE_STORAGE_BUCKET'] ?? '';
static String get firebaseMessagingSenderId => dotenv.env['FIREBASE_MESSAGING_SENDER_ID'] ?? '';
// Firebase Android Configuration
static String get firebaseAndroidApiKey => dotenv.env['FIREBASE_ANDROID_API_KEY'] ?? '';
static String get firebaseAndroidAppId => dotenv.env['FIREBASE_ANDROID_APP_ID'] ?? '';
// Firebase iOS Configuration
static String get firebaseIosApiKey => dotenv.env['FIREBASE_IOS_API_KEY'] ?? '';
static String get firebaseIosAppId => dotenv.env['FIREBASE_IOS_APP_ID'] ?? '';
static String get firebaseIosBundleId => dotenv.env['FIREBASE_IOS_BUNDLE_ID'] ?? '';
// App Store URLs
static String get appStoreUrl => dotenv.env['APP_STORE_URL'] ?? '';
static String get rustoreUrl => dotenv.env['RUSTORE_URL'] ?? '';
// Telegram Support
static String get telegramSupportUrl => dotenv.env['TELEGRAM_SUPPORT_URL'] ?? '';
static String get supportEmail => dotenv.env['SUPPORT_EMAIL'] ?? '';
// Test Credentials (for development only)
static String get testEmail => dotenv.env['TEST_EMAIL'] ?? '';
static String get testPassword => dotenv.env['TEST_PASSWORD'] ?? '';
// Email Validation API
static String get emailValidationApiUrl => dotenv.env['EMAIL_VALIDATION_API_URL'] ?? '';
// Connection Check URLs
static String get googleCheckUrl => dotenv.env['GOOGLE_CHECK_URL'] ?? '';
static String get yandexCheckUrl => dotenv.env['YANDEX_CHECK_URL'] ?? '';
}

View File

@@ -1,3 +1,10 @@
import 'dart:developer';
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
class FirebaseApi {
@@ -7,4 +14,22 @@ class FirebaseApi {
await _firebaseMessaging.requestPermission();
return await _firebaseMessaging.getToken();
}
Future<void> registerPushToken() async {
if (await TokenStorage.haveTokenInStorage()) {
try {
final deviceInfo = DeviceInfoPlugin();
await sl<Dio>().post(
'/api/profile/notification/token/register',
data: {
'platform': Platform.isIOS ? 'ios' : 'android',
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
'token': await getToken(),
},
);
} catch (e, st) {
log(e.toString(), stackTrace: st);
}
}
}
}

View File

@@ -244,7 +244,6 @@ Future<void> init() async {
'accept': 'application/json',
'content-type': 'application/json',
},
validateStatus: (status) => status != null && status < 500,
),
)..interceptors.addAll(
[

View File

@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/cupertino.dart';
@@ -11,6 +13,10 @@ class PermissionHandler {
required Future<void> Function() onGranted,
required BuildContext context,
}) async {
if (Platform.isAndroid) {
await onGranted();
return;
}
final permissionState = await permission.request();
if (permissionState.isGranted) {
await onGranted();

View File

@@ -21,3 +21,25 @@ abstract class TokenStorage {
static Future<void> removeTokenInStorage() async => (await const FlutterSecureStorage().delete(key: 'token'));
}
abstract class SocketIdStorage {
static Future<bool> haveSocketIdInStorage() async {
try {
if (await const FlutterSecureStorage().read(key: 'socketId') == null) {
return false;
} else {
return true;
}
} catch (e) {
return false;
}
}
static Future<String?> getSocketIdFromStorage() async => await const FlutterSecureStorage().read(key: 'socketId');
static Future<void> saveSocketIdInStorage(String socketId) async {
await const FlutterSecureStorage().write(key: 'socketId', value: socketId);
}
static Future<void> removeSocketIdInStorage() async => (await const FlutterSecureStorage().delete(key: 'socketId'));
}

View File

@@ -1,3 +1,4 @@
import 'dart:convert';
import 'dart:developer';
import 'package:dart_pusher_channels/dart_pusher_channels.dart';
@@ -59,6 +60,24 @@ class WsManager {
),
);
String? extractSocketId(String? data) {
if (data == null) return null;
try {
final decoded = jsonDecode(data);
return decoded['socket_id'] as String?;
} catch (_) {
return null;
}
}
client?.eventStream.listen((event) async {
if (event.name == 'pusher:connection_established') {
final data = event.data;
final socketId = extractSocketId(data);
await SocketIdStorage.saveSocketIdInStorage(socketId!);
}
});
await client?.connect();
client = client;
@@ -77,9 +96,8 @@ class WsManager {
EasyDebounce.debounce(
'searchChatRoom',
const Duration(milliseconds: 300),
() => sl<ChatsCubit>().getChatRooms(),
() => sl<ChatsCubit>().getChatRooms(),
);
log(event.rootObject.toString());
});
_subscribe();
} catch (e, st) {

View File

@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/firebase_api.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/image_constant.dart';
@@ -66,19 +67,20 @@ class _AuthScreenState extends State<AuthScreen> {
await sl<ChatsCubit>().connectToWebSocket();
await sl<NotificationsCubit>().getNotifications();
await sl<NotificationsCubit>().connectToWebSocket();
await sl<FirebaseApi>().registerPushToken();
showSuccessToast('Успешный вход');
} catch (e) {
rethrow;
}
}
AppRouter.navigatorKey.currentContext?.go(
'/feed-screen',
extra: {'needToInitializeWebSocket': false},
);
AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false});
}
@override
Widget build(BuildContext context) {
var shortestSide = MediaQuery.sizeOf(context).shortestSide;
final bool useMobileLayout = shortestSide < 600;
if (!useMobileLayout) isFocused = true;
final s = S.of(context);
FlutterNativeSplash.remove();
return BlocProvider<AuthCubit>(
@@ -97,11 +99,7 @@ class _AuthScreenState extends State<AuthScreen> {
GoRouter.of(context).pushNamed(DevToolsScreen.name);
}
},
child: SvgPicture.asset(
ImageConstant.imgLogoEatvio,
height: isFocused ? 64 : MediaQuery.of(context).size.width - 64,
fit: BoxFit.contain,
),
child: SvgPicture.asset(ImageConstant.imgLogoEatvio, height: isFocused ? 64 : MediaQuery.of(context).size.width - 64, fit: BoxFit.contain),
),
),
body: SafeArea(
@@ -132,10 +130,7 @@ class _AuthScreenState extends State<AuthScreen> {
hidePassword = !hidePassword;
});
},
icon: Icon(
hidePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined,
color: Colors.grey,
),
icon: Icon(hidePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined, color: Colors.grey),
),
),
const SizedBox(height: 16),
@@ -144,11 +139,7 @@ class _AuthScreenState extends State<AuthScreen> {
onTap: () => GoRouter.of(context).goNamed('forgetPasswordScreen', extra: false),
child: Text(
'Забыли пароль?',
style: TextStyle(
color: appTheme.blueA400,
decorationColor: appTheme.blueA400,
decoration: TextDecoration.underline,
),
style: TextStyle(color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline),
),
),
),
@@ -161,11 +152,7 @@ class _AuthScreenState extends State<AuthScreen> {
onTap: () => GoRouter.of(context).goNamed('registrationScreen'),
child: Text(
'Зарегистрироваться',
style: TextStyle(
color: appTheme.blueA400,
decorationColor: appTheme.blueA400,
decoration: TextDecoration.underline,
),
style: TextStyle(color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline),
),
),
],
@@ -176,23 +163,22 @@ class _AuthScreenState extends State<AuthScreen> {
textAlign: TextAlign.center,
text: TextSpan(
text: 'Нажимая "Войти" или "Зарегистрироваться", Вы соглашаетесь \n',
style: const TextStyle(
fontSize: 12,
color: Colors.black,
),
style: const TextStyle(fontSize: 12, color: Colors.black),
children: [
const TextSpan(text: 'c '),
TextSpan(
text: ' Публичной офертой',
style: TextStyle(
fontSize: 12,
color: appTheme.blueA400,
decorationColor: appTheme.blueA400,
decoration: TextDecoration.underline,
if (!hidePlans) ...[
TextSpan(
text: ' Публичной офертой',
style: TextStyle(
fontSize: 12,
color: appTheme.blueA400,
decorationColor: appTheme.blueA400,
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')),
),
recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')),
),
const TextSpan(text: ' и '),
const TextSpan(text: ' и '),
],
TextSpan(
text: 'Политикой конфиденциальности\nи защитой персональных данных',
style: TextStyle(
@@ -218,17 +204,12 @@ class _AuthScreenState extends State<AuthScreen> {
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400,
onPressed: state is AuthStateLoading
? null
: () async {
final isAuthenticated = await authCubit.getTokenByCredentials(
emailController.text,
passwordController.text,
);
final isAuthenticated = await authCubit.getTokenByCredentials(emailController.text, passwordController.text);
if (isAuthenticated) {
if (context.mounted) {
_reconnectWebSocket(context);
@@ -237,10 +218,7 @@ class _AuthScreenState extends State<AuthScreen> {
},
label: state is AuthStateLoading
? const CupertinoActivityIndicator()
: Text(
'Войти',
style: theme.textTheme.titleSmall!.copyWith(color: Colors.white),
),
: Text('Войти', style: theme.textTheme.titleSmall!.copyWith(color: Colors.white)),
),
),
const SizedBox(width: 8),
@@ -252,9 +230,7 @@ class _AuthScreenState extends State<AuthScreen> {
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400,
onPressed: state is AuthStateLoading
? null
@@ -269,14 +245,10 @@ class _AuthScreenState extends State<AuthScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Где взять QR-код?\n',
style: TextStyle(
fontSize: 18,
),
),
const Text('Где взять QR-код?\n', style: TextStyle(fontSize: 18)),
Text(
'1. Войдите на сайте $baseUrl в свой аккаунт\n\n2. Перейдите в настройки профиля\n\n3. В разделе "Доп информация" перейдите на "QR Login"'),
'1. Войдите на сайте $baseUrl в свой аккаунт\n\n2. Перейдите в настройки профиля\n\n3. В разделе "Доп информация" перейдите на "QR Login"',
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
@@ -288,15 +260,10 @@ class _AuthScreenState extends State<AuthScreen> {
},
child: Ink(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
child: const Text(
'Отмена',
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.w400,
),
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w400),
),
),
),
@@ -309,15 +276,10 @@ class _AuthScreenState extends State<AuthScreen> {
},
child: Ink(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
child: Text(
'Перейти',
style: TextStyle(
color: appTheme.blueA400,
fontWeight: FontWeight.w600,
),
style: TextStyle(color: appTheme.blueA400, fontWeight: FontWeight.w600),
),
),
),
@@ -332,10 +294,7 @@ class _AuthScreenState extends State<AuthScreen> {
},
label: state is AuthStateLoading
? const CupertinoActivityIndicator()
: const Icon(
Icons.qr_code_scanner_rounded,
color: Colors.white,
),
: const Icon(Icons.qr_code_scanner_rounded, color: Colors.white),
),
),
],

View File

@@ -22,13 +22,9 @@ abstract class ChatRoomModel with _$ChatRoomModel {
@freezed
abstract class Users with _$Users {
const factory Users({
@JsonKey(name: 'from_chat_user') required ChatUserModel sender,
@JsonKey(name: 'to_chat_user') required ChatUserModel receiver,
}) = _Users;
factory Users.fromJson(Map<String, Object?> json) => _$UsersFromJson(json);
}

View File

@@ -1,3 +1,4 @@
import 'package:eatvio_app/core/models/pagination_model.dart';
import 'package:eatvio_app/features/chat/models/chat_message_model.dart';
import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -8,29 +9,18 @@ part 'chat_room_pagination_model.g.dart';
@freezed
abstract class ChatRoomPaginationModel with _$ChatRoomPaginationModel {
const factory ChatRoomPaginationModel({
@JsonKey(name: "chatRooms") required ChatRooms chatRooms,
@JsonKey(name: "data") required List<ChatRoom> data,
@JsonKey(name: "number_of_unread_chat_rooms") required int unreadChatsCount,
required Meta meta,
}) = _ChatRoomPreviewModel;
factory ChatRoomPaginationModel.fromJson(Map<String, dynamic> json) => _$ChatRoomPaginationModelFromJson(json);
}
@freezed
abstract class ChatRooms with _$ChatRooms {
const factory ChatRooms({
@JsonKey(name: "data") required List<ChatRoom> data,
@JsonKey(name: "last_page") required int lastPage,
@JsonKey(name: "current_page") required int currentPage,
}) = _ChatRooms;
factory ChatRooms.fromJson(Map<String, dynamic> json) => _$ChatRoomsFromJson(json);
}
@freezed
abstract class ChatRoom with _$ChatRoom {
const factory ChatRoom({
@JsonKey(name: "id") required String id,
@JsonKey(name: "room_name") required String roomName,
@JsonKey(name: "last_message", fromJson: parseLastMessage) required ChatMessageModel? lastMessage,
@JsonKey(name: "users") required Users users,
@JsonKey(name: "unread_messages_count") required int unreadMessagesCount,

View File

@@ -6,11 +6,8 @@ part 'chat_room_settings_model.g.dart';
@freezed
abstract class ChatRoomSettingsModel with _$ChatRoomSettingsModel {
const factory ChatRoomSettingsModel({
required int id,
@JsonKey(name: 'room_name') required String roomName,
@JsonKey(name: 'room_name') required String name,
required String state,
@JsonKey(name: 'access_level') required String accessLevel,
@JsonKey(name: 'updated_at') required String updatedAt,
}) = _ChatRoomSettingsModel;
factory ChatRoomSettingsModel.fromJson(Map<String, Object?> json) => _$ChatRoomSettingsModelFromJson(json);

View File

@@ -47,14 +47,14 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
_timer?.cancel();
}
Future<void> loadNextPage(String chatId) async {
Future<void> loadNextPage(String uuid) async {
if (currentPage < lastPage && !isLoading) {
isLoading = true;
emit((state as ChatRoomStateLoaded).copyWith(isLoading: isLoading));
try {
final chatMessages = await chatRepository.getChatMessages(chatId, currentPage + 1);
final chatMessages = await chatRepository.getChatMessages(uuid, currentPage + 1);
isLoading = false;
emit(
@@ -89,17 +89,19 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
}
}
Future<void> initChat(String chatId, {bool sendInitialMessage = false}) async {
Future<void> initChat(
String uuid, {
bool sendInitialMessage = false,
}) async {
try {
final chatRoom = await chatRepository.getChatRoom(chatId);
final chatRoom = await chatRepository.getChatRoom(uuid);
ChosenPlanModel? chosenPlan;
if (chatRoom.chosenPlanId != null) {
chosenPlan = await chatRepository.getChosenPlan(id: chatRoom.chosenPlanId!);
}
final chatMessages = await chatRepository.getChatMessages(chatId, currentPage);
final chatMessages = await chatRepository.getChatMessages(uuid, currentPage);
lastPage = chatMessages.meta.lastPage;
await _subscribeToChatChannel(chatId: chatId);
await _subscribeToChatChannel(uuid: uuid, chatId: chatRoom.id);
emit(
ChatRoomState.loaded(
chatRoom: chatRoom.copyWith(chosenPlan: chosenPlan),
@@ -158,14 +160,14 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
}
}
Future<void> _subscribeToChatChannel({required String chatId}) async {
Future<void> _subscribeToChatChannel({required String uuid, required String chatId}) async {
final client = wsManager.client;
if (client == null) return;
await wsManager.initPrivateDirectChatChannel(chatId);
wsManager.subscribeDirectChatChannel();
messagesSubscription = wsManager.directChatStream?.listen((event) {
AudioPlayer().play((AssetSource('audio/recive_message.wav')));
final message = ChatMessageModel.fromJson(jsonDecode(event.data)['message']);
final message = ChatMessageModel.fromJson(jsonDecode(event.data));
List<ChatMessageModel> messagesList = [message, ...(state as ChatRoomStateLoaded).chatMessages];
chatRepository.emitNewMessageToChats(message);
emit((state as ChatRoomStateLoaded).copyWith(chatMessages: messagesList, isLoading: isLoading));
@@ -179,7 +181,15 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
typingSubscription = wsManager.clientTypingStream?.listen((event) {
emit((state as ChatRoomStateLoaded).copyWith(isTyping: true, isLoading: isLoading));
EasyDebounce.debounce(
'client-typing', const Duration(seconds: 3), () => emit((state as ChatRoomStateLoaded).copyWith(isTyping: false, isLoading: isLoading)));
'client-typing',
const Duration(seconds: 3),
() => emit(
(state as ChatRoomStateLoaded).copyWith(
isTyping: false,
isLoading: isLoading,
),
),
);
});
}
@@ -189,12 +199,31 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
required String userSlug,
}) async {
try {
final compressedFiles = files != null ? await ImageCompressUtility.compressFilesList(List<File>.from(files)) : null;
AudioPlayer().play((AssetSource('audio/send_message.wav')));
emit(
(state as ChatRoomStateLoaded).copyWith(
chatMessages: [
ChatMessageModel(
id: 1,
data: data ?? '',
mediaData: compressedFiles,
authorId: 1,
updatedAt: DateTime.now(),
isRead: true,
isCurrentUserAuthor: true,
roomId: '',
),
...(state as ChatRoomStateLoaded).chatMessages,
],
),
);
await chatRepository.sendMessage(
data: data,
files: files != null ? await ImageCompressUtility.compressFilesList(List<File>.from(files)) : null,
files: compressedFiles,
slug: userSlug,
);
AudioPlayer().play((AssetSource('audio/send_message.wav')));
sl<ChatsCubit>().getChatRooms();
return true;
} catch (e, st) {
log(e.toString(), stackTrace: st);

View File

@@ -21,7 +21,7 @@ class ChatsCubit extends Cubit<ChatsState> {
required this.chatRepository,
required this.wsManager,
required this.profileRepository,
}) : super(ChatsState()) {
}) : super(ChatsState()) {
newLastMessageSubscription = chatRepository.newUserMessagesStream.listen((message) {
_setNewLastMessage(message);
});
@@ -29,6 +29,7 @@ class ChatsCubit extends Cubit<ChatsState> {
String? _query;
int page = 1;
late int lastPage;
Future<void> connectToWebSocket() async {
await newIncomingMessagesSubscription?.cancel();
newIncomingMessagesSubscription = wsManager.newMessagesStream?.listen((event) {
@@ -43,10 +44,10 @@ class ChatsCubit extends Cubit<ChatsState> {
try {
final chatRoomsPaginationModel = await chatRepository.getChatRooms(query: query);
final unreadChats = chatRoomsPaginationModel.unreadChatsCount;
lastPage = chatRoomsPaginationModel.chatRooms.lastPage + 1;
lastPage = chatRoomsPaginationModel.meta.lastPage + 1;
emit(
state.copyWith(
chatRooms: chatRoomsPaginationModel.chatRooms.data,
chatRooms: chatRoomsPaginationModel.data,
chatsWithUnreadMessagesCount: unreadChats,
state: BaseState.success,
),
@@ -65,10 +66,10 @@ class ChatsCubit extends Cubit<ChatsState> {
try {
final chatRoomsPaginationModel = await chatRepository.getChatRooms(query: query);
final unreadChats = chatRoomsPaginationModel.unreadChatsCount;
lastPage = chatRoomsPaginationModel.chatRooms.lastPage + 1;
lastPage = chatRoomsPaginationModel.meta.lastPage + 1;
emit(
state.copyWith(
chatRooms: chatRoomsPaginationModel.chatRooms.data,
chatRooms: chatRoomsPaginationModel.data,
chatsWithUnreadMessagesCount: unreadChats,
state: BaseState.success,
),
@@ -90,7 +91,7 @@ class ChatsCubit extends Cubit<ChatsState> {
final unreadChats = chatRoomsPaginationModel.unreadChatsCount;
emit(
state.copyWith(
chatRooms: [...state.chatRooms, ...chatRoomsPaginationModel.chatRooms.data],
chatRooms: [...state.chatRooms, ...chatRoomsPaginationModel.data],
chatsWithUnreadMessagesCount: unreadChats,
state: BaseState.success,
isLoadingMoreChats: false,

View File

@@ -32,10 +32,10 @@ import 'package:intl/intl.dart';
import 'package:visibility_detector/visibility_detector.dart';
class ChatScreen extends StatefulWidget {
final String chatId;
final String uuid;
final bool? sendInitialMessage;
const ChatScreen({required this.chatId, super.key, required this.sendInitialMessage});
const ChatScreen({required this.uuid, super.key, required this.sendInitialMessage});
@override
State<ChatScreen> createState() => _ChatScreenState();
@@ -64,14 +64,14 @@ class _ChatScreenState extends State<ChatScreen> {
void initState() {
super.initState();
scrollController.addListener(_onScroll);
chatRoomCubit.initChat(widget.chatId, sendInitialMessage: widget.sendInitialMessage ?? false);
chatRoomCubit.initChat(widget.uuid, sendInitialMessage: widget.sendInitialMessage ?? false);
chatRoomCubit.setTimer();
_listener = AppLifecycleListener(onHide: _onHide, onResume: _onResume);
}
void _onScroll() {
if (scrollController.offset >= scrollController.position.maxScrollExtent && !scrollController.position.outOfRange) {
chatRoomCubit.loadNextPage(widget.chatId);
chatRoomCubit.loadNextPage(widget.uuid);
}
}
@@ -93,7 +93,7 @@ class _ChatScreenState extends State<ChatScreen> {
if (state is ChatRoomStateLoaded) {
return _buildChat(context, state);
} else if (state is ChatRoomStateError) {
return Scaffold(appBar: AppBar(), body: DataErrorWidget(onReload: () => chatRoomCubit.initChat(widget.chatId)));
return Scaffold(appBar: AppBar(), body: DataErrorWidget(onReload: () => chatRoomCubit.initChat(widget.uuid)));
} else {
return const Scaffold(body: Loader());
}

View File

@@ -145,7 +145,7 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
'chatScreen',
extra: {
'chatId': chatIdOrError,
'uuid': chatIdOrError,
},
);
}
@@ -186,7 +186,7 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
'chatScreen',
extra: {
'chatId': chatIdOrError,
'uuid': chatIdOrError,
},
);
}
@@ -214,7 +214,7 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
'chatScreen',
extra: {
'chatId': chatIdOrError,
'uuid': chatIdOrError,
},
);
}
@@ -264,7 +264,7 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
'chatScreen',
extra: {
'chatId': chatIdOrError,
'uuid': chatIdOrError,
},
);
}

View File

@@ -1,5 +1,3 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:dismissible_page/dismissible_page.dart';
import 'package:eatvio_app/core/const.dart';
@@ -11,7 +9,7 @@ import 'package:photo_view/photo_view_gallery.dart';
import 'package:url_launcher/url_launcher.dart';
class PhotoViewScreen extends StatefulWidget {
final List<String> imageUrls;
final List<dynamic> imageUrls;
final int index;
final bool isLocal;
@@ -50,14 +48,12 @@ class _PhotoViewScreenState extends State<PhotoViewScreen> {
),
scrollPhysics: const BouncingScrollPhysics(),
builder: (BuildContext context, int index) {
final imageProvider = widget.isLocal
? FileImage(widget.imageUrls[index]) as ImageProvider<Object>
: CachedNetworkImageProvider(widget.imageUrls[index]) as ImageProvider<Object>;
return PhotoViewGalleryPageOptions(
imageProvider: widget.isLocal
? FileImage(
File(widget.imageUrls[index]),
) as ImageProvider
: CachedNetworkImageProvider(
widget.imageUrls[index],
),
imageProvider: imageProvider,
initialScale: PhotoViewComputedScale.contained,
minScale: PhotoViewComputedScale.contained,
heroAttributes: PhotoViewHeroAttributes(

View File

@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:dismissible_page/dismissible_page.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
@@ -59,8 +61,9 @@ class ChatBubbleWidget extends StatelessWidget {
),
),
child: Padding(
padding:
mediaData.isNotEmpty ? const EdgeInsets.all(8) : EdgeInsets.only(top: 8, bottom: 8, left: isCurrentUser ? 16 : 8, right: isCurrentUser ? 8 : 16),
padding: mediaData.isNotEmpty
? const EdgeInsets.all(8)
: EdgeInsets.only(top: 8, bottom: 8, left: isCurrentUser ? 16 : 8, right: isCurrentUser ? 8 : 16),
child: Column(
crossAxisAlignment: isCurrentUser ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [
@@ -86,17 +89,17 @@ class ChatBubbleWidget extends StatelessWidget {
},
leading: const Icon(
Icons.file_copy_outlined,
color: Colors.white,
color: Colors.black,
),
trailing: const Icon(
Icons.download_outlined,
color: Colors.white,
color: Colors.black,
),
title: Text(
mediaData[index].toString().replaceAll('chat_files/', ''),
maxLines: 2,
style: const TextStyle(
color: Colors.white,
color: Colors.black,
fontSize: 12,
overflow: TextOverflow.ellipsis,
),
@@ -107,8 +110,9 @@ class ChatBubbleWidget extends StatelessWidget {
onTap: () {
context.pushTransparentRoute(
PhotoViewScreen(
imageUrls: mediaData.map((e) => '$e').toList(),
imageUrls: mediaData,
index: index,
isLocal: mediaData[index] is File ? true : false,
),
);
},
@@ -117,9 +121,11 @@ class ChatBubbleWidget extends StatelessWidget {
transitionOnUserGestures: true,
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: CachedNetworkImage(
imageUrl: '${mediaData[index]}',
),
child: mediaData[index] is File
? Image.file(mediaData[index])
: CachedNetworkImage(
imageUrl: '${mediaData[index]}',
),
),
),
);

View File

@@ -24,7 +24,7 @@ class _ChatEntryWidgetState extends State<ChatEntryWidget> {
onTap: () => GoRouter.of(context).pushNamed(
'chatScreen',
extra: {
'chatId': widget.chatRoom.id.toString(),
'uuid': widget.chatRoom.id.toString(),
},
),
child: Container(

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/features/chat/models/chat_message_model.dart';
import 'package:eatvio_app/features/chat/models/chat_messages_model.dart';
import 'package:eatvio_app/features/chat/models/chat_room_model.dart';
@@ -47,10 +48,11 @@ class ChatRepository {
}
}
await chatService.sendMessageByUserSlug(
userSlug: slug,
await chatService.sendMessage(
slug: slug,
data: data,
mediaData: multipartFiles.isEmpty ? null : multipartFiles,
socketId: (await SocketIdStorage.getSocketIdFromStorage())!,
);
} catch (e, st) {
log(e.toString(), stackTrace: st);
@@ -58,10 +60,11 @@ class ChatRepository {
}
}
Future<ChatRoomModel> getChatRoom(String chatId) async {
Future<ChatRoomModel> getChatRoom(String uuid) async {
try {
return (await chatService.getChatRoom(id: chatId))['chatRoom']!;
} catch (e) {
return (await chatService.getChatRoom(uuid: uuid));
} catch (e, st) {
log(e.toString(), stackTrace: st);
rethrow;
}
}
@@ -74,9 +77,9 @@ class ChatRepository {
}
}
Future<ChatMessagesModel> getChatMessages(String chatId, int page) async {
Future<ChatMessagesModel> getChatMessages(String id, int page) async {
try {
return (await chatService.getChatMessages(id: chatId, page: page));
return (await chatService.getChatMessages(id: id, page: page));
} catch (e) {
rethrow;
}
@@ -90,9 +93,9 @@ class ChatRepository {
}
}
Future<void> markMessagesAsRead(String chatId, List<int> messageIds) async {
Future<void> markMessagesAsRead(String id, List<int> messageIds) async {
try {
await chatService.markMessagesAsRead(id: chatId, body: {'messages_ids': messageIds});
await chatService.markMessagesAsRead(id: id, body: {'messages_ids': messageIds});
} catch (e) {
rethrow;
}

View File

@@ -13,32 +13,33 @@ part 'chat_service.g.dart';
abstract class ChatService {
factory ChatService(Dio dio, {String baseUrl}) = _ChatService;
@GET('/api/chat/index')
@GET('/api/v2/chat/index')
Future<ChatRoomPaginationModel> getChatRooms({
@Query('page') required int page,
@Query('find') String? query,
@Query('filters[data][\$conversation_name]') String? query,
});
@GET('/api/chat/show/{id}')
Future<Map<String, ChatRoomModel>> getChatRoom({
@Path() required String id,
@GET('/api/v2/chat/show/{uuid}')
Future<ChatRoomModel> getChatRoom({
@Path() required String uuid,
});
@POST('/api/v2/chat/message/send/{id}')
@POST('/api/v2/chat/message/send/{slug}')
@MultiPart()
Future<void> sendMessage({
@Path() required String id,
@Header('X-Socket-ID') required String socketId,
@Path() required String slug,
@Part() String? data,
@Part(name: 'media_data[]', contentType: 'multipart/form-data') List<MultipartFile>? mediaData,
});
@GET('/api/chat/show/{id}/messages?page={page}')
@GET('/api/v2/chat/show/{id}/messages?page={page}')
Future<ChatMessagesModel> getChatMessages({
@Path() required String id,
@Path() required int page,
});
@GET('/api/chat/get_by_user/{user}')
@GET('/api/v2/chat/get_by_user/{user}')
Future<ChatRoomModel> getChatRoomIdByUserSlug({
@Path() required String user,
});
@@ -81,16 +82,8 @@ abstract class ChatService {
@Body() required PlanComplainModel body,
});
@GET('/api/v2/chat/get_by_user/{slug}')
@GET('/api/chat/get_by_user/{slug}')
Future<HttpResponse> getChatByUserSlug({
@Path() required String slug,
});
@POST('/api/v2/chat/message/send/{userSlug}')
@MultiPart()
Future<void> sendMessageByUserSlug({
@Path() required String userSlug,
@Part() String? data,
@Part(name: 'media_data[]', contentType: 'multipart/form-data') List<MultipartFile>? mediaData,
});
}

View File

@@ -9,6 +9,7 @@ import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_cubit.dart';
import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_state.dart';
import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -194,63 +195,74 @@ class _LawViolationWidgetState extends State<_LawViolationWidget> {
@override
Widget build(BuildContext context) {
final s = S.of(context);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const BottomSheetHandlerBar(),
const SizedBox(height: 20),
Text(
s.lawViolation,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
const SizedBox(height: 8),
SizedBox(
width: MediaQuery.sizeOf(context).width * 0.7,
child: Text(
s.what_do_you_want_to_complaint_about,
textAlign: TextAlign.center,
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
),
),
const SizedBox(height: 24),
...tags.mapIndexed((index, element) {
return Row(
children: [
Checkbox(
activeColor: appTheme.blueA400,
side: BorderSide(width: 2, color: tags[index].$1 ? appTheme.blueA400 : appTheme.blueGray),
value: element.$1,
onChanged: (isChecked) {
setState(() {
if (isChecked == null) return;
if (index == tags.length - 1 && !isChecked) {
context.read<ComplainingCubit>().toggleDescription();
}
tags[index] = (isChecked, element.$2);
final activeTags = tags.where((elem) => elem.$1).map((e) => e.$2).toList();
context.read<ComplainingCubit>().setTags(activeTags);
});
return BlocBuilder<ComplainingCubit, ComplainingState>(
builder: (context, state) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const BottomSheetHandlerBar(),
const SizedBox(height: 20),
Text(
s.lawViolation,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
const SizedBox(height: 8),
SizedBox(
width: MediaQuery.sizeOf(context).width * 0.7,
child: Text(
s.what_do_you_want_to_complaint_about,
textAlign: TextAlign.center,
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
),
),
const SizedBox(height: 24),
...tags.mapIndexed((index, element) {
return Row(
children: [
Checkbox(
activeColor: appTheme.blueA400,
side: BorderSide(width: 2, color: tags[index].$1 ? appTheme.blueA400 : appTheme.blueGray),
value: element.$1,
onChanged: (isChecked) {
setState(() {
if (isChecked == null) return;
if (index == tags.length - 1 && !isChecked) {
context.read<ComplainingCubit>().toggleDescription();
}
tags[index] = (isChecked, element.$2);
final activeTags = tags.where((elem) => elem.$1).map((e) => e.$2).toList();
context.read<ComplainingCubit>().setTags(activeTags);
});
},
),
Text(
element.$2,
style: TextStyles.textLSemibold.copyWith(color: appTheme.black),
),
],
);
}),
if (tags.last.$1) ...[
const SizedBox(height: 12),
CustomTextFormField(
hintText: s.type_here_your_complaint,
hasError: (state.complainInfo.description?.length ?? 10) < 10,
onChanged: (value) {
context.read<ComplainingCubit>().setDescription(value);
},
),
Text(
element.$2,
style: TextStyles.textLSemibold.copyWith(color: appTheme.black),
),
if ((state.complainInfo.description?.length ?? 10) < 10) ...[
const SizedBox(height: 4),
const ValidationErrorWidget(error: 'Минимум 10 символов'),
]
],
);
}),
if (tags.last.$1) ...[
const SizedBox(height: 12),
CustomTextFormField(
hintText: s.type_here_your_complaint,
onChanged: (value) {
context.read<ComplainingCubit>().setDescription(value);
},
),
],
const SizedBox(height: 20),
const _ComplainButton(),
],
const SizedBox(height: 20),
_ComplainButton(
isDisabled: (state.complainInfo.description?.length ?? 0) < 10,
),
],
);
},
);
}
}
@@ -515,8 +527,8 @@ class _ComplaintFailedWidget extends StatelessWidget {
}
class _ComplainButton extends StatelessWidget {
const _ComplainButton();
const _ComplainButton({this.isDisabled = false});
final bool isDisabled;
@override
Widget build(BuildContext context) {
final s = S.of(context);
@@ -534,6 +546,10 @@ class _ComplainButton extends StatelessWidget {
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
),
onPressed: () async {
if (isDisabled) {
context.read<ComplainingCubit>().setDescription('');
return;
}
await context.read<ComplainingCubit>().complain();
},
);

View File

@@ -25,24 +25,16 @@ class _LastStepScreenState extends State<LastStepScreen> {
children: [
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text(
'Последний шаг',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w700,
height: 1.1,
),
),
child: Text('Последний шаг', style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, height: 1.1)),
),
const SizedBox(height: 16),
CheckboxListTile(
value: checkboxState,
title: const Text(
'Я ознакомлен и принимаю договор публичной офёрты',
style: TextStyle(
fontSize: 18,
height: 1,
),
title: Text(
hidePlans
? 'Я ознакомлен и принимаю политику конфиденциальности и защиту персональных данных'
: 'Я ознакомлен и принимаю договор публичной офёрты',
style: const TextStyle(fontSize: 18, height: 1),
),
onChanged: (_) {
setState(() {
@@ -53,25 +45,14 @@ class _LastStepScreenState extends State<LastStepScreen> {
activeColor: appTheme.blueA400,
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
child: _divider(context),
),
Padding(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), child: _divider(context)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: TextButton(
onPressed: () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')),
child: const Text(
'Договор офёрты',
style: TextStyle(
height: 1,
fontSize: 16,
color: Color(0xFF323F4B),
decoration: TextDecoration.underline,
),
onPressed: () async => await launchUrl(Uri.parse(hidePlans ? '$baseUrl/docs/privacy.pdf' : '$baseUrl/oferta_bs.pdf')),
child: Text(
hidePlans ? 'Политика конфиденциальности и пользовательское соглашение' : 'Договор офёрты',
style: const TextStyle(height: 1, fontSize: 16, color: Color(0xFF323F4B), decoration: TextDecoration.underline),
),
),
),
@@ -84,10 +65,7 @@ class _LastStepScreenState extends State<LastStepScreen> {
return Container(
height: 0.4,
width: MediaQuery.of(context).size.width - 32,
decoration: BoxDecoration(
color: Colors.black26,
borderRadius: BorderRadius.circular(16),
),
decoration: BoxDecoration(color: Colors.black26, borderRadius: BorderRadius.circular(16)),
);
}
}

View File

@@ -14,10 +14,7 @@ import 'package:go_router/go_router.dart';
class ConsultationCardWidget extends StatefulWidget {
final ConsultationModel consultationsModel;
const ConsultationCardWidget({
super.key,
required this.consultationsModel,
});
const ConsultationCardWidget({super.key, required this.consultationsModel});
@override
State<ConsultationCardWidget> createState() => _ConsultationCardWidgetState();
@@ -32,13 +29,7 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
'Дневник заполнен, прокомментирован консультантом и после этого клиент просмотрел комментарии.',
];
final List<Color> legendColors = const [
Colors.white,
Color(0xFFF0C497),
Color(0xFFFF9500),
Color(0xFF2F80ED),
Color(0xFF4CD964),
];
final List<Color> legendColors = const [Colors.white, Color(0xFFF0C497), Color(0xFFFF9500), Color(0xFF2F80ED), Color(0xFF4CD964)];
late final List<(int, DiaryModel)> givenDaysWithDayNumber;
final List<List<DiaryStateEnum>> legendStates = [
@@ -46,7 +37,7 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
[DiaryStateEnum.started, DiaryStateEnum.lastDayStarted],
[DiaryStateEnum.finished, DiaryStateEnum.lastDayFinished],
[DiaryStateEnum.commented, DiaryStateEnum.lastDayCommented],
[DiaryStateEnum.reviewedByUser]
[DiaryStateEnum.reviewedByUser],
];
@override
void initState() {
@@ -72,32 +63,19 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
children: [
Row(
children: [
CircleAvatar(
radius: 30,
backgroundImage: NetworkImage(baseUrl + widget.consultationsModel.userProfilePhotoUrl),
),
CircleAvatar(radius: 30, backgroundImage: NetworkImage(baseUrl + widget.consultationsModel.userProfilePhotoUrl)),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.consultationsModel.userName,
style: const TextStyle(
height: 1,
color: Color(0xFF323F4B),
fontSize: 16,
fontWeight: FontWeight.w600,
),
style: const TextStyle(height: 1, color: Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w600),
),
const SizedBox(height: 4),
Text(
widget.consultationsModel.planTitle,
style: const TextStyle(
height: 1,
color: Color(0xFF88949F),
fontSize: 14,
fontWeight: FontWeight.w500,
),
style: const TextStyle(height: 1, color: Color(0xFF88949F), fontSize: 14, fontWeight: FontWeight.w500),
),
],
),
@@ -127,12 +105,7 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
children: [
Text(
'День ${widget.consultationsModel.currentDiaryDay} из ${widget.consultationsModel.duration}',
style: const TextStyle(
height: 1,
color: Color(0xFF323F4B),
fontSize: 16,
fontWeight: FontWeight.w500,
),
style: const TextStyle(height: 1, color: Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w500),
),
GestureDetector(
onTap: () {
@@ -149,12 +122,7 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
children: [
const Text(
'Легенда',
style: TextStyle(
height: 1,
fontSize: 20,
fontWeight: FontWeight.w700,
color: Color(0xFF323F4B),
),
style: TextStyle(height: 1, fontSize: 20, fontWeight: FontWeight.w700, color: Color(0xFF323F4B)),
),
const SizedBox(height: 16),
ListView.separated(
@@ -170,11 +138,7 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
),
);
},
child: const Icon(
Icons.help_outline_rounded,
color: Color(0xFF323F4B),
size: 20,
),
child: const Icon(Icons.help_outline_rounded, color: Color(0xFF323F4B), size: 20),
),
],
),
@@ -185,7 +149,8 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
children: List.generate(widget.consultationsModel.duration, (index) {
final day = givenDaysWithDayNumber.firstWhereOrNull((e) => e.$1 == index);
final hasActivity = day != null && day.$2.state != DiaryStateEnum.init;
final canConsultantOpenThisDay = day != null &&
final canConsultantOpenThisDay =
day != null &&
(day.$2.state != DiaryStateEnum.init && day.$2.state != DiaryStateEnum.started && day.$2.state != DiaryStateEnum.lastDayStarted);
return Padding(
padding: const EdgeInsets.only(right: 12),
@@ -195,81 +160,74 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
InkWell(
onTap: () {
if (!hasActivity || !canConsultantOpenThisDay) {
showErrorDialog(context,
title: s.you_can_not_open_this_day, description: '${widget.consultationsModel.userName} ${s.has_not_filled_it_yet}');
showErrorDialog(
context,
title: s.you_can_not_open_this_day,
description: '${widget.consultationsModel.userName} ${s.has_not_filled_it_yet}',
);
return;
}
GoRouter.of(context).push(UserDiaryScreen.path, extra: {
'diaryId': day.$2.id,
'currentDiaryDay': index + 1,
});
GoRouter.of(context).push(UserDiaryScreen.path, extra: {'diaryId': day.$2.id, 'currentDiaryDay': index + 1});
},
borderRadius: BorderRadius.circular(100),
child: Ink(
decoration: BoxDecoration(
color: hasActivity ? appTheme.dividerColor : appTheme.dividerColor.withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(100)),
color: hasActivity ? appTheme.dividerColor : appTheme.dividerColor.withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(100),
),
width: 58,
height: 32,
child: Center(
child: Text(
(index + 1).toString(),
style: TextStyles.textMSemibold.copyWith(
color: hasActivity ? appTheme.gray : appTheme.gray.withValues(alpha: 0.4),
),
style: TextStyles.textMSemibold.copyWith(color: hasActivity ? appTheme.gray : appTheme.gray.withValues(alpha: 0.4)),
),
),
),
),
if (hasActivity) ...[
const SizedBox(
height: 4,
),
const SizedBox(height: 4),
switch (day.$2.state) {
DiaryStateEnum.init => const SizedBox.shrink(),
DiaryStateEnum.started => Container(
width: 8,
height: 8,
decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[1]),
),
width: 8,
height: 8,
decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[1]),
),
DiaryStateEnum.lastDayStarted => Container(
width: 8,
height: 8,
decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[1]),
),
width: 8,
height: 8,
decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[1]),
),
DiaryStateEnum.finished => Container(
width: 8,
height: 8,
decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[2]),
),
width: 8,
height: 8,
decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[2]),
),
DiaryStateEnum.lastDayFinished => Container(
width: 8,
height: 8,
decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[2]),
),
width: 8,
height: 8,
decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[2]),
),
DiaryStateEnum.commented => Container(
width: 8,
height: 8,
decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[3]),
),
width: 8,
height: 8,
decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[3]),
),
DiaryStateEnum.lastDayCommented => Container(
width: 8,
height: 8,
decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[3]),
),
width: 8,
height: 8,
decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[3]),
),
DiaryStateEnum.reviewedByUser => Container(
width: 8,
height: 8,
decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[4]),
),
width: 8,
height: 8,
decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[4]),
),
},
const SizedBox(
height: 12,
),
const SizedBox(height: 12),
] else
const SizedBox(
height: 24,
),
const SizedBox(height: 24),
],
),
);
@@ -280,11 +238,7 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
const SizedBox(height: 12),
Text(
'${widget.consultationsModel.consultationOptionPrice.toStringAsFixed(0)} ₽',
style: const TextStyle(
color: Color(0xFF019A3E),
fontSize: 20,
fontWeight: FontWeight.w600,
),
style: const TextStyle(color: Color(0xFF019A3E), fontSize: 20, fontWeight: FontWeight.w600),
),
const SizedBox(height: 16),
Row(
@@ -293,36 +247,26 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
borderRadius: BorderRadius.circular(20),
onTap: () {
final lastActiveDay = givenDaysWithDayNumber
.where(
(e) => e.$2.state == DiaryStateEnum.finished || e.$2.state == DiaryStateEnum.lastDayFinished,
)
.where((e) => e.$2.state == DiaryStateEnum.finished || e.$2.state == DiaryStateEnum.lastDayFinished)
.lastOrNull;
if (lastActiveDay != null) {
GoRouter.of(context).push(UserDiaryScreen.path, extra: {
'diaryId': lastActiveDay.$2.id,
'currentDiaryDay': lastActiveDay.$1,
});
GoRouter.of(context).push(UserDiaryScreen.path, extra: lastActiveDay.$2.id);
GoRouter.of(context).push(UserDiaryScreen.path, extra: {'diaryId': lastActiveDay.$2.id, 'currentDiaryDay': lastActiveDay.$1});
return;
}
showErrorDialog(context,
title: s.you_can_not_open_this_day, description: '${widget.consultationsModel.userName} ${s.has_not_filled_it_yet}');
showErrorDialog(
context,
title: s.you_can_not_open_this_day,
description: '${widget.consultationsModel.userName} ${s.has_not_filled_it_yet}',
);
},
child: Ink(
width: MediaQuery.of(context).size.width - 32 - 52 - 8,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: appTheme.blueA400,
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
child: const Center(
child: Text(
'Комментировать отчёт',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600),
),
),
),
@@ -334,16 +278,8 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
child: Ink(
height: 52,
width: 52,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: const Color(0xFFF6F6F6),
),
child: const Center(
child: Icon(
Icons.chat_outlined,
size: 24,
),
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFF6F6F6)),
child: const Center(child: Icon(Icons.chat_outlined, size: 24)),
),
),
],
@@ -361,21 +297,13 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
Container(
height: 20,
width: 20,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: color,
),
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
),
const SizedBox(width: 12),
Flexible(
child: Text(
text,
style: const TextStyle(
height: 1,
fontSize: 16,
fontWeight: FontWeight.w500,
color: Color(0xFF323F4B),
),
style: const TextStyle(height: 1, fontSize: 16, fontWeight: FontWeight.w500, color: Color(0xFF323F4B)),
),
),
],

View File

@@ -50,6 +50,7 @@ class ConsultationService {
else if (state != null && stateComparison != null)
'filters[last_state][${stateComparison.value}]': state,
if (dateComparison != null && date != null) 'filters[start_date][${dateComparison.value}]': date,
'sort[]': 'created_at:desc',
},
);
return ConsultationPaginationModel.fromJson(response.data);
@@ -77,7 +78,8 @@ class ConsultationService {
final response = await dio.get(
'/api/finances/my_finances',
queryParameters: {
if (dateComparison != null && date != null) 'filters[end_date]': date,
if (date != null && dateComparison != null) 'filters[start_date][${dateComparison.value}]': date,
'sort[]': 'start_date:desc',
},
);
final data = response.data as Map<String, dynamic>;
@@ -96,7 +98,8 @@ class ConsultationService {
final response = await dio.get(
'/api/finances/my_purchases',
queryParameters: {
if (dateComparison != null && date != null) 'filters[start_date]': date,
if (date != null && dateComparison != null) 'filters[start_date][${dateComparison.value}]': date,
'sort[]': 'start_date:desc',
},
);
final data = response.data as Map<String, dynamic>;

View File

@@ -57,7 +57,7 @@ class RecipeRepository {
'type': recipe.type,
'state': 'active',
'title': recipe.title,
'tags': recipe.tags,
'tags[]': recipe.tags ?? [],
'description_md': recipe.description,
'video_url': recipe.videoUrl,
'ingredients': ingredientsMap,

View File

@@ -9,6 +9,7 @@ import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/features/dev_tools/test_cases.dart';
import 'package:eatvio_app/core/env_config.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -97,10 +98,15 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Column(
children: [
Padding(padding: const EdgeInsets.all(8.0), child: Row(children: [Text('BaseUrl: $baseUrlForTest')])),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(children: [Text('BaseUrl: $baseUrlForTest')]),
),
Row(
children: [
Expanded(child: CustomTextFormField(controller: controller, hintText: 'type new base url')),
Expanded(
child: CustomTextFormField(controller: controller, hintText: 'type new base url'),
),
IconButton(
onPressed: () {
setState(() {
@@ -116,13 +122,13 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
),
const SizedBox(height: 10),
CustomOutlinedButton(
text: baseUrl == 'https://dev2.eatvio.com' ? 'Set production base url' : 'Set dev2 base url',
text: baseUrl == EnvConfig.devBaseUrl ? 'Set production base url' : 'Set dev2 base url',
onPressed: () {
setState(() {
if (baseUrl == 'https://eatvio.ru') {
baseUrlForTest = 'https://dev2.eatvio.com';
if (baseUrl == EnvConfig.baseUrl) {
baseUrlForTest = EnvConfig.devBaseUrl;
} else {
baseUrlForTest = 'https://eatvio.ru';
baseUrlForTest = EnvConfig.baseUrl;
}
baseUrl = baseUrlForTest.toLowerCase();
sl<Dio>().options.baseUrl = baseUrl;
@@ -132,7 +138,7 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
buttonStyle: const ButtonStyle(),
),
SizedBox(height: baseUrlForTest != baseUrl ? 40 : 10),
if (baseUrl.toLowerCase() != 'https://eatvio.ru')
if (baseUrl.toLowerCase() != EnvConfig.baseUrl.toLowerCase())
ListView.builder(
shrinkWrap: true,
itemCount: testCases.length,
@@ -175,12 +181,7 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
builder: (context) => AlertDialog(
title: const Text('Test Failed'),
content: SingleChildScrollView(child: Text(result.error ?? 'No error details.')),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('OK'),
),
],
actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('OK'))],
),
);
}
@@ -197,12 +198,7 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
builder: (context) => AlertDialog(
title: const Text('Test Failed'),
content: SingleChildScrollView(child: Text('$e\n$stack')),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('OK'),
),
],
actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('OK'))],
),
);
}
@@ -216,10 +212,10 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
TestState.fail => Container(width: 20, height: 20, color: Colors.red),
TestState.notRun => Container(width: 20, height: 20, color: Colors.grey),
TestState.loading => const SizedBox(
width: 20,
height: 20,
child: SizedBox(height: 20, width: 20, child: Center(child: CircularProgressIndicator())),
),
width: 20,
height: 20,
child: SizedBox(height: 20, width: 20, child: Center(child: CircularProgressIndicator())),
),
},
],
),

View File

@@ -35,6 +35,7 @@ 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/settings/services/settings_service.dart';
import 'package:eatvio_app/core/env_config.dart';
class TestResult {
final bool success;
@@ -477,7 +478,9 @@ class AuthServiceTest implements TestCases {
final dio = sl<Dio>();
final authService = AuthService(dio, baseUrl: baseUrl);
// Use dummy credentials, expecting failure but no crash
await authService.getTokenByCredentials(credentials: const CredentialsModel(email: 'reshetnick.mike+user1@gmail.com', password: '1234567890'));
await authService.getTokenByCredentials(
credentials: CredentialsModel(email: EnvConfig.testEmail, password: EnvConfig.testPassword),
);
return TestResult(false, 'Should not succeed');
} catch (e) {
return TestResult(true, 'Exception expected for dummy credentials');

View File

@@ -1,784 +0,0 @@
// dart format width=80
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'consultant_review_model.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$ConsultantReviewModel {
String get planName;
int get planDuration;
int get currentPlanDay;
ConsultantComments get consultantComments;
/// Create a copy of ConsultantReviewModel
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$ConsultantReviewModelCopyWith<ConsultantReviewModel> get copyWith =>
_$ConsultantReviewModelCopyWithImpl<ConsultantReviewModel>(
this as ConsultantReviewModel, _$identity);
/// Serializes this ConsultantReviewModel to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is ConsultantReviewModel &&
(identical(other.planName, planName) ||
other.planName == planName) &&
(identical(other.planDuration, planDuration) ||
other.planDuration == planDuration) &&
(identical(other.currentPlanDay, currentPlanDay) ||
other.currentPlanDay == currentPlanDay) &&
(identical(other.consultantComments, consultantComments) ||
other.consultantComments == consultantComments));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType, planName, planDuration, currentPlanDay, consultantComments);
@override
String toString() {
return 'ConsultantReviewModel(planName: $planName, planDuration: $planDuration, currentPlanDay: $currentPlanDay, consultantComments: $consultantComments)';
}
}
/// @nodoc
abstract mixin class $ConsultantReviewModelCopyWith<$Res> {
factory $ConsultantReviewModelCopyWith(ConsultantReviewModel value,
$Res Function(ConsultantReviewModel) _then) =
_$ConsultantReviewModelCopyWithImpl;
@useResult
$Res call(
{String planName,
int planDuration,
int currentPlanDay,
ConsultantComments consultantComments});
$ConsultantCommentsCopyWith<$Res> get consultantComments;
}
/// @nodoc
class _$ConsultantReviewModelCopyWithImpl<$Res>
implements $ConsultantReviewModelCopyWith<$Res> {
_$ConsultantReviewModelCopyWithImpl(this._self, this._then);
final ConsultantReviewModel _self;
final $Res Function(ConsultantReviewModel) _then;
/// Create a copy of ConsultantReviewModel
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? planName = null,
Object? planDuration = null,
Object? currentPlanDay = null,
Object? consultantComments = null,
}) {
return _then(_self.copyWith(
planName: null == planName
? _self.planName
: planName // ignore: cast_nullable_to_non_nullable
as String,
planDuration: null == planDuration
? _self.planDuration
: planDuration // ignore: cast_nullable_to_non_nullable
as int,
currentPlanDay: null == currentPlanDay
? _self.currentPlanDay
: currentPlanDay // ignore: cast_nullable_to_non_nullable
as int,
consultantComments: null == consultantComments
? _self.consultantComments
: consultantComments // ignore: cast_nullable_to_non_nullable
as ConsultantComments,
));
}
/// Create a copy of ConsultantReviewModel
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$ConsultantCommentsCopyWith<$Res> get consultantComments {
return $ConsultantCommentsCopyWith<$Res>(_self.consultantComments, (value) {
return _then(_self.copyWith(consultantComments: value));
});
}
}
/// @nodoc
@JsonSerializable()
class _ConsultantReviewModel implements ConsultantReviewModel {
_ConsultantReviewModel(
{required this.planName,
required this.planDuration,
required this.currentPlanDay,
required this.consultantComments});
factory _ConsultantReviewModel.fromJson(Map<String, dynamic> json) =>
_$ConsultantReviewModelFromJson(json);
@override
final String planName;
@override
final int planDuration;
@override
final int currentPlanDay;
@override
final ConsultantComments consultantComments;
/// Create a copy of ConsultantReviewModel
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$ConsultantReviewModelCopyWith<_ConsultantReviewModel> get copyWith =>
__$ConsultantReviewModelCopyWithImpl<_ConsultantReviewModel>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$ConsultantReviewModelToJson(
this,
);
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _ConsultantReviewModel &&
(identical(other.planName, planName) ||
other.planName == planName) &&
(identical(other.planDuration, planDuration) ||
other.planDuration == planDuration) &&
(identical(other.currentPlanDay, currentPlanDay) ||
other.currentPlanDay == currentPlanDay) &&
(identical(other.consultantComments, consultantComments) ||
other.consultantComments == consultantComments));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType, planName, planDuration, currentPlanDay, consultantComments);
@override
String toString() {
return 'ConsultantReviewModel(planName: $planName, planDuration: $planDuration, currentPlanDay: $currentPlanDay, consultantComments: $consultantComments)';
}
}
/// @nodoc
abstract mixin class _$ConsultantReviewModelCopyWith<$Res>
implements $ConsultantReviewModelCopyWith<$Res> {
factory _$ConsultantReviewModelCopyWith(_ConsultantReviewModel value,
$Res Function(_ConsultantReviewModel) _then) =
__$ConsultantReviewModelCopyWithImpl;
@override
@useResult
$Res call(
{String planName,
int planDuration,
int currentPlanDay,
ConsultantComments consultantComments});
@override
$ConsultantCommentsCopyWith<$Res> get consultantComments;
}
/// @nodoc
class __$ConsultantReviewModelCopyWithImpl<$Res>
implements _$ConsultantReviewModelCopyWith<$Res> {
__$ConsultantReviewModelCopyWithImpl(this._self, this._then);
final _ConsultantReviewModel _self;
final $Res Function(_ConsultantReviewModel) _then;
/// Create a copy of ConsultantReviewModel
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$Res call({
Object? planName = null,
Object? planDuration = null,
Object? currentPlanDay = null,
Object? consultantComments = null,
}) {
return _then(_ConsultantReviewModel(
planName: null == planName
? _self.planName
: planName // ignore: cast_nullable_to_non_nullable
as String,
planDuration: null == planDuration
? _self.planDuration
: planDuration // ignore: cast_nullable_to_non_nullable
as int,
currentPlanDay: null == currentPlanDay
? _self.currentPlanDay
: currentPlanDay // ignore: cast_nullable_to_non_nullable
as int,
consultantComments: null == consultantComments
? _self.consultantComments
: consultantComments // ignore: cast_nullable_to_non_nullable
as ConsultantComments,
));
}
/// Create a copy of ConsultantReviewModel
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$ConsultantCommentsCopyWith<$Res> get consultantComments {
return $ConsultantCommentsCopyWith<$Res>(_self.consultantComments, (value) {
return _then(_self.copyWith(consultantComments: value));
});
}
}
/// @nodoc
mixin _$ConsultantComments {
String? get dayComment;
List<MealComment> get mealsComments;
List<PortionComment> get portionsComments;
/// Create a copy of ConsultantComments
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$ConsultantCommentsCopyWith<ConsultantComments> get copyWith =>
_$ConsultantCommentsCopyWithImpl<ConsultantComments>(
this as ConsultantComments, _$identity);
/// Serializes this ConsultantComments to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is ConsultantComments &&
(identical(other.dayComment, dayComment) ||
other.dayComment == dayComment) &&
const DeepCollectionEquality()
.equals(other.mealsComments, mealsComments) &&
const DeepCollectionEquality()
.equals(other.portionsComments, portionsComments));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
dayComment,
const DeepCollectionEquality().hash(mealsComments),
const DeepCollectionEquality().hash(portionsComments));
@override
String toString() {
return 'ConsultantComments(dayComment: $dayComment, mealsComments: $mealsComments, portionsComments: $portionsComments)';
}
}
/// @nodoc
abstract mixin class $ConsultantCommentsCopyWith<$Res> {
factory $ConsultantCommentsCopyWith(
ConsultantComments value, $Res Function(ConsultantComments) _then) =
_$ConsultantCommentsCopyWithImpl;
@useResult
$Res call(
{String? dayComment,
List<MealComment> mealsComments,
List<PortionComment> portionsComments});
}
/// @nodoc
class _$ConsultantCommentsCopyWithImpl<$Res>
implements $ConsultantCommentsCopyWith<$Res> {
_$ConsultantCommentsCopyWithImpl(this._self, this._then);
final ConsultantComments _self;
final $Res Function(ConsultantComments) _then;
/// Create a copy of ConsultantComments
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? dayComment = freezed,
Object? mealsComments = null,
Object? portionsComments = null,
}) {
return _then(_self.copyWith(
dayComment: freezed == dayComment
? _self.dayComment
: dayComment // ignore: cast_nullable_to_non_nullable
as String?,
mealsComments: null == mealsComments
? _self.mealsComments
: mealsComments // ignore: cast_nullable_to_non_nullable
as List<MealComment>,
portionsComments: null == portionsComments
? _self.portionsComments
: portionsComments // ignore: cast_nullable_to_non_nullable
as List<PortionComment>,
));
}
}
/// @nodoc
@JsonSerializable()
class _ConsultantComments implements ConsultantComments {
_ConsultantComments(
{required this.dayComment,
required final List<MealComment> mealsComments,
required final List<PortionComment> portionsComments})
: _mealsComments = mealsComments,
_portionsComments = portionsComments;
factory _ConsultantComments.fromJson(Map<String, dynamic> json) =>
_$ConsultantCommentsFromJson(json);
@override
final String? dayComment;
final List<MealComment> _mealsComments;
@override
List<MealComment> get mealsComments {
if (_mealsComments is EqualUnmodifiableListView) return _mealsComments;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_mealsComments);
}
final List<PortionComment> _portionsComments;
@override
List<PortionComment> get portionsComments {
if (_portionsComments is EqualUnmodifiableListView)
return _portionsComments;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_portionsComments);
}
/// Create a copy of ConsultantComments
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$ConsultantCommentsCopyWith<_ConsultantComments> get copyWith =>
__$ConsultantCommentsCopyWithImpl<_ConsultantComments>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$ConsultantCommentsToJson(
this,
);
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _ConsultantComments &&
(identical(other.dayComment, dayComment) ||
other.dayComment == dayComment) &&
const DeepCollectionEquality()
.equals(other._mealsComments, _mealsComments) &&
const DeepCollectionEquality()
.equals(other._portionsComments, _portionsComments));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
dayComment,
const DeepCollectionEquality().hash(_mealsComments),
const DeepCollectionEquality().hash(_portionsComments));
@override
String toString() {
return 'ConsultantComments(dayComment: $dayComment, mealsComments: $mealsComments, portionsComments: $portionsComments)';
}
}
/// @nodoc
abstract mixin class _$ConsultantCommentsCopyWith<$Res>
implements $ConsultantCommentsCopyWith<$Res> {
factory _$ConsultantCommentsCopyWith(
_ConsultantComments value, $Res Function(_ConsultantComments) _then) =
__$ConsultantCommentsCopyWithImpl;
@override
@useResult
$Res call(
{String? dayComment,
List<MealComment> mealsComments,
List<PortionComment> portionsComments});
}
/// @nodoc
class __$ConsultantCommentsCopyWithImpl<$Res>
implements _$ConsultantCommentsCopyWith<$Res> {
__$ConsultantCommentsCopyWithImpl(this._self, this._then);
final _ConsultantComments _self;
final $Res Function(_ConsultantComments) _then;
/// Create a copy of ConsultantComments
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$Res call({
Object? dayComment = freezed,
Object? mealsComments = null,
Object? portionsComments = null,
}) {
return _then(_ConsultantComments(
dayComment: freezed == dayComment
? _self.dayComment
: dayComment // ignore: cast_nullable_to_non_nullable
as String?,
mealsComments: null == mealsComments
? _self._mealsComments
: mealsComments // ignore: cast_nullable_to_non_nullable
as List<MealComment>,
portionsComments: null == portionsComments
? _self._portionsComments
: portionsComments // ignore: cast_nullable_to_non_nullable
as List<PortionComment>,
));
}
}
/// @nodoc
mixin _$MealComment {
String get comment;
int get typeNo;
/// Create a copy of MealComment
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$MealCommentCopyWith<MealComment> get copyWith =>
_$MealCommentCopyWithImpl<MealComment>(this as MealComment, _$identity);
/// Serializes this MealComment to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is MealComment &&
(identical(other.comment, comment) || other.comment == comment) &&
(identical(other.typeNo, typeNo) || other.typeNo == typeNo));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, comment, typeNo);
@override
String toString() {
return 'MealComment(comment: $comment, typeNo: $typeNo)';
}
}
/// @nodoc
abstract mixin class $MealCommentCopyWith<$Res> {
factory $MealCommentCopyWith(
MealComment value, $Res Function(MealComment) _then) =
_$MealCommentCopyWithImpl;
@useResult
$Res call({String comment, int typeNo});
}
/// @nodoc
class _$MealCommentCopyWithImpl<$Res> implements $MealCommentCopyWith<$Res> {
_$MealCommentCopyWithImpl(this._self, this._then);
final MealComment _self;
final $Res Function(MealComment) _then;
/// Create a copy of MealComment
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? comment = null,
Object? typeNo = null,
}) {
return _then(_self.copyWith(
comment: null == comment
? _self.comment
: comment // ignore: cast_nullable_to_non_nullable
as String,
typeNo: null == typeNo
? _self.typeNo
: typeNo // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// @nodoc
@JsonSerializable()
class _MealComment implements MealComment {
_MealComment({required this.comment, required this.typeNo});
factory _MealComment.fromJson(Map<String, dynamic> json) =>
_$MealCommentFromJson(json);
@override
final String comment;
@override
final int typeNo;
/// Create a copy of MealComment
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$MealCommentCopyWith<_MealComment> get copyWith =>
__$MealCommentCopyWithImpl<_MealComment>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$MealCommentToJson(
this,
);
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _MealComment &&
(identical(other.comment, comment) || other.comment == comment) &&
(identical(other.typeNo, typeNo) || other.typeNo == typeNo));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, comment, typeNo);
@override
String toString() {
return 'MealComment(comment: $comment, typeNo: $typeNo)';
}
}
/// @nodoc
abstract mixin class _$MealCommentCopyWith<$Res>
implements $MealCommentCopyWith<$Res> {
factory _$MealCommentCopyWith(
_MealComment value, $Res Function(_MealComment) _then) =
__$MealCommentCopyWithImpl;
@override
@useResult
$Res call({String comment, int typeNo});
}
/// @nodoc
class __$MealCommentCopyWithImpl<$Res> implements _$MealCommentCopyWith<$Res> {
__$MealCommentCopyWithImpl(this._self, this._then);
final _MealComment _self;
final $Res Function(_MealComment) _then;
/// Create a copy of MealComment
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$Res call({
Object? comment = null,
Object? typeNo = null,
}) {
return _then(_MealComment(
comment: null == comment
? _self.comment
: comment // ignore: cast_nullable_to_non_nullable
as String,
typeNo: null == typeNo
? _self.typeNo
: typeNo // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// @nodoc
mixin _$PortionComment {
@JsonKey(name: 'id')
int get portionId;
String get comment;
/// Create a copy of PortionComment
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$PortionCommentCopyWith<PortionComment> get copyWith =>
_$PortionCommentCopyWithImpl<PortionComment>(
this as PortionComment, _$identity);
/// Serializes this PortionComment to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is PortionComment &&
(identical(other.portionId, portionId) ||
other.portionId == portionId) &&
(identical(other.comment, comment) || other.comment == comment));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, portionId, comment);
@override
String toString() {
return 'PortionComment(portionId: $portionId, comment: $comment)';
}
}
/// @nodoc
abstract mixin class $PortionCommentCopyWith<$Res> {
factory $PortionCommentCopyWith(
PortionComment value, $Res Function(PortionComment) _then) =
_$PortionCommentCopyWithImpl;
@useResult
$Res call({@JsonKey(name: 'id') int portionId, String comment});
}
/// @nodoc
class _$PortionCommentCopyWithImpl<$Res>
implements $PortionCommentCopyWith<$Res> {
_$PortionCommentCopyWithImpl(this._self, this._then);
final PortionComment _self;
final $Res Function(PortionComment) _then;
/// Create a copy of PortionComment
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? portionId = null,
Object? comment = null,
}) {
return _then(_self.copyWith(
portionId: null == portionId
? _self.portionId
: portionId // ignore: cast_nullable_to_non_nullable
as int,
comment: null == comment
? _self.comment
: comment // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
@JsonSerializable()
class _PortionComment implements PortionComment {
_PortionComment(
{@JsonKey(name: 'id') required this.portionId, required this.comment});
factory _PortionComment.fromJson(Map<String, dynamic> json) =>
_$PortionCommentFromJson(json);
@override
@JsonKey(name: 'id')
final int portionId;
@override
final String comment;
/// Create a copy of PortionComment
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$PortionCommentCopyWith<_PortionComment> get copyWith =>
__$PortionCommentCopyWithImpl<_PortionComment>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$PortionCommentToJson(
this,
);
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _PortionComment &&
(identical(other.portionId, portionId) ||
other.portionId == portionId) &&
(identical(other.comment, comment) || other.comment == comment));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, portionId, comment);
@override
String toString() {
return 'PortionComment(portionId: $portionId, comment: $comment)';
}
}
/// @nodoc
abstract mixin class _$PortionCommentCopyWith<$Res>
implements $PortionCommentCopyWith<$Res> {
factory _$PortionCommentCopyWith(
_PortionComment value, $Res Function(_PortionComment) _then) =
__$PortionCommentCopyWithImpl;
@override
@useResult
$Res call({@JsonKey(name: 'id') int portionId, String comment});
}
/// @nodoc
class __$PortionCommentCopyWithImpl<$Res>
implements _$PortionCommentCopyWith<$Res> {
__$PortionCommentCopyWithImpl(this._self, this._then);
final _PortionComment _self;
final $Res Function(_PortionComment) _then;
/// Create a copy of PortionComment
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$Res call({
Object? portionId = null,
Object? comment = null,
}) {
return _then(_PortionComment(
portionId: null == portionId
? _self.portionId
: portionId // ignore: cast_nullable_to_non_nullable
as int,
comment: null == comment
? _self.comment
: comment // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
// dart format on

View File

@@ -1,67 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'consultant_review_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_ConsultantReviewModel _$ConsultantReviewModelFromJson(
Map<String, dynamic> json) =>
_ConsultantReviewModel(
planName: json['planName'] as String,
planDuration: (json['planDuration'] as num).toInt(),
currentPlanDay: (json['currentPlanDay'] as num).toInt(),
consultantComments: ConsultantComments.fromJson(
json['consultantComments'] as Map<String, dynamic>),
);
Map<String, dynamic> _$ConsultantReviewModelToJson(
_ConsultantReviewModel instance) =>
<String, dynamic>{
'planName': instance.planName,
'planDuration': instance.planDuration,
'currentPlanDay': instance.currentPlanDay,
'consultantComments': instance.consultantComments,
};
_ConsultantComments _$ConsultantCommentsFromJson(Map<String, dynamic> json) =>
_ConsultantComments(
dayComment: json['dayComment'] as String?,
mealsComments: (json['mealsComments'] as List<dynamic>)
.map((e) => MealComment.fromJson(e as Map<String, dynamic>))
.toList(),
portionsComments: (json['portionsComments'] as List<dynamic>)
.map((e) => PortionComment.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$ConsultantCommentsToJson(_ConsultantComments instance) =>
<String, dynamic>{
'dayComment': instance.dayComment,
'mealsComments': instance.mealsComments,
'portionsComments': instance.portionsComments,
};
_MealComment _$MealCommentFromJson(Map<String, dynamic> json) => _MealComment(
comment: json['comment'] as String,
typeNo: (json['typeNo'] as num).toInt(),
);
Map<String, dynamic> _$MealCommentToJson(_MealComment instance) =>
<String, dynamic>{
'comment': instance.comment,
'typeNo': instance.typeNo,
};
_PortionComment _$PortionCommentFromJson(Map<String, dynamic> json) =>
_PortionComment(
portionId: (json['id'] as num).toInt(),
comment: json['comment'] as String,
);
Map<String, dynamic> _$PortionCommentToJson(_PortionComment instance) =>
<String, dynamic>{
'id': instance.portionId,
'comment': instance.comment,
};

View File

@@ -136,15 +136,7 @@ class DiaryCubit extends Cubit<DiaryState> {
final fifthMeal = MealModel(id: 0, time: '0', typeNumber: 5, satiety: 0, mood: 0, portions: fifthMealPortions ?? [], photos: []);
emit(
DiaryState.loadedTomorrowWithDiet(
diary: diaryDay.copyWith(
meals: [
firstMeal,
secondMeal,
thirdMeal,
fourthMeal,
fifthMeal,
],
),
diary: diaryDay.copyWith(meals: [firstMeal, secondMeal, thirdMeal, fourthMeal, fifthMeal]),
selectedDay: selectedDay,
selectedMeal: selectedMeal,
),
@@ -156,7 +148,7 @@ class DiaryCubit extends Cubit<DiaryState> {
Future<void> fetchDiary([bool needLoading = false, int? diaryId, int? currentDay]) async {
this.diaryId ??= diaryId;
final isConsultantView = this.diaryId != null;
if (needLoading) {
emit(const DiaryState.loading());
selectedMeal = 1;
@@ -179,17 +171,22 @@ class DiaryCubit extends Cubit<DiaryState> {
int? currentPlanDay;
String? planName;
if (diaryDay.chosenPlanId != null) {
consultantReview = await diaryRepository.getDiaryComments(planId: diaryDay.chosenPlanId!, diaryId: diaryDay.id);
final chosenPlanModel = await chatRepository.getChosenPlan(id: diaryDay.chosenPlanId!);
if (!isConsultantView &&
(diaryDay.typeOfDiary.diaryState == DiaryStateEnum.lastDayCommented || diaryDay.typeOfDiary.diaryState == DiaryStateEnum.commented)) {
await diaryRepository.viewCommentOfDiaryDayByConsultant(diaryId: diaryDay.id);
}
consultantReview = await diaryRepository.getDiaryComments(planId: diaryDay.chosenPlanId!, diaryId: diaryDay.id);
planDuration = chosenPlanModel.plan.duration;
planName = chosenPlanModel.plan.name;
currentPlanDay = currentDay ?? DateTime.now().difference(chosenPlanModel.startDate).inDays + 1;
currentPlanDay = consultantReview.currentPlanDay;
}
final nutrition = await getNutrition(diaryDay.id);
final isDiaryFinished = diaryDay.typeOfDiary.diaryState == DiaryStateEnum.finished || diaryDay.typeOfDiary.diaryState == DiaryStateEnum.lastDayFinished;
final isDiaryCommented = diaryDay.typeOfDiary.diaryState == DiaryStateEnum.commented ||
final isDiaryCommented =
diaryDay.typeOfDiary.diaryState == DiaryStateEnum.commented ||
diaryDay.typeOfDiary.diaryState == DiaryStateEnum.lastDayCommented ||
diaryDay.typeOfDiary.diaryState == DiaryStateEnum.reviewedByUser;
emit(
@@ -198,7 +195,7 @@ class DiaryCubit extends Cubit<DiaryState> {
nutrition: nutrition,
selectedDay: selectedDay,
selectedMeal: selectedMeal,
isConsultantView: this.diaryId != null,
isConsultantView: isConsultantView,
consultantReview: consultantReview,
isDiaryFinished: isDiaryFinished,
isDiaryCommented: isDiaryCommented,
@@ -206,7 +203,7 @@ class DiaryCubit extends Cubit<DiaryState> {
chosenPlanDuration: planDuration,
chosenPlanName: planName,
isMoodEnabled: isMoodEnabled,
isWakeUpAndRetireEnabled: isWakeUpAndRetireTimeEnabled
isWakeUpAndRetireEnabled: isWakeUpAndRetireTimeEnabled,
),
);
} catch (e, st) {
@@ -215,17 +212,8 @@ class DiaryCubit extends Cubit<DiaryState> {
}
}
Future<void> finishDiaryDay({
required int diaryId,
required String wakeUp,
required String retire,
}) async {
await diaryRepository.finishDiaryDay(
diaryId: diaryId,
wakeUp: wakeUp,
retire: retire,
comment: comment.isEmpty ? null : comment,
);
Future<void> finishDiaryDay({required int diaryId, required String wakeUp, required String retire}) async {
await diaryRepository.finishDiaryDay(diaryId: diaryId, wakeUp: wakeUp, retire: retire, comment: comment.isEmpty ? null : comment);
await fetchDiary();
}
@@ -241,6 +229,7 @@ class DiaryCubit extends Cubit<DiaryState> {
required int quantity,
int? productNutritionalValueId,
}) async {
emit((state as DiaryStateLoaded).copyWith(isLoadingAddingProductInMeal: true));
await diaryRepository.addPortionToMeal(
diaryId: diaryId,
mealId: mealId,

View File

@@ -29,6 +29,7 @@ abstract class DiaryState with _$DiaryState {
@Default(false) bool isDiaryCommented,
@Default(false) bool isWakeUpAndRetireEnabled,
@Default(false) bool isMoodEnabled,
@Default(false) bool isLoadingAddingProductInMeal,
}) = DiaryStateLoaded;
const factory DiaryState.loadedTomorrowWithDiet({

View File

@@ -0,0 +1,26 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class ProductLoaderWidget extends StatefulWidget {
const ProductLoaderWidget({
super.key,
});
@override
State<ProductLoaderWidget> createState() => _ProductLoaderWidgetState();
}
class _ProductLoaderWidgetState extends State<ProductLoaderWidget> {
@override
Widget build(BuildContext context) {
return ListTile(
horizontalTitleGap: 0,
contentPadding: const EdgeInsets.only(right: 24, left: 12),
title: const CupertinoActivityIndicator(),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
tileColor: const Color(0xFFF6FAFF),
);
}
}

View File

@@ -1,5 +1,6 @@
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/product_loader_widget.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/product_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -25,6 +26,11 @@ class _ProductsListWidgetState extends State<ProductsListWidget> {
ProductWidget(portion: state.diary.meals[mealIndex].portions[i]),
i < state.diary.meals[mealIndex].portions.length - 1 ? const SizedBox(height: 8) : const SizedBox.shrink(),
],
if (state.isLoadingAddingProductInMeal)
const Padding(
padding: EdgeInsets.only(top: 8),
child: ProductLoaderWidget(),
),
],
);
}

View File

@@ -297,4 +297,12 @@ class DiaryRepository {
rethrow;
}
}
Future<void> viewCommentOfDiaryDayByConsultant({required int diaryId}) async {
try {
await diaryService.viewCommentOfDiaryDayByConsultant(diaryId: diaryId);
} catch (e) {
rethrow;
}
}
}

View File

@@ -150,4 +150,9 @@ abstract class DiaryService {
@Path() required int planId,
@Path() required int diaryId,
});
@POST('/api/diary/{diaryId}/consultant/comment/view')
Future<void> viewCommentOfDiaryDayByConsultant({
@Path() required int diaryId,
});
}

View File

@@ -1,4 +1,5 @@
import 'package:easy_debounce/easy_debounce.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/widgets/add_item_button.dart';
import 'package:eatvio_app/core/widgets/custom_search_bar.dart';
import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart';
@@ -32,24 +33,12 @@ class FeedScreen extends StatelessWidget {
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<RecipesCubit>(
create: (context) => sl<RecipesCubit>()..loadRecipesList(),
),
BlocProvider<FeedProfileCubit>(
create: (context) => sl<FeedProfileCubit>()..getProfile(),
),
BlocProvider<FeedCubit>(
create: (context) => sl<FeedCubit>()..loadFeedBlogPostList(),
),
BlocProvider<DietsCubit>(
create: (context) => sl<DietsCubit>()..loadDiets(),
),
BlocProvider<PlansCubit>(
create: (context) => sl<PlansCubit>()..loadPlans(),
),
BlocProvider<ConsultantsCubit>(
create: (context) => sl<ConsultantsCubit>()..getConsultants(),
),
BlocProvider<RecipesCubit>(create: (context) => sl<RecipesCubit>()..loadRecipesList()),
BlocProvider<FeedProfileCubit>(create: (context) => sl<FeedProfileCubit>()..getProfile()),
BlocProvider<FeedCubit>(create: (context) => sl<FeedCubit>()..loadFeedBlogPostList()),
BlocProvider<DietsCubit>(create: (context) => sl<DietsCubit>()..loadDiets()),
if (!hidePlans) BlocProvider<PlansCubit>(create: (context) => sl<PlansCubit>()..loadPlans()),
BlocProvider<ConsultantsCubit>(create: (context) => sl<ConsultantsCubit>()..getConsultants()),
],
child: const FeedScreenContent(),
);
@@ -69,7 +58,8 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
final dietsController = TextEditingController();
final plansController = TextEditingController();
final consultantsController = TextEditingController();
final List<String> chipsLabelList = ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты'];
late final List<String> chipsLabelList;
int _selectedChipIndex = 0;
final blogPostsScrollController = ScrollController();
final recipesScrollController = ScrollController();
@@ -78,12 +68,13 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
final consultantsScrollController = ScrollController();
@override
void initState() {
void initState() {
// Initialize chipsLabelList based on hidePlans setting
chipsLabelList = hidePlans ? ['Все', 'Рецепты', 'Диеты', 'Консультанты'] : ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты'];
consultantsScrollController.addListener(_onScrollConsultants);
super.initState();
}
}
void _onScrollConsultants() {
if (consultantsScrollController.offset >= consultantsScrollController.position.maxScrollExtent && !consultantsScrollController.position.outOfRange) {}
@@ -108,45 +99,36 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
heroTag: 'recipesTagsFilter',
backgroundColor: appTheme.blueA400,
shape: const CircleBorder(),
child: const Icon(
Icons.filter_list_rounded,
color: Colors.white,
),
child: const Icon(Icons.filter_list_rounded, color: Colors.white),
onPressed: () async {
final recipesCubit = context.read<RecipesCubit>();
await showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (context) => TagsModalWidget(
recipesCubit: recipesCubit,
),
builder: (context) => TagsModalWidget(recipesCubit: recipesCubit),
);
recipesCubit.loadRecipesList();
},
)
: null,
appBar: AppBar(
backgroundColor: Colors.white,
toolbarHeight: 0,
),
appBar: AppBar(backgroundColor: Colors.white, toolbarHeight: 0),
backgroundColor: Colors.grey.shade200,
body: NestedScrollView(
controller: switch (_selectedChipIndex) {
0 => blogPostsScrollController,
1 => recipesScrollController,
2 => dietsScrollController,
3 => plansScrollController,
4 => consultantsScrollController,
_ => blogPostsScrollController
3 when !hidePlans => plansScrollController,
3 when hidePlans => consultantsScrollController,
4 when !hidePlans => consultantsScrollController,
_ => blogPostsScrollController,
},
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return [
BlocBuilder<FeedProfileCubit, ProfileState>(
builder: (context, state) {
return SliverAppBar(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(bottom: Radius.circular(12)),
),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(bottom: Radius.circular(12))),
backgroundColor: Colors.white,
foregroundColor: Colors.white,
automaticallyImplyLeading: false,
@@ -172,78 +154,71 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
searchController: switch (_selectedChipIndex) {
1 => recipesController,
2 => dietsController,
3 => plansController,
4 => consultantsController,
3 when !hidePlans => plansController,
3 when hidePlans => consultantsController,
4 when !hidePlans => consultantsController,
_ => postsController,
},
onChanged: (query) {
if (query.length > 2) {
EasyDebounce.debounce(
'searchFeed',
const Duration(milliseconds: 500),
() {
switch (_selectedChipIndex) {
case 0:
context.read<FeedCubit>().loadFeedBlogPostList(query: query);
break;
case 1:
context.read<RecipesCubit>().loadRecipesList(query: query);
break;
case 2:
context.read<DietsCubit>().loadDiets(query: query);
break;
case 3:
context.read<PlansCubit>().loadPlans(query: query);
break;
case 4:
context.read<ConsultantsCubit>().getConsultants();
break;
}
},
);
EasyDebounce.debounce('searchFeed', const Duration(milliseconds: 500), () {
switch (_selectedChipIndex) {
case 0:
context.read<FeedCubit>().loadFeedBlogPostList(query: query);
break;
case 1:
context.read<RecipesCubit>().loadRecipesList(query: query);
break;
case 2:
context.read<DietsCubit>().loadDiets(query: query);
break;
case 3 when !hidePlans:
context.read<PlansCubit>().loadPlans(query: query);
break;
case 3 when hidePlans:
case 4 when !hidePlans:
context.read<ConsultantsCubit>().getConsultants();
break;
}
});
} else {
EasyDebounce.debounce(
'searchFeed',
const Duration(milliseconds: 500),
() {
switch (_selectedChipIndex) {
case 0:
context.read<FeedCubit>().loadFeedBlogPostList();
break;
case 1:
context.read<RecipesCubit>().loadRecipesList();
break;
case 2:
context.read<DietsCubit>().loadDiets();
break;
case 3:
context.read<PlansCubit>().loadPlans();
break;
case 4:
context.read<ConsultantsCubit>().getConsultants();
break;
}
},
);
EasyDebounce.debounce('searchFeed', const Duration(milliseconds: 500), () {
switch (_selectedChipIndex) {
case 0:
context.read<FeedCubit>().loadFeedBlogPostList();
break;
case 1:
context.read<RecipesCubit>().loadRecipesList();
break;
case 2:
context.read<DietsCubit>().loadDiets();
break;
case 3 when !hidePlans:
context.read<PlansCubit>().loadPlans();
break;
case 3 when hidePlans:
case 4 when !hidePlans:
context.read<ConsultantsCubit>().getConsultants();
break;
}
});
}
},
),
const SizedBox(width: 12),
switch (_selectedChipIndex) {
1 => AddItemButton(
onTap: () => GoRouter.of(context).pushNamed(RecipeCreateScreen.name),
),
1 => AddItemButton(onTap: () => GoRouter.of(context).pushNamed(RecipeCreateScreen.name)),
2 => AddItemButton(
onTap: state.profile?.roles.contains('consultant') ?? false
? () async {
await GoRouter.of(context).pushNamed(CreateDietScreen.name);
}
: null,
),
3 => AddItemButton(
onTap: state.profile?.roles.contains('consultant') ?? false ? () => GoRouter.of(context).pushNamed(CreatePlanScreen.name) : null,
),
_ => const AddItemButton()
onTap: state.profile?.roles.contains('consultant') ?? false
? () async {
await GoRouter.of(context).pushNamed(CreateDietScreen.name);
}
: null,
),
3 when !hidePlans => AddItemButton(
onTap: state.profile?.roles.contains('consultant') ?? false ? () => GoRouter.of(context).pushNamed(CreatePlanScreen.name) : null,
),
_ => const AddItemButton(),
},
],
),
@@ -256,8 +231,9 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
0 => const BlogPosts(),
1 => const Recipes(),
2 => const Diets(),
3 => const Plans(),
4 => const Consultants(),
3 when !hidePlans => const Plans(),
3 when hidePlans => const Consultants(),
4 when !hidePlans => const Consultants(),
_ => Center(child: Text(chipsLabelList[_selectedChipIndex])),
},
),

View File

@@ -2,12 +2,7 @@ import 'package:eatvio_app/features/feed/presentation/widgets/choice_chip_widget
import 'package:flutter/widgets.dart';
class ChipGroup extends StatelessWidget {
const ChipGroup({
super.key,
required this.selectedChipIndex,
required this.chipsLabelList,
required this.setSelectedChipIndex,
});
const ChipGroup({super.key, required this.selectedChipIndex, required this.chipsLabelList, required this.setSelectedChipIndex});
final int selectedChipIndex;
final List<String> chipsLabelList;
final void Function(int) setSelectedChipIndex;
@@ -22,7 +17,7 @@ class ChipGroup extends StatelessWidget {
child: Row(
children: [
...List.generate(
5,
chipsLabelList.length,
(index) => Padding(
padding: const EdgeInsets.only(right: 8.0),
child: ChoiceChipWidget(

View File

@@ -31,9 +31,7 @@ class ConsultationsArchiveCubit extends Cubit<ConsultationsArchiveState> {
dateComparison: state.periodFilterStatus == PeriodFilterStatus.all ? null : FiltersComparisonEnum.greaterThanOrEqual,
stateComparison: state.consultationFilterStatus == ConsultationFilterStatus.all
? null
: state.consultationFilterStatus == ConsultationFilterStatus.inProgress
? FiltersComparisonEnum.notInArray
: FiltersComparisonEnum.equal,
: FiltersComparisonEnum.equal,
);
emit(state.copyWith(consultations: consultations, isLoading: false));
} catch (e) {
@@ -67,20 +65,17 @@ class ConsultationsArchiveCubit extends Cubit<ConsultationsArchiveState> {
enum ConsultationFilterStatus {
all,
canceled,
finished,
inProgress;
finished;
String get name => switch (this) {
ConsultationFilterStatus.all => 'Все консультации',
ConsultationFilterStatus.canceled => 'Отменён',
ConsultationFilterStatus.finished => 'Завершён',
ConsultationFilterStatus.inProgress => 'В процессе',
};
String? get value => switch (this) {
ConsultationFilterStatus.all => null,
ConsultationFilterStatus.canceled => 'canceled_by_the_initiator',
ConsultationFilterStatus.finished => 'charge_was_collected',
ConsultationFilterStatus.inProgress => 'active',
};
}

View File

@@ -14,7 +14,7 @@ abstract class ConsultationsArchiveState with _$ConsultationsArchiveState {
@Default([]) List<ConsultationModel> consultations,
@Default(false) isLoading,
@Default(null) String? error,
@Default(null) PeriodFilterStatus? periodFilterStatus,
@Default(PeriodFilterStatus.all) PeriodFilterStatus? periodFilterStatus,
@Default(null) ConsultationFilterStatus? consultationFilterStatus,
required StatisticModel statisticModel,
}) = _ConsultationsArchiveState;

View File

@@ -10,7 +10,7 @@ abstract class ConsultantFinancesState with _$ConsultantFinancesState {
@Default([]) List<ConsultantFinanceModel> financesModels,
@Default(false) isLoading,
@Default(null) String? error,
@Default(null) PeriodFilterStatus? periodFilterStatus,
@Default(PeriodFilterStatus.all) PeriodFilterStatus? periodFilterStatus,
}) = _ConsultantFinancesState;
}

View File

@@ -10,6 +10,6 @@ abstract class UserPurchasesState with _$UserPurchasesState {
@Default([]) List<UserPurchasesModel> purchasesModels,
@Default(false) isLoading,
@Default(null) String? error,
@Default(null) PeriodFilterStatus? periodFilterStatus,
@Default(PeriodFilterStatus.all) PeriodFilterStatus? periodFilterStatus,
}) = _UserPurchasesState;
}

View File

@@ -96,7 +96,7 @@ class _ConsultantFinancesScreenState extends State<ConsultantFinancesScreen> {
if (value == null) return;
context.read<ConsultantFinancesCubit>().setFinancesPeriod(value);
},
value: state.periodFilterStatus ?? PeriodFilterStatus.values.first,
value: state.periodFilterStatus ?? PeriodFilterStatus.all,
),
const SizedBox(height: 12),
if (state.financesModels.isNotEmpty)
@@ -129,7 +129,7 @@ class _ConsultantFinancesScreenState extends State<ConsultantFinancesScreen> {
);
}),
Text(
state.financesModels[index].startDate.format('yyyy.MM.dd'),
state.financesModels[index].startDate.format('dd.MM.yy'),
style: const TextStyle(
color: Color(0xFF6D6F71),
height: 1,

View File

@@ -195,7 +195,7 @@ class _ConsultationsArchiveScreenState extends State<ConsultationsArchiveScreen>
),
),
Text(
state.consultations[index].startDate.format('yyyy.MM.dd'),
state.consultations[index].startDate.format('dd.MM.yy'),
style: const TextStyle(
color: Color(0xFF6D6F71),
height: 1,

View File

@@ -10,6 +10,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:flutter_svg/svg.dart';
import 'package:http/http.dart' as http;
import 'package:eatvio_app/core/env_config.dart';
class InternetConnectionCheckerScreen extends StatefulWidget {
const InternetConnectionCheckerScreen({super.key, required this.child});
@@ -66,12 +67,7 @@ class _InternetConnectionCheckerScreenState extends State<InternetConnectionChec
return false;
}
final uris = [
Uri.parse('https://google.com'),
Uri.parse('https://yandex.ru'),
Uri.parse('https://dev2.eatvio.com'),
Uri.parse('https://eatvio.ru'),
];
final uris = [Uri.parse(EnvConfig.googleCheckUrl), Uri.parse(EnvConfig.yandexCheckUrl), Uri.parse(EnvConfig.devBaseUrl), Uri.parse(EnvConfig.baseUrl)];
for (final uri in uris) {
try {
final response = await http.get(uri).timeout(const Duration(seconds: 3));
@@ -98,13 +94,9 @@ class _InternetConnectionCheckerScreenState extends State<InternetConnectionChec
if (!_hasInternet)
NoInternetScreen(
onTryAgain: () async {
EasyDebounce.debounce(
'internet connection check',
const Duration(seconds: 2),
() async {
await _checkConnection();
},
);
EasyDebounce.debounce('internet connection check', const Duration(seconds: 2), () async {
await _checkConnection();
});
},
),
],
@@ -129,9 +121,7 @@ class NoInternetScreen extends StatelessWidget {
child: Text(
'Отсутствует подключение \nк интернету',
textAlign: TextAlign.center,
style: TextStyles.headingXLSemibold.copyWith(
color: appTheme.black,
),
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
),
const SizedBox(height: 16),
@@ -140,9 +130,7 @@ class NoInternetScreen extends StatelessWidget {
child: Text(
'Проверьте соединение с сетью\nи попробуйте ещё раз',
textAlign: TextAlign.center,
style: TextStyles.textMMedium.copyWith(
color: appTheme.gray,
),
style: TextStyles.textMMedium.copyWith(color: appTheme.gray),
),
),
const SizedBox(height: 20),
@@ -153,10 +141,7 @@ class NoInternetScreen extends StatelessWidget {
height: 52,
text: 'Попробовать ещё раз',
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
side: const WidgetStatePropertyAll(BorderSide.none),
),
buttonStyle: ButtonStyle(backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), side: const WidgetStatePropertyAll(BorderSide.none)),
),
),
const Spacer(),

View File

@@ -37,18 +37,20 @@ class MenuScreen extends StatelessWidget {
if (profileModel.roles.contains('consultant')) _divider(context),
if (profileModel.roles.contains('consultant'))
_tile(leading: ImageConstant.icnPlans, title: 'Мои планы', onTap: () => GoRouter.of(context).push(MyPlansScreen.path)),
_divider(context),
_tile(
leading: 'assets/icons/finance.svg',
title: 'Мои финансы',
onTap: () {
if (profileModel.roles.contains('consultant')) {
GoRouter.of(context).push(ConsultantFinancesScreen.path);
return;
}
GoRouter.of(context).push(UserPurchasesScreen.path);
},
),
if (!hidePlans) ...[
_divider(context),
_tile(
leading: 'assets/icons/finance.svg',
title: 'Мои финансы',
onTap: () {
if (profileModel.roles.contains('consultant')) {
GoRouter.of(context).push(ConsultantFinancesScreen.path);
return;
}
GoRouter.of(context).push(UserPurchasesScreen.path);
},
),
],
_divider(context),
_tile(leading: ImageConstant.icnGlossary, title: 'Глоссарий', onTap: () => GoRouter.of(context).push('/glossary-screen')),
_divider(context),

View File

@@ -1,8 +1,7 @@
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
sealed class BaseNotificationModel {
abstract final NotificationType type;
abstract final bool isRead;
abstract final DateTime createdAt;
}
class PlanSubscriptionStartedToAuthorNotification implements BaseNotificationModel {
@@ -13,6 +12,8 @@ class PlanSubscriptionStartedToAuthorNotification implements BaseNotificationMod
final String planName;
final String customerName;
final String chatId;
@override
final DateTime createdAt;
PlanSubscriptionStartedToAuthorNotification({
required this.type,
@@ -20,15 +21,17 @@ class PlanSubscriptionStartedToAuthorNotification implements BaseNotificationMod
required this.customerName,
required this.chatId,
required this.isRead,
required this.createdAt,
});
factory PlanSubscriptionStartedToAuthorNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
factory PlanSubscriptionStartedToAuthorNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return PlanSubscriptionStartedToAuthorNotification(
type: type,
planName: json['plan_name'],
customerName: json['user_name'],
chatId: json['chat_id'],
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
}
}
@@ -40,20 +43,24 @@ class PlanSubscriptionCanceledToAuthorNotification implements BaseNotificationMo
final bool isRead;
final String planName;
final String customerName;
@override
final DateTime createdAt;
PlanSubscriptionCanceledToAuthorNotification({
required this.type,
required this.planName,
required this.customerName,
required this.isRead,
required this.createdAt,
});
factory PlanSubscriptionCanceledToAuthorNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
factory PlanSubscriptionCanceledToAuthorNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return PlanSubscriptionCanceledToAuthorNotification(
type: type,
planName: json['plan_name'],
customerName: json['user_name'],
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
}
}
@@ -65,20 +72,18 @@ class ChosenPlanStartedToClientNotification implements BaseNotificationModel {
final bool isRead;
final String planName;
final int planId;
@override
final DateTime createdAt;
ChosenPlanStartedToClientNotification({
required this.type,
required this.planName,
required this.planId,
required this.isRead,
});
ChosenPlanStartedToClientNotification({required this.type, required this.planName, required this.planId, required this.isRead, required this.createdAt});
factory ChosenPlanStartedToClientNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
factory ChosenPlanStartedToClientNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return ChosenPlanStartedToClientNotification(
type: type,
planName: json['plan_name'],
planId: json['plan_id'],
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
}
}
@@ -89,15 +94,18 @@ class ChosenPlanTomorrowReminderToClientNotification implements BaseNotification
@override
final bool isRead;
final String planName;
@override
final DateTime createdAt;
ChosenPlanTomorrowReminderToClientNotification({
required this.type,
required this.planName,
required this.isRead,
});
ChosenPlanTomorrowReminderToClientNotification({required this.type, required this.planName, required this.isRead, required this.createdAt});
factory ChosenPlanTomorrowReminderToClientNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
return ChosenPlanTomorrowReminderToClientNotification(type: type, planName: json['plan_name'], isRead: isRead ?? false);
factory ChosenPlanTomorrowReminderToClientNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return ChosenPlanTomorrowReminderToClientNotification(
type: type,
planName: json['plan_name'],
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
}
}
@@ -106,12 +114,17 @@ class DiaryReminderNotification implements BaseNotificationModel {
final NotificationType type;
@override
final bool isRead;
DiaryReminderNotification({required this.type, required this.isRead});
@override
final DateTime createdAt;
factory DiaryReminderNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
return DiaryReminderNotification(type: type, isRead: isRead ?? false);
DiaryReminderNotification({required this.type, required this.isRead, required this.createdAt});
factory DiaryReminderNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return DiaryReminderNotification(
type: type,
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
}
}
@@ -121,18 +134,17 @@ class ConsultantLeftCommentNotification implements BaseNotificationModel {
@override
final bool isRead;
final DateTime date;
@override
final DateTime createdAt;
ConsultantLeftCommentNotification({
required this.type,
required this.date,
required this.isRead,
});
ConsultantLeftCommentNotification({required this.type, required this.date, required this.isRead, required this.createdAt});
factory ConsultantLeftCommentNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
factory ConsultantLeftCommentNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return ConsultantLeftCommentNotification(
type: type,
date: parseDate(json['diary_date']),
date: parseDate(json['diary_date'] ?? DateTime.now().toIso8601String()),
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
}
}
@@ -142,16 +154,16 @@ class PersonalizedDietIsDefinedNotification implements BaseNotificationModel {
final NotificationType type;
@override
final bool isRead;
@override
final DateTime createdAt;
PersonalizedDietIsDefinedNotification({
required this.type,
required this.isRead,
});
PersonalizedDietIsDefinedNotification({required this.type, required this.isRead, required this.createdAt});
factory PersonalizedDietIsDefinedNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
factory PersonalizedDietIsDefinedNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return PersonalizedDietIsDefinedNotification(
type: type,
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
}
}
@@ -162,17 +174,16 @@ class ChosenPlanFinishedToClientNotification implements BaseNotificationModel {
@override
final bool isRead;
final String planName;
ChosenPlanFinishedToClientNotification({
required this.type,
required this.planName,
required this.isRead,
});
@override
final DateTime createdAt;
ChosenPlanFinishedToClientNotification({required this.type, required this.planName, required this.isRead, required this.createdAt});
factory ChosenPlanFinishedToClientNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
factory ChosenPlanFinishedToClientNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return ChosenPlanFinishedToClientNotification(
type: type,
planName: json['plan_name'],
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
}
}
@@ -183,17 +194,16 @@ class PlanSubscriptionCanceledToCustomerNotification implements BaseNotification
@override
final bool isRead;
final String planName;
PlanSubscriptionCanceledToCustomerNotification({
required this.type,
required this.planName,
required this.isRead,
});
@override
final DateTime createdAt;
PlanSubscriptionCanceledToCustomerNotification({required this.type, required this.planName, required this.isRead, required this.createdAt});
factory PlanSubscriptionCanceledToCustomerNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
factory PlanSubscriptionCanceledToCustomerNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return PlanSubscriptionCanceledToCustomerNotification(
type: type,
planName: json['plan_name'],
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
}
}
@@ -205,19 +215,23 @@ class ChosenPlanStartedToConsultantNotification implements BaseNotificationModel
final bool isRead;
final String planName;
final String customerName;
@override
final DateTime createdAt;
ChosenPlanStartedToConsultantNotification({
required this.type,
required this.isRead,
required this.planName,
required this.customerName,
required this.createdAt,
});
factory ChosenPlanStartedToConsultantNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
factory ChosenPlanStartedToConsultantNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return ChosenPlanStartedToConsultantNotification(
type: type,
isRead: isRead ?? false,
planName: json['plan_name'],
customerName: json['client_name'],
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
}
}
@@ -229,19 +243,23 @@ class ChosenPlanTomorrowReminderToConsultantNotification implements BaseNotifica
final bool isRead;
final String planName;
final String customerName;
@override
final DateTime createdAt;
ChosenPlanTomorrowReminderToConsultantNotification({
required this.type,
required this.planName,
required this.customerName,
required this.isRead,
required this.createdAt,
});
factory ChosenPlanTomorrowReminderToConsultantNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
factory ChosenPlanTomorrowReminderToConsultantNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return ChosenPlanTomorrowReminderToConsultantNotification(
type: type,
planName: json['plan_name'],
customerName: json['client_name'],
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
}
}
@@ -253,19 +271,23 @@ class PlanSubscriptionPaidToAuthorNotification implements BaseNotificationModel
final bool isRead;
final String planName;
final String customerName;
@override
final DateTime createdAt;
PlanSubscriptionPaidToAuthorNotification({
required this.type,
required this.isRead,
required this.planName,
required this.customerName,
required this.createdAt,
});
factory PlanSubscriptionPaidToAuthorNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
factory PlanSubscriptionPaidToAuthorNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return PlanSubscriptionPaidToAuthorNotification(
type: type,
isRead: isRead ?? false,
planName: json['plan_name'],
customerName: json['user_name'],
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
}
}
@@ -278,21 +300,240 @@ class ChosenPlanFinishedToConsultantNotification implements BaseNotificationMode
final String planName;
final String customerName;
final int planId;
@override
final DateTime createdAt;
ChosenPlanFinishedToConsultantNotification({
required this.type,
required this.planName,
required this.customerName,
required this.planId,
required this.isRead,
required this.createdAt,
});
factory ChosenPlanFinishedToConsultantNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
factory ChosenPlanFinishedToConsultantNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return ChosenPlanFinishedToConsultantNotification(
type: type,
planName: json['plan_name'],
customerName: json['client_name'],
planId: json['plan_id'],
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
}
}
class ChosenPlanLastDayFinishedForUser implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
@override
final DateTime createdAt;
ChosenPlanLastDayFinishedForUser({required this.type, required this.isRead, required this.createdAt});
factory ChosenPlanLastDayFinishedForUser.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return ChosenPlanLastDayFinishedForUser(
type: type,
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
}
}
class ChosenPlanConsultantLastDayStarted implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
final String planName;
final int planId;
final String chatId;
final String chatUrl;
@override
final DateTime createdAt;
ChosenPlanConsultantLastDayStarted({
required this.type,
required this.planName,
required this.planId,
required this.chatId,
required this.chatUrl,
required this.isRead,
required this.createdAt,
});
factory ChosenPlanConsultantLastDayStarted.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return ChosenPlanConsultantLastDayStarted(
type: type,
planName: json['plan_name'],
planId: json['plan_id'],
chatId: json['chat_id'],
chatUrl: json['chat_url'],
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
}
}
class ComplaintToModerationForUser implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
final String message;
final int entityId;
final String entityType;
@override
final DateTime createdAt;
ComplaintToModerationForUser({
required this.type,
required this.message,
required this.entityId,
required this.entityType,
required this.isRead,
required this.createdAt,
});
factory ComplaintToModerationForUser.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return ComplaintToModerationForUser(
type: type,
message: json['message'],
entityId: json['entity_id'],
entityType: json['entity_type'],
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
}
}
class CommentReplied implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
final String entityName;
final int commentedEntityId;
final String commentedEntityType;
final String author;
final int authorId;
final int parentCommentId;
@override
final DateTime createdAt;
CommentReplied({
required this.type,
required this.entityName,
required this.commentedEntityId,
required this.commentedEntityType,
required this.author,
required this.authorId,
required this.parentCommentId,
required this.isRead,
required this.createdAt,
});
factory CommentReplied.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return CommentReplied(
type: type,
entityName: json['entity_name'],
commentedEntityId: json['commented_entity_id'],
commentedEntityType: json['commented_entity_type'],
author: json['author'],
authorId: json['author_id'],
parentCommentId: json['parrent_comment_id'],
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
}
}
class ScoreAdded implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
final String scoreKey;
final int scorableId;
final String scorableType;
final String scorableName;
final String scorableSlug;
final int scoreValue;
@override
final DateTime createdAt;
ScoreAdded({
required this.type,
required this.scoreKey,
required this.scorableId,
required this.scorableType,
required this.scorableName,
required this.scorableSlug,
required this.scoreValue,
required this.isRead,
required this.createdAt,
});
factory ScoreAdded.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return ScoreAdded(
type: type,
scoreKey: json['score_key'],
scorableId: json['scorable_id'],
scorableType: json['scorable_type'],
scorableName: json['scorable_name'] ?? '',
scorableSlug: json['scorable_slug'] ?? '',
scoreValue: json['score_value'],
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
}
}
class EntityCommented implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
final String entityName;
final int entityId;
final String entityType;
final String entityTranslatedType;
final String entityUrl;
final String authorName;
final int authorId;
final String authorSlug;
@override
final DateTime createdAt;
EntityCommented({
required this.type,
required this.entityName,
required this.entityId,
required this.entityType,
required this.entityTranslatedType,
required this.entityUrl,
required this.authorName,
required this.authorId,
required this.authorSlug,
required this.isRead,
required this.createdAt,
});
factory EntityCommented.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return EntityCommented(
type: type,
entityName: json['entity_name'],
entityId: json['entity_id'],
entityType: json['entity_type'],
entityTranslatedType: json['entity_traslated_type'],
entityUrl: json['entity_url'],
authorName: json['author_name'],
authorId: json['author_id'],
authorSlug: json['author_slug'],
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
}
}
@@ -302,11 +543,10 @@ class UnknownNotification implements BaseNotificationModel {
final NotificationType type;
@override
final bool isRead;
@override
final DateTime createdAt;
UnknownNotification({
required this.type,
this.isRead = true,
});
UnknownNotification({required this.type, this.isRead = true, DateTime? createdAt}) : createdAt = createdAt ?? DateTime.fromMillisecondsSinceEpoch(0);
}
enum NotificationType {
@@ -323,22 +563,38 @@ enum NotificationType {
chosenPlanTomorrowReminderToConsultantNotification,
planSubscriptionPaidToAuthor,
chosenPlanFinishedToConsultant,
chosenPlanLastDayFinishedForUser,
// Новые типы
chosenPlanConsultantLastDayStarted,
complaintToModerationForUser,
commentReplied,
scoreAdded,
entityCommented,
unknown;
static NotificationType getTypeFromString(String type) => switch (type) {
'App\\Notifications\\Plan\\PlanSubscriptionStartedToAuthor' => NotificationType.planSubscriptionStartedToAuthor,
'App\\Notifications\\Plan\\PlanSubscriptionCanceledToAuthor' => NotificationType.planSubscriptionCanceledToAuthor,
'App\\Notifications\\Plan\\ChosenPlanStartedToClientNotification' => NotificationType.chosenPlanStartedToClientNotification,
'App\\Notifications\\Plan\\ChosenPlanTomorrowReminderToClientNotification' => NotificationType.chosenPlanStartedToClientNotification,
'App\\Notifications\\DiaryReminderNotification' => NotificationType.diaryReminderNotification,
'App\\Notifications\\Plan\\ConsultantLeftComment' => NotificationType.consultantLeftComment,
'App\\Notifications\\Plan\\PersonalizedDietIsDefined' => NotificationType.personalizedDietIsDefined,
'App\\Notifications\\Plan\\ChosenPlanFinishedToClient' => NotificationType.chosenPlanFinishedToClient,
'App\\Notifications\\Plan\\PlanSubscriptionCanceledToCustomer' => NotificationType.planSubscriptionCanceledToCustomer,
'App\\Notifications\\Plan\\ChosenPlanStartedToConsultantNotification' => NotificationType.chosenPlanStartedToConsultantNotification,
'App\\Notifications\\Plan\\ChosenPlanTomorrowReminderToConsultantNotification' => NotificationType.chosenPlanTomorrowReminderToConsultantNotification,
'App\\Notifications\\Plan\\PlanSubscriptionPaidToAuthor' => NotificationType.planSubscriptionPaidToAuthor,
'App\\Notifications\\Plan\\ChosenPlanFinishedToConsultant' => NotificationType.chosenPlanFinishedToConsultant,
_ => NotificationType.unknown,
};
'App\\Notifications\\Plan\\PlanSubscriptionStartedToAuthor' => NotificationType.planSubscriptionStartedToAuthor,
'App\\Notifications\\Plan\\PlanSubscriptionCanceledToAuthor' => NotificationType.planSubscriptionCanceledToAuthor,
'App\\Notifications\\Plan\\ChosenPlanStartedToClientNotification' => NotificationType.chosenPlanStartedToClientNotification,
'App\\Notifications\\Plan\\ChosenPlanTomorrowReminderToClientNotification' => NotificationType.chosenPlanStartedToClientNotification,
'App\\Notifications\\DiaryReminderNotification' => NotificationType.diaryReminderNotification,
'App\\Notifications\\Plan\\ConsultantLeftComment' => NotificationType.consultantLeftComment,
'App\\Notifications\\Plan\\PersonalizedDietIsDefined' => NotificationType.personalizedDietIsDefined,
'App\\Notifications\\Plan\\ChosenPlanFinishedToClient' => NotificationType.chosenPlanFinishedToClient,
'App\\Notifications\\Plan\\PlanSubscriptionCanceledToCustomer' => NotificationType.planSubscriptionCanceledToCustomer,
'App\\Notifications\\Plan\\ChosenPlanStartedToConsultantNotification' => NotificationType.chosenPlanStartedToConsultantNotification,
'App\\Notifications\\Plan\\ChosenPlanTomorrowReminderToConsultantNotification' => NotificationType.chosenPlanTomorrowReminderToConsultantNotification,
'App\\Notifications\\Plan\\PlanSubscriptionPaidToAuthor' => NotificationType.planSubscriptionPaidToAuthor,
'App\\Notifications\\Plan\\ChosenPlanFinishedToConsultant' => NotificationType.chosenPlanFinishedToConsultant,
'App\\Notifications\\ChosenPlan\\LastDayFinishedForUser' => NotificationType.chosenPlanLastDayFinishedForUser,
// Новые маппинги
'App\\Notifications\\ChosenPlan\\LastDayStartedForConsultant' => NotificationType.chosenPlanConsultantLastDayStarted,
'App\\Notifications\\ComplaintToModerationNotify' => NotificationType.complaintToModerationForUser,
'App\\Notifications\\Comment\\Replied' => NotificationType.commentReplied,
'App\\Notifications\\Score\\ScoreAdded' => NotificationType.scoreAdded,
'App\\Notifications\\Comment\\EntityCommented' => NotificationType.entityCommented,
_ => NotificationType.unknown,
};
}
DateTime parseDate(String date) => DateTime.parse(date).toLocal();

View File

@@ -13,10 +13,7 @@ class NotificationsCubit extends Cubit<NotificationsState> {
final WsManager wsManager;
final NotificationsRepository notificationsRepository;
StreamSubscription? _newNotificationsSubscription;
NotificationsCubit({
required this.wsManager,
required this.notificationsRepository,
}) : super(const NotificationsState.empty());
NotificationsCubit({required this.wsManager, required this.notificationsRepository}) : super(const NotificationsState.empty());
Future<void> connectToWebSocket() async {
await _newNotificationsSubscription?.cancel();
@@ -27,21 +24,95 @@ class NotificationsCubit extends Cubit<NotificationsState> {
BaseNotificationModel _parseNotification(Map<String, dynamic> notificationJson, NotificationType type) {
try {
final createdAt = notificationJson['created_at'];
final isRead = notificationJson['read_at'] != null;
return switch (type) {
NotificationType.planSubscriptionStartedToAuthor => PlanSubscriptionStartedToAuthorNotification.fromJson(notificationJson, type),
NotificationType.planSubscriptionCanceledToAuthor => PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson, type),
NotificationType.chosenPlanStartedToClientNotification => ChosenPlanStartedToClientNotification.fromJson(notificationJson, type),
NotificationType.chosenPlanTomorrowReminderToClientNotification => ChosenPlanTomorrowReminderToClientNotification.fromJson(notificationJson, type),
NotificationType.diaryReminderNotification => DiaryReminderNotification.fromJson(notificationJson, type),
NotificationType.consultantLeftComment => ConsultantLeftCommentNotification.fromJson(notificationJson, type),
NotificationType.personalizedDietIsDefined => PersonalizedDietIsDefinedNotification.fromJson(notificationJson, type),
NotificationType.chosenPlanFinishedToClient => ChosenPlanFinishedToClientNotification.fromJson(notificationJson, type),
NotificationType.planSubscriptionCanceledToCustomer => PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson, type),
NotificationType.chosenPlanStartedToConsultantNotification => ChosenPlanStartedToConsultantNotification.fromJson(notificationJson, type),
NotificationType.chosenPlanTomorrowReminderToConsultantNotification =>
ChosenPlanTomorrowReminderToConsultantNotification.fromJson(notificationJson, type),
NotificationType.planSubscriptionPaidToAuthor => PlanSubscriptionPaidToAuthorNotification.fromJson(notificationJson, type),
NotificationType.chosenPlanFinishedToConsultant => ChosenPlanFinishedToConsultantNotification.fromJson(notificationJson, type),
NotificationType.planSubscriptionStartedToAuthor => PlanSubscriptionStartedToAuthorNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.planSubscriptionCanceledToAuthor => PlanSubscriptionCanceledToAuthorNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.chosenPlanStartedToClientNotification => ChosenPlanStartedToClientNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.chosenPlanTomorrowReminderToClientNotification => ChosenPlanTomorrowReminderToClientNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.chosenPlanConsultantLastDayStarted => ChosenPlanConsultantLastDayStarted.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.diaryReminderNotification => DiaryReminderNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.consultantLeftComment => ConsultantLeftCommentNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.personalizedDietIsDefined => PersonalizedDietIsDefinedNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.chosenPlanFinishedToClient => ChosenPlanFinishedToClientNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.planSubscriptionCanceledToCustomer => PlanSubscriptionCanceledToAuthorNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.chosenPlanStartedToConsultantNotification => ChosenPlanStartedToConsultantNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.chosenPlanTomorrowReminderToConsultantNotification => ChosenPlanTomorrowReminderToConsultantNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.planSubscriptionPaidToAuthor => PlanSubscriptionPaidToAuthorNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.chosenPlanFinishedToConsultant => ChosenPlanFinishedToConsultantNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.chosenPlanLastDayFinishedForUser => ChosenPlanLastDayFinishedForUser.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
// New notification types
NotificationType.complaintToModerationForUser => ComplaintToModerationForUser.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.commentReplied => CommentReplied.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.scoreAdded => ScoreAdded.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.entityCommented => EntityCommented.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.unknown => UnknownNotification(type: type),
};
} catch (e) {
@@ -55,32 +126,80 @@ class NotificationsCubit extends Cubit<NotificationsState> {
final type = NotificationType.getTypeFromString(stringifiedType);
final notification = _parseNotification(json, type);
_notifications.insert(0, notification);
emit(
NotificationsState.loaded(
notifications: _notifications,
notificationsCount: _notifications.length,
hasUnreadNotifications: true,
),
);
final currentState = state;
if (currentState is NotificationsStateLoaded) {
emit(currentState.copyWith(notifications: _notifications, notificationsCount: _notifications.length, hasUnreadNotifications: true));
}
}
Future<void> getNotifications() async {
try {
final notifications = await notificationsRepository.getNotifications(page: 2);
_notifications.clear();
_notifications.addAll(notifications ?? []);
emit(
NotificationsState.loaded(
notifications: _notifications,
notificationsCount: _notifications.length,
hasUnreadNotifications: (notifications ?? []).where((elem) => elem.isRead == false).isNotEmpty,
),
);
final result = await notificationsRepository.getNotifications(page: 1);
if (result != null) {
final notifications = result['notifications'] as List<BaseNotificationModel>;
final pagination = result['pagination'] as Map<String, dynamic>;
_notifications.clear();
_notifications.addAll(notifications);
final currentPage = pagination['current_page'] as int;
final lastPage = pagination['last_page'] as int;
final hasNextPage = currentPage < lastPage;
emit(
NotificationsState.loaded(
notifications: _notifications,
notificationsCount: _notifications.length,
hasUnreadNotifications: notifications.where((elem) => elem.isRead == false).isNotEmpty,
currentPage: currentPage,
hasNextPage: hasNextPage,
),
);
}
} catch (e, st) {
log(e.toString(), stackTrace: st);
}
}
Future<void> loadMoreNotifications() async {
final currentState = state;
if (currentState is NotificationsStateLoaded && currentState.hasNextPage && !currentState.isLoadingMore) {
try {
emit(currentState.copyWith(isLoadingMore: true));
final nextPage = currentState.currentPage + 1;
final result = await notificationsRepository.getNotifications(page: nextPage);
if (result != null) {
final newNotifications = result['notifications'] as List<BaseNotificationModel>;
final pagination = result['pagination'] as Map<String, dynamic>;
_notifications.addAll(newNotifications);
final currentPage = pagination['current_page'] as int;
final lastPage = pagination['last_page'] as int;
final hasNextPage = currentPage < lastPage;
emit(
currentState.copyWith(
notifications: _notifications,
notificationsCount: _notifications.length,
currentPage: currentPage,
hasNextPage: hasNextPage,
isLoadingMore: false,
),
);
} else {
emit(currentState.copyWith(isLoadingMore: false));
}
} catch (e, st) {
log(e.toString(), stackTrace: st);
emit(currentState.copyWith(isLoadingMore: false));
}
}
}
Future<void> markNotificationsAsRead() async {
final currentState = state;
if (currentState is NotificationsStateLoaded) {

View File

@@ -14,6 +14,9 @@ abstract class NotificationsState with _$NotificationsState {
required int notificationsCount,
@Default(false) bool isLoading,
@Default(false) bool hasUnreadNotifications,
@Default(1) int currentPage,
@Default(true) bool hasNextPage,
@Default(false) bool isLoadingMore,
}) = NotificationsStateLoaded;
const factory NotificationsState.error({

View File

@@ -10,9 +10,38 @@ import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class NotificationsScreen extends StatelessWidget {
class NotificationsScreen extends StatefulWidget {
const NotificationsScreen({super.key});
@override
State<NotificationsScreen> createState() => _NotificationsScreenState();
}
class _NotificationsScreenState extends State<NotificationsScreen> {
final ScrollController _scrollController = ScrollController();
@override
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
void _onScroll() {
if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) {
final cubit = context.read<NotificationsCubit>();
final state = cubit.state;
if (state is NotificationsStateLoaded && state.hasNextPage && !state.isLoadingMore) {
cubit.loadMoreNotifications();
}
}
}
@override
Widget build(BuildContext context) {
final s = S.of(context);
@@ -20,62 +49,65 @@ class NotificationsScreen extends StatelessWidget {
builder: (BuildContext context, NotificationsState state) {
return Scaffold(
backgroundColor: appTheme.gray100,
appBar: AppBar(
title: const Text('Уведомления'),
),
appBar: AppBar(title: const Text('Уведомления')),
body: state is NotificationsStateLoaded
? Stack(children: [
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Container(
padding: const EdgeInsets.only(top: 16),
decoration: BoxDecoration(
color: appTheme.whiteA700,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
? Stack(
children: [
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Container(
padding: const EdgeInsets.only(top: 16),
decoration: BoxDecoration(
color: appTheme.whiteA700,
borderRadius: const BorderRadius.only(topLeft: Radius.circular(12), topRight: Radius.circular(12)),
),
child: state.isLoading
? const Center(child: Loader())
: CustomScrollView(
controller: _scrollController,
slivers: [
SliverList.separated(
separatorBuilder: (context, index) =>
const Padding(padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16), child: CustomDivider()),
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: NotificationWidget(notification: state.notifications[index]),
);
},
itemCount: state.notifications.length,
),
if (state.isLoadingMore)
const SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Center(child: Loader()),
),
),
],
),
),
),
Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16),
child: CustomOutlinedButton(
text: s.markAsRead,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
),
onPressed: () async {
context.read<NotificationsCubit>().markNotificationsAsRead();
},
),
),
child: state.isLoading
? const Center(child: Loader())
: CustomScrollView(
slivers: [
SliverList.separated(
separatorBuilder: (context, index) => const Padding(
padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16),
child: CustomDivider(),
),
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: NotificationWidget(
notification: state.notifications[index],
),
);
},
itemCount: state.notifications.length,
)
],
),
),
),
Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16),
child: CustomOutlinedButton(
text: s.markAsRead,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle:
ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
onPressed: () async {
context.read<NotificationsCubit>().markNotificationsAsRead();
},
),
),
)
])
],
)
: const Placeholder(),
);
},

View File

@@ -15,45 +15,39 @@ class NotificationWidget extends StatelessWidget {
Widget build(BuildContext context) {
return switch (notification) {
PlanSubscriptionStartedToAuthorNotification() => _PlanSubscriptionStartedToAuthorWidget(
notification: notification as PlanSubscriptionStartedToAuthorNotification,
),
notification: notification as PlanSubscriptionStartedToAuthorNotification,
),
PlanSubscriptionCanceledToAuthorNotification() => _PlanSubscriptionCanceledToAuthorWidget(
notification: notification as PlanSubscriptionCanceledToAuthorNotification,
),
ChosenPlanStartedToClientNotification() => _ChosenPlanStartedToClientWidget(
notification: notification as ChosenPlanStartedToClientNotification,
),
notification: notification as PlanSubscriptionCanceledToAuthorNotification,
),
ChosenPlanStartedToClientNotification() => _ChosenPlanStartedToClientWidget(notification: notification as ChosenPlanStartedToClientNotification),
ChosenPlanTomorrowReminderToClientNotification() => _ChosenPlanTomorrowReminderToClientWidget(
notification: notification as ChosenPlanTomorrowReminderToClientNotification,
),
DiaryReminderNotification() => _DiaryReminderWidget(
notification: notification as DiaryReminderNotification,
),
ConsultantLeftCommentNotification() => _ConsultantLeftCommentWidget(
notification: notification as ConsultantLeftCommentNotification,
),
PersonalizedDietIsDefinedNotification() => _PersonalizedDietIsDefinedWidget(
notification: notification as PersonalizedDietIsDefinedNotification,
),
ChosenPlanFinishedToClientNotification() => _ChosenPlanFinishedToClientWidget(
notification: notification as ChosenPlanFinishedToClientNotification,
),
notification: notification as ChosenPlanTomorrowReminderToClientNotification,
),
DiaryReminderNotification() => _DiaryReminderWidget(notification: notification as DiaryReminderNotification),
ConsultantLeftCommentNotification() => _ConsultantLeftCommentWidget(notification: notification as ConsultantLeftCommentNotification),
PersonalizedDietIsDefinedNotification() => _PersonalizedDietIsDefinedWidget(notification: notification as PersonalizedDietIsDefinedNotification),
ChosenPlanFinishedToClientNotification() => _ChosenPlanFinishedToClientWidget(notification: notification as ChosenPlanFinishedToClientNotification),
PlanSubscriptionCanceledToCustomerNotification() => _PlanSubscriptionCanceledToCustomerWidget(
notification: notification as PlanSubscriptionCanceledToCustomerNotification,
),
notification: notification as PlanSubscriptionCanceledToCustomerNotification,
),
ChosenPlanStartedToConsultantNotification() => _ChosenPlanStartedToConsultantWidget(
notification: notification as ChosenPlanStartedToConsultantNotification,
),
notification: notification as ChosenPlanStartedToConsultantNotification,
),
ChosenPlanTomorrowReminderToConsultantNotification() => _ChosenPlanTomorrowReminderToConsultantWidget(
notification: notification as ChosenPlanTomorrowReminderToConsultantNotification,
),
PlanSubscriptionPaidToAuthorNotification() => _PlanSubscriptionPaidToAuthorWidget(
notification: notification as PlanSubscriptionPaidToAuthorNotification,
),
notification: notification as ChosenPlanTomorrowReminderToConsultantNotification,
),
PlanSubscriptionPaidToAuthorNotification() => _PlanSubscriptionPaidToAuthorWidget(notification: notification as PlanSubscriptionPaidToAuthorNotification),
ChosenPlanFinishedToConsultantNotification() => _ChosenPlanFinishedToConsultantWidget(
notification: notification as ChosenPlanFinishedToConsultantNotification,
),
UnknownNotification() => const SizedBox.shrink(),
notification: notification as ChosenPlanFinishedToConsultantNotification,
),
ChosenPlanLastDayFinishedForUser() => _ChosenPlanLastDayFinishedForUserWidget(notification: notification as ChosenPlanLastDayFinishedForUser),
ChosenPlanConsultantLastDayStarted() => _ChosenPlanConsultantLastDayStartedWidget(notification: notification as ChosenPlanConsultantLastDayStarted),
ComplaintToModerationForUser() => _ComplaintToModerationForUserWidget(notification: notification as ComplaintToModerationForUser),
CommentReplied() => _CommentRepliedWidget(notification: notification as CommentReplied),
ScoreAdded() => _ScoreAddedWidget(notification: notification as ScoreAdded),
EntityCommented() => _EntityCommentedWidget(notification: notification as EntityCommented),
UnknownNotification() => _UnknownNotificationWidget(notification: notification as UnknownNotification),
};
}
}
@@ -67,7 +61,8 @@ class _PlanSubscriptionStartedToAuthorWidget extends StatelessWidget {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: s.fillYasterday,
notificationsText: s.userSubscribedToPlan(notification.customerName, notification.planName),
createdAt: notification.createdAt,
);
}
}
@@ -81,7 +76,8 @@ class _PlanSubscriptionCanceledToAuthorWidget extends StatelessWidget {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: s.fillYasterday,
notificationsText: s.userCanceldThePlanSubscription(notification.customerName, notification.planName),
createdAt: notification.createdAt,
);
}
}
@@ -93,10 +89,7 @@ class _ChosenPlanStartedToClientWidget extends StatelessWidget {
Widget build(BuildContext context) {
final s = S.of(context);
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: s.fillYasterday,
);
return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: s.planStarted(notification.planName), createdAt: notification.createdAt);
}
}
@@ -109,7 +102,8 @@ class _ChosenPlanTomorrowReminderToClientWidget extends StatelessWidget {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: s.fillYasterday,
notificationsText: s.tomorrowYourPlanWillStart(notification.planName),
createdAt: notification.createdAt,
);
}
}
@@ -120,10 +114,7 @@ class _DiaryReminderWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final s = S.of(context);
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: s.fillYasterday,
);
return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: s.fillYasterday, createdAt: notification.createdAt);
}
}
@@ -137,6 +128,7 @@ class _ConsultantLeftCommentWidget extends StatelessWidget {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: s.consultantLeftComment(notification.date.format('dd.MM.yyyy')),
createdAt: notification.createdAt,
);
}
}
@@ -146,7 +138,7 @@ class _PersonalizedDietIsDefinedWidget extends StatelessWidget {
final PersonalizedDietIsDefinedNotification notification;
@override
Widget build(BuildContext context) {
return Container();
return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: 'Персональная диета определена', createdAt: notification.createdAt);
}
}
@@ -160,6 +152,7 @@ class _ChosenPlanFinishedToClientWidget extends StatelessWidget {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: '${s.congratsYouHaveFinishedPlan} «${notification.planName}»',
createdAt: notification.createdAt,
);
}
}
@@ -174,6 +167,7 @@ class _PlanSubscriptionCanceledToCustomerWidget extends StatelessWidget {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: s.youCanceledSubscriptionToPlan(notification.planName),
createdAt: notification.createdAt,
);
}
}
@@ -188,6 +182,7 @@ class _ChosenPlanStartedToConsultantWidget extends StatelessWidget {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: s.planWithUserStarted(notification.planName, notification.customerName),
createdAt: notification.createdAt,
);
}
}
@@ -202,6 +197,7 @@ class _ChosenPlanTomorrowReminderToConsultantWidget extends StatelessWidget {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: s.tommorowPlanWithUserWillStart(notification.planName, notification.customerName),
createdAt: notification.createdAt,
);
}
}
@@ -216,6 +212,7 @@ class _PlanSubscriptionPaidToAuthorWidget extends StatelessWidget {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: s.userPaidThePlan(notification.customerName, notification.planName),
createdAt: notification.createdAt,
);
}
}
@@ -226,58 +223,223 @@ class _ChosenPlanFinishedToConsultantWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final s = S.of(context);
return Column(children: [
Text(
'${s.congratsYouHaveFinishedPlan} «${notification.planName}»',
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
),
const SizedBox(height: 12),
Text(
s.userConfirmedPlanCompletionWithoutSum,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
const SizedBox(height: 12),
CustomOutlinedButton(
text: s.collectCharge,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.green)),
onPressed: () {
GoRouter.of(context).pushNamed(CardSettingsScreen.name, extra: notification.planId);
},
),
const SizedBox(height: 12),
]);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${s.congratsYouHaveFinishedPlan} «${notification.planName}»', style: TextStyles.textMSemibold.copyWith(color: appTheme.black)),
const SizedBox(height: 12),
Text(s.userConfirmedPlanCompletionWithoutSum, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const SizedBox(height: 12),
CustomOutlinedButton(
text: s.collectCharge,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.green)),
onPressed: () {
GoRouter.of(context).pushNamed(CardSettingsScreen.name, extra: notification.planId);
},
),
],
),
),
if (!notification.isRead)
Container(
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
width: 8,
height: 8,
),
],
),
const SizedBox(height: 4),
Text(
'${notification.createdAt.day} ${_getMonthName(notification.createdAt.month)} ${notification.createdAt.hour.toString().padLeft(2, '0')}:${notification.createdAt.minute.toString().padLeft(2, '0')}',
style: TextStyles.textSMedium.copyWith(color: appTheme.gray600),
),
],
);
}
String _getMonthName(int month) {
switch (month) {
case 1:
return 'янв';
case 2:
return 'фев';
case 3:
return 'мар';
case 4:
return 'апр';
case 5:
return 'май';
case 6:
return 'июн';
case 7:
return 'июл';
case 8:
return 'авг';
case 9:
return 'сен';
case 10:
return 'окт';
case 11:
return 'ноя';
case 12:
return 'дек';
default:
return '';
}
}
}
// CardSettingsScreen
class _BaseNotificationWidget extends StatelessWidget {
const _BaseNotificationWidget({required this.notificationsText, required this.isRead});
const _BaseNotificationWidget({required this.notificationsText, required this.isRead, required this.createdAt});
final String notificationsText;
final bool isRead;
final DateTime createdAt;
@override
Widget build(BuildContext context) {
return Row(
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: MediaQuery.sizeOf(context).width * 0.6,
child: Text(
notificationsText,
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
),
),
if (!isRead) ...[
const Spacer(),
Container(
decoration: BoxDecoration(
color: appTheme.blueA400,
shape: BoxShape.circle,
Row(
children: [
Expanded(
child: Text(notificationsText, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
),
width: 8,
height: 8,
)
]
if (!isRead)
Container(
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
width: 8,
height: 8,
),
],
),
const SizedBox(height: 4),
Text(
'${createdAt.day} ${_getMonthName(createdAt.month)} ${createdAt.hour.toString().padLeft(2, '0')}:${createdAt.minute.toString().padLeft(2, '0')}',
style: TextStyles.textSMedium.copyWith(color: appTheme.gray600),
),
],
);
}
String _getMonthName(int month) {
switch (month) {
case 1:
return 'янв';
case 2:
return 'фев';
case 3:
return 'мар';
case 4:
return 'апр';
case 5:
return 'май';
case 6:
return 'июн';
case 7:
return 'июл';
case 8:
return 'авг';
case 9:
return 'сен';
case 10:
return 'окт';
case 11:
return 'ноя';
case 12:
return 'дек';
default:
return '';
}
}
}
class _ChosenPlanLastDayFinishedForUserWidget extends StatelessWidget {
const _ChosenPlanLastDayFinishedForUserWidget({required this.notification});
final ChosenPlanLastDayFinishedForUser notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: 'Последний день плана завершен', createdAt: notification.createdAt);
}
}
class _ChosenPlanConsultantLastDayStartedWidget extends StatelessWidget {
const _ChosenPlanConsultantLastDayStartedWidget({required this.notification});
final ChosenPlanConsultantLastDayStarted notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Последний день плана начался: ${notification.planName}',
createdAt: notification.createdAt,
);
}
}
class _ComplaintToModerationForUserWidget extends StatelessWidget {
const _ComplaintToModerationForUserWidget({required this.notification});
final ComplaintToModerationForUser notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: 'Жалоба отправлена на модерацию', createdAt: notification.createdAt);
}
}
class _CommentRepliedWidget extends StatelessWidget {
const _CommentRepliedWidget({required this.notification});
final CommentReplied notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Ответ на комментарий: ${notification.entityName}',
createdAt: notification.createdAt,
);
}
}
class _ScoreAddedWidget extends StatelessWidget {
const _ScoreAddedWidget({required this.notification});
final ScoreAdded notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Добавлены баллы: ${notification.scoreValue}',
createdAt: notification.createdAt,
);
}
}
class _EntityCommentedWidget extends StatelessWidget {
const _EntityCommentedWidget({required this.notification});
final EntityCommented notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Комментарий к ${notification.entityTranslatedType}: ${notification.entityName}',
createdAt: notification.createdAt,
);
}
}
class _UnknownNotificationWidget extends StatelessWidget {
const _UnknownNotificationWidget({required this.notification});
final UnknownNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Неизвестное уведомление: ${notification.type.toString()}',
createdAt: notification.createdAt,
);
}
}

View File

@@ -1,14 +1,15 @@
import 'package:eatvio_app/features/notifications/models/notification_model.dart';
import 'dart:developer';
import 'package:eatvio_app/features/notifications/service/notifications_service.dart';
class NotificationsRepository {
final NotificationsService notificationsService;
NotificationsRepository({required this.notificationsService});
Future<List<BaseNotificationModel>?> getNotifications({required int page}) {
Future<Map<String, dynamic>?> getNotifications({required int page}) {
try {
return notificationsService.getNotifications(page: page);
} catch (e) {
log(e.toString());
rethrow;
}
}

View File

@@ -5,48 +5,137 @@ class NotificationsService {
final Dio dio;
NotificationsService(this.dio);
Future<List<BaseNotificationModel>?> getNotifications({required int page}) async {
Future<Map<String, dynamic>?> getNotifications({required int page}) async {
try {
final response = await dio.get('/api/profile/notifications?page=$page');
if (response.statusCode == 200) {
final data = response.data['data'] as List<dynamic>;
final notifications = data.map((notificationJson) {
try {
final stringifiedType = notificationJson['data']['notification_class_name'];
final readDate = notificationJson['read_at'];
final createdAt = notificationJson['created_at'];
final type = NotificationType.getTypeFromString(stringifiedType);
return switch (type) {
NotificationType.planSubscriptionStartedToAuthor =>
PlanSubscriptionStartedToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
NotificationType.planSubscriptionCanceledToAuthor =>
PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
NotificationType.chosenPlanStartedToClientNotification =>
ChosenPlanStartedToClientNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
NotificationType.chosenPlanTomorrowReminderToClientNotification =>
ChosenPlanTomorrowReminderToClientNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
NotificationType.diaryReminderNotification => DiaryReminderNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
NotificationType.consultantLeftComment => ConsultantLeftCommentNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
NotificationType.personalizedDietIsDefined =>
PersonalizedDietIsDefinedNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
NotificationType.chosenPlanFinishedToClient =>
ChosenPlanFinishedToClientNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
NotificationType.planSubscriptionCanceledToCustomer =>
PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
NotificationType.chosenPlanStartedToConsultantNotification =>
ChosenPlanStartedToConsultantNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
NotificationType.chosenPlanTomorrowReminderToConsultantNotification =>
ChosenPlanTomorrowReminderToConsultantNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
NotificationType.planSubscriptionPaidToAuthor =>
PlanSubscriptionPaidToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
NotificationType.chosenPlanFinishedToConsultant =>
ChosenPlanFinishedToConsultantNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
NotificationType.unknown => UnknownNotification(type: type),
NotificationType.planSubscriptionStartedToAuthor => PlanSubscriptionStartedToAuthorNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.planSubscriptionCanceledToAuthor => PlanSubscriptionCanceledToAuthorNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.chosenPlanStartedToClientNotification => ChosenPlanStartedToClientNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.chosenPlanTomorrowReminderToClientNotification => ChosenPlanTomorrowReminderToClientNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.diaryReminderNotification => DiaryReminderNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.consultantLeftComment => ConsultantLeftCommentNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.personalizedDietIsDefined => PersonalizedDietIsDefinedNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.chosenPlanFinishedToClient => ChosenPlanFinishedToClientNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.planSubscriptionCanceledToCustomer => PlanSubscriptionCanceledToAuthorNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.chosenPlanStartedToConsultantNotification => ChosenPlanStartedToConsultantNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.chosenPlanTomorrowReminderToConsultantNotification => ChosenPlanTomorrowReminderToConsultantNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.planSubscriptionPaidToAuthor => PlanSubscriptionPaidToAuthorNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.chosenPlanFinishedToConsultant => ChosenPlanFinishedToConsultantNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.chosenPlanLastDayFinishedForUser => ChosenPlanLastDayFinishedForUser.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
// Новые типы уведомлений
NotificationType.chosenPlanConsultantLastDayStarted => ChosenPlanConsultantLastDayStarted.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.complaintToModerationForUser => ComplaintToModerationForUser.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.commentReplied => CommentReplied.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt),
NotificationType.scoreAdded => ScoreAdded.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt),
NotificationType.entityCommented => EntityCommented.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt),
NotificationType.unknown => UnknownNotification(type: type, createdAt: parseDate(createdAt)),
};
} catch (e) {
return UnknownNotification(type: NotificationType.unknown);
return UnknownNotification(type: NotificationType.unknown, createdAt: DateTime.now());
}
}).toList();
return notifications.where((notification) => notification.type != NotificationType.unknown).toList();
// Извлекаем информацию о пагинации из корня ответа
final currentPage = response.data['current_page'] as int;
final lastPage = response.data['last_page'] as int;
final perPage = response.data['per_page'] as int;
final total = response.data['total'] as int;
return {
'notifications': notifications,
'pagination': {'current_page': currentPage, 'last_page': lastPage, 'per_page': perPage, 'total': total},
};
}
return null;
} catch (e) {
@@ -62,3 +151,5 @@ class NotificationsService {
}
}
}
DateTime parseDate(String date) => DateTime.parse(date).toLocal();

View File

@@ -208,7 +208,7 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
GoRouter.of(context).pushReplacementNamed(
'chatScreen',
extra: {
'chatId': chosenPlan.chatId,
'uuid': chosenPlan.chatId,
'sendInitialMessage': true,
},
);

View File

@@ -14,9 +14,11 @@ abstract class ProfileModel with _$ProfileModel {
@JsonKey(name: "email") required String email,
@JsonKey(name: "rating_place") int? ratingPlace,
@JsonKey(name: "email_verified_at") DateTime? emailVerifiedAt,
@JsonKey(name: "phone_confirmation_state") required String phoneConfirmationState,
@JsonKey(name: "created_at") DateTime? createdAt,
@JsonKey(name: "updated_at") DateTime? updatedAt,
@JsonKey(name: "is_public_profile", fromJson: parseBool) required bool isPublicProfile,
@JsonKey(name: "is_blocked_by_me", fromJson: parseBool) required bool isBlockedByMe,
@JsonKey(name: 'is_entity_subscribed_by_me') required bool isEntitySubscribedByMe,
@JsonKey(name: "slug") required String slug,
@JsonKey(name: "profile_photo_url") required String profilePhotoUrl,

View File

@@ -25,6 +25,7 @@ class ProfileCubit extends Cubit<ProfileState> {
}) : super(const ProfileState.loading());
Future<void> getProfileBySlug(String slug) async {
emit(const ProfileState.loading());
try {
final profile = await profileRepository.getProfileBySlug(slug);
emit(
@@ -44,6 +45,7 @@ class ProfileCubit extends Cubit<ProfileState> {
Future<void> getMyProfile({
String period = '1',
}) async {
emit(const ProfileState.loading());
_currentPeriod = period;
try {
final profile = await profileRepository.getMyProfile();
@@ -123,7 +125,7 @@ class ProfileCubit extends Cubit<ProfileState> {
AppRouter.navigatorKey.currentContext?.pushNamed(
'chatScreen',
extra: {
'chatId': chatIdOrError,
'uuid': chatIdOrError,
},
);
return chatIdOrError;

File diff suppressed because it is too large Load Diff

View File

@@ -69,7 +69,7 @@ class _SubActionButtonWidgetState extends State<SubActionButtonWidget> {
GoRouter.of(context).pushNamed(
'chatScreen',
extra: {
'chatId': await sl<ProfileCubit>().getChatRoomIdBySlug(
'uuid': await sl<ProfileCubit>().getChatRoomIdBySlug(
author: Author(
authorSlug: widget.slug,
authorName: widget.name,

View File

@@ -203,4 +203,12 @@ class ProfileRepository {
return false;
}
}
Future<void> deleteAccount() async {
try {
await profileService.deleteAccount();
} catch (e) {
rethrow;
}
}
}

View File

@@ -119,4 +119,7 @@ abstract class ProfileService {
Future<void> tryVerifyEmailByCode({
@Body() required String code,
});
@DELETE('/api/profile/delete_my_account')
Future<void> deleteAccount();
}

View File

@@ -1,9 +1,10 @@
import 'dart:developer';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/firebase_api.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
import 'package:eatvio_app/features/registration/models/user_registration_model.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_state.dart';
import 'package:eatvio_app/features/registration/repositories/registration_repository.dart';
@@ -51,25 +52,19 @@ class RegistrationCubit extends Cubit<RegistrationState> {
void updateSheet(UserRegistrationSheet sheet) => emit(RegistrationState(sheet: sheet));
Future<void> tryRegistration() async {
if (await registrationRepository.emailIsReachable(state.sheet.email)) {
try {
final token = await registrationRepository.tryRegister(
state.sheet.copyWith(
phone: state.sheet.phone.replaceAll(' ', '').replaceAll('(', '').replaceAll(')', ''),
responseBody: '{}',
),
);
await TokenStorage.saveTokenInStorage(token);
AppRouter.navigatorKey.currentContext?.pushNamed('verifyEmailScreen');
} catch (e) {
log(e.toString());
}
} else {
showErrorDialog(
AppRouter.navigatorKey.currentContext!,
title: 'Ошибка регистрации',
description: 'Несуществующая почта.',
try {
final token = await registrationRepository.tryRegister(
state.sheet.copyWith(
phone: state.sheet.phone.replaceAll(' ', '').replaceAll('(', '').replaceAll(')', ''),
responseBody: '{}',
),
);
await TokenStorage.saveTokenInStorage(token);
await sl<FirebaseApi>().registerPushToken();
AppRouter.navigatorKey.currentContext?.replaceNamed('verifyEmailScreen');
} catch (e, st) {
log(e.toString(), stackTrace: st);
emit(RegistrationState(sheet: state.sheet, error: e.toString()));
}
}

View File

@@ -7,5 +7,6 @@ part 'registration_state.freezed.dart';
abstract class RegistrationState with _$RegistrationState {
const factory RegistrationState({
required UserRegistrationSheet sheet,
String? error,
}) = _RegistrationState;
}

View File

@@ -43,142 +43,161 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
Widget build(BuildContext context) {
return BlocProvider<RegistrationCubit>(
create: (BuildContext context) => registrationCubit,
child: BlocBuilder<RegistrationCubit, RegistrationState>(
builder: (BuildContext context, RegistrationState state) {
return Scaffold(
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: Row(
children: [
Container(
height: 48,
width: MediaQuery.of(context).size.width * 0.2,
padding: const EdgeInsets.only(left: 16, right: 4),
child: FloatingActionButton.extended(
heroTag: 'backButtonRegistration',
highlightElevation: 0,
focusElevation: 0,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
backgroundColor: const Color(0xFFEAF2FD),
onPressed: () async {
setState(() {
pageController.previousPage(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
);
});
},
label: Icon(
Icons.arrow_back_ios_new_rounded,
color: appTheme.blueA400,
),
child: BlocListener<RegistrationCubit, RegistrationState>(
listener: (context, state) {
if (state.error != null && state.error!.isNotEmpty) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Ошибка при регистрации'),
content: Text(state.error!),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
),
Container(
height: 48,
width: MediaQuery.of(context).size.width * 0.8,
padding: const EdgeInsets.only(left: 4, right: 16),
child: FloatingActionButton.extended(
heroTag: 'nextButtonRegistration',
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
backgroundColor: appTheme.blueA400,
onPressed: () async {
if (await canGoNext(state, context)) {
],
),
);
}
},
child: BlocBuilder<RegistrationCubit, RegistrationState>(
builder: (BuildContext context, RegistrationState state) {
return Scaffold(
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: Row(
children: [
Container(
height: 48,
width: MediaQuery.of(context).size.width * 0.2,
padding: const EdgeInsets.only(left: 16, right: 4),
child: FloatingActionButton.extended(
heroTag: 'backButtonRegistration',
highlightElevation: 0,
focusElevation: 0,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
backgroundColor: const Color(0xFFEAF2FD),
onPressed: () async {
setState(() {
pageController.nextPage(
pageController.previousPage(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
);
});
}
},
label: Text(
'Продолжить',
style: theme.textTheme.titleSmall!.copyWith(color: Colors.white),
},
label: Icon(
Icons.arrow_back_ios_new_rounded,
color: appTheme.blueA400,
),
),
),
),
],
),
appBar: AppBar(
automaticallyImplyLeading: false,
leading: Builder(
builder: (context) => IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () => GoRouter.of(context).pop(),
),
),
centerTitle: true,
title: SvgPicture.asset(
ImageConstant.imgLogoEatvio,
height: 48,
fit: BoxFit.contain,
),
actions: [
Padding(
padding: const EdgeInsets.only(right: 16),
child: Text('шаг $step из 15'),
),
],
bottom: PreferredSize(
preferredSize: Size(MediaQuery.of(context).size.width, 32),
child: Padding(
padding: const EdgeInsets.all(16),
child: TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
tween: Tween<double>(
begin: 1 / 15,
end: progressValue,
Container(
height: 48,
width: MediaQuery.of(context).size.width * 0.8,
padding: const EdgeInsets.only(left: 4, right: 16),
child: FloatingActionButton.extended(
heroTag: 'nextButtonRegistration',
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
backgroundColor: appTheme.blueA400,
onPressed: () async {
if (await canGoNext(state, context)) {
setState(() {
pageController.nextPage(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
);
});
}
},
label: Text(
'Продолжить',
style: theme.textTheme.titleSmall!.copyWith(color: Colors.white),
),
),
builder: (context, value, _) => LinearProgressIndicator(
value: value,
borderRadius: BorderRadius.circular(16),
minHeight: 8,
backgroundColor: Colors.grey.shade100,
valueColor: AlwaysStoppedAnimation<Color>(appTheme.blueA400),
),
],
),
appBar: AppBar(
automaticallyImplyLeading: false,
leading: Builder(
builder: (context) => IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () => GoRouter.of(context).pop(),
),
),
centerTitle: true,
title: SvgPicture.asset(
ImageConstant.imgLogoEatvio,
height: 48,
fit: BoxFit.contain,
),
actions: [
Padding(
padding: const EdgeInsets.only(right: 16),
child: Text('шаг $step из 15'),
),
],
bottom: PreferredSize(
preferredSize: Size(MediaQuery.of(context).size.width, 32),
child: Padding(
padding: const EdgeInsets.all(16),
child: TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
tween: Tween<double>(
begin: 1 / 15,
end: progressValue,
),
builder: (context, value, _) => LinearProgressIndicator(
value: value,
borderRadius: BorderRadius.circular(16),
minHeight: 8,
backgroundColor: Colors.grey.shade100,
valueColor: AlwaysStoppedAnimation<Color>(appTheme.blueA400),
),
),
),
),
),
),
body: PageView(
physics: const NeverScrollableScrollPhysics(),
controller: pageController,
onPageChanged: (index) {
setState(() {
step = index + 1;
progressValue = step / 15;
});
},
children: [
// LanguageSelectScreen(sheet: state.sheet),
PhoneScreen(sheet: state.sheet),
CredentialsScreen(sheet: state.sheet),
NameScreen(sheet: state.sheet),
GenderScreen(sheet: state.sheet),
DateOfBirthScreen(sheet: state.sheet),
CityOfLivingScreen(sheet: state.sheet),
BodyCircumferencesScreen(sheet: state.sheet),
WeightScreen(sheet: state.sheet),
HeightScreen(sheet: state.sheet),
BloodPressureScreen(sheet: state.sheet),
AllergiesScreen(sheet: state.sheet),
MedicineIntakeScreen(sheet: state.sheet),
FavoriteSportsScreen(sheet: state.sheet),
FavoriteFoodsScreen(sheet: state.sheet),
AdditionalInformationScreen(sheet: state.sheet),
],
),
);
},
body: PageView(
physics: const NeverScrollableScrollPhysics(),
controller: pageController,
onPageChanged: (index) {
setState(() {
step = index + 1;
progressValue = step / 15;
});
},
children: [
// LanguageSelectScreen(sheet: state.sheet),
PhoneScreen(sheet: state.sheet),
CredentialsScreen(sheet: state.sheet),
NameScreen(sheet: state.sheet),
GenderScreen(sheet: state.sheet),
DateOfBirthScreen(sheet: state.sheet),
CityOfLivingScreen(sheet: state.sheet),
BodyCircumferencesScreen(sheet: state.sheet),
WeightScreen(sheet: state.sheet),
HeightScreen(sheet: state.sheet),
BloodPressureScreen(sheet: state.sheet),
AllergiesScreen(sheet: state.sheet),
MedicineIntakeScreen(sheet: state.sheet),
FavoriteSportsScreen(sheet: state.sheet),
FavoriteFoodsScreen(sheet: state.sheet),
AdditionalInformationScreen(sheet: state.sheet),
],
),
);
},
),
),
);
}

View File

@@ -1,5 +1,6 @@
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:lottie/lottie.dart';
@@ -19,6 +20,7 @@ class VerifyScreen extends StatefulWidget {
class _VerifyScreenState extends State<VerifyScreen> {
late final WebViewController _controller;
bool _isLoading = true;
@override
void initState() {
@@ -27,6 +29,11 @@ class _VerifyScreenState extends State<VerifyScreen> {
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onPageFinished: (url) {
setState(() {
_isLoading = false;
});
},
onNavigationRequest: (NavigationRequest request) async {
if (request.url.contains('user_detailz') && !request.url.contains('change-phone-verify') || request.url.contains('email/verify')) {
GoRouter.of(context).pop();
@@ -105,11 +112,17 @@ class _VerifyScreenState extends State<VerifyScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
toolbarHeight: 0,
),
appBar: AppBar(toolbarHeight: 0),
body: SafeArea(
child: WebViewWidget(controller: _controller),
child: Stack(
children: [
WebViewWidget(controller: _controller),
if (_isLoading)
const Center(
child: Loader(),
),
],
),
),
);
}

View File

@@ -3,6 +3,7 @@ import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/features/registration/models/user_registration_model.dart';
import 'package:eatvio_app/features/registration/models/location_model.dart';
import 'package:eatvio_app/features/registration/services/registration_service.dart';
import 'package:eatvio_app/core/env_config.dart';
class RegistrationRepository {
final RegistrationService registrationService;
@@ -11,8 +12,56 @@ class RegistrationRepository {
Future<String> tryRegister(UserRegistrationSheet sheet) async {
try {
return (await registrationService.register(sheet: sheet.toJson()))['token']!;
} catch (e) {
final response = await registrationService.register(sheet: sheet.toJson());
if (response.containsKey('token')) {
return response['token']!;
} else {
// Если нет токена, но есть message или errors
String errorMsg = '';
if (response.containsKey('message')) {
errorMsg += '${response['message']!}\n';
}
if (response.containsKey('errors')) {
final errors = response['errors'];
if (errors != null && errors is Map) {
(errors as Map).forEach((key, value) {
if (value is List && value.isNotEmpty) {
errorMsg += '${value.join('\n')}\n';
}
});
}
}
if (errorMsg.isNotEmpty) {
throw errorMsg.trim();
} else {
throw 'Ошибка регистрации: $response';
}
}
} on DioException catch (e) {
if (e.response != null && e.response?.statusCode == 422) {
final data = e.response?.data;
String errorMsg = '';
if (data is Map<String, dynamic>) {
if (data.containsKey('message')) {
errorMsg += data['message'] + '\n';
}
if (data.containsKey('errors')) {
final errors = data['errors'];
if (errors != null && errors is Map) {
(errors).forEach((key, value) {
if (value is List && value.isNotEmpty) {
errorMsg += '${value.join('\n')}\n';
}
});
}
}
}
if (errorMsg.isNotEmpty) {
throw errorMsg.trim();
} else {
throw 'Ошибка регистрации: $data';
}
}
rethrow;
}
}
@@ -35,7 +84,7 @@ class RegistrationRepository {
Future<bool> emailIsReachable(String email) async {
try {
final response = await sl<Dio>().getUri(Uri.parse('https://api.validate.email/validate?email=$email'));
final response = await sl<Dio>().getUri(Uri.parse('${EnvConfig.emailValidationApiUrl}?email=$email'));
if (response.data['result']['reachable'] != 'invalid') {
return true;
} else {

View File

@@ -22,7 +22,11 @@ class SettingsCubit extends Cubit<SettingsState> {
final profile = await profileRepository.getMyProfile();
late final ConsultationRequestModel? consultantInfo;
try {
consultantInfo = await consultantRepository.getMyRequestStatus(profile.slug);
if (profile.phoneConfirmationState.contains('confirmed')) {
consultantInfo = await consultantRepository.getMyRequestStatus(profile.slug);
} else {
consultantInfo = null;
}
} catch (e) {
consultantInfo = null;
}
@@ -67,4 +71,13 @@ class SettingsCubit extends Cubit<SettingsState> {
await init();
}
}
Future<bool> deleteAccount() async {
try {
await profileRepository.deleteAccount();
return true;
} catch (e) {
return false;
}
}
}

View File

@@ -1,12 +1,19 @@
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
import 'package:eatvio_app/features/settings/entities/settings_entity.dart';
import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart';
import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_state.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/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -44,9 +51,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
builder: (BuildContext context, SettingsState state) {
if (state is SettingsStateLoaded) {
return Scaffold(
appBar: AppBar(
title: const Text('Настройки'),
),
appBar: AppBar(title: const Text('Настройки')),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 16),
@@ -57,10 +62,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
const SizedBox(height: 12),
_notifications(state),
const SizedBox(height: 12),
_aboutEatvio(state),
const SizedBox(height: 12),
if (!hidePlans) ...[_aboutEatvio(state), const SizedBox(height: 12)],
_becomeConsultantButton(state),
_deleteAccountButton(state),
_deleteAccountButton(state, settingsCubit),
],
),
),
@@ -69,16 +73,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
}
if (state is SettingsStateError) {
return Scaffold(
appBar: AppBar(
title: const Text('Настройки'),
),
appBar: AppBar(title: const Text('Настройки')),
body: DataErrorWidget(onReload: () => settingsCubit.init()),
);
}
return Scaffold(
appBar: AppBar(
title: const Text('Настройки'),
),
appBar: AppBar(title: const Text('Настройки')),
body: const Loader(),
);
},
@@ -94,22 +94,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text(
'Дополнительаная информация',
style: TextStyle(
fontSize: 16,
height: 1,
color: Colors.grey,
fontFamily: 'Mulish',
),
style: TextStyle(fontSize: 16, height: 1, color: Colors.grey, fontFamily: 'Mulish'),
),
),
_tile(
onTap: () => GoRouter.of(context).pushNamed(
'generalInfoScreen',
extra: SettingsEntity(
profileSettingsModel: state.profileSettings,
profileDetailsModel: state.profileDetails,
settingsCubit: settingsCubit,
),
extra: SettingsEntity(profileSettingsModel: state.profileSettings, profileDetailsModel: state.profileDetails, settingsCubit: settingsCubit),
),
title: 'Общая информация',
),
@@ -117,11 +108,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
_tile(
onTap: () => GoRouter.of(context).pushNamed(
'physicalFormScreen',
extra: SettingsEntity(
profileSettingsModel: state.profileSettings,
profileDetailsModel: state.profileDetails,
settingsCubit: settingsCubit,
),
extra: SettingsEntity(profileSettingsModel: state.profileSettings, profileDetailsModel: state.profileDetails, settingsCubit: settingsCubit),
),
title: 'Физическая форма',
),
@@ -129,11 +116,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
_tile(
onTap: () => GoRouter.of(context).pushNamed(
'pressureScreen',
extra: SettingsEntity(
profileSettingsModel: state.profileSettings,
profileDetailsModel: state.profileDetails,
settingsCubit: settingsCubit,
),
extra: SettingsEntity(profileSettingsModel: state.profileSettings, profileDetailsModel: state.profileDetails, settingsCubit: settingsCubit),
),
title: 'Давление',
),
@@ -141,11 +124,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
_tile(
onTap: () => GoRouter.of(context).pushNamed(
'otherInfoScreen',
extra: SettingsEntity(
profileSettingsModel: state.profileSettings,
profileDetailsModel: state.profileDetails,
settingsCubit: settingsCubit,
),
extra: SettingsEntity(profileSettingsModel: state.profileSettings, profileDetailsModel: state.profileDetails, settingsCubit: settingsCubit),
),
title: 'Прочая информация',
),
@@ -153,31 +132,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
_tile(
onTap: () => GoRouter.of(context).pushNamed(
'physicalActivityScreen',
extra: SettingsEntity(
profileSettingsModel: state.profileSettings,
profileDetailsModel: state.profileDetails,
settingsCubit: settingsCubit,
),
extra: SettingsEntity(profileSettingsModel: state.profileSettings, profileDetailsModel: state.profileDetails, settingsCubit: settingsCubit),
),
title: 'Физическая нагрузка',
),
_divider(context),
_tile(
onTap: () => GoRouter.of(context).pushNamed(
'forgetPasswordScreen',
extra: true,
),
title: 'Сменить пароль',
),
_tile(onTap: () => GoRouter.of(context).pushNamed('forgetPasswordScreen', extra: true), title: 'Сменить пароль'),
_divider(context),
_tile(
onTap: () => GoRouter.of(context).pushNamed(
'changeEmailScreen',
extra: SettingsEntity(
profileSettingsModel: state.profileSettings,
profileDetailsModel: state.profileDetails,
settingsCubit: settingsCubit,
),
extra: SettingsEntity(profileSettingsModel: state.profileSettings, profileDetailsModel: state.profileDetails, settingsCubit: settingsCubit),
),
title: 'Сменить Email',
),
@@ -191,14 +156,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
children: [
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text(
'Уведомления',
style: TextStyle(
fontSize: 16,
height: 1,
color: Colors.grey,
),
),
child: Text('Уведомления', style: TextStyle(fontSize: 16, height: 1, color: Colors.grey)),
),
_tile(
padding: const EdgeInsets.symmetric(horizontal: 16),
@@ -206,11 +164,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
setState(() {
updatesNotificationsSwitchState = !updatesNotificationsSwitchState;
});
settingsCubit.updateProfileSettings(
profileSettingsModel: state.profileSettings.copyWith(
newUpdatesNotifications: updatesNotificationsSwitchState,
),
);
settingsCubit.updateProfileSettings(profileSettingsModel: state.profileSettings.copyWith(newUpdatesNotifications: updatesNotificationsSwitchState));
},
title: 'Уведомление о новых обновлениях',
trailing: CupertinoSwitch(
@@ -221,9 +175,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
updatesNotificationsSwitchState = newValue;
});
settingsCubit.updateProfileSettings(
profileSettingsModel: state.profileSettings.copyWith(
newUpdatesNotifications: updatesNotificationsSwitchState,
),
profileSettingsModel: state.profileSettings.copyWith(newUpdatesNotifications: updatesNotificationsSwitchState),
);
},
),
@@ -236,9 +188,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
fillDiaryNotificationsSwitchState = !fillDiaryNotificationsSwitchState;
});
settingsCubit.updateProfileSettings(
profileSettingsModel: state.profileSettings.copyWith(
diaryReminderNotifications: fillDiaryNotificationsSwitchState,
),
profileSettingsModel: state.profileSettings.copyWith(diaryReminderNotifications: fillDiaryNotificationsSwitchState),
);
},
title: 'Напоминание о заполнении дневника',
@@ -250,9 +200,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
fillDiaryNotificationsSwitchState = newValue;
});
settingsCubit.updateProfileSettings(
profileSettingsModel: state.profileSettings.copyWith(
diaryReminderNotifications: fillDiaryNotificationsSwitchState,
),
profileSettingsModel: state.profileSettings.copyWith(diaryReminderNotifications: fillDiaryNotificationsSwitchState),
);
},
),
@@ -264,11 +212,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
setState(() {
moodSwitchState = !moodSwitchState;
});
settingsCubit.updateProfileSettings(
profileSettingsModel: state.profileSettings.copyWith(
isMoodFillable: moodSwitchState,
),
);
settingsCubit.updateProfileSettings(profileSettingsModel: state.profileSettings.copyWith(isMoodFillable: moodSwitchState));
},
title: 'Указывать “настроение” в заполнение дневника',
trailing: CupertinoSwitch(
@@ -278,11 +222,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
setState(() {
moodSwitchState = newValue;
});
settingsCubit.updateProfileSettings(
profileSettingsModel: state.profileSettings.copyWith(
isMoodFillable: moodSwitchState,
),
);
settingsCubit.updateProfileSettings(profileSettingsModel: state.profileSettings.copyWith(isMoodFillable: moodSwitchState));
},
),
),
@@ -293,11 +233,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
setState(() {
bedTimeSwitchState = !bedTimeSwitchState;
});
settingsCubit.updateProfileSettings(
profileSettingsModel: state.profileSettings.copyWith(
isBedtimeFillable: bedTimeSwitchState,
),
);
settingsCubit.updateProfileSettings(profileSettingsModel: state.profileSettings.copyWith(isBedtimeFillable: bedTimeSwitchState));
},
title: 'Указывать “отход ко сну” в заполнение дневника',
trailing: CupertinoSwitch(
@@ -307,11 +243,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
setState(() {
bedTimeSwitchState = newValue;
});
settingsCubit.updateProfileSettings(
profileSettingsModel: state.profileSettings.copyWith(
isBedtimeFillable: bedTimeSwitchState,
),
);
settingsCubit.updateProfileSettings(profileSettingsModel: state.profileSettings.copyWith(isBedtimeFillable: bedTimeSwitchState));
},
),
),
@@ -325,24 +257,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
children: [
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text(
'О Eatvio',
style: TextStyle(
fontSize: 16,
height: 1,
color: Colors.grey,
),
),
),
_tile(
title: 'О компании',
onTap: () async => await launchUrl(Uri.parse('$baseUrl/ru/company-details')),
child: Text('О Eatvio', style: TextStyle(fontSize: 16, height: 1, color: Colors.grey)),
),
_tile(title: 'О компании', onTap: () async => await launchUrl(Uri.parse('$baseUrl/ru/company-details'))),
_divider(context),
_tile(
title: 'Пользовательское соглашение',
onTap: () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')),
),
_tile(title: 'Пользовательское соглашение', onTap: () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf'))),
],
);
}
@@ -357,19 +276,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
child: Ink(
height: 52,
width: MediaQuery.of(context).size.width - 16,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: appTheme.blueA400,
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
child: const Center(
child: Text(
'Стать консультантом',
style: TextStyle(
color: Colors.white,
height: 1,
fontSize: 18,
),
),
child: Text('Стать консультантом', style: TextStyle(color: Colors.white, height: 1, fontSize: 18)),
),
),
),
@@ -381,19 +290,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
child: Ink(
height: 52,
width: MediaQuery.of(context).size.width - 16,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: const Color(0xFFEAEAEA),
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFEAEAEA)),
child: const Center(
child: Text(
'Данные на проверке',
style: TextStyle(
color: Colors.black,
height: 1,
fontSize: 18,
),
),
child: Text('Данные на проверке', style: TextStyle(color: Colors.black, height: 1, fontSize: 18)),
),
),
);
@@ -406,19 +305,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
child: Ink(
height: 52,
width: MediaQuery.of(context).size.width - 16,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: const Color(0x33FFCC00),
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0x33FFCC00)),
child: const Center(
child: Text(
'Заявка обработана',
style: TextStyle(
color: Colors.black,
height: 1,
fontSize: 18,
),
),
child: Text('Заявка обработана', style: TextStyle(color: Colors.black, height: 1, fontSize: 18)),
),
),
),
@@ -428,57 +317,37 @@ class _SettingsScreenState extends State<SettingsScreen> {
return const SizedBox.shrink();
}
Padding _deleteAccountButton(SettingsStateLoaded state) {
Padding _deleteAccountButton(SettingsStateLoaded state, SettingsCubit settingsCubit) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: InkWell(
onTap: () {},
onTap: () async {
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
height: null,
child: DeleteAccountBottomSheet(cubit: settingsCubit),
);
},
borderRadius: BorderRadius.circular(20),
child: Ink(
height: 52,
width: MediaQuery.of(context).size.width - 16,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: const Color(0xFFEAEAEA),
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFEAEAEA)),
child: const Center(
child: Text(
'Удалить аккаунт',
style: TextStyle(
color: Colors.black,
height: 1,
fontSize: 18,
),
),
child: Text('Удалить аккаунт', style: TextStyle(color: Colors.black, height: 1, fontSize: 18)),
),
),
),
);
}
ListTile _tile({
required String title,
required Function() onTap,
Widget? trailing,
EdgeInsets? padding,
}) {
ListTile _tile({required String title, required Function() onTap, Widget? trailing, EdgeInsets? padding}) {
return ListTile(
contentPadding: padding,
onTap: onTap,
title: Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1,
),
),
trailing: trailing ??
const Icon(
Icons.arrow_forward_ios_rounded,
size: 12,
color: Colors.black38,
),
title: Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500, height: 1)),
trailing: trailing ?? const Icon(Icons.arrow_forward_ios_rounded, size: 12, color: Colors.black38),
);
}
@@ -487,10 +356,65 @@ class _SettingsScreenState extends State<SettingsScreen> {
child: Container(
height: 0.4,
width: MediaQuery.of(context).size.width - 32,
decoration: BoxDecoration(
color: Colors.black26,
borderRadius: BorderRadius.circular(16),
),
decoration: BoxDecoration(color: Colors.black26, borderRadius: BorderRadius.circular(16)),
),
);
}
}
class DeleteAccountBottomSheet extends StatefulWidget {
const DeleteAccountBottomSheet({super.key, required this.cubit});
final SettingsCubit cubit;
@override
State<DeleteAccountBottomSheet> createState() => _DeleteAccountBottomSheet();
}
class _DeleteAccountBottomSheet extends State<DeleteAccountBottomSheet> {
bool? isFailure;
@override
Widget build(BuildContext context) {
final s = S.of(context);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 8),
const BottomSheetHandlerBar(),
const SizedBox(height: 20),
Text(s.accountDeleting, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
const SizedBox(height: 16),
Text(
s.youShureYouWantToDeleteAccount,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
s.itCantBeRecovered,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
CustomOutlinedButton(
text: s.delete,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
onPressed: () async {
final result = await widget.cubit.deleteAccount();
if (result == true) {
AppRouter.navigatorKey.currentContext?.go('/auth-screen');
return;
}
AppRouter.navigatorKey.currentContext?.pop();
if (!context.mounted) return;
showErrorDialog(context, description: s.cannotDeleteAccount);
},
),
const SizedBox(height: 40),
],
),
);
}

View File

@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:eatvio_app/core/env_config.dart';
class SupportScreen extends StatelessWidget {
const SupportScreen({super.key});
@@ -15,48 +16,28 @@ class SupportScreen extends StatelessWidget {
final s = S.of(context);
return Scaffold(
appBar: AppBar(
title: const Text('Поддержка'),
),
appBar: AppBar(title: const Text('Поддержка')),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.all(16),
child: Text(
'Если у вас есть вопросы по работе нашего приложения, вы можете задать их, перейдя по ссылке:',
style: TextStyle(
fontSize: 16,
),
),
child: Text('Если у вас есть вопросы по работе нашего приложения, вы можете задать их, перейдя по ссылке:', style: TextStyle(fontSize: 16)),
),
InkWell(
onTap: () {
launchUrl(
Uri.parse('https://t.me/eatvio'),
mode: LaunchMode.externalApplication,
);
launchUrl(Uri.parse(EnvConfig.telegramSupportUrl), mode: LaunchMode.externalApplication);
},
borderRadius: BorderRadius.circular(16),
child: Ink(
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(16)),
padding: const EdgeInsets.all(16),
child: Row(
children: [
SvgPicture.asset(
'assets/icons/telegram.svg',
colorFilter: const ColorFilter.mode(Colors.black, BlendMode.srcIn),
),
SvgPicture.asset('assets/icons/telegram.svg', colorFilter: const ColorFilter.mode(Colors.black, BlendMode.srcIn)),
const SizedBox(width: 12),
const Text(
'Telegram: t.me/eatvio',
style: TextStyle(
fontSize: 16,
),
),
Text('Telegram: ${EnvConfig.telegramSupportUrl.replaceFirst('https://', '')}', style: const TextStyle(fontSize: 16)),
],
),
),
@@ -64,26 +45,19 @@ class SupportScreen extends StatelessWidget {
const SizedBox(height: 8),
InkWell(
onTap: () {
Clipboard.setData(const ClipboardData(text: 'help@eatv.io'));
Clipboard.setData(ClipboardData(text: EnvConfig.supportEmail));
showInfoToast(s.mail_was_copied);
},
borderRadius: BorderRadius.circular(16),
child: Ink(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(16)),
width: double.infinity,
padding: const EdgeInsets.all(16),
child: const Row(
child: Row(
children: [
Icon(Icons.alternate_email_rounded),
SizedBox(width: 12),
Text(
'E-mail: help@eatv.io',
style: TextStyle(
fontSize: 16,
),
),
const Icon(Icons.alternate_email_rounded),
const SizedBox(width: 12),
Text('E-mail: ${EnvConfig.supportEmail}', style: const TextStyle(fontSize: 16)),
],
),
),
@@ -92,54 +66,26 @@ class SupportScreen extends StatelessWidget {
const SizedBox(height: 16),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text(
'Правовые документы',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
child: Text('Правовые документы', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500)),
),
const SizedBox(height: 12),
InkWell(
onTap: () async => await launchUrl(Uri.parse('$baseUrl/docs/public_offer.pdf')),
borderRadius: BorderRadius.circular(16),
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Text(
'Публичная офёрта',
style: TextStyle(
fontSize: 16,
),
if (!hidePlans) ...[
InkWell(
onTap: () async => await launchUrl(Uri.parse('$baseUrl/docs/privacy.pdf')),
borderRadius: BorderRadius.circular(16),
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Text('Политика конфиденциальности', style: TextStyle(fontSize: 16)),
),
),
),
const SizedBox(height: 8),
InkWell(
onTap: () async => await launchUrl(Uri.parse('$baseUrl/docs/privacy.pdf')),
borderRadius: BorderRadius.circular(16),
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Text(
'Политика конфиденциальности',
style: TextStyle(
fontSize: 16,
),
),
),
),
const SizedBox(height: 8),
const SizedBox(height: 8),
],
InkWell(
onTap: () async => await launchUrl(Uri.parse('$baseUrl/company-details')),
borderRadius: BorderRadius.circular(16),
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Text(
'О компании',
style: TextStyle(
fontSize: 16,
),
),
child: Text('О компании', style: TextStyle(fontSize: 16)),
),
),
],

View File

@@ -6,6 +6,7 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../../core/theme/theme_helper.dart';
import '../../../../core/env_config.dart';
class UpdateScreen extends StatelessWidget {
const UpdateScreen({super.key});
@@ -17,16 +18,7 @@ class UpdateScreen extends StatelessWidget {
body: Stack(
clipBehavior: Clip.none,
children: [
Positioned(
left: -20,
top: -20,
right: -20,
bottom: -20,
child: Image.asset(
'assets/images/bg_welcome.png',
fit: BoxFit.cover,
),
),
Positioned(left: -20, top: -20, right: -20, bottom: -20, child: Image.asset('assets/images/bg_welcome.png', fit: BoxFit.cover)),
Positioned.fill(
child: SafeArea(
child: Padding(
@@ -48,47 +40,26 @@ class UpdateScreen extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 16),
const Text(
'Пора обновиться! ⚡',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.w800,
height: 1,
),
),
const Text('Пора обновиться! ⚡', style: TextStyle(fontSize: 28, fontWeight: FontWeight.w800, height: 1)),
const SizedBox(height: 20),
const Text(
'Мы выпустили важное обновление\nс новыми функциями и исправлениями.\nЧтобы продолжить пользоваться\nприложением без сбоев, обновите\nего прямо сейчас.',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.15,
),
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, height: 1.15),
),
const SizedBox(height: 32),
InkWell(
onTap: () async {
final Uri url = Platform.isAndroid
? Uri.parse('https://www.rustore.ru/catalog/app/com.eatvioapp.app')
: Uri.parse('https://apps.apple.com/app/idYOUR_APP_ID');
final Uri url = Platform.isAndroid ? Uri.parse(EnvConfig.rustoreUrl) : Uri.parse(EnvConfig.appStoreUrl);
await launchUrl(url);
},
borderRadius: BorderRadius.circular(8),
child: Ink(
height: 52,
decoration: BoxDecoration(
color: appTheme.blueA400,
borderRadius: BorderRadius.circular(8),
),
decoration: BoxDecoration(color: appTheme.blueA400, borderRadius: BorderRadius.circular(8)),
child: const Center(
child: Text(
'Обновить',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
height: 1,
color: Colors.white,
),
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, height: 1, color: Colors.white),
),
),
),

View File

@@ -20,19 +20,11 @@ class UserDetailsCubit extends Cubit<UserDetailsState> {
}
}
void getChartData({
String period = '1',
required String slug,
}) async {
void getChartData({String period = '1', required String slug}) async {
try {
final weightPoints = await profileRepository.getWeight(period: period);
final circumferencePoints = await profileRepository.getCircumferences(period: period);
emit(
state.copyWith(
weightPoints: weightPoints,
circumferencePoints: circumferencePoints,
),
);
final weightPoints = await profileRepository.getWeight(period: period, slug: slug);
final circumferencePoints = await profileRepository.getCircumferences(period: period, slug: slug);
emit(state.copyWith(weightPoints: weightPoints, circumferencePoints: circumferencePoints));
} catch (e, stackTrace) {
log(e.toString(), stackTrace: stackTrace);
emit(state.copyWith(error: e.toString(), isLoading: false));

View File

@@ -31,9 +31,7 @@ class UserDetailsScreen extends StatelessWidget {
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => sl<UserDetailsCubit>()..getUserDetails(slug),
child: UserDetailsContent(
slug: slug,
),
child: UserDetailsContent(slug: slug),
);
}
}
@@ -79,27 +77,15 @@ class _UserDetailsContentState extends State<UserDetailsContent> {
actions: [
PortalTarget(
visible: isMoreOptionsVisible,
portalFollower: MoreOptions(
path: 'profile/${widget.slug}',
complainedContentType: ComplainedContent.profile,
slug: widget.slug,
),
anchor: const Aligned(
shiftToWithinBound: AxisFlag(x: true, y: true),
follower: Alignment.topRight,
target: Alignment.bottomRight,
),
portalFollower: MoreOptions(path: 'profile/${widget.slug}', complainedContentType: ComplainedContent.profile, slug: widget.slug),
anchor: const Aligned(shiftToWithinBound: AxisFlag(x: true, y: true), follower: Alignment.topRight, target: Alignment.bottomRight),
child: IconButton(
onPressed: () {
setState(() {
isMoreOptionsVisible = !isMoreOptionsVisible;
});
},
icon: SvgPicture.asset(
'assets/icons/more_button.svg',
width: 20,
height: 20,
),
icon: SvgPicture.asset('assets/icons/more_button.svg', width: 20, height: 20),
),
),
],
@@ -124,25 +110,16 @@ class _UserDetailsContentState extends State<UserDetailsContent> {
initiallyExpanded: true,
title: s.general_info,
children: [
Text(
s.date_of_birth,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
Text(s.date_of_birth, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const SizedBox(height: 4),
CustomTextFormField(
readOnly: true,
controller: TextEditingController(text: DateTime.parse(profileDetails.birthdate).format('dd.MM.yyyy')),
),
const SizedBox(height: 16),
Text(
s.sex,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
Text(s.sex, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const SizedBox(height: 4),
CustomTextFormField(
readOnly: true,
controller: TextEditingController(text: profileDetails.gender.parseGender()),
),
CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.gender.parseGender())),
const SizedBox(height: 16),
],
),
@@ -155,15 +132,9 @@ class _UserDetailsContentState extends State<UserDetailsContent> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
s.height,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
Text(s.height, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const SizedBox(height: 4),
CustomTextFormField(
readOnly: true,
controller: TextEditingController(text: profileDetails.height.toString()),
),
CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.height.toString())),
],
),
),
@@ -172,15 +143,9 @@ class _UserDetailsContentState extends State<UserDetailsContent> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
s.weight,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
Text(s.weight, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const SizedBox(height: 4),
CustomTextFormField(
readOnly: true,
controller: TextEditingController(text: profileDetails.weight.toStringAsFixed(0)),
),
CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.weight.toStringAsFixed(0))),
],
),
),
@@ -193,15 +158,9 @@ class _UserDetailsContentState extends State<UserDetailsContent> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
s.chest_circumference,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
Text(s.chest_circumference, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const SizedBox(height: 4),
CustomTextFormField(
readOnly: true,
controller: TextEditingController(text: profileDetails.chestCircumference.toString()),
),
CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.chestCircumference.toString())),
],
),
),
@@ -210,15 +169,9 @@ class _UserDetailsContentState extends State<UserDetailsContent> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
s.waist_circumference,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
Text(s.waist_circumference, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const SizedBox(height: 4),
CustomTextFormField(
readOnly: true,
controller: TextEditingController(text: profileDetails.waistCircumference.toString()),
),
CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.waistCircumference.toString())),
],
),
),
@@ -227,15 +180,9 @@ class _UserDetailsContentState extends State<UserDetailsContent> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
s.hip_circumference,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
Text(s.hip_circumference, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const SizedBox(height: 4),
CustomTextFormField(
readOnly: true,
controller: TextEditingController(text: profileDetails.hipCircumference.toString()),
),
CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.hipCircumference.toString())),
],
),
),
@@ -253,15 +200,9 @@ class _UserDetailsContentState extends State<UserDetailsContent> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
s.upper,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
Text(s.upper, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const SizedBox(height: 4),
CustomTextFormField(
readOnly: true,
controller: TextEditingController(text: profileDetails.bloodPressureUp.toString()),
),
CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.bloodPressureUp.toString())),
],
),
),
@@ -270,10 +211,7 @@ class _UserDetailsContentState extends State<UserDetailsContent> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
s.lower,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
Text(s.lower, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const SizedBox(height: 4),
CustomTextFormField(
readOnly: true,
@@ -291,15 +229,9 @@ class _UserDetailsContentState extends State<UserDetailsContent> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
s.has_hypo,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
Text(s.has_hypo, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const SizedBox(height: 4),
CustomTextFormField(
readOnly: true,
controller: TextEditingController(text: profileDetails.hasHypo.toString()),
),
CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.hasHypo.toString())),
],
),
),
@@ -308,15 +240,9 @@ class _UserDetailsContentState extends State<UserDetailsContent> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
s.has_hyper,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
Text(s.has_hyper, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const SizedBox(height: 4),
CustomTextFormField(
readOnly: true,
controller: TextEditingController(text: profileDetails.hasHyper.toString()),
),
CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.hasHyper.toString())),
],
),
),
@@ -328,60 +254,27 @@ class _UserDetailsContentState extends State<UserDetailsContent> {
ExpandableBouble(
title: s.other_info,
children: [
Text(
s.has_alergy,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
Text(s.has_alergy, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const SizedBox(height: 4),
CustomTextFormField(
readOnly: true,
controller: TextEditingController(text: profileDetails.aboutAlergy),
),
CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.aboutAlergy)),
const SizedBox(height: 16),
Text(
s.takes_pills,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
Text(s.takes_pills, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const SizedBox(height: 4),
CustomTextFormField(
readOnly: true,
controller: TextEditingController(text: profileDetails.aboutTakingPils),
),
CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.aboutTakingPils)),
const SizedBox(height: 16),
Text(
s.sport_hobbies,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
Text(s.sport_hobbies, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const SizedBox(height: 4),
CustomTextFormField(
readOnly: true,
controller: TextEditingController(text: profileDetails.aboutSportHobbies),
),
CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.aboutSportHobbies)),
const SizedBox(height: 16),
Text(
s.liked_groceries,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
Text(s.liked_groceries, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const SizedBox(height: 4),
CustomTextFormField(
readOnly: true,
controller: TextEditingController(text: profileDetails.favoriteFoods),
),
CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.favoriteFoods)),
const SizedBox(height: 16),
Text(
s.not_liked_groceries,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
Text(s.not_liked_groceries, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const SizedBox(height: 4),
CustomTextFormField(
readOnly: true,
controller: TextEditingController(text: profileDetails.unfavoriteFoods),
),
CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.unfavoriteFoods)),
const SizedBox(height: 16),
Text(
s.additional_information,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
Text(s.additional_information, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const SizedBox(height: 4),
SizedBox(
height: 112,
@@ -413,10 +306,7 @@ class _UserDetailsContentState extends State<UserDetailsContent> {
),
);
}
return Scaffold(
appBar: AppBar(),
body: const Loader(),
);
return Scaffold(appBar: AppBar(), body: const Loader());
},
);
}
@@ -432,31 +322,17 @@ class _UserDetailsContentState extends State<UserDetailsContent> {
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
child: Row(
children: [
_choiceChipWidget(0),
const SizedBox(width: 8),
_choiceChipWidget(1),
const SizedBox(width: 8),
_choiceChipWidget(2),
],
),
child: Row(children: [_choiceChipWidget(0), const SizedBox(width: 8), _choiceChipWidget(1), const SizedBox(width: 8), _choiceChipWidget(2)]),
),
Builder(
builder: (context) {
switch (_selectedChipIndex) {
case 0:
return WeightChartWidget(
weightPoints: weightPoints,
);
return WeightChartWidget(weightPoints: weightPoints);
case 1:
return CircumferenceChartWidget(
circumferencePoints: circumferencePoints,
);
return CircumferenceChartWidget(circumferencePoints: circumferencePoints);
case 2:
return ImtChartWidget(
weightPoints: weightPoints,
);
return ImtChartWidget(weightPoints: weightPoints);
default:
return const SizedBox.shrink();
}
@@ -489,16 +365,10 @@ class _UserDetailsContentState extends State<UserDetailsContent> {
pressElevation: 0,
showCheckmark: false,
side: BorderSide.none,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
label: Text(chipsIntervalLabelList[index]),
labelStyle: TextStyle(
color: appTheme.gray600,
fontWeight: FontWeight.w500,
fontSize: 12,
),
labelStyle: TextStyle(color: appTheme.gray600, fontWeight: FontWeight.w500, fontSize: 12),
selected: _selectedIntervalChipIndex == index,
onSelected: (bool selected) {
context.read<UserDetailsCubit>().getChartData(period: chipsIntervalList[index], slug: widget.slug);
@@ -515,16 +385,10 @@ class _UserDetailsContentState extends State<UserDetailsContent> {
pressElevation: 0,
showCheckmark: false,
side: BorderSide.none,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
label: Text(chipsLabelList[index]),
labelStyle: TextStyle(
color: index == _selectedChipIndex ? Colors.white : appTheme.gray600,
fontWeight: FontWeight.w500,
fontSize: 12,
),
labelStyle: TextStyle(color: index == _selectedChipIndex ? Colors.white : appTheme.gray600, fontWeight: FontWeight.w500, fontSize: 12),
selected: _selectedChipIndex == index,
backgroundColor: appTheme.gray100,
onSelected: (bool selected) {

View File

@@ -1,8 +1,8 @@
// File generated by FlutterFire CLI.
// ignore_for_file: type=lint
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb, TargetPlatform;
import 'package:eatvio_app/core/env_config.dart';
/// Default [FirebaseOptions] for use with your Firebase apps.
///
@@ -43,28 +43,26 @@ class DefaultFirebaseOptions {
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
throw UnsupportedError('DefaultFirebaseOptions are not supported for this platform.');
}
}
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyBUe1UqAAMfgMcd4hWdRoLF1Os-yn-HE9k',
appId: '1:613980912922:android:4de95e5f2d50ba69ecc0a3',
messagingSenderId: '613980912922',
projectId: 'eatvio-3467b',
databaseURL: 'https://eatvio-3467b-default-rtdb.europe-west1.firebasedatabase.app',
storageBucket: 'eatvio-3467b.firebasestorage.app',
static FirebaseOptions get android => FirebaseOptions(
apiKey: EnvConfig.firebaseAndroidApiKey,
appId: EnvConfig.firebaseAndroidAppId,
messagingSenderId: EnvConfig.firebaseMessagingSenderId,
projectId: EnvConfig.firebaseProjectId,
databaseURL: EnvConfig.firebaseDatabaseUrl,
storageBucket: EnvConfig.firebaseStorageBucket,
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyBnE8tQjL9_1HuXrTnqNy6qbXpoenO36yQ',
appId: '1:613980912922:ios:c557d140a1212e63ecc0a3',
messagingSenderId: '613980912922',
projectId: 'eatvio-3467b',
databaseURL: 'https://eatvio-3467b-default-rtdb.europe-west1.firebasedatabase.app',
storageBucket: 'eatvio-3467b.firebasestorage.app',
iosBundleId: 'com.eatvioapp.app',
static FirebaseOptions get ios => FirebaseOptions(
apiKey: EnvConfig.firebaseIosApiKey,
appId: EnvConfig.firebaseIosAppId,
messagingSenderId: EnvConfig.firebaseMessagingSenderId,
projectId: EnvConfig.firebaseProjectId,
databaseURL: EnvConfig.firebaseDatabaseUrl,
storageBucket: EnvConfig.firebaseStorageBucket,
iosBundleId: EnvConfig.firebaseIosBundleId,
);
}

View File

@@ -51,6 +51,9 @@ class MessageLookup extends MessageLookupByLibrary {
"about_diet": MessageLookupByLibrary.simpleMessage("Об этой диете"),
"about_plan": MessageLookupByLibrary.simpleMessage("Об этом плане"),
"about_recipe": MessageLookupByLibrary.simpleMessage("Об этом рецепте"),
"accountDeleting": MessageLookupByLibrary.simpleMessage(
"Удаление аккаунта",
),
"active": MessageLookupByLibrary.simpleMessage("Активен"),
"active_consultations_count": MessageLookupByLibrary.simpleMessage(
"Кол-во активных консультаций",
@@ -124,6 +127,9 @@ class MessageLookup extends MessageLookupByLibrary {
),
"cancel_diet": MessageLookupByLibrary.simpleMessage("Отменить диету?"),
"cancel_order": MessageLookupByLibrary.simpleMessage("Отменить заказ"),
"cannotDeleteAccount": MessageLookupByLibrary.simpleMessage(
"Не получилось удалить ваш аккаунт",
),
"carb_type": MessageLookupByLibrary.simpleMessage("Мучное"),
"carb_type_desc": MessageLookupByLibrary.simpleMessage(
"Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.",
@@ -256,6 +262,7 @@ class MessageLookup extends MessageLookupByLibrary {
),
"days": MessageLookupByLibrary.simpleMessage("дней"),
"deception": MessageLookupByLibrary.simpleMessage("Обман"),
"delete": MessageLookupByLibrary.simpleMessage("Удалить"),
"deleteComment": MessageLookupByLibrary.simpleMessage(
"Удалить комментарий",
),
@@ -411,6 +418,9 @@ class MessageLookup extends MessageLookupByLibrary {
"invited_people_count": MessageLookupByLibrary.simpleMessage(
"Количество приглашенных",
),
"itCantBeRecovered": MessageLookupByLibrary.simpleMessage(
"Его нельзя будет восстановить",
),
"label": MessageLookupByLibrary.simpleMessage("Название"),
"language": MessageLookupByLibrary.simpleMessage("Язык"),
"last_name": MessageLookupByLibrary.simpleMessage("Фамилия"),
@@ -780,6 +790,9 @@ class MessageLookup extends MessageLookupByLibrary {
"youShureYouWantToCancelOrder": MessageLookupByLibrary.simpleMessage(
"Вы уверены, что хотите отменить заказ?",
),
"youShureYouWantToDeleteAccount": MessageLookupByLibrary.simpleMessage(
"Вы уверены, что хотите удалить аккаунт?",
),
"you_can_not_open_this_day": MessageLookupByLibrary.simpleMessage(
"Вы не можете перейти в этот день",
),

View File

@@ -3414,6 +3414,51 @@ class S {
args: [],
);
}
/// `Удаление аккаунта`
String get accountDeleting {
return Intl.message(
'Удаление аккаунта',
name: 'accountDeleting',
desc: '',
args: [],
);
}
/// `Вы уверены, что хотите удалить аккаунт?`
String get youShureYouWantToDeleteAccount {
return Intl.message(
'Вы уверены, что хотите удалить аккаунт?',
name: 'youShureYouWantToDeleteAccount',
desc: '',
args: [],
);
}
/// `Его нельзя будет восстановить`
String get itCantBeRecovered {
return Intl.message(
'Его нельзя будет восстановить',
name: 'itCantBeRecovered',
desc: '',
args: [],
);
}
/// `Удалить`
String get delete {
return Intl.message('Удалить', name: 'delete', desc: '', args: []);
}
/// `Не получилось удалить ваш аккаунт`
String get cannotDeleteAccount {
return Intl.message(
'Не получилось удалить ваш аккаунт',
name: 'cannotDeleteAccount',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<S> {

View File

@@ -402,10 +402,15 @@
"allowAcessToGaleryDescription": "Предоставление доступа позволит вам добавлять фото к записям",
"allowAccessToCamera": "Разрешите доступ к камере",
"allowAccessToCameraDescription": "Предоставление доступа позволит вам делать фотографии и добавлять к записям",
"allowAccess": "Разрешить доступ",
"allowAccess": "Разрешить доступ" ,
"copy": "Скопировать",
"reply": "Ответить",
"deleteComment": "Удалить комментарий"
"deleteComment": "Удалить комментарий",
"accountDeleting": "Удаление аккаунта",
"youShureYouWantToDeleteAccount": "Вы уверены, что хотите удалить аккаунт?",
"itCantBeRecovered": "Его нельзя будет восстановить",
"delete": "Удалить",
"cannotDeleteAccount": "Не получилось удалить ваш аккаунт"
}

View File

@@ -1,3 +1,4 @@
import 'dart:io' as io;
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/locator_service.dart' as di;
import 'package:eatvio_app/core/locator_service.dart';
@@ -11,6 +12,8 @@ import 'package:eatvio_app/features/internet_connection_checker/internet_connect
import 'package:eatvio_app/firebase_options.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
@@ -18,22 +21,28 @@ import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:flutter_portal/flutter_portal.dart';
import 'package:keyboard_dismisser/keyboard_dismisser.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:eatvio_app/core/env_config.dart';
Future<void> main() async {
WidgetsBinding widgetsBinding = SentryWidgetsFlutterBinding.ensureInitialized();
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
await dotenv.load(fileName: "env/.env");
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
await FirebaseMessaging.instance.requestPermission();
await di.init();
await SentryFlutter.init(
(options) {
options.dsn = 'http://ba7f2653067776f3034e6e89850e85a1@sentry.md-desk.ru:9000/2';
options.tracesSampleRate = 1.0;
options.debug = false;
},
appRunner: () => runApp(const MyApp()),
);
if (kDebugMode) {
io.HttpClient.enableTimelineLogging = true;
}
await SentryFlutter.init((options) {
options.dsn = EnvConfig.sentryDsn;
options.tracesSampleRate = 1.0;
options.debug = false;
}, appRunner: () => runApp(const MyApp()));
}
class MyApp extends StatelessWidget {

View File

@@ -1,10 +1,10 @@
name: eatvio_app
description: A new Flutter project.
version: 1.1.16+46
version: 1.2.8+68
publish_to: 'none'
environment:
sdk: '>=3.3.4 <4.0.0'
sdk: '>=3.8.0 <4.0.0'
dependencies:
flutter:
@@ -13,33 +13,37 @@ dependencies:
flutter_localizations:
sdk: flutter
flutter_secure_storage: ^9.2.4
flutter_secure_storage:
git:
url: https://github.com/sherlockvn/flutter_secure_storage.git
ref: remove-old-encrypt-algorithm
path: flutter_secure_storage
flutter_bloc: ^9.1.1
cached_network_image: ^3.4.1
flutter_svg: ^2.2.0
equatable: ^2.0.7
freezed_annotation: ^3.0.0
freezed_annotation: ^3.1.0
json_annotation: ^4.9.0
validators: ^3.0.0
path_provider: ^2.1.5
carousel_slider_plus: ^7.1.1
flutter_form_builder: ^10.0.1
flutter_form_builder: ^10.1.0
image_picker: ^1.1.2
flutter_typeahead: ^5.2.0
go_router: ^16.0.0
keyboard_dismisser: ^3.0.0
retrofit: ^4.5.0
get_it: ^8.0.3
retrofit: ^4.7.0
get_it: ^8.1.0
dio: ^5.8.0+1
dart_pusher_channels: ^1.2.3
photo_view: ^0.15.0
dismissible_page: ^1.0.2
url_launcher: ^6.3.1
url_launcher: ^6.3.2
easy_debounce: ^2.0.3
badges: ^3.1.2
google_fonts: ^6.2.1
syncfusion_flutter_charts: ^30.1.38
syncfusion_flutter_gauges: ^30.1.38
syncfusion_flutter_charts: ^30.1.42
syncfusion_flutter_gauges: ^30.1.42
table_calendar: ^3.2.0
flutter_keyboard_visibility: ^6.0.0
share_plus: ^11.0.0
@@ -58,11 +62,11 @@ dependencies:
http: ^1.4.0
flutter_native_splash: ^2.4.6
webview_flutter: ^4.13.0
sentry_flutter: ^8.14.2
sentry_flutter: ^9.5.0
connectivity_plus: ^6.1.4
visibility_detector: ^0.4.0+2
firebase_core: ^3.15.0
firebase_messaging: ^15.2.8
firebase_core: ^3.15.2
firebase_messaging: ^15.2.10
http_parser: ^4.1.2
path: ^1.9.1
collection: ^1.19.1
@@ -70,15 +74,16 @@ dependencies:
version: ^3.0.2
package_info_plus: ^8.3.0
device_info_plus: ^11.5.0
permission_handler: ^12.0.0+1
permission_handler: ^12.0.1
qr_code_scanner_plus: ^2.0.10+1
flutter_dotenv: ^5.1.0
dev_dependencies:
build_runner: ^2.5.4
freezed: ^3.0.6
json_serializable: ^6.9.5
retrofit_generator: ^9.2.0
mockito: ^5.4.6
build_runner: ^2.6.0
freezed: ^3.2.0
json_serializable: ^6.10.0
retrofit_generator: ^10.0.1
mockito: ^5.5.0
flutter_test:
sdk: flutter
@@ -88,6 +93,7 @@ dev_dependencies:
flutter:
uses-material-design: true
assets:
- env/.env
- assets/
- assets/images/
- assets/audio/

View File

@@ -19,8 +19,10 @@ void main() {
id: 0,
name: 'Alexander Avanesyan',
email: 'mso7nueh@mail.ru',
phoneConfirmationState: '',
isPublicProfile: true,
isEntitySubscribedByMe: false,
isBlockedByMe: false,
slug: 'slug',
profilePhotoUrl: '',
roles: ['user'],

View File

@@ -1,526 +0,0 @@
// Mocks generated by Mockito 5.4.6 from annotations
// in eatvio_app/test/features/profile/services/profile_service_test.dart.
// Do not manually edit this file.
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:async' as _i6;
import 'dart:io' as _i10;
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'
as _i7;
import 'package:eatvio_app/features/profile/models/chosen_diets_pagination_model.dart'
as _i4;
import 'package:eatvio_app/features/profile/models/circumference_model.dart'
as _i9;
import 'package:eatvio_app/features/profile/models/profile_model.dart' as _i2;
import 'package:eatvio_app/features/profile/models/sub_model.dart' as _i3;
import 'package:eatvio_app/features/profile/models/weight_model.dart' as _i8;
import 'package:eatvio_app/features/profile/services/profile_service.dart'
as _i5;
import 'package:mockito/mockito.dart' as _i1;
import 'package:mockito/src/dummies.dart' as _i11;
// ignore_for_file: type=lint
// ignore_for_file: avoid_redundant_argument_values
// ignore_for_file: avoid_setters_without_getters
// ignore_for_file: comment_references
// ignore_for_file: deprecated_member_use
// ignore_for_file: deprecated_member_use_from_same_package
// ignore_for_file: implementation_imports
// ignore_for_file: invalid_use_of_visible_for_testing_member
// ignore_for_file: must_be_immutable
// ignore_for_file: prefer_const_constructors
// ignore_for_file: unnecessary_parenthesis
// ignore_for_file: camel_case_types
// ignore_for_file: subtype_of_sealed_class
class _FakeProfileModel_0 extends _i1.SmartFake implements _i2.ProfileModel {
_FakeProfileModel_0(
Object parent,
Invocation parentInvocation,
) : super(
parent,
parentInvocation,
);
}
class _FakeProfileDetailsModel_1 extends _i1.SmartFake
implements _i2.ProfileDetailsModel {
_FakeProfileDetailsModel_1(
Object parent,
Invocation parentInvocation,
) : super(
parent,
parentInvocation,
);
}
class _FakeSubPaginationModel_2 extends _i1.SmartFake
implements _i3.SubPaginationModel {
_FakeSubPaginationModel_2(
Object parent,
Invocation parentInvocation,
) : super(
parent,
parentInvocation,
);
}
class _FakeChosenDietsPaginationModel_3 extends _i1.SmartFake
implements _i4.ChosenDietsPaginationModel {
_FakeChosenDietsPaginationModel_3(
Object parent,
Invocation parentInvocation,
) : super(
parent,
parentInvocation,
);
}
/// A class which mocks [ProfileService].
///
/// See the documentation for Mockito's code generation for more information.
class MockProfileService extends _i1.Mock implements _i5.ProfileService {
@override
_i6.Future<_i2.ProfileModel> getProfileBySlug({required String? slug}) =>
(super.noSuchMethod(
Invocation.method(
#getProfileBySlug,
[],
{#slug: slug},
),
returnValue: _i6.Future<_i2.ProfileModel>.value(_FakeProfileModel_0(
this,
Invocation.method(
#getProfileBySlug,
[],
{#slug: slug},
),
)),
returnValueForMissingStub:
_i6.Future<_i2.ProfileModel>.value(_FakeProfileModel_0(
this,
Invocation.method(
#getProfileBySlug,
[],
{#slug: slug},
),
)),
) as _i6.Future<_i2.ProfileModel>);
@override
_i6.Future<_i2.ProfileModel> getMyProfile() => (super.noSuchMethod(
Invocation.method(
#getMyProfile,
[],
),
returnValue: _i6.Future<_i2.ProfileModel>.value(_FakeProfileModel_0(
this,
Invocation.method(
#getMyProfile,
[],
),
)),
returnValueForMissingStub:
_i6.Future<_i2.ProfileModel>.value(_FakeProfileModel_0(
this,
Invocation.method(
#getMyProfile,
[],
),
)),
) as _i6.Future<_i2.ProfileModel>);
@override
_i6.Future<_i2.ProfileDetailsModel> getMyProfileDetails() =>
(super.noSuchMethod(
Invocation.method(
#getMyProfileDetails,
[],
),
returnValue: _i6.Future<_i2.ProfileDetailsModel>.value(
_FakeProfileDetailsModel_1(
this,
Invocation.method(
#getMyProfileDetails,
[],
),
)),
returnValueForMissingStub: _i6.Future<_i2.ProfileDetailsModel>.value(
_FakeProfileDetailsModel_1(
this,
Invocation.method(
#getMyProfileDetails,
[],
),
)),
) as _i6.Future<_i2.ProfileDetailsModel>);
@override
_i6.Future<_i2.ProfileDetailsModel> getProfileDetailsBySlug(
{required String? slug}) =>
(super.noSuchMethod(
Invocation.method(
#getProfileDetailsBySlug,
[],
{#slug: slug},
),
returnValue: _i6.Future<_i2.ProfileDetailsModel>.value(
_FakeProfileDetailsModel_1(
this,
Invocation.method(
#getProfileDetailsBySlug,
[],
{#slug: slug},
),
)),
returnValueForMissingStub: _i6.Future<_i2.ProfileDetailsModel>.value(
_FakeProfileDetailsModel_1(
this,
Invocation.method(
#getProfileDetailsBySlug,
[],
{#slug: slug},
),
)),
) as _i6.Future<_i2.ProfileDetailsModel>);
@override
_i6.Future<void> addCircumference({
required int? chestCircumference,
required int? waistCircumference,
required int? hipCircumference,
}) =>
(super.noSuchMethod(
Invocation.method(
#addCircumference,
[],
{
#chestCircumference: chestCircumference,
#waistCircumference: waistCircumference,
#hipCircumference: hipCircumference,
},
),
returnValue: _i6.Future<void>.value(),
returnValueForMissingStub: _i6.Future<void>.value(),
) as _i6.Future<void>);
@override
_i6.Future<void> addWeight({required double? weight}) => (super.noSuchMethod(
Invocation.method(
#addWeight,
[],
{#weight: weight},
),
returnValue: _i6.Future<void>.value(),
returnValueForMissingStub: _i6.Future<void>.value(),
) as _i6.Future<void>);
@override
_i6.Future<List<_i7.FeedBlogPostModel>> getBlogPostByAuthorSlug(
{required String? slug}) =>
(super.noSuchMethod(
Invocation.method(
#getBlogPostByAuthorSlug,
[],
{#slug: slug},
),
returnValue: _i6.Future<List<_i7.FeedBlogPostModel>>.value(
<_i7.FeedBlogPostModel>[]),
returnValueForMissingStub:
_i6.Future<List<_i7.FeedBlogPostModel>>.value(
<_i7.FeedBlogPostModel>[]),
) as _i6.Future<List<_i7.FeedBlogPostModel>>);
@override
_i6.Future<List<_i8.WeightModel>> getWeight({required String? period}) =>
(super.noSuchMethod(
Invocation.method(
#getWeight,
[],
{#period: period},
),
returnValue:
_i6.Future<List<_i8.WeightModel>>.value(<_i8.WeightModel>[]),
returnValueForMissingStub:
_i6.Future<List<_i8.WeightModel>>.value(<_i8.WeightModel>[]),
) as _i6.Future<List<_i8.WeightModel>>);
@override
_i6.Future<List<_i8.WeightModel>> getWeightBySlug({
required String? period,
required String? slug,
}) =>
(super.noSuchMethod(
Invocation.method(
#getWeightBySlug,
[],
{
#period: period,
#slug: slug,
},
),
returnValue:
_i6.Future<List<_i8.WeightModel>>.value(<_i8.WeightModel>[]),
returnValueForMissingStub:
_i6.Future<List<_i8.WeightModel>>.value(<_i8.WeightModel>[]),
) as _i6.Future<List<_i8.WeightModel>>);
@override
_i6.Future<List<_i9.CircumferenceModel>> getCircumferences(
{required String? period}) =>
(super.noSuchMethod(
Invocation.method(
#getCircumferences,
[],
{#period: period},
),
returnValue: _i6.Future<List<_i9.CircumferenceModel>>.value(
<_i9.CircumferenceModel>[]),
returnValueForMissingStub:
_i6.Future<List<_i9.CircumferenceModel>>.value(
<_i9.CircumferenceModel>[]),
) as _i6.Future<List<_i9.CircumferenceModel>>);
@override
_i6.Future<List<_i9.CircumferenceModel>> getCircumferencesBySlug({
required String? period,
required String? slug,
}) =>
(super.noSuchMethod(
Invocation.method(
#getCircumferencesBySlug,
[],
{
#period: period,
#slug: slug,
},
),
returnValue: _i6.Future<List<_i9.CircumferenceModel>>.value(
<_i9.CircumferenceModel>[]),
returnValueForMissingStub:
_i6.Future<List<_i9.CircumferenceModel>>.value(
<_i9.CircumferenceModel>[]),
) as _i6.Future<List<_i9.CircumferenceModel>>);
@override
_i6.Future<_i3.SubPaginationModel> getSubscribers({
required String? slug,
String? query,
}) =>
(super.noSuchMethod(
Invocation.method(
#getSubscribers,
[],
{
#slug: slug,
#query: query,
},
),
returnValue:
_i6.Future<_i3.SubPaginationModel>.value(_FakeSubPaginationModel_2(
this,
Invocation.method(
#getSubscribers,
[],
{
#slug: slug,
#query: query,
},
),
)),
returnValueForMissingStub:
_i6.Future<_i3.SubPaginationModel>.value(_FakeSubPaginationModel_2(
this,
Invocation.method(
#getSubscribers,
[],
{
#slug: slug,
#query: query,
},
),
)),
) as _i6.Future<_i3.SubPaginationModel>);
@override
_i6.Future<_i3.SubPaginationModel> getSubscriptions({
required String? slug,
String? query,
}) =>
(super.noSuchMethod(
Invocation.method(
#getSubscriptions,
[],
{
#slug: slug,
#query: query,
},
),
returnValue:
_i6.Future<_i3.SubPaginationModel>.value(_FakeSubPaginationModel_2(
this,
Invocation.method(
#getSubscriptions,
[],
{
#slug: slug,
#query: query,
},
),
)),
returnValueForMissingStub:
_i6.Future<_i3.SubPaginationModel>.value(_FakeSubPaginationModel_2(
this,
Invocation.method(
#getSubscriptions,
[],
{
#slug: slug,
#query: query,
},
),
)),
) as _i6.Future<_i3.SubPaginationModel>);
@override
_i6.Future<void> subscribe({required String? slug}) => (super.noSuchMethod(
Invocation.method(
#subscribe,
[],
{#slug: slug},
),
returnValue: _i6.Future<void>.value(),
returnValueForMissingStub: _i6.Future<void>.value(),
) as _i6.Future<void>);
@override
_i6.Future<void> unsubscribe({required String? slug}) => (super.noSuchMethod(
Invocation.method(
#unsubscribe,
[],
{#slug: slug},
),
returnValue: _i6.Future<void>.value(),
returnValueForMissingStub: _i6.Future<void>.value(),
) as _i6.Future<void>);
@override
_i6.Future<void> uploadPhoto({required _i10.File? photo}) =>
(super.noSuchMethod(
Invocation.method(
#uploadPhoto,
[],
{#photo: photo},
),
returnValue: _i6.Future<void>.value(),
returnValueForMissingStub: _i6.Future<void>.value(),
) as _i6.Future<void>);
@override
_i6.Future<void> deletePhoto() => (super.noSuchMethod(
Invocation.method(
#deletePhoto,
[],
),
returnValue: _i6.Future<void>.value(),
returnValueForMissingStub: _i6.Future<void>.value(),
) as _i6.Future<void>);
@override
_i6.Future<_i4.ChosenDietsPaginationModel> getChosenDiets() =>
(super.noSuchMethod(
Invocation.method(
#getChosenDiets,
[],
),
returnValue: _i6.Future<_i4.ChosenDietsPaginationModel>.value(
_FakeChosenDietsPaginationModel_3(
this,
Invocation.method(
#getChosenDiets,
[],
),
)),
returnValueForMissingStub:
_i6.Future<_i4.ChosenDietsPaginationModel>.value(
_FakeChosenDietsPaginationModel_3(
this,
Invocation.method(
#getChosenDiets,
[],
),
)),
) as _i6.Future<_i4.ChosenDietsPaginationModel>);
@override
_i6.Future<String> getPassKey({required String? json}) => (super.noSuchMethod(
Invocation.method(
#getPassKey,
[],
{#json: json},
),
returnValue: _i6.Future<String>.value(_i11.dummyValue<String>(
this,
Invocation.method(
#getPassKey,
[],
{#json: json},
),
)),
returnValueForMissingStub:
_i6.Future<String>.value(_i11.dummyValue<String>(
this,
Invocation.method(
#getPassKey,
[],
{#json: json},
),
)),
) as _i6.Future<String>);
@override
_i6.Future<String> getMyPhone() => (super.noSuchMethod(
Invocation.method(
#getMyPhone,
[],
),
returnValue: _i6.Future<String>.value(_i11.dummyValue<String>(
this,
Invocation.method(
#getMyPhone,
[],
),
)),
returnValueForMissingStub:
_i6.Future<String>.value(_i11.dummyValue<String>(
this,
Invocation.method(
#getMyPhone,
[],
),
)),
) as _i6.Future<String>);
@override
_i6.Future<void> sendEmailCode() => (super.noSuchMethod(
Invocation.method(
#sendEmailCode,
[],
),
returnValue: _i6.Future<void>.value(),
returnValueForMissingStub: _i6.Future<void>.value(),
) as _i6.Future<void>);
@override
_i6.Future<void> tryVerifyEmailByCode({required String? code}) =>
(super.noSuchMethod(
Invocation.method(
#tryVerifyEmailByCode,
[],
{#code: code},
),
returnValue: _i6.Future<void>.value(),
returnValueForMissingStub: _i6.Future<void>.value(),
) as _i6.Future<void>);
}