Compare commits

..

165 Commits

Author SHA1 Message Date
Username1548
48f433288f feat(chats): added message resend and deleting, feat(menu): added build number 2025-08-11 02:52:41 +07: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
97 changed files with 2875 additions and 3379 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"

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

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.377 11L15.3193 6.94225C15.1641 6.78709 15.0865 6.60892 15.0865 6.40775C15.0865 6.20642 15.1641 6.02817 15.3193 5.873C15.4744 5.72817 15.6542 5.65734 15.8587 5.6605C16.0631 5.66384 16.2345 5.73467 16.373 5.873L20.8673 10.3673C20.9609 10.4609 21.0269 10.5597 21.0653 10.6635C21.1038 10.7673 21.123 10.8795 21.123 11C21.123 11.1205 21.1038 11.2327 21.0653 11.3365C21.0269 11.4403 20.9609 11.5391 20.8673 11.6328L16.373 16.127C16.2178 16.282 16.0422 16.357 15.8462 16.352C15.6501 16.3468 15.4744 16.2718 15.3193 16.127C15.1641 15.9718 15.0856 15.7946 15.0837 15.5953C15.0817 15.3959 15.1551 15.2218 15.3038 15.073L19.377 11ZM14.0693 11.75H7.25C6.3525 11.75 5.5865 12.0673 4.952 12.702C4.31733 13.3365 4 14.1025 4 15V17.75C4 17.9628 3.92817 18.141 3.7845 18.2845C3.641 18.4282 3.46283 18.5 3.25 18.5C3.03717 18.5 2.859 18.4282 2.7155 18.2845C2.57183 18.141 2.5 17.9628 2.5 17.75V15C2.5 13.6872 2.9635 12.5673 3.8905 11.6405C4.81733 10.7135 5.93717 10.25 7.25 10.25H14.0693L10.7615 6.94225C10.6065 6.78709 10.529 6.60892 10.529 6.40775C10.529 6.20642 10.6065 6.02817 10.7615 5.873C10.9167 5.72817 11.0965 5.65734 11.301 5.6605C11.5055 5.66384 11.677 5.73467 11.8155 5.873L16.3095 10.3673C16.4032 10.4609 16.4692 10.5597 16.5078 10.6635C16.5463 10.7673 16.5655 10.8795 16.5655 11C16.5655 11.1205 16.5463 11.2327 16.5078 11.3365C16.4692 11.4403 16.4032 11.5391 16.3095 11.6328L11.8155 16.127C11.6603 16.282 11.4847 16.357 11.2885 16.352C11.0923 16.3468 10.9167 16.2718 10.7615 16.127C10.6065 15.9718 10.528 15.7946 10.526 15.5953C10.524 15.3959 10.5974 15.2218 10.7463 15.073L14.0693 11.75Z" fill="#2F80ED"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

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

@@ -1,11 +1,10 @@
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_action_widget.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart';
import 'package:eatvio_app/core/widgets/custom_divider.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:go_router/go_router.dart';
void showCommentsActionsBottomSheet(
@@ -23,7 +22,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,
@@ -123,7 +122,7 @@ class _CommentsActionsBottomSheetState extends State<CommentsActionsBottomSheet>
),
const BottomSheetHandlerBar(),
if (widget.isMyComment)
_ActionWidget(
BottomSheetActionWidget(
text: s.edit,
iconPath: 'assets/icons/edit_paint.svg',
needToPop: false,
@@ -132,13 +131,13 @@ class _CommentsActionsBottomSheetState extends State<CommentsActionsBottomSheet>
}),
)
else if (widget.reply != null)
_ActionWidget(
BottomSheetActionWidget(
text: s.reply,
iconPath: 'assets/icons/reply.svg',
onTap: widget.reply!,
),
if (widget.reply != null) const CustomDivider(),
_ActionWidget(
BottomSheetActionWidget(
text: s.copy,
iconPath: 'assets/icons/copy2.svg',
onTap: widget.copy,
@@ -146,21 +145,21 @@ class _CommentsActionsBottomSheetState extends State<CommentsActionsBottomSheet>
if (widget.isMyComment) ...[
if (widget.reply != null) ...[
const CustomDivider(),
_ActionWidget(
BottomSheetActionWidget(
text: s.reply,
iconPath: 'assets/icons/reply.svg',
onTap: widget.reply!,
),
],
const CustomDivider(),
_ActionWidget(
BottomSheetActionWidget(
text: s.deleteComment,
iconPath: 'assets/icons/red_trash.svg',
onTap: widget.delete,
),
] else ...[
const CustomDivider(),
_ActionWidget(
BottomSheetActionWidget(
text: s.complain,
iconPath: 'assets/icons/complain.svg',
onTap: widget.complain,
@@ -172,32 +171,3 @@ class _CommentsActionsBottomSheetState extends State<CommentsActionsBottomSheet>
}
}
class _ActionWidget extends StatelessWidget {
const _ActionWidget({required this.text, required this.iconPath, required this.onTap, this.needToPop = true});
final String text;
final String iconPath;
final void Function() onTap;
final bool needToPop;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
if (needToPop) Navigator.pop(context);
onTap();
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Row(
children: [
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

@@ -0,0 +1,33 @@
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
class BottomSheetActionWidget extends StatelessWidget {
const BottomSheetActionWidget({super.key, required this.text, required this.iconPath, required this.onTap, this.needToPop = true, this.color});
final String text;
final String iconPath;
final void Function() onTap;
final bool needToPop;
final Color? color;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
if (needToPop) Navigator.pop(context);
onTap();
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Row(
children: [
Expanded(
child: Text(text, style: TextStyles.textLMedium.copyWith(color: appTheme.black)),
),
SvgPicture.asset(iconPath, colorFilter: color != null ? ColorFilter.mode(color!, BlendMode.srcIn) : null),
],
),
),
);
}
}

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,6 +67,7 @@ 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;
@@ -79,6 +81,9 @@ class _AuthScreenState extends State<AuthScreen> {
@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>(

View File

@@ -15,7 +15,11 @@ abstract class ChatMessageModel with _$ChatMessageModel {
@JsonKey(name: 'read_by_to_user', defaultValue: false) required bool isRead,
@JsonKey(name: 'is_author_message_auth_user') required bool? isCurrentUserAuthor,
@JsonKey(name: 'room_id') required String? roomId,
@Default(MessageState.sent) @JsonKey(includeToJson: false, includeFromJson: false) MessageState messageState,
@Default(null) @JsonKey(includeToJson: false, includeFromJson: false) String? uuid,
}) = _ChatMessageModel;
factory ChatMessageModel.fromJson(Map<String, Object?> json) => _$ChatMessageModelFromJson(json);
}
enum MessageState { loading, sent, error }

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

@@ -4,6 +4,7 @@ import 'dart:developer';
import 'dart:io';
import 'package:audioplayers/audioplayers.dart';
import 'package:collection/collection.dart';
import 'package:dart_pusher_channels/dart_pusher_channels.dart';
import 'package:easy_debounce/easy_debounce.dart';
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
@@ -19,6 +20,7 @@ import 'package:eatvio_app/features/my_finance/models/card_model.dart';
import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart';
import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:uuid/uuid.dart';
class ChatRoomCubit extends Cubit<ChatRoomState> {
final ChatRepository chatRepository;
@@ -31,12 +33,9 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
late int lastPage;
bool isLoading = false;
Timer? _timer;
var uuid = const Uuid();
ChatRoomCubit({
required this.chatRepository,
required this.wsManager,
required this.cardsRepository,
}) : super(const ChatRoomState.loading());
ChatRoomCubit({required this.chatRepository, required this.wsManager, required this.cardsRepository}) : super(const ChatRoomState.loading());
void setTimer() {
_timer?.cancel();
@@ -47,14 +46,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 +88,16 @@ 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,43 +156,80 @@ 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));
EasyDebounce.debounce(
'searchChatRoom',
const Duration(milliseconds: 300),
() => sl<ChatsCubit>().getChatRooms(),
);
EasyDebounce.debounce('searchChatRoom', const Duration(milliseconds: 300), () => sl<ChatsCubit>().getChatRooms());
});
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)),
);
});
}
Future<bool> sendMessage({
String? data,
List<File>? files,
required String userSlug,
}) async {
Future<bool> sendMessage({String? data, List<File>? files, required String userSlug}) async {
try {
await chatRepository.sendMessage(
data: data,
files: files != null ? await ImageCompressUtility.compressFilesList(List<File>.from(files)) : null,
slug: userSlug,
);
final messageUuid = uuid.v4();
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: '',
messageState: MessageState.loading,
uuid: messageUuid,
),
...(state as ChatRoomStateLoaded).chatMessages,
],
),
);
try {
await chatRepository.sendMessage(data: data, files: compressedFiles, slug: userSlug);
emit(
(state as ChatRoomStateLoaded).copyWith(
chatMessages: (state as ChatRoomStateLoaded).chatMessages.map((elem) {
if (elem.uuid == messageUuid) {
return elem.copyWith(messageState: MessageState.sent);
}
return elem;
}).toList(),
),
);
} catch (e) {
emit(
(state as ChatRoomStateLoaded).copyWith(
chatMessages: (state as ChatRoomStateLoaded).chatMessages.map((elem) {
if (elem.uuid == messageUuid) {
return elem.copyWith(messageState: MessageState.error);
}
return elem;
}).toList(),
),
);
}
sl<ChatsCubit>().getChatRooms();
return true;
} catch (e, st) {
log(e.toString(), stackTrace: st);
@@ -239,7 +274,10 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
try {
final planId = (state as ChatRoomStateLoaded).chatRoom.chosenPlan?.id;
if (planId == null) return false;
await chatRepository.complainAboutConsultation(id: planId, body: PlanComplainModel(description: description));
await chatRepository.complainAboutConsultation(
id: planId,
body: PlanComplainModel(description: description),
);
return true;
} catch (e) {
log(e.toString());
@@ -267,4 +305,70 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
typingSubscription?.cancel();
super.close();
}
void deleteMessage(String uuid) {
final currentState = state;
if (currentState is ChatRoomStateLoaded) {
emit(currentState.copyWith(chatMessages: currentState.chatMessages.where((message) => message.uuid != uuid).toList()));
}
}
Future<void> resendMessage({required String uuid, required String userSlug}) async {
final currentState = state;
if (currentState is ChatRoomStateLoaded) {
final message = currentState.chatMessages.firstWhereOrNull((message) => message.uuid == uuid);
if (message != null) {
try {
AudioPlayer().play((AssetSource('audio/send_message.wav')));
emit(
(state as ChatRoomStateLoaded).copyWith(
chatMessages: [
ChatMessageModel(
id: 1,
data: message.data,
mediaData: message.mediaData,
authorId: 1,
updatedAt: DateTime.now(),
isRead: true,
isCurrentUserAuthor: true,
roomId: '',
messageState: MessageState.loading,
uuid: uuid,
),
...(state as ChatRoomStateLoaded).chatMessages.where((message) => message.uuid != uuid),
],
),
);
try {
await chatRepository.sendMessage(data: message.data, files: message.mediaData as List<File>?, slug: userSlug);
emit(
(state as ChatRoomStateLoaded).copyWith(
chatMessages: (state as ChatRoomStateLoaded).chatMessages.map((elem) {
if (elem.uuid == uuid) {
return elem.copyWith(messageState: MessageState.sent);
}
return elem;
}).toList(),
),
);
} catch (e) {
emit(
(state as ChatRoomStateLoaded).copyWith(
chatMessages: (state as ChatRoomStateLoaded).chatMessages.map((elem) {
if (elem.uuid == uuid) {
return elem.copyWith(messageState: MessageState.error);
}
return elem;
}).toList(),
),
);
}
sl<ChatsCubit>().getChatRooms();
} 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,10 @@ 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());
}
@@ -168,34 +171,19 @@ class _ChatScreenState extends State<ChatScreen> {
width: 40,
height: 40,
alignment: Alignment.center,
decoration: const BoxDecoration(
color: Color(0xFFEBF4FF),
shape: BoxShape.circle,
),
decoration: const BoxDecoration(color: Color(0xFFEBF4FF), shape: BoxShape.circle),
child: Text(
state.chatRoom.users.receiver.name.split(' ').first[0] + state.chatRoom.users.receiver.name.split(' ')[1][0],
style: const TextStyle(
color: Color(0xFF7F9CF5),
fontWeight: FontWeight.w400,
fontSize: 20,
),
style: const TextStyle(color: Color(0xFF7F9CF5), fontWeight: FontWeight.w400, fontSize: 20),
),
),
const SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
state.chatRoom.users.receiver.name,
style: CustomTextStyles.titleSmallBold.copyWith(
color: PrimaryColors().black,
),
),
Text(state.chatRoom.users.receiver.name, style: CustomTextStyles.titleSmallBold.copyWith(color: PrimaryColors().black)),
AnimatedCrossFade(
firstChild: Text(
'Печатает...',
style: CustomTextStyles.bodySmallMulishPrimary,
),
firstChild: Text('Печатает...', style: CustomTextStyles.bodySmallMulishPrimary),
firstCurve: Curves.easeIn,
secondCurve: Curves.easeOut,
secondChild: const SizedBox.shrink(),
@@ -251,9 +239,9 @@ class _ChatScreenState extends State<ChatScreen> {
if (!result) return;
if (!context.mounted) return;
context.read<ChatsCubit>().reduceUnreadMessagesCount(
chatId: state.chatRoom.id,
count: unreadMessagesIdsCopy.length,
);
chatId: state.chatRoom.id,
count: unreadMessagesIdsCopy.length,
);
unreadMessagesIdsCopy.clear();
});
}
@@ -264,6 +252,9 @@ class _ChatScreenState extends State<ChatScreen> {
text: state.chatMessages[index].data.trim(),
isCurrentUser: isCurrentUserAuthor,
mediaData: state.chatMessages[index].mediaData ?? [],
messageState: state.chatMessages[index].messageState,
messageUuid: state.chatMessages[index].uuid ?? '',
userSlug: state.chatRoom.users.receiver.slug,
),
),
],
@@ -290,6 +281,9 @@ class _ChatScreenState extends State<ChatScreen> {
text: state.chatMessages[index].data.trim(),
isCurrentUser: isCurrentUserAuthor,
mediaData: state.chatMessages[index].mediaData ?? [],
messageState: state.chatMessages[index].messageState,
messageUuid: state.chatMessages[index].uuid ?? '',
userSlug: state.chatRoom.users.receiver.slug,
),
);
},
@@ -361,11 +355,7 @@ class _ChatScreenState extends State<ChatScreen> {
final imageIndex = _imageIndexAt(pickedFiles, i);
context.pushTransparentRoute(
PhotoViewScreen(
imageUrls: imageFiles.map((e) => e.path).toList(),
isLocal: true,
index: imageIndex,
),
PhotoViewScreen(imageUrls: imageFiles.map((e) => e.path).toList(), isLocal: true, index: imageIndex),
);
},
child: Hero(
@@ -373,14 +363,7 @@ class _ChatScreenState extends State<ChatScreen> {
transitionOnUserGestures: true,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: SizedBox(
height: 104,
width: 104,
child: Image.file(
pickedFiles[i],
fit: BoxFit.cover,
),
),
child: SizedBox(height: 104, width: 104, child: Image.file(pickedFiles[i], fit: BoxFit.cover)),
),
),
)
@@ -397,15 +380,8 @@ class _ChatScreenState extends State<ChatScreen> {
},
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: appTheme.gray50,
shape: BoxShape.circle,
),
child: const Icon(
size: 20,
Icons.close,
color: Colors.black,
),
decoration: BoxDecoration(color: appTheme.gray50, shape: BoxShape.circle),
child: const Icon(size: 20, Icons.close, color: Colors.black),
),
),
),
@@ -427,7 +403,10 @@ class _ChatScreenState extends State<ChatScreen> {
child: Ink(
height: 40,
width: 40,
child: Transform.rotate(angle: pi / 4, child: const Icon(Icons.attach_file_rounded, size: 30, color: Color(0xFF858E99))),
child: Transform.rotate(
angle: pi / 4,
child: const Icon(Icons.attach_file_rounded, size: 30, color: Color(0xFF858E99)),
),
),
),
const SizedBox(width: 8),
@@ -449,7 +428,9 @@ class _ChatScreenState extends State<ChatScreen> {
hintStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w400, height: 1, color: Color(0xFFAEAEB2)),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(100), borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1)),
borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
@@ -525,13 +506,7 @@ class _ChatScreenState extends State<ChatScreen> {
const Icon(Icons.attach_file, size: 32, color: Colors.black54),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Text(
fileName,
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 12),
),
child: Text(fileName, textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12)),
),
],
),
@@ -579,13 +554,7 @@ class _ChatScreenState extends State<ChatScreen> {
// await chatRoomCubit.sendMessage(files: [File(image.path)], userSlug: slug);
// }
},
child: const Row(
children: [
Icon(Icons.photo_camera),
SizedBox(width: 8),
Text('Фото'),
],
),
child: const Row(children: [Icon(Icons.photo_camera), SizedBox(width: 8), Text('Фото')]),
),
CupertinoActionSheetAction(
onPressed: () async {
@@ -600,13 +569,7 @@ class _ChatScreenState extends State<ChatScreen> {
// await chatRoomCubit.sendMessage(files: files, userSlug: slug);
}
},
child: const Row(
children: [
Icon(Icons.photo_library_rounded),
SizedBox(width: 8),
Text('Галерея'),
],
),
child: const Row(children: [Icon(Icons.photo_library_rounded), SizedBox(width: 8), Text('Галерея')]),
),
CupertinoActionSheetAction(
onPressed: () async {
@@ -643,20 +606,10 @@ class _ChatScreenState extends State<ChatScreen> {
// await chatRoomCubit.sendMessage(files: files, userSlug: slug);
}
},
child: const Row(
children: [
Icon(Icons.file_present_rounded),
SizedBox(width: 8),
Text('Файлы'),
],
),
child: const Row(children: [Icon(Icons.file_present_rounded), SizedBox(width: 8), Text('Файлы')]),
),
],
cancelButton: CupertinoActionSheetAction(
onPressed: () => Navigator.of(context).pop(),
isDestructiveAction: true,
child: const Text('Отмена'),
),
cancelButton: CupertinoActionSheetAction(onPressed: () => Navigator.of(context).pop(), isDestructiveAction: true, child: const Text('Отмена')),
),
);
}
@@ -665,7 +618,10 @@ class _ChatScreenState extends State<ChatScreen> {
return Center(
child: Padding(
padding: const EdgeInsets.only(top: 32),
child: Text(DateFormat.yMMMMd('RU').format(date), style: const TextStyle(height: 1, color: Color(0xFF8393A1), fontWeight: FontWeight.w500)),
child: Text(
DateFormat.yMMMMd('RU').format(date),
style: const TextStyle(height: 1, color: Color(0xFF8393A1), fontWeight: FontWeight.w500),
),
),
);
}

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,10 +1,17 @@
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';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/features/chat/models/chat_message_model.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart';
import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart';
import 'package:eatvio_app/features/chat/presentation/widgets/message_action_bottom_sheet.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:url_launcher/url_launcher.dart';
class ChatBubbleWidget extends StatelessWidget {
@@ -15,6 +22,9 @@ class ChatBubbleWidget extends StatelessWidget {
required this.isCurrentUser,
required this.mediaData,
required this.isRead,
required this.messageState,
required this.messageUuid,
required this.userSlug
});
final DateTime sendTime;
@@ -22,7 +32,9 @@ class ChatBubbleWidget extends StatelessWidget {
final bool isCurrentUser;
final bool isRead;
final List<dynamic> mediaData;
final MessageState messageState;
final String messageUuid;
final String userSlug;
@override
Widget build(BuildContext context) {
final size = MediaQuery.sizeOf(context);
@@ -36,17 +48,33 @@ class ChatBubbleWidget extends StatelessWidget {
mainAxisAlignment: isCurrentUser ? MainAxisAlignment.end : MainAxisAlignment.start,
children: [
if (isCurrentUser)
Padding(
padding: const EdgeInsets.only(right: 4, bottom: 4),
child: Text(
sendTime.format('HH:mm', 'ru'),
style: const TextStyle(
color: Color(0xFF7B8794),
height: 1,
fontSize: 10,
),
switch (messageState) {
MessageState.loading => Padding(
padding: const EdgeInsets.only(right: 4, bottom: 4),
child: SizedBox(width: 10, height: 10, child: CircularProgressIndicator(strokeWidth: 2, color: appTheme.blueA400)),
),
),
MessageState.sent => Padding(
padding: const EdgeInsets.only(right: 4, bottom: 4),
child: Text(sendTime.format('HH:mm', 'ru'), style: const TextStyle(color: Color(0xFF7B8794), height: 1, fontSize: 10)),
),
MessageState.error => GestureDetector(
onTap: () {
showMessageActionsBottomSheet(
context,
resend: () {
context.read<ChatRoomCubit>().resendMessage(uuid: messageUuid, userSlug: userSlug);
},
delete: () {
context.read<ChatRoomCubit>().deleteMessage(messageUuid);
},
);
},
child: Padding(padding: const EdgeInsets.only(right: 4, bottom: 4), child: SvgPicture.asset('assets/icons/error_icon.svg')),
),
},
Container(
constraints: BoxConstraints(maxWidth: size.width * 0.7),
decoration: BoxDecoration(
@@ -59,8 +87,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: [
@@ -84,32 +113,19 @@ class ChatBubbleWidget extends StatelessWidget {
throw Exception('Could not launch ${mediaData[index].toString()}');
}
},
leading: const Icon(
Icons.file_copy_outlined,
color: Colors.white,
),
trailing: const Icon(
Icons.download_outlined,
color: Colors.white,
),
leading: const Icon(Icons.file_copy_outlined, color: Colors.black),
trailing: const Icon(Icons.download_outlined, color: Colors.black),
title: Text(
mediaData[index].toString().replaceAll('chat_files/', ''),
maxLines: 2,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
overflow: TextOverflow.ellipsis,
),
style: const TextStyle(color: Colors.black, fontSize: 12, overflow: TextOverflow.ellipsis),
),
);
}
return GestureDetector(
onTap: () {
context.pushTransparentRoute(
PhotoViewScreen(
imageUrls: mediaData.map((e) => '$e').toList(),
index: index,
),
PhotoViewScreen(imageUrls: mediaData, index: index, isLocal: mediaData[index] is File ? true : false),
);
},
child: Hero(
@@ -117,9 +133,7 @@ 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]}'),
),
),
);
@@ -131,11 +145,7 @@ class ChatBubbleWidget extends StatelessWidget {
),
),
if (mediaData.isNotEmpty && text.isNotEmpty) const SizedBox(height: 8),
if (text.isNotEmpty)
Text(
text,
style: TextStyles.textFS13FW400.copyWith(color: appTheme.black, fontSize: 16),
),
if (text.isNotEmpty) Text(text, style: TextStyles.textFS13FW400.copyWith(color: appTheme.black, fontSize: 16)),
],
),
),
@@ -145,12 +155,7 @@ class ChatBubbleWidget extends StatelessWidget {
padding: const EdgeInsets.only(left: 4, bottom: 4),
child: Text(
sendTime.format('HH:mm', 'ru'),
style: const TextStyle(
color: Color(0xFF7B8794),
height: 1,
fontSize: 10,
fontWeight: FontWeight.w500,
),
style: const TextStyle(color: Color(0xFF7B8794), height: 1, fontSize: 10, fontWeight: FontWeight.w500),
),
),
],

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

@@ -0,0 +1,48 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_action_widget.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart';
import 'package:eatvio_app/core/widgets/custom_divider.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
void showMessageActionsBottomSheet(BuildContext context, {required void Function() resend, required void Function() delete}) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => Container(
margin: EdgeInsets.only(left: 8, right: 8, bottom: MediaQuery.of(context).padding.bottom + 20),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: appTheme.whiteA700),
child: MessageActionsBottomSheet(resend: resend, delete: delete),
),
);
}
class MessageActionsBottomSheet extends StatefulWidget {
const MessageActionsBottomSheet({super.key, required this.resend, required this.delete});
final void Function() resend;
final void Function() delete;
@override
State<MessageActionsBottomSheet> createState() => _MessageActionsBottomSheet();
}
class _MessageActionsBottomSheet extends State<MessageActionsBottomSheet> {
@override
Widget build(BuildContext context) {
final s = S.of(context);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 7),
const BottomSheetHandlerBar(),
BottomSheetActionWidget(text: s.resend, iconPath: 'assets/icons/resend.svg', onTap: widget.resend),
const CustomDivider(),
BottomSheetActionWidget(text: s.delete, iconPath: 'assets/icons/trash_can.svg', onTap: widget.delete, color: appTheme.red),
],
),
);
}
}

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

@@ -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

@@ -77,7 +77,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 +97,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

@@ -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

@@ -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

@@ -1,5 +1,6 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/core/utils/image_constant.dart';
import 'package:eatvio_app/core/locator_service.dart';
@@ -14,6 +15,7 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:package_info_plus/package_info_plus.dart';
class MenuScreen extends StatelessWidget {
final ProfileModel profileModel;
@@ -37,18 +39,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),
@@ -65,6 +69,8 @@ class MenuScreen extends StatelessWidget {
// ),
_divider(context),
_tile(leading: ImageConstant.icnExit, title: 'Выход', onTap: _onTapLogout(context)),
const SizedBox(height: 10),
const AppInfo(),
],
),
),
@@ -122,3 +128,36 @@ class MenuScreen extends StatelessWidget {
};
}
}
class AppInfo extends StatefulWidget {
const AppInfo({super.key});
@override
State<AppInfo> createState() => _AppInfoState();
}
class _AppInfoState extends State<AppInfo> {
String version = '';
String buildNumber = '';
@override
void didChangeDependencies() {
PackageInfo.fromPlatform().then((PackageInfo packageInfo) {
setState(() {
version = packageInfo.version;
buildNumber = packageInfo.buildNumber;
});
});
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsetsGeometry.only(left: 16),
child: Text('Версия приложения: $version ($buildNumber)', style: TextStyles.textXSSemibold.copyWith(color: appTheme.gray)),
),
);
}
}

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';
@@ -60,7 +67,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
_aboutEatvio(state),
const SizedBox(height: 12),
_becomeConsultantButton(state),
_deleteAccountButton(state),
_deleteAccountButton(state, settingsCubit),
],
),
),
@@ -428,11 +435,20 @@ 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,
@@ -495,3 +511,73 @@ class _SettingsScreenState extends State<SettingsScreen> {
);
}
}
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,13 +66,7 @@ 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(
@@ -106,12 +74,7 @@ class SupportScreen extends StatelessWidget {
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)),
),
),
const SizedBox(height: 8),
@@ -120,12 +83,7 @@ class SupportScreen extends StatelessWidget {
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)),
),
),
const SizedBox(height: 8),
@@ -134,12 +92,7 @@ class SupportScreen extends StatelessWidget {
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("Фамилия"),
@@ -624,6 +634,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Снять с публикации",
),
"reply": MessageLookupByLibrary.simpleMessage("Ответить"),
"resend": MessageLookupByLibrary.simpleMessage("Отправить повторно"),
"save": MessageLookupByLibrary.simpleMessage("Сохранить"),
"send": MessageLookupByLibrary.simpleMessage("Отправить"),
"send_complain": MessageLookupByLibrary.simpleMessage("Отправить жалобу"),
@@ -780,6 +791,9 @@ class MessageLookup extends MessageLookupByLibrary {
"youShureYouWantToCancelOrder": MessageLookupByLibrary.simpleMessage(
"Вы уверены, что хотите отменить заказ?",
),
"youShureYouWantToDeleteAccount": MessageLookupByLibrary.simpleMessage(
"Вы уверены, что хотите удалить аккаунт?",
),
"you_can_not_open_this_day": MessageLookupByLibrary.simpleMessage(
"Вы не можете перейти в этот день",
),

View File

@@ -3414,6 +3414,61 @@ 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: [],
);
}
/// `Отправить повторно`
String get resend {
return Intl.message(
'Отправить повторно',
name: 'resend',
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": "Не получилось удалить ваш аккаунт",
"resend": "Отправить повторно"
}

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,17 @@ 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
uuid: ^4.5.1
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 +94,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>);
}