Compare commits

...

276 Commits

Author SHA1 Message Date
Username1548
45774c3875 fix 2025-08-14 16:58:00 +03:00
Username1548
62c0914fe2 fix 2025-08-14 16:55:15 +03:00
Username1548
48f5b64223 feat(emailVerification): added timer for email code resending 2025-08-14 16:54:13 +03:00
Username1548
91bb5cf483 feat(menu): fixed indentation 2025-08-12 09:01:31 +03:00
Username1548
c4dedac3f3 added build date 2025-08-12 12:27:58 +07:00
mso7nueh
8ed243f63c Merge pull request 'fix: upd ver' (#638) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#638
2025-08-11 14:19:46 +00:00
mso7nueh
a10fe6d2f5 fix: upd ver 2025-08-11 17:19:14 +03:00
mso7nueh
35b78bdeee Merge pull request 'fix(blackList+notifications): Issue-523, 191' (#636) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#636
2025-08-11 14:00:25 +00:00
mso7nueh
ef037eb4d5 Merge pull request 'feat(chats): added message resend and deleting, feat(menu): added build number' (#633) from misha/eatvio_app:issue-594 into main
Reviewed-on: eatvio/eatvio_app#633
2025-08-11 14:00:14 +00:00
mso7nueh
5a3385cacf Merge pull request 'removed public offer from ui on ios' (#637) from misha/eatvio_app:issue-635 into main
Reviewed-on: eatvio/eatvio_app#637
2025-08-11 14:00:05 +00:00
Username1548
ef4d91a5d8 removed public offert from ui on ios 2025-08-11 20:46:09 +07:00
mso7nueh
dca4b4c471 fix(blackList+notifications): Issue-523, 191 2025-08-11 14:53:43 +03:00
Username1548
48f433288f feat(chats): added message resend and deleting, feat(menu): added build number 2025-08-11 02:52:41 +07:00
mso7nueh
cfbd7597fa Merge pull request 'fix(archive_of_consultatons): Issue-331' (#631) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#631
2025-08-08 10:43:27 +00:00
mso7nueh
412ac0aeeb fix(archive_of_consultatons): Issue-331 2025-08-08 13:43:04 +03:00
mso7nueh
6211332a07 Merge pull request 'fix(finances): Issue-186' (#630) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#630
2025-08-08 10:30:31 +00:00
mso7nueh
4b1d21b4af fix(finances): Issue-186 2025-08-08 13:29:32 +03:00
mso7nueh
3b89a69d3b Merge pull request 'fix(env): remove default value from config' (#629) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#629
Reviewed-by: Анатолий Полоз <t0ly.od@ya.ru>
2025-08-08 09:24:00 +00:00
mso7nueh
3e8c8c7e9f fix(env): remove default value from config 2025-08-08 12:16:20 +03:00
mso7nueh
e73463dcf7 Merge pull request 'fix(fastlane): update version' (#628) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#628
2025-08-08 08:41:03 +00:00
mso7nueh
60d30163e7 fix(fastlane): update version 2025-08-08 11:40:32 +03:00
mso7nueh
04c1b889a0 Merge pull request 'fix: fastlane' (#627) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#627
2025-08-08 08:35:04 +00:00
mso7nueh
06117ddb6a fix: fastlane 2025-08-08 11:34:41 +03:00
mso7nueh
15f0538f04 Merge pull request 'fix: fastlane' (#626) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#626
2025-08-08 08:30:12 +00:00
mso7nueh
3c56c59c9f fix: fastlane 2025-08-08 11:29:47 +03:00
mso7nueh
038e30b198 Merge pull request 'fix: fastlane' (#625) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#625
2025-08-08 08:24:17 +00:00
mso7nueh
cdb6fcb462 fix: fastlane 2025-08-08 11:23:55 +03:00
mso7nueh
e93a573a72 Merge pull request 'fix: fastlane' (#624) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#624
2025-08-08 08:18:14 +00:00
mso7nueh
d8378e3686 fix: fastlane 2025-08-08 11:17:48 +03:00
mso7nueh
f464da6f9d Merge pull request 'fix(fastlane): testflight' (#623) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#623
2025-08-08 08:15:09 +00:00
mso7nueh
73f27efadd fix(fastlane): testflight 2025-08-08 11:14:41 +03:00
mso7nueh
90cac4940f Merge pull request 'feat(profile): Issue-248' (#621) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#621
2025-08-08 08:11:24 +00:00
mso7nueh
2fac007675 feat(fastlane): Issue-622 2025-08-08 11:11:00 +03:00
mso7nueh
1cafff65dc merge upstream 2025-08-07 19:56:45 +00:00
mso7nueh
fe99890c0e fix: analyzer 2025-08-07 21:29:30 +03:00
mso7nueh
15248a65fe feat(profile): Issue-248 2025-08-07 18:05:11 +03:00
ee0d4d17af Merge pull request 'feat(fastlane): reverted changes' (#620) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#620
2025-08-07 12:27:18 +00:00
Username1548
6fa7beb070 reverted changes 2025-08-07 19:23:12 +07:00
24d0d6eedc Merge pull request 'feat(fastlane): changed fastlane build uploading' (#619) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#619
2025-08-07 12:07:01 +00:00
Username1548
905559b1f0 feat(fastlane): changed fastlane build uploading 2025-08-07 19:06:27 +07:00
6cb330752f Merge pull request 'version update' (#616) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#616
2025-08-07 11:27:02 +00:00
Username1548
69995289c5 Merge remote-tracking branch 'upstream/main' 2025-08-07 18:24:13 +07:00
Username1548
f9b63f3401 version update 2025-08-07 18:20:26 +07:00
3068d82b3a Merge pull request 'feat(env): added env loading to ci/cd' (#615) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#615
2025-08-07 11:12:12 +00:00
Username1548
1305e67817 feat(env): added env loading to ci/cd 2025-08-07 18:10:58 +07:00
mso7nueh
9be8ec7ac3 Merge pull request 'feat(diary): fixed commenting of diary' (#611) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#611
2025-08-06 14:42:15 +00:00
mso7nueh
7ad01d3d4e Merge pull request 'fix: secure storage' (#612) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#612
2025-08-06 14:42:07 +00:00
mso7nueh
832ba421e9 fix: secure storage 2025-08-06 17:41:35 +03:00
Username1548
7e7f8bfb03 fix 2025-08-06 20:36:50 +07:00
Username1548
1f714b3bc2 feat(diary): fixed commenting of diary 2025-08-06 20:29:56 +07:00
mso7nueh
187d60ffb9 Merge pull request 'fix: fastlane' (#608) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#608
2025-08-06 12:53:56 +00:00
mso7nueh
64b158d1ff fix: fastlane 2025-08-06 15:53:32 +03:00
mso7nueh
62f5cab746 Merge pull request 'main' (#606) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#606
2025-08-06 12:43:13 +00:00
mso7nueh
0d27da3ec9 Merge branch 'main' of http://gitea.office.local:3000/mso7nueh/eatvio_app 2025-08-06 15:42:37 +03:00
mso7nueh
1387e20553 fix: fastlane 2025-08-06 15:42:02 +03:00
mso7nueh
13fc59ff62 Merge pull request 'test-flight' (#604) from test-flight into main
Reviewed-on: eatvio/eatvio_app#604
2025-08-06 12:39:08 +00:00
mso7nueh
d3ed027c38 Merge pull request 'fix: fastlane' (#603) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#603
2025-08-06 12:38:45 +00:00
mso7nueh
108e067b11 fix: fastlane 2025-08-06 15:38:21 +03:00
mso7nueh
c58cc54e43 Merge pull request 'fix: fastlane' (#602) from main into test-flight
Reviewed-on: eatvio/eatvio_app#602
2025-08-06 12:29:46 +00:00
mso7nueh
6df116ef74 Merge pull request 'fix: fastlane' (#601) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#601
2025-08-06 12:29:29 +00:00
mso7nueh
b051a588e0 fix: fastlane 2025-08-06 15:29:10 +03:00
mso7nueh
721430301a Merge pull request 'build 66' (#599) from main into test-flight
Reviewed-on: eatvio/eatvio_app#599
2025-08-06 11:50:46 +00:00
mso7nueh
98d7b727a3 Merge pull request 'feat(app): changed name of the app, feat(settings): hid user purcahses and finances' (#597) from misha/eatvio_app:issue-595 into main
Reviewed-on: eatvio/eatvio_app#597
2025-08-06 11:48:39 +00:00
mso7nueh
236abdd1d2 Merge pull request 'feat(version): update' (#598) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#598
2025-08-06 11:46:42 +00:00
mso7nueh
138a81d9d1 feat(version): update 2025-08-06 14:46:06 +03:00
mso7nueh
f43059c820 Merge pull request 'Issue-191, Iussue-588: Notifications, env' (#596) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#596
2025-08-06 10:34:18 +00:00
mso7nueh
c4ff14c7b2 fix(env): repo branch 2025-08-06 13:04:30 +03:00
Username1548
33151b7dae changed lable of the app and hid user purchases and finaces 2025-08-06 16:55:42 +07:00
mso7nueh
b302be66d7 fix(env): add token 2025-08-06 12:55:21 +03:00
mso7nueh
4f0f146fd2 feat(env): from repo 2025-08-06 12:44:19 +03:00
mso7nueh
7986119054 feat(env): add .env from repo 2025-08-06 12:42:37 +03:00
mso7nueh
76582fd487 feat(notifications): Issue-191 2025-08-06 11:28:31 +03:00
mso7nueh
cd9357c050 feat(notifications): Issue-191 2025-08-06 11:28:02 +03:00
mso7nueh
d53fdd1ccb Merge pull request 'env' (#590) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#590
2025-08-04 10:18:00 +00:00
mso7nueh
e75539338b fix: env 2025-08-04 13:09:05 +03:00
mso7nueh
7267fc454b Добавить env/.env 2025-08-04 10:08:55 +00:00
mso7nueh
9e0c89c04e Удалить .env 2025-08-04 10:08:38 +00:00
mso7nueh
a9a479b2d3 fix: print 2025-08-04 12:48:51 +03:00
mso7nueh
12af348000 .env 2025-08-04 09:48:44 +00:00
mso7nueh
039c09962a fix: env 2025-08-04 12:40:21 +03:00
mso7nueh
681c11fed9 feat(env): Issue-588 2025-08-01 16:58:48 +03:00
mso7nueh
5b616a9dc1 feat(env): Issue-588 2025-08-01 16:52:53 +03:00
mso7nueh
3e5cc4f8d7 feat(env): Issue-588 2025-08-01 16:52:03 +03:00
mso7nueh
3cc7809210 fix: hidePlans to env 2025-08-01 16:31:05 +03:00
mso7nueh
be1d8c295a Merge pull request 'Build 1.2.4+54' (#571) from main into test-flight
Reviewed-on: eatvio/eatvio_app#571
2025-08-01 12:00:33 +00:00
mso7nueh
7a177f7517 Merge pull request 'upd ver' (#589) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#589
2025-08-01 11:34:14 +00:00
mso7nueh
c718875b49 upd ver 2025-08-01 14:33:52 +03:00
mso7nueh
c618a6a80d Merge pull request 'fix: reg, verify, loaders, grey screen when like, hide plans' (#583) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#583
2025-08-01 11:28:49 +00:00
mso7nueh
3e7202d65e upd ver 2025-08-01 14:28:04 +03:00
mso7nueh
f78ccd01b6 Обновить .gitea/workflows/test-flight-cd.yaml 2025-08-01 11:18:13 +00:00
mso7nueh
a4d0fd0108 Обновить ios/fastlane/Fastfile 2025-08-01 11:12:46 +00:00
mso7nueh
6f1368f3c6 Обновить .gitea/workflows/test-flight-cd.yaml 2025-08-01 11:11:21 +00:00
mso7nueh
83a39835fa Обновить ios/fastlane/Fastfile 2025-08-01 10:34:39 +00:00
mso7nueh
49b597d5ae fix: update versions 2025-08-01 11:30:58 +03:00
mso7nueh
f5d27ed056 feat(plans): hide for ios 2025-08-01 10:53:28 +03:00
mso7nueh
8a26fa0990 fix: upd ver sdk 2025-07-31 14:55:35 +03:00
mso7nueh
15a9e2b1bd merge upstream 2025-07-31 11:40:19 +00:00
mso7nueh
7192db31e4 fix: upd ver 2025-07-31 14:27:01 +03:00
mso7nueh
190e1781ee fix: recipe create 2025-07-30 15:41:43 +03:00
mso7nueh
a134c2043b upd ver pack 2025-07-30 14:33:28 +03:00
mso7nueh
09a1ec0947 fix: reg, verify, loaders, grey screen when like 2025-07-30 12:34:27 +03:00
mso7nueh
1db9c57140 Merge pull request 'fix: tf' (#579) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#579
2025-07-28 13:18:55 +00:00
mso7nueh
aacbc0ce5a fix: tf 2025-07-28 16:18:34 +03:00
mso7nueh
1bdc1c53f6 Merge pull request 'fix: testflight' (#578) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#578
2025-07-28 13:16:24 +00:00
mso7nueh
20e71a77d8 fix: testflight 2025-07-28 16:14:43 +03:00
mso7nueh
d037b6aaa7 Обновить .gitea/workflows/test-flight-cd.yaml 2025-07-28 13:10:43 +00:00
mso7nueh
43495f407e Обновить .gitea/workflows/test-flight-cd.yaml 2025-07-28 11:02:17 +00:00
mso7nueh
f0c94c4f82 fix: testflight 2025-07-28 13:58:48 +03:00
60b0b20844 Merge pull request 'feat(diary): fixed logic of review viewing, feat(complaints): added validation for complaints' (#577) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#577
2025-07-25 12:41:15 +00:00
Username1548
969f8d37af feat(diary): fixed logic of review viewing, feat(complaints): added validation for complaints 2025-07-25 19:39:02 +07:00
d1c9c1f292 Merge pull request 'feat(testFlight): updated script' (#575) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#575
2025-07-21 13:16:03 +00:00
Username1548
ec05b0c8fb feat(testFlight): updated script 2025-07-21 20:14:02 +07:00
5eaab6b213 Merge pull request 'feat(testFlight): updated script' (#574) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#574
2025-07-21 13:11:02 +00:00
Username1548
5db29ae681 feat(testFlight): updated script 2025-07-21 20:10:23 +07:00
b55a0d0590 Merge pull request 'feat(testFlight): updated script' (#573) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#573
2025-07-21 13:04:23 +00:00
Username1548
44a938ea29 feat(testFlight): updated script 2025-07-21 20:03:22 +07:00
be78fca7d0 Merge pull request 'feat(testFlight): updated uploading script' (#572) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#572
2025-07-21 12:53:44 +00:00
Username1548
955daf4933 feat(testFlight): updated uploading script 2025-07-21 19:52:33 +07:00
8bdbb9059f Merge pull request 'fix: fixed analyzer issue' (#570) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#570
2025-07-16 15:05:23 +00:00
Username1548
27a0bcd6ce fix: fixed analyzer issue 2025-07-16 22:02:53 +07:00
d5e2af2de5 Merge pull request 'Уведомления и дневник (план)' (#564) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#564
2025-07-16 14:48:18 +00:00
Username1548
5a8dcafa8e Merge remote-tracking branch 'upstream/main' 2025-07-16 21:41:14 +07:00
be5796fbfd Merge pull request 'version update' (#567) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#567
2025-07-16 12:57:39 +00:00
Username1548
02b621d85a version update 2025-07-16 19:56:54 +07:00
683a7e1066 Merge pull request 'account deleting' (#565) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#565
2025-07-16 12:37:25 +00:00
Username1548
773359e900 feat(setting): added account deleting 2025-07-16 19:30:30 +07:00
Username1548
74fa65a1ba Merge remote-tracking branch 'upstream/main' 2025-07-16 17:04:06 +07:00
mso7nueh
46f11a8e78 fix(chat): photo view 2025-07-16 12:19:29 +03:00
mso7nueh
3e2aff3ad4 fix(chat): model fields 2025-07-15 13:47:00 +03:00
mso7nueh
09c53a6492 feat(notificaion): add notificaion for last day 2025-07-14 17:08:53 +03:00
mso7nueh
452c603b95 feat: diary comments view 2025-07-14 15:59:43 +03:00
mso7nueh
6bcd540d54 fix: analyzer 2025-07-14 15:16:11 +03:00
mso7nueh
2c38be9fa4 fix(diary): current plan day 2025-07-14 14:49:33 +03:00
mso7nueh
74df7d82db feat(notifications): Issue-562 2025-07-14 14:06:07 +03:00
Username1548
c8d5fc756e Merge remote-tracking branch 'upstream/main' 2025-07-14 12:16:28 +07:00
mso7nueh
1b9534b690 Merge pull request 'build' (#557) from main into test-flight
Reviewed-on: eatvio/eatvio_app#557
2025-07-11 11:49:36 +00:00
mso7nueh
5ac5deec48 Merge branch 'mso7nueh-main'
# Conflicts:
#	ios/Runner.xcodeproj/project.pbxproj
#	pubspec.yaml
2025-07-11 14:47:58 +03:00
mso7nueh
08cf1ad370 Удалены сгенерированные файлы (freezed, g.dart, build) 2025-07-11 14:44:44 +03:00
mso7nueh
2c59fd0923 Удалены сгенерированные файлы (freezed, g.dart, build) 2025-07-11 14:43:56 +03:00
mso7nueh
958d09906b baseurl 2025-07-11 14:34:48 +03:00
mso7nueh
631efe6f9e fix: chats 2025-07-11 14:22:11 +03:00
mso7nueh
0749a53e24 fix: horizontal layout 2025-07-11 14:05:14 +03:00
0982c97d66 Merge pull request 'app version update' (#556) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#556
2025-07-10 15:08:02 +00:00
Username1548
e4168862ad app version update 2025-07-10 22:03:38 +07:00
59f26ee977 Merge pull request 'build 40' (#549) from main into test-flight
Reviewed-on: eatvio/eatvio_app#549
2025-07-10 14:58:33 +00:00
187b056413 Merge pull request 'fix(notifications): fixed wrong notifications rendring and changed requested pagination page' (#551) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#551
2025-07-10 14:39:08 +00:00
Username1548
e546cacd0c Merge remote-tracking branch 'upstream/main' 2025-07-10 21:36:43 +07:00
e1266a18f4 Merge pull request 'feat(comments)' (#550) from misha/eatvio_app:comments into main
Reviewed-on: eatvio/eatvio_app#550
2025-07-10 14:34:10 +00:00
Username1548
9bbd149d03 fix(permissions): added galery permissions for android 2025-07-10 08:58:05 +07: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
6dc30f4c89 feat(comments): added comments deleting and updating 2025-07-09 12:34:51 +07: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
Username1548
0a636e05b8 feat(comments): added actions bottom sheet with different actions for comments, added copy, reply and complain 2025-07-08 09:17:20 +07: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
0886d1cd7b Merge pull request 'fix(password+recipe): Issue-221 and Issue-418 and Issue-317' (#545) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#545
2025-07-04 08:05:13 +00:00
mso7nueh
2759cc64c9 Merge branch 'main' of http://gitea.office.local:3000/mso7nueh/eatvio_app 2025-07-04 10:54:14 +03:00
mso7nueh
cfe6b50c28 fix(profile): Issue-317 2025-07-04 10:54:00 +03:00
mso7nueh
61d1ad1f55 merge upstream 2025-07-04 06:48:38 +00:00
mso7nueh
ebe28136af fix(password+recipe): Issue-221 and Issue-418 2025-07-04 09:37:37 +03:00
mso7nueh
ad538472f0 Merge pull request 'feat(qrcode): Issue-515' (#544) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#544
2025-07-02 13:42:10 +00:00
mso7nueh
49827e979e feat(qrcode): Issue-515 2025-07-02 16:41:49 +03:00
mso7nueh
24cff8b4e9 Merge pull request 'feat(deepLinks)' (#518) from misha/eatvio_app:deepLinks into main
Reviewed-on: eatvio/eatvio_app#518
2025-07-02 12:23:16 +00: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
Username1548
be5a858c73 Merge branch 'main' into deepLinks 2025-07-02 19:06:04 +07:00
Username1548
e80b660210 fix: fixed analyzer issues and merge conflicts 2025-07-02 19:01:37 +07:00
Username1548
d42604dc87 Merge branch 'main' into deepLinks 2025-07-02 19:00:49 +07:00
mso7nueh
4bed103cec Merge pull request 'fix: analyzer' (#543) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#543
2025-07-02 11:47:19 +00:00
mso7nueh
8d429862dd fix: analyzer 2025-07-02 14:46:54 +03:00
mso7nueh
dd90f62286 Merge pull request 'fix: go router ver' (#542) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#542
2025-07-02 11:39:21 +00:00
mso7nueh
7809624186 fix: go router ver 2025-07-02 14:39:08 +03:00
mso7nueh
7c131f8970 Merge pull request 'fix(descriptions): Issue-540' (#541) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#541
2025-07-02 11:33:53 +00:00
mso7nueh
0926b50537 fix(descriptions): Issue-540 2025-07-02 14:33:12 +03:00
Username1548
3e96fdcfc2 fix(deepLinks): fixed gray screen problem on android 2025-07-02 17:46:41 +07:00
mso7nueh
3ce6caa0af Merge pull request 'feat(test): Issue-165' (#539) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#539
2025-07-02 07:54:04 +00:00
mso7nueh
58855b7f11 feat(test): Issue-165 2025-07-01 16:53:11 +03: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
f0d5dacc00 Merge pull request 'feat(blackList): Issue-523' (#537) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#537
2025-06-30 12:34:49 +00:00
mso7nueh
86199d08f0 fix: build error sentry 2025-06-30 15:00:43 +03:00
mso7nueh
1801324fb5 fix: build error 2025-06-30 14:45:06 +03:00
mso7nueh
d4a05f47d1 feat(blackList): Issue-523 2025-06-30 14:27:10 +03: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
300a708dfc Merge pull request 'fix(chat): multi images' (#535) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#535
2025-06-27 13:48:59 +00:00
mso7nueh
693260afb6 fix(chat): multi images 2025-06-27 16:48:12 +03: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
9b4328a2ee Merge pull request 'feat(chat): add chat images/files preview' (#533) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#533
2025-06-27 12:07:41 +00:00
mso7nueh
591a8a597d feat(chat): add chat images/files preview 2025-06-27 15:06:46 +03:00
Username1548
bc660e2aff removed processing from info plist 2025-06-27 16:18:50 +07:00
Username1548
d129687fc6 changed build version and number 2025-06-27 15:58:27 +07: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
2e5e1b3af4 Merge pull request 'Issue-440: chat(s) screen' (#530) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#530
2025-06-26 13:12:38 +00:00
mso7nueh
c103d214c4 feat(testflight): new version 2025-06-26 16:11:31 +03:00
mso7nueh
9946abc0be feat(chat): add refresh when new message 2025-06-26 16:10:34 +03:00
mso7nueh
87bf277248 feat(chat): add refresh in chats screen 2025-06-26 15:49:06 +03:00
Username1548
23a606bfd4 merged main into deepLinks 2025-06-25 22:55:58 +07:00
Username1548
5f57bded51 added deeplink for diets 2025-06-25 22:43:29 +07: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
535dd595b7 Merge pull request 'fix: black screen Issue-514' (#528) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#528
2025-06-25 12:38:06 +00:00
mso7nueh
80afcd8949 upd ver to 32 2025-06-25 15:36:27 +03:00
mso7nueh
8c7c7f51cf fix: auth 2025-06-25 15:32:37 +03:00
mso7nueh
9c168d37ce fix: black screen 2025-06-25 14:56:59 +03:00
mso7nueh
55bc361b05 fix(qr+menu): text 2025-06-25 14:47:49 +03: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
d14fbbb8d3 Merge pull request 'feat(chat+qr+devtools): fix chat first message, add pick images from gallery, qrcode, devtools' (#526) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#526
2025-06-25 09:24:28 +00:00
mso7nueh
7530270a2e feat(qr+devtools): scan qr code and dev tools 2025-06-25 12:22:14 +03:00
Username1548
afc36e6b63 feat(deepLinks): added domains for ios 2025-06-25 14:15:34 +07:00
mso7nueh
f7e40e654d merge upstream 2025-06-24 17:28:49 +00:00
Username1548
aa78d385a2 feat(deeplinks): added backwards novigation for contant that was oppened via deeplinks, added routing to plans 2025-06-24 03:18:03 +07:00
mso7nueh
75fa9ebc5c feat(chat): fix chat first message, add pick images from gallery 2025-06-23 19:46:23 +03:00
Username1548
105c7cb462 added deep link for blog posts 2025-06-23 13:30:32 +07: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
c3647288a6 Merge pull request 'fix(chat): change sendChatMessage method' (#524) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#524
2025-06-22 15:29:47 +00:00
mso7nueh
85d67ba9c4 fix(chat): change sendChatMessage method 2025-06-22 18:27:25 +03: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
15ffb4a3ec Merge pull request 'feat(testflight): update version' (#521) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#521
2025-06-20 12:46:11 +00:00
mso7nueh
2a705fdb66 feat(testflight): update version 2025-06-20 15:45:50 +03:00
mso7nueh
41ca97d030 Merge pull request 'feat(auth): reset password' (#520) from mso7nueh/eatvio_app:Issue-497 into main
Reviewed-on: eatvio/eatvio_app#520
2025-06-20 11:14:49 +00:00
mso7nueh
84b88282ad feat(auth): reset password 2025-06-20 14:12:54 +03:00
mso7nueh
84d52de935 Merge pull request 'fix(chat): Issue-440 chat v2' (#519) from mso7nueh/eatvio_app:Issue-440 into main
Reviewed-on: eatvio/eatvio_app#519
2025-06-20 08:48:07 +00:00
mso7nueh
fee92801b8 Merge pull request 'Issue-514 and Issue-497' (#516) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#516
2025-06-20 08:41:24 +00:00
mso7nueh
3b25e30b41 fix(chat): Issue-440 chat v2 2025-06-20 11:38:31 +03:00
Username1548
6aef2a9a08 feat(deepLinks): fixed gitignore 2025-06-20 06:41:39 +07:00
Username1548
6a2525dbfd feat(deepLinks): Remove Xcode build cache from repo and add to .gitignore 2025-06-20 06:41:16 +07:00
Username1548
21ce0fc9f8 feat(deepLinks): added deep link for android and implemented recipes links handling 2025-06-20 06:36:47 +07: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
b0c29345a2 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	lib/main.dart
2025-06-18 16:57:22 +03:00
mso7nueh
7682859d32 fix: Issue-514 BlackScreens + info for apple 2025-06-18 16:56:42 +03:00
mso7nueh
d0426f6e1e merge upstream 2025-06-18 06:56:09 +00:00
mso7nueh
ca296040f2 fix: Issue-514 and Issue-497 2025-06-18 09:55:33 +03:00
mso7nueh
0581da038a feat(chat): Issue-440 add photo/file picker, fix black screen 2025-06-16 17:33:17 +03: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
fa1192c5e5 Merge pull request 'upd ver -> 28' (#510) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#510
2025-06-16 11:33:55 +00:00
mso7nueh
41f948c8f7 upd ver -> 28 2025-06-16 14:33:32 +03:00
mso7nueh
896b2783ae Merge pull request 'feat(consultantFinances), feat(userPurchases)' (#494) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#494
2025-06-16 11:31:05 +00:00
mso7nueh
9c004f8593 Merge pull request 'fix(diary): fixed app review issues' (#502) from misha/eatvio_app:diaryFixes into main
Reviewed-on: eatvio/eatvio_app#502
2025-06-16 11:30:56 +00:00
mso7nueh
a577a8d58a Merge pull request 'fix(chat): time Issue-440' (#509) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#509
2025-06-16 11:25:29 +00:00
mso7nueh
3b2c4fdff5 fix(chat): time 2025-06-16 13:41:39 +03: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
44d6487709 Merge pull request 'fix: testflight' (#507) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#507
2025-06-11 13:35:40 +00:00
mso7nueh
319fd2dfe3 fix: testflight 2025-06-11 16:35:21 +03:00
mso7nueh
a98043758b Merge pull request 'fix: testflight' (#506) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#506
2025-06-11 13:28:49 +00:00
mso7nueh
a84d2d7daa fix: testflight 2025-06-11 16:28:27 +03:00
Username1548
4acb861b69 merged upstram/main 2025-06-11 20:24:18 +07:00
Username1548
f163608a90 feat(permissions): added galery and camera permisshion checker 2025-06-11 20:13:26 +07:00
6bbec1f022 Merge pull request 'Issue-440 & Issue-194 & build 27' (#503) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#503
2025-06-11 12:49:09 +00:00
mso7nueh
c0311bf213 feat(chat&notifications): Issue-191 and Issue-440 and auth screen policy 2025-06-11 15:34:31 +03:00
mso7nueh
9d1e621769 feat(chat&notifications): Issue-191 and Issue-440 and auth screen policy 2025-06-11 15:34:16 +03:00
mso7nueh
11b695cefd feat(dev-tools): Issue-441 2025-06-10 17:04:29 +03:00
Username1548
926f0de450 feat(userPurchases, consultantFinances): added status to models and layout for status 2025-06-10 19:57:20 +07:00
mso7nueh
dc81085ae4 fix(chat&reg): fix chat screen, reg email valid 2025-06-09 17:57:10 +03:00
mso7nueh
8c0efc1a34 fix(chat&reg): fix chat screen, reg email valid 2025-06-09 17:57:01 +03:00
Username1548
483cf6d8ee feat(userPurchases): added user purchases, fix(consultations): fixed consultations bug 2025-06-09 14:01:05 +07: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
87d3f058f6 Merge pull request 'Issue-329' (#500) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#500
2025-06-06 12:41:37 +00:00
mso7nueh
7a3c6e6b07 Merge remote-tracking branch 'origin/main' 2025-06-06 15:40:37 +03:00
mso7nueh
e6bad9d35d fix(blogPost): Issue-329 2025-06-06 15:40:27 +03: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
72cf203658 Merge pull request 'build 26 + 1.1.3+33' (#495) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#495
2025-06-06 08:33:16 +00:00
Username1548
086fd1cac4 Merge remote-tracking branch 'upstream/main' 2025-06-05 22:43:50 +07:00
88e33765d3 Merge pull request 'feat(chats): added refresh timer for chat room, feat(diary): fixed plan duration in plan widget and added date to app bar)' (#492) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#492
2025-06-05 15:43:21 +00:00
Username1548
6fa4bffd7e feat(consultantFinaces): added consultant finaces screen 2025-06-05 22:42:50 +07:00
mso7nueh
70620d8d9e fix(blogPost): update without image 2025-06-05 17:36:49 +03:00
mso7nueh
0b2464ea19 fix(blogPost): update without image 2025-06-05 17:36:15 +03: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
227 changed files with 8945 additions and 5572 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.0"
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,11 @@ jobs:
run: flutter test
- name: Build apk
run: flutter build apk --release
run: |
BUILD_DATE=$(TZ=Europe/Moscow date +'%Y-%m-%d %H:%M:%S')
flutter build apk --release \
--dart-define=HIDE_PLANS=false \
--dart-define=BUILD_DATE="$BUILD_DATE"
- name: Rename and move apk
run: |

9
.gitignore vendored
View File

@@ -11,4 +11,11 @@
/android/app/upload-keystore.jks
/android/app/.cxx
*.g.dart
*.freezed.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

@@ -16,20 +16,20 @@ if (keystorePropertiesFile.exists()) {
android {
namespace = "com.eatvioapp.app"
compileSdk = flutter.compileSdkVersion
ndkVersion = "27.0.12077973"
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

@@ -2,9 +2,13 @@
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<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">
<activity
@@ -25,8 +29,30 @@
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Deep link intent -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="eatvio.ru" />
<data android:host="www.eatvio.ru" />
<data android:pathPattern="/ru/recipe/.*" />
<data android:pathPattern="/en/recipe/.*" />
<data android:pathPattern="/ru/blog_post/.*" />
<data android:pathPattern="/en/blog_post/.*" />
<data android:pathPattern="/ru/diet/.*" />
<data android:pathPattern="/en/diet/.*" />
<data android:pathPattern="/ru/plan/.*" />
<data android:pathPattern="/en/plan/.*" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.

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"

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="19.633818mm"
height="19.633816mm"
viewBox="0 0 19.633818 19.633816"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="layer1"
transform="translate(-64.066064,-72.435122)">
<g
id="g2">
<path
id="path1"
style="fill-opacity:0;stroke:#2f80ed;stroke-width:1.065"
d="m 83.167381,82.252022 a 9.2844086,9.2844086 0 0 1 -9.284408,9.284408 9.2844086,9.2844086 0 0 1 -9.284409,-9.284408 9.2844086,9.2844086 0 0 1 9.284409,-9.284409 9.2844086,9.2844086 0 0 1 9.284408,9.284409 z" />
<path
style="fill:#000000;fill-opacity:0;stroke:#2f80ed;stroke-width:1.065;stroke-dasharray:none;stroke-opacity:1"
d="M 68.517124,89.22945 79.520546,75.54452"
id="path2" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 986 B

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

@@ -0,0 +1,3 @@
<svg width="19" height="14" viewBox="0 0 19 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.56039 6.73215L7.06827 10.2925C7.22519 10.452 7.3026 10.6385 7.30049 10.8521C7.29855 11.0656 7.21569 11.2555 7.0519 11.4218C6.88812 11.577 6.70269 11.6573 6.49563 11.6629C6.28874 11.6682 6.1034 11.5879 5.93961 11.4218L1.19568 6.60653C1.0048 6.41278 0.909363 6.1868 0.909363 5.92858C0.909363 5.67037 1.0048 5.44439 1.19568 5.25064L5.93961 0.435368C6.08581 0.287154 6.26675 0.211261 6.48243 0.207689C6.69829 0.204297 6.88812 0.280189 7.0519 0.435368C7.21569 0.601618 7.29759 0.792601 7.29759 1.00831C7.29759 1.22385 7.21569 1.41474 7.0519 1.58099L3.56039 5.12501H13.4583C14.8441 5.12501 16.0261 5.62162 17.0045 6.61483C17.983 7.60787 18.4722 8.80769 18.4722 10.2143V13.1607C18.4722 13.3888 18.3964 13.5797 18.2448 13.7334C18.0933 13.8873 17.9052 13.9643 17.6806 13.9643C17.4559 13.9643 17.2678 13.8873 17.1164 13.7334C16.9647 13.5797 16.8889 13.3888 16.8889 13.1607V10.2143C16.8889 9.25269 16.5539 8.43197 15.884 7.75215C15.2143 7.07215 14.4057 6.73215 13.4583 6.73215H3.56039Z" fill="#2F80ED"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

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

@@ -40,6 +40,17 @@ post_install do |installer|
target.build_configurations.each do |config|
config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64"
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0'
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
## dart: PermissionGroup.camera
'PERMISSION_CAMERA=1',
## dart: PermissionGroup.photos
'PERMISSION_PHOTOS=1',
'PERMISSION_NOTIFICATIONS=1',
## dart: PermissionGroup.mediaLibrary
'PERMISSION_MEDIA_LIBRARY=1',
]
end
end
end

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 = 24;
CURRENT_PROJECT_VERSION = 68;
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 = 24;
CURRENT_PROJECT_VERSION = 68;
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 = 24;
CURRENT_PROJECT_VERSION = 68;
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>24</string>
<string>68</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>
@@ -31,12 +31,14 @@
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSAppleMusicUsageDescription</key>
<string>This permission is not needed by the app, but it is required by an underlying API. If you see this dialog, contact us.</string>
<key>NSCameraUsageDescription</key>
<string>Нужно для загрузки фото</string>
<string>Предоставление доступа позволит Вам делать фотографии и добавлять к записям</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string></string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Нужно для загрузки фото</string>
<string>Предоставление доступа позволит Вам добавлять фото к записям</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>

View File

@@ -4,5 +4,10 @@
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:eatvio.ru</string>
<string>applinks:www.eatvio.ru</string>
</array>
</dict>
</plist>

View File

@@ -18,9 +18,68 @@ 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_date = Time.now.getlocal("+03:00").strftime("%Y-%m-%d %H:%M:%S")
build_app(
workspace: "Runner.xcworkspace",
scheme: "Runner",
export_method: "app-store",
codesigning_identity: "Apple Distribution",
xcargs: "--dart-define=HIDE_PLANS=true --dart-define=BUILD_DATE='#{build_date}'"
)
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,8 +1,12 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/firebase_api.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/core/websockets/ws_manager.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart';
import 'package:eatvio_app/features/auth/presentation/pages/forget_password_screen.dart';
import 'package:eatvio_app/features/auth/presentation/pages/scan_qr_code_screen.dart';
import 'package:eatvio_app/features/blog_post/presentation/pages/blog_post_screen.dart';
import 'package:eatvio_app/features/blog_post_create/presentation/pages/blog_post_create_screen.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
@@ -11,11 +15,15 @@ import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/b
import 'package:eatvio_app/features/calendar/presentation/pages/calendar_screen.dart';
import 'package:eatvio_app/features/chat/presentation/pages/chat_screen.dart';
import 'package:eatvio_app/features/chat/presentation/pages/chats_screen.dart';
import 'package:eatvio_app/features/chat/presentation/pages/empty_chat_screen.dart';
import 'package:eatvio_app/features/chat/presentation/pages/payment_screen.dart';
import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/become_consultant_screen.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/stepper_become_consultant_screen.dart';
import 'package:eatvio_app/features/consultations/presentation/pages/consultations_screen.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart';
import 'package:eatvio_app/features/menu/presentation/pages/blocked_users_screen.dart';
import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart';
import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart';
import 'package:eatvio_app/features/create_diet/presentation/diet_preview_screen.dart';
@@ -36,7 +44,6 @@ import 'package:eatvio_app/features/finances_and_consultations_archive/presentat
import 'package:eatvio_app/features/my_finance/presentation/pages/user_finance_screen.dart';
import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_screen.dart';
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
import 'package:eatvio_app/features/plans/presentation/pages/about_plan_screen.dart';
import 'package:eatvio_app/features/profile/presentation/pages/profile_screen.dart';
import 'package:eatvio_app/features/profile/presentation/pages/subs_screen.dart';
@@ -61,10 +68,12 @@ import 'package:eatvio_app/features/user_details/presentation/user_details_scree
import 'package:eatvio_app/features/welcome/presentation/pages/welcome_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:go_router/go_router.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:version/version.dart';
import 'dart:developer';
class AppRouter {
static GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
@@ -72,6 +81,30 @@ class AppRouter {
static GoRouter appRouter = GoRouter(
initialLocation: '/update-screen',
navigatorKey: navigatorKey,
redirect: (context, state) {
final segments = state.uri.pathSegments;
if (segments.length >= 3) {
final type = segments[1];
final contentIdentifier = segments[2];
switch (type) {
case 'recipe':
FlutterNativeSplash.remove();
return '/about-recipes-screen?slug=$contentIdentifier';
case 'blog_post':
FlutterNativeSplash.remove();
return '/blog-post-screen?id=$contentIdentifier';
case 'plan':
FlutterNativeSplash.remove();
return '/about-plan-screen?slug=$contentIdentifier';
case 'diet':
FlutterNativeSplash.remove();
return '/about-diet-screen?slug=$contentIdentifier';
}
}
return null;
},
routes: [
GoRoute(
path: '/auth-screen',
@@ -83,6 +116,18 @@ class AppRouter {
name: 'registrationScreen',
builder: (context, state) => const RegistrationScreen(),
),
GoRoute(
path: 'forget-password-screen',
name: 'forgetPasswordScreen',
builder: (context, state) => ForgetPasswordScreen(
isAuth: state.extra as bool,
),
),
GoRoute(
path: 'scan-qr-code-screen',
name: 'scanQRCodeScreen',
builder: (context, state) => const ScanQRCodeScreen(),
),
],
),
ShellRoute(
@@ -111,10 +156,10 @@ class AppRouter {
redirect: (context, state) async {
try {
await sl<WsManager>().initialize();
} catch (e) {
rethrow;
} catch (e, st) {
log(e.toString(), stackTrace: st);
}
return '/feed-screen';
return null;
},
),
GoRoute(
@@ -149,17 +194,29 @@ class AppRouter {
),
],
),
GoRoute(
name: 'blockedUsersScreen',
path: '/blocked-users-screen',
builder: (context, state) => const BlockedUsersScreen(),
),
GoRoute(
path: '/welcome-screen',
name: 'welcomeScreen',
builder: (context, state) => const WelcomeScreen(),
redirect: (context, state) async {
final sharedPreferences = await SharedPreferences.getInstance();
final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed');
if (welcomeScreenWasShowed == true) {
try {
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()) {
return '/feed-screen';
} else {
return '/auth-screen';
}
} catch (e) {
return '/feed-screen';
} else {
return null;
}
},
),
@@ -168,23 +225,37 @@ class AppRouter {
name: 'updateScreen',
builder: (context, state) => const UpdateScreen(),
redirect: (context, state) async {
final versions = (await sl<Dio>().get('$baseUrl/api/app_versions')).data;
PackageInfo packageInfo = await PackageInfo.fromPlatform();
String version = packageInfo.version;
Version currentVersion = Version.parse(version);
Version latestVersion = Version.parse(versions['min_version']);
if (latestVersion > currentVersion) {
return null;
} else {
try {
final versions = (await sl<Dio>().get('$baseUrl/api/app_versions')).data;
PackageInfo packageInfo = await PackageInfo.fromPlatform();
String version = packageInfo.version;
Version currentVersion = Version.parse(version);
Version latestVersion = Version.parse(versions['min_version']);
if (latestVersion > currentVersion) {
return null;
} else {
return '/welcome-screen';
}
} catch (e) {
return '/welcome-screen';
}
},
),
GoRoute(
path: '/consultant-finance-screen',
name: 'consultantFinanceScreen',
path: ConsultationsArchiveScreen.path,
name: ConsultationsArchiveScreen.name,
builder: (context, state) => const ConsultationsArchiveScreen(),
),
GoRoute(
path: ConsultantFinancesScreen.path,
name: ConsultantFinancesScreen.name,
builder: (context, state) => const ConsultantFinancesScreen(),
),
GoRoute(
path: UserPurchasesScreen.path,
name: UserPurchasesScreen.name,
builder: (context, state) => const UserPurchasesScreen(),
),
GoRoute(
path: '/user-finance-screen',
name: 'userFinanceScreen',
@@ -202,8 +273,17 @@ class AppRouter {
path: '/about-plan-screen',
name: 'aboutPlanScreen',
builder: (context, state) {
final slug = state.uri.queryParameters['slug'];
if (slug != null) {
return AboutPlanScreen(
slug: slug,
navigateToComments: false,
index: null,
);
}
final data = state.extra as Map<String, dynamic>;
return AboutPlanScreen(plan: data['plan'] as PlanModel, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
return AboutPlanScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
},
),
GoRoute(path: '/glossary-screen', name: 'glossaryScreen', builder: (context, state) => const GlossaryScreen()),
@@ -211,9 +291,18 @@ class AppRouter {
path: '/about-recipes-screen',
name: 'aboutRecipesScreen',
builder: (context, state) {
final slug = state.uri.queryParameters['slug'];
if (slug != null) {
return AboutRecipeScreen(
recipeSlug: slug,
navigateToComments: false,
index: null,
);
}
final data = state.extra as Map<String, dynamic>;
return AboutRecipeScreen(
recipe: data['recipe'] as Recipe,
recipeSlug: data['recipe'] as String,
index: data['index'] as int?,
navigateToComments: data['navigateToComments'] as bool,
);
@@ -236,8 +325,16 @@ class AppRouter {
path: '/about-diet-screen',
name: 'aboutDietScreen',
builder: (context, state) {
final slug = state.uri.queryParameters['slug'];
if (slug != null) {
return AboutDietScreen(
slug: slug,
index: null,
navigateToComments: false,
);
}
final data = state.extra as Map<String, dynamic>;
return AboutDietScreen(diet: data['diet'] as Diet, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
return AboutDietScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
},
),
GoRoute(
@@ -245,20 +342,38 @@ class AppRouter {
name: 'blogPostCreateScreen',
builder: (context, state) => BlogPostCreateScreen(feedBlogPostModel: state.extra != null ? state.extra as FeedBlogPostModel : null),
),
GoRoute(path: '/blog-post-screen', name: 'blogPostScreen', builder: (context, state) => BlogPostScreen(blogPost: state.extra as FeedBlogPostModel)),
GoRoute(
path: '/blog-post-screen',
name: 'blogPostScreen',
builder: (context, state) {
final id = state.uri.queryParameters['id'];
final parsedId = int.tryParse(id ?? '');
if (parsedId != null) {
return BlogPostScreen(
id: parsedId,
);
}
return BlogPostScreen(id: state.extra as int);
}),
GoRoute(
path: '/chat-screen',
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,
);
},
),
GoRoute(
path: '/empty-chat-screen',
name: 'emptyChatScreen',
builder: (context, state) => EmptyChatScreen(author: state.extra as Author),
),
GoRoute(
path: '/settings-screen',
name: 'settingsScreen',

View File

@@ -1,20 +1,25 @@
import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_state.dart';
import 'dart:developer';
import 'package:eatvio_app/core/comments/comments_cubit/comments_state.dart';
import 'package:eatvio_app/core/comments/repositories/comments_repository.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart';
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class CommentsCubit extends Cubit<CommentsState> {
final FeedRepository feedRepository;
final CommentsRepository commentsRepository;
CommentsCubit({
required this.feedRepository,
required this.commentsRepository,
}) : super(const CommentsState());
setReplyId(int? replyId) {
void setReplyId(int? replyId) {
emit((state as CommentsStateLoaded).copyWith(replyId: replyId));
}
getComments(String id, CommentSourceType commentSourceType, [bool isFirstFetch = false]) async {
Future<void> getComments(String id, CommentSourceType commentSourceType, [bool isFirstFetch = false]) async {
if (isFirstFetch) emit(const CommentsState.loading());
try {
switch (commentSourceType) {
@@ -27,7 +32,7 @@ class CommentsCubit extends Cubit<CommentsState> {
case CommentSourceType.recipe:
emit(CommentsState.loaded(comment: await feedRepository.getRecipesComments(id: id)));
break;
case CommentSourceType.plan:
case CommentSourceType.plan:
emit(CommentsState.loaded(comment: await feedRepository.getPlansComments(slug: id)));
break;
}
@@ -36,7 +41,7 @@ class CommentsCubit extends Cubit<CommentsState> {
}
}
sendComment({
Future<void> sendComment({
required String id,
required String commentText,
required CommentSourceType commentSourceType,
@@ -78,4 +83,34 @@ class CommentsCubit extends Cubit<CommentsState> {
emit(CommentsState.error(errorMessage: e.toString()));
}
}
Future<bool> deleteComment({required int id}) async {
if (state is! CommentsStateLoaded) return false;
final commentModel = (state as CommentsStateLoaded).comment;
final updatedCommentModel = commentModel.copyWith(data: commentModel.data.where((comment) => comment.id != id).toList());
try {
await commentsRepository.deleteComment(id: id);
emit(CommentsState.loaded(comment: updatedCommentModel));
return true;
} catch (e, st) {
emit(CommentsState.loaded(comment: commentModel));
log(e.toString(), stackTrace: st);
}
return false;
}
Future<bool> updateComment({required int id, required String body}) async {
final commentModel = (state as CommentsStateLoaded).comment;
final updatedCommentModel =
commentModel.copyWith(data: commentModel.data.map((comment) => comment.id == id ? comment.copyWith(body: body) : comment).toList());
try {
await commentsRepository.updateComment(id: id, body: body);
emit(CommentsState.loaded(comment: updatedCommentModel));
return true;
} catch (e, st) {
emit(CommentsState.loaded(comment: commentModel));
log(e.toString(), stackTrace: st);
}
return false;
}
}

View File

@@ -12,6 +12,7 @@ abstract class CommentsState with _$CommentsState {
const factory CommentsState.loaded({
required CommentModel comment,
int? replyId,
int? editedCommentId,
}) = CommentsStateLoaded;
const factory CommentsState.error({

View File

@@ -0,0 +1,12 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'comment_update_model.freezed.dart';
part 'comment_update_model.g.dart';
@freezed
abstract class CommentUpdateModel with _$CommentUpdateModel {
factory CommentUpdateModel({required String body}) = _CommentUpdateModel;
factory CommentUpdateModel.fromJson(Map<String, dynamic> json) => _$CommentUpdateModelFromJson(json);
}

View File

@@ -0,0 +1,173 @@
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:go_router/go_router.dart';
void showCommentsActionsBottomSheet(
BuildContext context, {
required void Function()? reply,
required void Function() copy,
required void Function(String text) edit,
required void Function() delete,
required void Function() complain,
required String commentText,
required bool isMyComment,
}) {
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: CommentsActionsBottomSheet(
isMyComment: isMyComment,
reply: reply,
copy: copy,
edit: edit,
delete: delete,
complain: complain,
commentText: commentText,
),
));
}
class CommentsActionsBottomSheet extends StatefulWidget {
const CommentsActionsBottomSheet({
super.key,
required this.isMyComment,
required this.reply,
required this.copy,
required this.edit,
required this.delete,
required this.complain,
required this.commentText,
});
final bool isMyComment;
final void Function()? reply;
final void Function() copy;
final void Function(String text) edit;
final void Function() delete;
final void Function() complain;
final String commentText;
@override
State<CommentsActionsBottomSheet> createState() => _CommentsActionsBottomSheetState();
}
class _CommentsActionsBottomSheetState extends State<CommentsActionsBottomSheet> {
bool isUserEditingComment = false;
final controller = TextEditingController();
@override
void initState() {
super.initState();
controller.text = widget.commentText;
}
@override
Widget build(BuildContext context) {
final s = S.of(context);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: isUserEditingComment
? Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(children: [
Expanded(
child: CustomTextFormField(
autofocus: true,
controller: controller,
onChanged: (value) => setState(() {}),
),
),
const SizedBox(width: 8),
GestureDetector(
onTap: () {
widget.edit(controller.text);
GoRouter.of(context).pop();
},
child: Container(
decoration: BoxDecoration(
color: controller.text.length > 2 ? appTheme.blueA400 : appTheme.gray50,
shape: BoxShape.circle,
),
height: 32,
width: 32,
child: Icon(
Icons.send_rounded,
size: 18,
color: controller.text.length > 2 ? appTheme.whiteA700 : appTheme.black,
),
),
)
])
],
),
)
: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
height: 7,
),
const BottomSheetHandlerBar(),
if (widget.isMyComment)
BottomSheetActionWidget(
text: s.edit,
iconPath: 'assets/icons/edit_paint.svg',
needToPop: false,
onTap: () => setState(() {
isUserEditingComment = true;
}),
)
else if (widget.reply != null)
BottomSheetActionWidget(
text: s.reply,
iconPath: 'assets/icons/reply.svg',
onTap: widget.reply!,
),
if (widget.reply != null) const CustomDivider(),
BottomSheetActionWidget(
text: s.copy,
iconPath: 'assets/icons/copy2.svg',
onTap: widget.copy,
),
if (widget.isMyComment) ...[
if (widget.reply != null) ...[
const CustomDivider(),
BottomSheetActionWidget(
text: s.reply,
iconPath: 'assets/icons/reply.svg',
onTap: widget.reply!,
),
],
const CustomDivider(),
BottomSheetActionWidget(
text: s.deleteComment,
iconPath: 'assets/icons/red_trash.svg',
onTap: widget.delete,
),
] else ...[
const CustomDivider(),
BottomSheetActionWidget(
text: s.complain,
iconPath: 'assets/icons/complain.svg',
onTap: widget.complain,
),
]
],
),
);
}
}

View File

@@ -0,0 +1,24 @@
import 'package:eatvio_app/core/comments/models/comment_update_model.dart';
import 'package:eatvio_app/core/comments/services/comments_service.dart';
class CommentsRepository {
final CommentsService service;
CommentsRepository({required this.service});
Future<void> deleteComment({required int id}) async {
try {
return service.deleteComment(id);
} catch (e) {
rethrow;
}
}
Future<void> updateComment({required int id, required String body}) async {
try {
return service.updateComment(id, CommentUpdateModel(body: body));
} catch (e) {
rethrow;
}
}
}

View File

@@ -0,0 +1,16 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/comments/models/comment_update_model.dart';
import 'package:retrofit/retrofit.dart';
part 'comments_service.g.dart';
@RestApi()
abstract class CommentsService {
factory CommentsService(Dio dio, {String baseUrl}) = _CommentsService;
@DELETE('/api/comments/{id}')
Future<void> deleteComment(@Path('id') int id);
@PATCH('/api/comments/{id}')
Future<void> updateComment(@Path('id') int id, @Body() CommentUpdateModel body);
}

View File

@@ -1,5 +1,8 @@
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 buildDate = String.fromEnvironment('BUILD_DATE', defaultValue: '');
const List<int> durationValues = [7, 14, 21];

View File

@@ -20,12 +20,12 @@ abstract class CredentialsStorage {
};
}
static saveCredentials(String email, String password) async {
static Future<void> saveCredentials(String email, String password) async {
await const FlutterSecureStorage().write(key: 'email', value: email);
await const FlutterSecureStorage().write(key: 'password', value: password);
}
static removeCredentials() async {
static Future<void> removeCredentials() async {
await const FlutterSecureStorage().delete(key: 'email');
await const FlutterSecureStorage().delete(key: 'password');
}

View File

@@ -1,7 +1 @@
enum ComplainedContent {
diet,
plan,
recipe,
profile,
posts
}
enum ComplainedContent { diet, plan, recipe, profile, posts, comment }

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,11 +1,35 @@
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 {
final _firebaseMessaging = FirebaseMessaging.instance;
Future<String?> getToken() async{
Future<String?> getToken() async {
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

@@ -7,11 +7,25 @@ abstract class ImageCompressUtility {
static Future<List<File>> compressFilesList(List<File> files) async {
final List<File> compressedFilesList = [];
for (int i = 0; i < files.length; i++) {
compressedFilesList.add(await compressFile(files[i]));
if (_isImageFile(files[i].path)) {
compressedFilesList.add(await compressFile(files[i]));
} else {
compressedFilesList.add(files[i]);
}
}
return compressedFilesList;
}
static bool _isImageFile(String path) {
final lower = path.toLowerCase();
return lower.endsWith('.png') ||
lower.endsWith('.jpg') ||
lower.endsWith('.jpeg') ||
lower.endsWith('.gif') ||
lower.endsWith('.bmp') ||
lower.endsWith('.webp');
}
static Future<File> compressFile(File file) async {
final image = img.decodeImage(file.readAsBytesSync())!;
int width = image.width;

View File

@@ -1,12 +1,19 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/comments/repositories/comments_repository.dart';
import 'package:eatvio_app/core/comments/services/comments_service.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/dio/phone_verify_error_interceptor.dart';
import 'package:eatvio_app/core/firebase_api.dart';
import 'package:eatvio_app/core/interceptors/token_interceptor.dart';
import 'package:eatvio_app/core/websockets/ws_manager.dart';
import 'package:eatvio_app/core/websockets/ws_service.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart';
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
import 'package:eatvio_app/features/auth/repositories/reset_password_repository.dart';
import 'package:eatvio_app/features/auth/services/auth_service.dart';
import 'package:eatvio_app/features/auth/services/reset_password_service.dart';
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_cubit.dart';
import 'package:eatvio_app/features/cancel_plan_bottom_sheet/cubit/cancel_plan_cubit.dart';
import 'package:eatvio_app/features/cancel_plan_bottom_sheet/repository/cancel_plan_repository.dart';
import 'package:eatvio_app/features/cancel_plan_bottom_sheet/service/cancel_plan_service.dart';
@@ -30,6 +37,11 @@ import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit
import 'package:eatvio_app/features/blog_post_create/repositories/blog_post_repository.dart';
import 'package:eatvio_app/features/blog_post_create/services/blog_post_service.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_cubit.dart';
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart';
import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart';
import 'package:eatvio_app/features/menu/services/blocked_users_service.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
import 'package:eatvio_app/features/calendar/presentation/bloc/calendar_cubit.dart';
@@ -46,7 +58,7 @@ import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:eatvio_app/features/diary/repositories/diary_repository.dart';
import 'package:eatvio_app/features/diary/services/diary_service.dart';
import 'package:eatvio_app/features/diets/presentation/cubit/about_diet_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_cubit.dart';
import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/consultants_cubit/consultants_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
@@ -118,7 +130,7 @@ Future<void> init() async {
sl.registerFactory<ProfileCubit>(() => ProfileCubit(profileRepository: sl(), chatRepository: sl(), consultantRepository: sl()));
sl.registerFactory<profile_plans.PlansCubit>(() => profile_plans.PlansCubit(plansRepository: sl()));
sl.registerFactory<DietsCubit>(() => DietsCubit(feedRepository: sl(), dietsRepository: sl()));
sl.registerFactory<CommentsCubit>(() => CommentsCubit(feedRepository: sl()));
sl.registerFactory<CommentsCubit>(() => CommentsCubit(feedRepository: sl(), commentsRepository: sl()));
sl.registerFactory<PlansCubit>(() => PlansCubit(feedRepository: sl(), planRepository: sl()));
sl.registerFactory<SettingsCubit>(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl(), consultantRepository: sl()));
sl.registerFactory<RegisterConsultantCubit>(() => RegisterConsultantCubit(consultantRepository: sl()));
@@ -153,6 +165,11 @@ Future<void> init() async {
sl.registerFactory<VerifyEmailCubit>(() => VerifyEmailCubit(profileRepository: sl()));
sl.registerFactory<MyRecipesCubit>(() => MyRecipesCubit(myRecipesRepository: sl(), aboutRecipeRepository: sl()));
sl.registerFactory<ConsultationsArchiveCubit>(() => ConsultationsArchiveCubit(consultationsRepository: sl()));
sl.registerFactory<ConsultantFinancesCubit>(() => ConsultantFinancesCubit(consultationsRepository: sl()));
sl.registerFactory<UserPurchasesCubit>(() => UserPurchasesCubit(consultationsRepository: sl()));
sl.registerFactory<ResetPasswordCubit>(() => ResetPasswordCubit(resetPasswordRepository: sl()));
sl.registerFactory<BlogPostCubit>(() => BlogPostCubit(sl()));
sl.registerFactory<BlockedUsersCubit>(() => BlockedUsersCubit(blockedUsersRepository: sl()));
// Repository
sl.registerLazySingleton<ChatRepository>(() => ChatRepository(chatService: sl()));
@@ -181,6 +198,9 @@ Future<void> init() async {
sl.registerLazySingleton<CreatePlanRepository>(() => CreatePlanRepository(createPlanService: sl()));
sl.registerLazySingleton<NotificationsRepository>(() => NotificationsRepository(notificationsService: sl()));
sl.registerFactory<MyRecipesRepository>(() => MyRecipesRepository(myRecipesService: sl()));
sl.registerFactory<ResetPasswordRepository>(() => ResetPasswordRepository(resetPasswordService: sl()));
sl.registerLazySingleton<BlockedUsersRepository>(() => BlockedUsersRepository(blockedUserService: sl()));
sl.registerLazySingleton<CommentsRepository>(() => CommentsRepository(service: sl()));
// Services
sl.registerLazySingleton<ChatService>(() => ChatService(sl()));
@@ -210,6 +230,10 @@ Future<void> init() async {
sl.registerLazySingleton<CreatePlanService>(() => CreatePlanService(sl()));
sl.registerLazySingleton<NotificationsService>(() => NotificationsService(sl()));
sl.registerLazySingleton<MyRecipesService>(() => MyRecipesService(sl()));
sl.registerLazySingleton<FirebaseApi>(() => FirebaseApi());
sl.registerLazySingleton<ResetPasswordService>(() => ResetPasswordService(sl()));
sl.registerLazySingleton<BlockedUserService>(() => BlockedUserService(sl()));
sl.registerLazySingleton<CommentsService>(() => CommentsService(sl()));
// External
sl.registerLazySingleton(

View File

@@ -0,0 +1,29 @@
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';
import 'package:permission_handler/permission_handler.dart';
class PermissionHandler {
static Future<void> checkPermission({
required Permission permission,
required String title,
required String description,
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();
return;
}
if (context.mounted) {
showErrorDialog(context, title: title, description: description, buttonTitle: S.of(context).allowAccess, needToOpenSettings: true);
}
}
}

View File

@@ -7,162 +7,162 @@ import 'package:flutter/material.dart';
class CustomTextStyles {
// Headline style
static get headlineSmallMulish => theme.textTheme.headlineSmall!.mulish;
static TextStyle get headlineSmallMulish => theme.textTheme.headlineSmall!.mulish;
// Title text style
static get titleMediumSegoeUI => theme.textTheme.titleMedium!.segoeUI.copyWith(
static TextStyle get titleMediumSegoeUI => theme.textTheme.titleMedium!.segoeUI.copyWith(
fontWeight: FontWeight.w600,
);
static get bodyMediumBlack900 => theme.textTheme.bodyMedium!.copyWith(
static TextStyle get bodyMediumBlack900 => theme.textTheme.bodyMedium!.copyWith(
color: appTheme.black900,
);
static get bodyMediumBlack90013 => theme.textTheme.bodyMedium!.copyWith(
static TextStyle get bodyMediumBlack90013 => theme.textTheme.bodyMedium!.copyWith(
color: appTheme.black900,
fontSize: 13,
);
static get bodyMediumBluegray800 => theme.textTheme.bodyMedium!.copyWith(
static TextStyle get bodyMediumBluegray800 => theme.textTheme.bodyMedium!.copyWith(
color: appTheme.black,
);
static get bodyMediumBluegray800_1 => theme.textTheme.bodyMedium!.copyWith(
static TextStyle get bodyMediumBluegray800_1 => theme.textTheme.bodyMedium!.copyWith(
color: appTheme.black,
);
static get bodyMediumBluegray800_2 => theme.textTheme.bodyMedium!.copyWith(
static TextStyle get bodyMediumBluegray800_2 => theme.textTheme.bodyMedium!.copyWith(
color: appTheme.black,
);
static get bodyMediumWhiteA700 => theme.textTheme.bodyMedium!.copyWith(
static TextStyle get bodyMediumWhiteA700 => theme.textTheme.bodyMedium!.copyWith(
color: appTheme.whiteA700,
);
static get bodySmallMulishBlack900 => theme.textTheme.bodySmall!.mulish.copyWith(
static TextStyle get bodySmallMulishBlack900 => theme.textTheme.bodySmall!.mulish.copyWith(
color: appTheme.black900,
fontSize: 12,
);
static get bodySmallMulishBluegray800 => theme.textTheme.bodySmall!.mulish.copyWith(
static TextStyle get bodySmallMulishBluegray800 => theme.textTheme.bodySmall!.mulish.copyWith(
color: appTheme.black,
fontSize: 12,
);
static get bodySmallMulishBluegray80012 => theme.textTheme.bodySmall!.mulish.copyWith(
static TextStyle get bodySmallMulishBluegray80012 => theme.textTheme.bodySmall!.mulish.copyWith(
color: appTheme.black,
fontSize: 12,
);
static get bodySmallMulishBluegray80012_1 => theme.textTheme.bodySmall!.mulish.copyWith(
static TextStyle get bodySmallMulishBluegray80012_1 => theme.textTheme.bodySmall!.mulish.copyWith(
color: appTheme.black,
fontSize: 12,
);
static get bodySmallMulishBluegray900 => theme.textTheme.bodySmall!.mulish.copyWith(
static TextStyle get bodySmallMulishBluegray900 => theme.textTheme.bodySmall!.mulish.copyWith(
color: appTheme.blueGray900,
fontSize: 12,
);
static get bodySmallMulishGray600 => theme.textTheme.bodySmall!.mulish.copyWith(
static TextStyle get bodySmallMulishGray600 => theme.textTheme.bodySmall!.mulish.copyWith(
color: appTheme.gray600,
);
static get bodySmallMulishPrimary => theme.textTheme.bodySmall!.mulish.copyWith(
static TextStyle get bodySmallMulishPrimary => theme.textTheme.bodySmall!.mulish.copyWith(
color: theme.colorScheme.primary,
fontSize: 12,
);
static get bodySmallSegoeUIBlack900 => theme.textTheme.bodySmall!.segoeUI.copyWith(
static TextStyle get bodySmallSegoeUIBlack900 => theme.textTheme.bodySmall!.segoeUI.copyWith(
color: appTheme.black900,
fontSize: 12,
);
// Label text style
static get labelLargeGray60003 => theme.textTheme.labelLarge!.copyWith(
static TextStyle get labelLargeGray60003 => theme.textTheme.labelLarge!.copyWith(
color: appTheme.gray60003,
);
static get labelLargeWhiteA700 => theme.textTheme.labelLarge!.copyWith(
static TextStyle get labelLargeWhiteA700 => theme.textTheme.labelLarge!.copyWith(
color: appTheme.whiteA700,
);
// Title text style
static get titleMedium16 => theme.textTheme.titleMedium!.copyWith(
static TextStyle get titleMedium16 => theme.textTheme.titleMedium!.copyWith(
fontSize: 16,
);
static get titleMediumBlack => theme.textTheme.titleMedium!.copyWith(
static TextStyle get titleMediumBlack => theme.textTheme.titleMedium!.copyWith(
fontSize: 17,
fontWeight: FontWeight.w900,
);
static get titleMediumBlack900 => theme.textTheme.titleMedium!.copyWith(
static TextStyle get titleMediumBlack900 => theme.textTheme.titleMedium!.copyWith(
color: appTheme.black900,
);
static get titleMediumSemiBold => theme.textTheme.titleMedium!.copyWith(
static TextStyle get titleMediumSemiBold => theme.textTheme.titleMedium!.copyWith(
fontWeight: FontWeight.w600,
);
static get titleMediumWhiteA700 => theme.textTheme.titleMedium!.copyWith(
static TextStyle get titleMediumWhiteA700 => theme.textTheme.titleMedium!.copyWith(
color: appTheme.whiteA700,
fontWeight: FontWeight.w600,
);
static get titleSmallBlue800 => theme.textTheme.titleSmall!.copyWith(
static TextStyle get titleSmallBlue800 => theme.textTheme.titleSmall!.copyWith(
color: appTheme.blue800,
fontWeight: FontWeight.w800,
);
static get titleSmallBluegray800 => theme.textTheme.titleSmall!.copyWith(
static TextStyle get titleSmallBluegray800 => theme.textTheme.titleSmall!.copyWith(
color: appTheme.black,
);
static get titleSmallBluegray800Bold => theme.textTheme.titleSmall!.copyWith(
static TextStyle get titleSmallBluegray800Bold => theme.textTheme.titleSmall!.copyWith(
color: appTheme.black,
fontWeight: FontWeight.w700,
);
static get titleSmallBluegray800ExtraBold => theme.textTheme.titleSmall!.copyWith(
static TextStyle get titleSmallBluegray800ExtraBold => theme.textTheme.titleSmall!.copyWith(
color: appTheme.black,
fontWeight: FontWeight.w800,
);
static get titleSmallBold => theme.textTheme.titleSmall!.copyWith(
static TextStyle get titleSmallBold => theme.textTheme.titleSmall!.copyWith(
fontSize: 15,
fontWeight: FontWeight.w700,
);
static get titleSmallGreenA70001 => theme.textTheme.titleSmall!.copyWith(
static TextStyle get titleSmallGreenA70001 => theme.textTheme.titleSmall!.copyWith(
color: appTheme.greenA70001,
);
static get titleSmallGreenA70001ExtraBold => theme.textTheme.titleSmall!.copyWith(
static TextStyle get titleSmallGreenA70001ExtraBold => theme.textTheme.titleSmall!.copyWith(
color: appTheme.greenA70001,
fontWeight: FontWeight.w800,
);
static get titleSmallLightgreen300 => theme.textTheme.titleSmall!.copyWith(
static TextStyle get titleSmallLightgreen300 => theme.textTheme.titleSmall!.copyWith(
color: appTheme.lightGreen300,
fontWeight: FontWeight.w800,
);
static get titleSmallRed200 => theme.textTheme.titleSmall!.copyWith(
static TextStyle get titleSmallRed200 => theme.textTheme.titleSmall!.copyWith(
color: appTheme.red200,
fontWeight: FontWeight.w800,
);
static get titleSmallSecondaryContainer => theme.textTheme.titleSmall!.copyWith(
static TextStyle get titleSmallSecondaryContainer => theme.textTheme.titleSmall!.copyWith(
color: theme.colorScheme.secondaryContainer,
fontWeight: FontWeight.w800,
);
static get titleSmallSecondaryContainer_1 => theme.textTheme.titleSmall!.copyWith(
static TextStyle get titleSmallSecondaryContainer_1 => theme.textTheme.titleSmall!.copyWith(
color: theme.colorScheme.secondaryContainer,
);
static get titleSmallSegoeUIOnPrimary => theme.textTheme.titleSmall!.segoeUI.copyWith(
static TextStyle get titleSmallSegoeUIOnPrimary => theme.textTheme.titleSmall!.segoeUI.copyWith(
color: theme.colorScheme.onPrimary,
);
}

View File

@@ -15,9 +15,31 @@ abstract class TokenStorage {
static Future<String?> getTokenFromStorage() async => await const FlutterSecureStorage().read(key: 'token');
static saveTokenInStorage(String token) async {
static Future<void> saveTokenInStorage(String token) async {
await const FlutterSecureStorage().write(key: 'token', value: token);
}
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

@@ -8,31 +8,39 @@ extension DateTimeExtension on DateTime {
String pattern = dateTimeFormatPattern,
String? locale,
]) {
final localDate = isUtc ? toLocal() : this;
if (locale != null && locale.isNotEmpty) {
initializeDateFormatting(locale);
}
return DateFormat(pattern, locale).format(this);
return DateFormat(pattern, locale).format(localDate);
}
bool isYesterday(DateTime date) {
final DateTime localDate = date.isUtc ? date.toLocal() : date;
final yesterday = DateTime.now().subtract(const Duration(days: 1));
return yesterday.day == localDate.day && yesterday.month == localDate.month && yesterday.year == localDate.year;
}
bool isToday(DateTime date) {
final DateTime localDate = date.isUtc ? date.toLocal() : date;
bool isYesterday() {
final localDate = isUtc ? toLocal() : this;
final now = DateTime.now();
return now.day == localDate.day && now.month == localDate.month && now.year == localDate.year;
final yesterday = DateTime(now.year, now.month, now.day).subtract(const Duration(days: 1));
return localDate.year == yesterday.year &&
localDate.month == yesterday.month &&
localDate.day == yesterday.day;
}
bool isToday() {
final localDate = isUtc ? toLocal() : this;
final now = DateTime.now();
return now.year == localDate.year &&
now.month == localDate.month &&
now.day == localDate.day;
}
}
String calculateDifference(DateTime dateTime) {
if (dateTime.isToday(dateTime)) {
String calculateDifference(DateTime dateTime, [String? format]) {
final localDateTime = dateTime.isUtc ? dateTime.toLocal() : dateTime;
if (localDateTime.isToday()) {
return 'Сегодня';
} else if (dateTime.isYesterday(dateTime)) {
} else if (localDateTime.isYesterday()) {
return 'Вчера';
} else {
return dateTime.format('d MMMM', 'ru');
return localDateTime.format(format ?? 'd MMMM', 'ru');
}
}

View File

@@ -1,9 +1,13 @@
import 'dart:convert';
import 'dart:developer';
import 'package:dart_pusher_channels/dart_pusher_channels.dart';
import 'package:easy_debounce/easy_debounce.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/websockets/ws_service.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
class WsManager {
final WsService wsService;
@@ -39,6 +43,7 @@ class WsManager {
metadata: const PusherChannelsOptionsMetadata.byDefault(),
port: 443,
);
client = PusherChannelsClient.websocket(
options: options,
connectionErrorHandler: (exception, trace, refresh) {
@@ -55,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;
@@ -70,7 +93,11 @@ class WsManager {
}
});
client?.eventStream.listen((event) {
log(event.rootObject.toString());
EasyDebounce.debounce(
'searchChatRoom',
const Duration(milliseconds: 300),
() => sl<ChatsCubit>().getChatRooms(),
);
});
_subscribe();
} catch (e, st) {
@@ -136,7 +163,7 @@ class WsManager {
_privateDirectChatChannel?.unsubscribe();
}
Future<void> clear() async{
Future<void> clear() async {
await client?.disconnect();
client = null;
userId = null;

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

@@ -56,7 +56,7 @@ class CustomDropDown extends StatelessWidget {
: _buildDropDownWidget(context);
}
_buildDropDownWidget(BuildContext context) {
DropdownButtonFormField<SelectionPopupModel> _buildDropDownWidget(BuildContext context) {
return DropdownButtonFormField<SelectionPopupModel>(
focusNode: focusNode ?? FocusNode(),
icon: const Icon(Icons.keyboard_arrow_down_rounded),
@@ -84,7 +84,7 @@ class CustomDropDown extends StatelessWidget {
);
}
void onDropDownChanged(value) {
void onDropDownChanged(dynamic value) {
onChanged!(value!);
}

View File

@@ -11,6 +11,7 @@ import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart';
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart';
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
@@ -28,6 +29,7 @@ class CustomPopup extends StatelessWidget {
final FeedBlogPostModel? feedBlogPostModel;
final Recipe? recipe;
final Diet? diet;
final String userSlug;
const CustomPopup({
super.key,
@@ -39,6 +41,7 @@ class CustomPopup extends StatelessWidget {
this.feedBlogPostModel,
this.recipe,
this.diet,
required this.userSlug
});
@override
@@ -73,7 +76,7 @@ class CustomPopup extends StatelessWidget {
);
}
List<PopupMenuEntry> _buildMenuItems(s, context) {
List<PopupMenuEntry> _buildMenuItems(S s,BuildContext context) {
final entries = <PopupMenuEntry>[];
void addItem(String icon, String text, Function() onTap) {
@@ -107,28 +110,28 @@ class CustomPopup extends StatelessWidget {
addItem('assets/icons/red_trash.svg', 'Удалить', () async {
if (complainedContentType == ComplainedContent.posts) {
final result = await sl<BlogPostCreateCubit>().deleteBlogPost(id!);
if (!result) {
if (!result && context.mounted) {
showErrorDialog(context, title: S.current.could_not_delete_blog_post);
return;
}
showSuccessToast('Пост удален, обновите ленту');
} else if (complainedContentType == ComplainedContent.recipe) {
final result = await sl<RecipesCubit>().deleteRecipe(slug!);
if (!result) {
if (!result && context.mounted) {
showErrorDialog(context, title: 'Не удалось удалить рецепт');
return;
}
showSuccessToast('Рецепт удален, обновите ленту');
} else if (complainedContentType == ComplainedContent.diet) {
final result = await sl<DietsCubit>().deleteDiet(id!);
if (!result) {
if (!result && context.mounted) {
showErrorDialog(context, title: 'Не удалось удалить диету');
return;
}
showSuccessToast('Диета удалена, обновите ленту');
} else if (complainedContentType == ComplainedContent.plan) {
final result = await sl<MyPlansCubit>().deletePlan(slug!);
if (!result) {
if (!result && context.mounted) {
showErrorDialog(context, title: 'Не удалось удалить план');
return;
}
@@ -164,6 +167,13 @@ class CustomPopup extends StatelessWidget {
);
}
});
addItem(
'assets/icons/complain.svg',
'Заблокировать автора',
() {
sl<BlockedUsersCubit>().blockUser(userSlug);
},
);
}
if (entries.isNotEmpty && entries.last is PopupMenuItem) {

View File

@@ -4,8 +4,9 @@ import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:permission_handler/permission_handler.dart';
Future<void> showErrorDialog(BuildContext context, {String? title, String? description}) async {
Future<void> showErrorDialog(BuildContext context, {String? title, String? description, String? buttonTitle, bool? needToOpenSettings}) async {
return await showDialog<void>(
context: context,
barrierColor: Colors.transparent,
@@ -43,8 +44,13 @@ Future<void> showErrorDialog(BuildContext context, {String? title, String? descr
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () => AppRouter.navigatorKey.currentContext?.pop(),
child: Text(s.ok, style: TextStyles.textLSemibold.copyWith(color: appTheme.blueA400)),
onPressed: () {
AppRouter.navigatorKey.currentContext?.pop();
if (needToOpenSettings ?? false) {
openAppSettings();
}
},
child: Text(buttonTitle ?? s.ok, style: TextStyles.textLSemibold.copyWith(color: appTheme.blueA400)),
),
),
const SizedBox(height: 12),

View File

@@ -26,9 +26,13 @@ class SuccessPublishedRecipeBottomSheet extends StatelessWidget {
s.recipe_publishing,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
Text(
'"$title" ${s.successfully_published}',
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Text(
'"$title" ${s.successfully_published}',
textAlign: TextAlign.center,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
),
const SizedBox(
height: 20,

View File

@@ -2,7 +2,7 @@ import 'dart:developer';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_state.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state.dart';
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

View File

@@ -0,0 +1,35 @@
import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_state.dart';
import 'package:eatvio_app/features/auth/repositories/reset_password_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class ResetPasswordCubit extends Cubit<ResetPasswordState> {
final ResetPasswordRepository resetPasswordRepository;
ResetPasswordCubit({required this.resetPasswordRepository}) : super(const ResetPasswordState.empty());
Future<void> trySendEmailCode(String email) async {
emit(const ResetPasswordState.loadingSendEmailCode());
try {
if (await resetPasswordRepository.sendEmailCodeForPasswordChange(email)) {
emit(const ResetPasswordState.codeSendSuccess());
} else {
emit(const ResetPasswordState.codeSendError());
}
} catch (e) {
emit(const ResetPasswordState.codeSendError());
}
}
Future<void> tryResetPassword(String email, String code, String password, String passwordConfirmation) async {
emit(const ResetPasswordState.loadingResetPassword());
try {
if (await resetPasswordRepository.confirmPasswordChange(code, password, passwordConfirmation, email)) {
emit(const ResetPasswordState.resetPasswordSuccess());
} else {
emit(const ResetPasswordState.resetPasswordError());
}
} catch (e) {
emit(const ResetPasswordState.resetPasswordError());
}
}
}

View File

@@ -0,0 +1,22 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'reset_password_state.freezed.dart';
@freezed
abstract class ResetPasswordState with _$ResetPasswordState {
const factory ResetPasswordState() = _ResetPasswordState;
const factory ResetPasswordState.empty() = ResetPasswordStateEmpty;
const factory ResetPasswordState.loadingSendEmailCode() = ResetPasswordStateLoadingSendEmailCode;
const factory ResetPasswordState.codeSendSuccess() = ResetPasswordStateCodeSendSuccess;
const factory ResetPasswordState.codeSendError() = ResetPasswordStateCodeSendError;
const factory ResetPasswordState.loadingResetPassword() = ResetPasswordStateLoadingResetPassword;
const factory ResetPasswordState.resetPasswordError() = ResetPasswordStateResetPasswordError;
const factory ResetPasswordState.resetPasswordSuccess() = ResetPasswordStateResetPassowrdSuccess;
}

View File

@@ -1,16 +1,18 @@
import 'dart:async';
import 'package:easy_debounce/easy_debounce.dart';
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';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/core/websockets/ws_manager.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/cupertino.dart';
@@ -23,8 +25,6 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:url_launcher/url_launcher.dart';
import '../bloc/auth_state.dart';
class AuthScreen extends StatefulWidget {
const AuthScreen({super.key});
@@ -41,6 +41,7 @@ class _AuthScreenState extends State<AuthScreen> {
late StreamSubscription<bool> keyboardSubscription;
bool isFocused = false;
int tapsCount = 0;
@override
void initState() {
super.initState();
@@ -53,21 +54,33 @@ class _AuthScreenState extends State<AuthScreen> {
}
Future<void> _reconnectWebSocket(BuildContext context) async {
await sl<WsManager>().clear();
await sl<WsManager>().initialize();
if (context.mounted) {
context.read<ProfileAvatarCubit>().getMyProfile();
context.read<ChatsCubit>().getChatRoomsWithLoading();
context.read<ChatsCubit>().connectToWebSocket();
context.read<NotificationsCubit>().getNotifications();
context.read<NotificationsCubit>().connectToWebSocket();
showSuccessToast('Успешный вход');
GoRouter.of(context).go('/feed-screen', extra: {'needToInitializeWebSocket': false});
try {
await sl<WsManager>().clear();
await sl<WsManager>().initialize();
} catch (e) {
rethrow;
}
if (context.mounted) {
try {
await sl<ProfileAvatarCubit>().getMyProfile();
await sl<ChatsCubit>().getChatRoomsWithLoading();
await sl<ChatsCubit>().connectToWebSocket();
await sl<NotificationsCubit>().getNotifications();
await sl<NotificationsCubit>().connectToWebSocket();
await sl<FirebaseApi>().registerPushToken();
showSuccessToast('Успешный вход');
} catch (e) {
rethrow;
}
}
AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false});
}
@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>(
@@ -81,19 +94,12 @@ class _AuthScreenState extends State<AuthScreen> {
title: GestureDetector(
onTap: () {
tapsCount++;
if (tapsCount == 6) {
if (tapsCount == 7) {
tapsCount = 0;
// showModalBottomSheet(context: context, builder: (context) => );
GoRouter.of(context).pushNamed(DevToolsScreen.name);
}
EasyDebounce.debounce('change env', Durations.medium4, () {
tapsCount = 0;
});
},
child: SvgPicture.asset(
ImageConstant.imgLogoEatvio,
height: isFocused ? 64 : MediaQuery.of(context).size.width - 64,
fit: BoxFit.contain,
),
child: SvgPicture.asset(ImageConstant.imgLogoEatvio, height: isFocused ? 64 : MediaQuery.of(context).size.width - 64, fit: BoxFit.contain),
),
),
body: SafeArea(
@@ -124,9 +130,16 @@ class _AuthScreenState extends State<AuthScreen> {
hidePassword = !hidePassword;
});
},
icon: Icon(
hidePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined,
color: Colors.grey,
icon: Icon(hidePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined, color: Colors.grey),
),
),
const SizedBox(height: 16),
Center(
child: GestureDetector(
onTap: () => GoRouter.of(context).goNamed('forgetPasswordScreen', extra: false),
child: Text(
'Забыли пароль?',
style: TextStyle(color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline),
),
),
),
@@ -139,11 +152,7 @@ class _AuthScreenState extends State<AuthScreen> {
onTap: () => GoRouter.of(context).goNamed('registrationScreen'),
child: Text(
'Зарегистрироваться',
style: TextStyle(
color: appTheme.blueA400,
decorationColor: appTheme.blueA400,
decoration: TextDecoration.underline,
),
style: TextStyle(color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline),
),
),
],
@@ -153,60 +162,142 @@ class _AuthScreenState extends State<AuthScreen> {
child: RichText(
textAlign: TextAlign.center,
text: TextSpan(
text: 'Нажимая "Войти", Вы соглашаетесь \n',
style: const TextStyle(
fontSize: 12,
color: Colors.black,
),
text: 'Нажимая "Войти" или "Зарегистрироваться", Вы соглашаетесь \n',
style: const TextStyle(fontSize: 12, color: Colors.black),
children: [
const TextSpan(text: 'c '),
if (!hidePlans) ...[
TextSpan(
text: ' Публичной офертой',
style: TextStyle(
fontSize: 12,
color: appTheme.blueA400,
decorationColor: appTheme.blueA400,
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')),
),
const TextSpan(text: ' и '),
],
TextSpan(
text: ' Публичной офертой',
text: 'Политикой конфиденциальности\nи защитой персональных данных',
style: TextStyle(
fontSize: 12,
color: appTheme.blueA400,
decorationColor: appTheme.blueA400,
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')),
recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/docs/privacy.pdf')),
),
],
),
),
),
const SizedBox(height: 16),
SizedBox(
height: 48,
width: MediaQuery.of(context).size.width - 32,
child: FloatingActionButton.extended(
heroTag: 'authButton',
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
Row(
children: [
SizedBox(
height: 48,
width: MediaQuery.of(context).size.width - 88,
child: FloatingActionButton.extended(
heroTag: 'authButton',
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400,
onPressed: state is AuthStateLoading
? null
: () async {
final isAuthenticated = await authCubit.getTokenByCredentials(emailController.text, passwordController.text);
if (isAuthenticated) {
if (context.mounted) {
_reconnectWebSocket(context);
}
}
},
label: state is AuthStateLoading
? const CupertinoActivityIndicator()
: Text('Войти', style: theme.textTheme.titleSmall!.copyWith(color: Colors.white)),
),
),
backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400,
onPressed: state is AuthStateLoading
? null
: () async {
final isAuthenticated = await authCubit.getTokenByCredentials(
emailController.text,
passwordController.text,
);
if (isAuthenticated) {
if (context.mounted) {
_reconnectWebSocket(context);
}
}
},
label: state is AuthStateLoading
? const CupertinoActivityIndicator()
: Text(
'Войти',
style: theme.textTheme.titleSmall!.copyWith(color: Colors.white),
),
),
const SizedBox(width: 8),
SizedBox(
height: 48,
width: 48,
child: FloatingActionButton.extended(
heroTag: 'scanQrCodeButton',
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400,
onPressed: state is AuthStateLoading
? null
: () async {
await showDialog(
context: context,
builder: (BuildContext context) {
return Dialog(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text('Где взять QR-код?\n', style: TextStyle(fontSize: 18)),
Text(
'1. Войдите на сайте $baseUrl в свой аккаунт\n\n2. Перейдите в настройки профиля\n\n3. В разделе "Доп информация" перейдите на "QR Login"',
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () {
AppRouter.navigatorKey.currentContext?.pop();
},
child: Ink(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
child: const Text(
'Отмена',
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w400),
),
),
),
const SizedBox(width: 8),
InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () {
AppRouter.navigatorKey.currentContext?.pop();
AppRouter.navigatorKey.currentContext?.pushNamed('scanQRCodeScreen');
},
child: Ink(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
child: Text(
'Перейти',
style: TextStyle(color: appTheme.blueA400, fontWeight: FontWeight.w600),
),
),
),
],
),
],
),
),
);
},
);
},
label: state is AuthStateLoading
? const CupertinoActivityIndicator()
: const Icon(Icons.qr_code_scanner_rounded, color: Colors.white),
),
),
],
),
],
),

View File

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

View File

@@ -0,0 +1,109 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/core/websockets/ws_manager.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart';
class ScanQRCodeScreen extends StatefulWidget {
const ScanQRCodeScreen({super.key});
@override
State<ScanQRCodeScreen> createState() => _ScanQRCodeScreenState();
}
class _ScanQRCodeScreenState extends State<ScanQRCodeScreen> {
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
Barcode? result;
QRViewController? controller;
@override
void reassemble() {
super.reassemble();
if (Platform.isAndroid) {
controller!.pauseCamera();
} else if (Platform.isIOS) {
controller!.resumeCamera();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Сканирование QR-кода'),
centerTitle: true,
),
body: Column(
children: <Widget>[
Expanded(
child: QRView(
key: qrKey,
onQRViewCreated: _onQRViewCreated,
),
),
],
),
);
}
void _onQRViewCreated(QRViewController controller) {
this.controller = controller;
controller.scannedDataStream.listen((scanData) async {
setState(() {
result = scanData;
});
await controller.pauseCamera();
try {
final scannedUrl = scanData.code;
final uri = Uri.parse(scannedUrl ?? '');
final scannedBaseUrl = (uri.hasAuthority) ? '${uri.scheme}://${uri.host}' : '';
baseUrl = scannedBaseUrl;
sl<Dio>().options.baseUrl = baseUrl;
final response = await Dio().getUri(
uri,
options: Options(
headers: {
'accept': 'application/json',
'content-type': 'application/json',
},
),
);
final token = response.data['token'];
await TokenStorage.saveTokenInStorage(token);
try {
await sl<WsManager>().clear();
await sl<WsManager>().initialize();
} catch (e) {
rethrow;
}
try {
await sl<ProfileAvatarCubit>().getMyProfile();
await sl<ChatsCubit>().getChatRoomsWithLoading();
await sl<ChatsCubit>().connectToWebSocket();
await sl<NotificationsCubit>().getNotifications();
await sl<NotificationsCubit>().connectToWebSocket();
} catch (e) {
rethrow;
}
AppRouter.navigatorKey.currentContext?.go(
'/feed-screen',
extra: {'needToInitializeWebSocket': false},
);
showSuccessToast('Добро пожаловать!');
} catch (e) {
await controller.resumeCamera();
showErrorToast('Что-то пошло не так. Попробуйте обновить страницу с QR-кодом или войдите с помощью логина и пароля.');
}
});
}
}

View File

@@ -0,0 +1,53 @@
import 'package:eatvio_app/features/auth/services/reset_password_service.dart';
class ResetPasswordRepository {
final ResetPasswordService resetPasswordService;
ResetPasswordRepository({
required this.resetPasswordService,
});
Future<bool> sendEmailCodeForPasswordChange(String email) async {
try {
final response = await resetPasswordService.sendEmailCodeForPasswordChange(
data: {
'email': email,
},
);
int statusCode = response.response.statusCode ?? 0;
if (statusCode >= 200 && statusCode < 300) {
return true;
} else {
return false;
}
} catch (e) {
rethrow;
}
}
Future<bool> confirmPasswordChange(
String code,
String password,
String passwordConfirmation,
String email,
) async {
try {
final response = await resetPasswordService.confirmPasswordChange(
data: {
'code': code,
'password': password,
'password_confirmation': passwordConfirmation,
'email': email,
},
);
int statusCode = response.response.statusCode ?? 0;
if (statusCode >= 200 && statusCode < 300) {
return true;
} else {
return false;
}
} catch (e) {
rethrow;
}
}
}

View File

@@ -0,0 +1,19 @@
import 'package:dio/dio.dart';
import 'package:retrofit/retrofit.dart';
part 'reset_password_service.g.dart';
@RestApi()
abstract class ResetPasswordService {
factory ResetPasswordService(Dio dio, {String baseUrl}) = _ResetPasswordService;
@POST('/api/send_password_reset_code')
Future<HttpResponse> sendEmailCodeForPasswordChange({
@Body() required Map<String, dynamic> data,
});
@POST('/api/confirm_password_reset_code')
Future<HttpResponse> confirmPasswordChange({
@Body() required Map<String, dynamic> data,
});
}

View File

@@ -0,0 +1,21 @@
import 'dart:developer';
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_state.dart';
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class BlogPostCubit extends Cubit<BlogPostState> {
final FeedRepository feedRepository;
BlogPostCubit(this.feedRepository) : super(BlogPostState());
Future<void> getBlogPost(int id) async {
try {
emit(state.copyWith(isLoading: true));
final postModel = await feedRepository.getBlogPost(id: id);
emit(state.copyWith(blogPostModel: postModel, isLoading: false));
} catch (e, st) {
log(e.toString(), stackTrace: st);
emit(state.copyWith(isLoading: false));
}
}
}

View File

@@ -0,0 +1,17 @@
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'blog_post_state.freezed.dart';
part 'blog_post_state.g.dart';
@freezed
abstract class BlogPostState with _$BlogPostState {
factory BlogPostState({
@Default(null) FeedBlogPostModel? blogPostModel,
@Default(false) bool isLoading,
@Default(null) String? error,
}) = _BlogPostState;
factory BlogPostState.fromJson(Map<String, dynamic> json) => _$BlogPostStateFromJson(json);
}

View File

@@ -5,9 +5,10 @@ import 'package:eatvio_app/core/enums/complained_content.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/core/widgets/custom_popup.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_state.dart';
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_cubit.dart';
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_state.dart';
import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart';
import 'package:eatvio_app/core/comments/comments_cubit/comments_state.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart';
import 'package:eatvio_app/core/locator_service.dart';
@@ -19,9 +20,9 @@ import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
class BlogPostScreen extends StatefulWidget {
final FeedBlogPostModel blogPost;
final int id;
const BlogPostScreen({super.key, required this.blogPost});
const BlogPostScreen({super.key, required this.id});
@override
State<BlogPostScreen> createState() => _BlogPostScreenState();
@@ -31,260 +32,294 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
final CommentsCubit commentsCubit = sl<CommentsCubit>();
final textEditingController = TextEditingController();
int? replyId;
late bool isLiked;
late int totalLikes;
bool isLiked = false;
int totalLikes = 0;
@override
void initState() {
isLiked = widget.blogPost.wasLikedByMe;
totalLikes = widget.blogPost.totalLikes;
context.read<FeedCubit>().viewPost(widget.blogPost.id);
context.read<FeedCubit>().viewPost(widget.id);
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocProvider<CommentsCubit>(
create: (BuildContext context) => commentsCubit..getComments(widget.blogPost.id.toString(), CommentSourceType.blogPost, true),
child: BlocConsumer<CommentsCubit, CommentsState>(
listener: (BuildContext context, CommentsState state) {
if (state is CommentsStateLoaded) {
setState(() {
replyId = state.replyId;
});
}
},
builder: (BuildContext context, CommentsState state) {
return Scaffold(
resizeToAvoidBottomInset: true,
bottomSheet: Container(
padding: const EdgeInsets.symmetric(vertical: 12) + (Platform.isIOS ? const EdgeInsets.only(bottom: 16) : EdgeInsets.zero),
decoration: BoxDecoration(color: Colors.white, border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1)))),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (replyId != null)
GestureDetector(
onTap: () {
context.read<CommentsCubit>().setReplyId(null);
setState(() {
replyId = null;
textEditingController.clear();
});
FocusManager.instance.primaryFocus?.unfocus();
},
child: Container(
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
height: 32,
width: 32,
child: const Icon(Icons.close_rounded, size: 18, color: Colors.white),
),
),
if (replyId != null) const SizedBox(width: 8),
SizedBox(
width: MediaQuery.of(context).size.width - (replyId == null ? 64 : 104),
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 52),
child: TextFormField(
textCapitalization: TextCapitalization.sentences,
onChanged: (_) {
setState(() {});
},
controller: textEditingController,
maxLines: 5,
minLines: 1,
keyboardType: TextInputType.multiline,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Colors.black,
height: 1.2,
),
decoration: InputDecoration(
isDense: false,
// убираем true, чтобы padding учитывался корректно
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
// вертикальный паддинг увеличен
hintText: 'Комментарий',
hintStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
height: 1.2,
color: Color(0xFFAEAEB2),
),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
),
),
),
),
const SizedBox(width: 8),
GestureDetector(
onTap: () {
if (textEditingController.text.length > 2) {
commentsCubit.sendComment(
id: widget.blogPost.id.toString(),
commentText: textEditingController.text,
parentCommentId: replyId,
commentSourceType: CommentSourceType.blogPost,
);
context.read<CommentsCubit>().setReplyId(null);
setState(() {
textEditingController.clear();
replyId = null;
});
FocusManager.instance.primaryFocus?.unfocus();
}
},
child: Container(
decoration: BoxDecoration(color: textEditingController.text.length > 2 ? appTheme.blueA400 : Colors.grey, shape: BoxShape.circle),
height: 32,
width: 32,
child: const Icon(Icons.send_rounded, size: 18, color: Colors.white),
),
),
],
),
),
appBar: AppBar(
title: Text(widget.blogPost.title),
actions: [
CustomPopup(
isAuthor: widget.blogPost.author.authorIsMe,
complainedContentType: ComplainedContent.posts,
path: 'blog_post/${(widget.blogPost.id)}',
id: widget.blogPost.id,
feedBlogPostModel: widget.blogPost,
),
const SizedBox(width: 16),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 80),
physics: const BouncingScrollPhysics(),
child: Column(
children: [
GestureDetector(
onTap: () => GoRouter.of(context).pushNamed('profileScreen', extra: widget.blogPost.author),
child: Ink(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
children: [
CircleAvatar(
radius: 16,
foregroundImage: NetworkImage(
baseUrl + widget.blogPost.author.authorProfileIcon,
),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.blogPost.author.authorName,
style: const TextStyle(color: Colors.black, fontSize: 14, fontWeight: FontWeight.w400, height: 1),
),
const SizedBox(height: 2),
Text(
widget.blogPost.createdAt.format('dd MMMM', 'ru'),
style: TextStyle(color: Colors.grey.shade600, fontSize: 12, fontWeight: FontWeight.w400, height: 1),
),
],
),
],
),
),
),
if (widget.blogPost.description.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Html(data: widget.blogPost.description, style: {"*": Style(color: Colors.black87)}),
),
const SizedBox(height: 8),
CachedNetworkImage(imageUrl: '$baseUrl/storage/${widget.blogPost.image}'),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
_iconButton(
icon: Icons.favorite_border_rounded,
text: totalLikes.toString(),
iconColor: isLiked ? Colors.red.withValues(alpha: 0.8) : null,
backgroundColor: isLiked ? Colors.red.withValues(alpha: 0.1) : null,
textColor: isLiked ? Colors.red.withValues(alpha: 0.8) : null,
onTap: () async {
setState(() {
isLiked = !isLiked;
isLiked ? totalLikes++ : totalLikes--;
});
isLiked
? await context.read<FeedCubit>().likePost(widget.blogPost.id)
: await context.read<FeedCubit>().unlikedPost(widget.blogPost.id);
sl<FeedCubit>().updateFeedBlogPostList(widget.blogPost, isLiked);
return BlocProvider(
create: (context) => sl<BlogPostCubit>()..getBlogPost(widget.id),
child: BlocProvider<CommentsCubit>(
create: (BuildContext context) => commentsCubit..getComments(widget.id.toString(), CommentSourceType.blogPost, true),
child: BlocBuilder<BlogPostCubit, BlogPostState>(
builder: (context, blogPostState) {
return BlocConsumer<CommentsCubit, CommentsState>(
listener: (BuildContext context, CommentsState state) {
if (state is CommentsStateLoaded) {
setState(() {
replyId = state.replyId;
});
}
},
builder: (BuildContext context, CommentsState state) {
if (blogPostState.isLoading) {
return Scaffold(
appBar: AppBar(
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go('/feed-screen');
},
),
),
body: const Loader());
}
isLiked = blogPostState.blogPostModel?.wasLikedByMe ?? false;
totalLikes = blogPostState.blogPostModel?.totalLikes ?? 0;
return Scaffold(
resizeToAvoidBottomInset: true,
bottomSheet: Container(
padding: const EdgeInsets.symmetric(vertical: 12) + (Platform.isIOS ? const EdgeInsets.only(bottom: 16) : EdgeInsets.zero),
decoration: BoxDecoration(color: Colors.white, border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1)))),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (replyId != null)
GestureDetector(
onTap: () {
context.read<CommentsCubit>().setReplyId(null);
setState(() {
replyId = null;
textEditingController.clear();
});
FocusManager.instance.primaryFocus?.unfocus();
},
child: Container(
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
height: 32,
width: 32,
child: const Icon(Icons.close_rounded, size: 18, color: Colors.white),
),
),
if (replyId != null) const SizedBox(width: 8),
SizedBox(
width: MediaQuery.of(context).size.width - (replyId == null ? 64 : 104),
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 52),
child: TextFormField(
textCapitalization: TextCapitalization.sentences,
onChanged: (_) {
setState(() {});
},
controller: textEditingController,
maxLines: 5,
minLines: 1,
keyboardType: TextInputType.multiline,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Colors.black,
height: 1.2,
),
decoration: InputDecoration(
isDense: false,
// убираем true, чтобы padding учитывался корректно
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
// вертикальный паддинг увеличен
hintText: 'Комментарий',
hintStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
height: 1.2,
color: Color(0xFFAEAEB2),
),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
),
),
),
),
const SizedBox(width: 8),
_iconButton(
icon: Icons.reply_rounded,
GestureDetector(
onTap: () {
final box = context.findRenderObject() as RenderBox?;
SharePlus.instance.share(
ShareParams(
uri: Uri.parse('$baseUrl/ru/blog_post/${widget.blogPost.id}'),
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
),
);
if (textEditingController.text.length > 2) {
commentsCubit.sendComment(
id: widget.id.toString(),
commentText: textEditingController.text,
parentCommentId: replyId,
commentSourceType: CommentSourceType.blogPost,
);
context.read<CommentsCubit>().setReplyId(null);
setState(() {
textEditingController.clear();
replyId = null;
});
FocusManager.instance.primaryFocus?.unfocus();
}
},
child: Container(
decoration: BoxDecoration(color: textEditingController.text.length > 2 ? appTheme.blueA400 : Colors.grey, shape: BoxShape.circle),
height: 32,
width: 32,
child: const Icon(Icons.send_rounded, size: 18, color: Colors.white),
),
),
const Spacer(),
Icon(Icons.visibility_rounded, size: 16, color: appTheme.gray500),
const SizedBox(width: 4),
Text(widget.blogPost.totalViews.toString(), style: TextStyle(fontSize: 12, color: appTheme.gray500, fontWeight: FontWeight.w400)),
],
),
),
Container(
width: MediaQuery.of(context).size.width - 32,
height: 0.4,
decoration: BoxDecoration(color: Colors.black.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(16)),
appBar: AppBar(
title: Text(blogPostState.blogPostModel?.title ?? ''),
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go('/feed-screen');
},
),
actions: [
CustomPopup(
isAuthor: blogPostState.blogPostModel?.author.authorIsMe ?? false,
complainedContentType: ComplainedContent.posts,
path: 'blog_post/${(widget.id)}',
id: widget.id,
feedBlogPostModel: blogPostState.blogPostModel,
userSlug: blogPostState.blogPostModel?.author.authorSlug ?? '',
),
const SizedBox(width: 16),
],
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
alignment: Alignment.centerLeft,
child: Text(
'${widget.blogPost.totalComments} КОММЕНТАРИЕВ',
style: TextStyle(fontSize: 12, color: appTheme.gray500, fontWeight: FontWeight.w400),
body: SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 80),
physics: const BouncingScrollPhysics(),
child: Column(
children: [
GestureDetector(
onTap: () => GoRouter.of(context).pushNamed('profileScreen', extra: blogPostState.blogPostModel?.author),
child: Ink(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
children: [
CircleAvatar(
radius: 16,
foregroundImage: NetworkImage(
baseUrl + (blogPostState.blogPostModel?.author.authorProfileIcon ?? ''),
),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
blogPostState.blogPostModel?.author.authorName ?? '',
style: const TextStyle(color: Colors.black, fontSize: 14, fontWeight: FontWeight.w400, height: 1),
),
const SizedBox(height: 2),
Text(
(blogPostState.blogPostModel?.createdAt ?? DateTime.now()).format('dd MMMM', 'ru'),
style: TextStyle(color: Colors.grey.shade600, fontSize: 12, fontWeight: FontWeight.w400, height: 1),
),
],
),
],
),
),
),
if (blogPostState.blogPostModel?.description.isNotEmpty ?? false)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Html(data: blogPostState.blogPostModel?.description.replaceAll('\n', '<br>'), style: {"*": Style(color: Colors.black87)}),
),
const SizedBox(height: 8),
if (blogPostState.blogPostModel?.image.isNotEmpty ?? false)
CachedNetworkImage(
imageUrl: '$baseUrl/storage/${blogPostState.blogPostModel?.image ?? ''}',
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
_iconButton(
icon: Icons.favorite_border_rounded,
text: totalLikes.toString(),
iconColor: isLiked ? Colors.red.withValues(alpha: 0.8) : null,
backgroundColor: isLiked ? Colors.red.withValues(alpha: 0.1) : null,
textColor: isLiked ? Colors.red.withValues(alpha: 0.8) : null,
onTap: () async {
setState(() {
isLiked = !isLiked;
isLiked ? totalLikes++ : totalLikes--;
});
isLiked ? await context.read<FeedCubit>().likePost(widget.id) : await context.read<FeedCubit>().unlikedPost(widget.id);
sl<FeedCubit>().updateFeedBlogPostList(blogPostState.blogPostModel, isLiked);
},
),
const SizedBox(width: 8),
_iconButton(
icon: Icons.reply_rounded,
onTap: () {
final box = context.findRenderObject() as RenderBox?;
SharePlus.instance.share(
ShareParams(
uri: Uri.parse('$baseUrl/ru/blog_post/${widget.id}'),
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
),
);
},
),
const Spacer(),
Icon(Icons.visibility_rounded, size: 16, color: appTheme.gray500),
const SizedBox(width: 4),
Text(blogPostState.blogPostModel?.totalViews.toString() ?? '',
style: TextStyle(fontSize: 12, color: appTheme.gray500, fontWeight: FontWeight.w400)),
],
),
),
Container(
width: MediaQuery.of(context).size.width - 32,
height: 0.4,
decoration: BoxDecoration(color: Colors.black.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(16)),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
alignment: Alignment.centerLeft,
child: Text(
'${blogPostState.blogPostModel?.totalComments} КОММЕНТАРИЕВ',
style: TextStyle(fontSize: 12, color: appTheme.gray500, fontWeight: FontWeight.w400),
),
),
const SizedBox(height: 16),
if (state is CommentsStateLoading) const Loader(),
if (state is CommentsStateLoaded)
CommentsWidget(id: widget.id.toString(), commentSourceType: CommentSourceType.blogPost, comment: state.comment, replyId: replyId),
],
),
),
const SizedBox(height: 16),
if (state is CommentsStateLoading) const Loader(),
if (state is CommentsStateLoaded)
CommentsWidget(id: widget.blogPost.id.toString(), commentSourceType: CommentSourceType.blogPost, comment: state.comment, replyId: replyId),
],
),
),
);
},
);
},
);
},
),
),
);
}
_iconButton({required IconData icon, String? text, Function()? onTap, Color? iconColor, Color? backgroundColor, Color? textColor}) {
InkWell _iconButton({required IconData icon, String? text, Function()? onTap, Color? iconColor, Color? backgroundColor, Color? textColor}) {
return InkWell(
onTap: onTap ?? () {},
borderRadius: BorderRadius.circular(16),

View File

@@ -1,6 +1,8 @@
import 'dart:async';
import 'dart:io';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/permission_handler/permission_handler.dart';
import 'package:eatvio_app/core/theme/app_decoration.dart';
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart';
@@ -17,6 +19,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import 'package:permission_handler/permission_handler.dart';
class BlogPostCreateScreen extends StatefulWidget {
final FeedBlogPostModel? feedBlogPostModel;
@@ -77,54 +80,72 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
);
}
_showImagePicker() {
void _showImagePicker() {
showCupertinoModalPopup<void>(
context: context,
builder: (context) => CupertinoActionSheet(
actions: [
CupertinoActionSheetAction(
onPressed: () async {
final picker = ImagePicker();
image = await picker.pickImage(source: ImageSource.camera);
setState(() {});
if (!context.mounted) return;
Navigator.of(context).pop();
},
child: const Row(
children: [
Icon(Icons.photo_camera),
SizedBox(width: 8),
Text('Фото'),
],
builder: (context) {
final s = S.of(context);
return CupertinoActionSheet(
actions: [
CupertinoActionSheetAction(
onPressed: () async {
PermissionHandler.checkPermission(
permission: Permission.camera,
title: s.allowAccessToCamera,
description: s.allowAccessToCameraDescription,
onGranted: () async {
final picker = ImagePicker();
image = await picker.pickImage(source: ImageSource.camera);
setState(() {});
AppRouter.navigatorKey.currentContext?.pop();
},
context: context,
);
},
child: const Row(
children: [
Icon(Icons.photo_camera),
SizedBox(width: 8),
Text('Фото'),
],
),
),
),
CupertinoActionSheetAction(
onPressed: () async {
final picker = ImagePicker();
image = await picker.pickImage(source: ImageSource.gallery);
setState(() {});
if (!context.mounted) return;
Navigator.of(context).pop();
},
child: const Row(
children: [
Icon(Icons.photo),
SizedBox(width: 8),
Text('Галерея'),
],
CupertinoActionSheetAction(
onPressed: () async {
PermissionHandler.checkPermission(
permission: Permission.photos,
title: s.allowAcessToGalery,
description: s.allowAcessToGaleryDescription,
onGranted: () async {
final picker = ImagePicker();
image = await picker.pickImage(source: ImageSource.gallery);
setState(() {});
if (!context.mounted) return;
AppRouter.navigatorKey.currentContext?.pop();
},
context: context,
);
},
child: const Row(
children: [
Icon(Icons.photo),
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('Отмена'),
),
),
);
},
);
}
_buildBlogPostCreate(BuildContext context) {
Scaffold _buildBlogPostCreate(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: true,
appBar: AppBar(),
@@ -232,7 +253,7 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
);
}
_buildAddPhoto() {
GestureDetector _buildAddPhoto() {
return GestureDetector(
onTap: () {
_showImagePicker();

View File

@@ -73,7 +73,7 @@ class BlogPostRepository {
'video_url': videoUrl,
'image': image != null
? await MultipartFile.fromFile((await ImageCompressUtility.compressFile(File(image.path))).path, filename: image.name)
: oldImageUrl != null
: oldImageUrl != '' && oldImageUrl != null
? await MultipartFile.fromFile((await downloadImage(oldImageUrl))?.path ?? '', filename: oldImageUrl.split('/').last)
: null,
});

View File

@@ -6,7 +6,7 @@ class NewMessagesCubit extends Cubit<NewMessagesState> {
NewMessagesCubit() : super(const NewMessagesState.empty());
incrementCounter() {
void incrementCounter() {
_messagesCount++;
emit(NewMessagesState.loaded(messagesCount: _messagesCount));
}

View File

@@ -1,6 +1,8 @@
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class ProfileAvatarCubit extends Cubit<ProfileAvatarState> {
final ProfileRepository profileRepository;
@@ -9,7 +11,7 @@ class ProfileAvatarCubit extends Cubit<ProfileAvatarState> {
required this.profileRepository,
}) : super(const ProfileAvatarState.loading());
getMyProfile() async {
Future<void> getMyProfile() async {
emit(const ProfileAvatarState.loading());
try {
emit(
@@ -18,6 +20,7 @@ class ProfileAvatarCubit extends Cubit<ProfileAvatarState> {
),
);
} catch (e) {
AppRouter.navigatorKey.currentContext?.go('/auth-screen');
emit(ProfileAvatarState.error(errorMessage: e.toString()));
}
}

View File

@@ -1,6 +1,7 @@
import 'package:badges/badges.dart' as badges;
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/utils/image_constant.dart';
import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_state.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
@@ -30,6 +31,7 @@ class BottomNavigationBarWidget extends StatefulWidget {
class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
int currentBottomNavBarIndex = 1;
late final List<String> pages;
int tapsCount = 0;
@override
void initState() {
@@ -58,12 +60,29 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
}
void _onItemTapped(int index) async {
if (pages.length == 7) {
if (index == 6) {
tapsCount++;
if (tapsCount == 7) {
tapsCount = 0;
GoRouter.of(context).pushNamed(DevToolsScreen.name);
}
}
} else {
if (index == 5) {
tapsCount++;
if (tapsCount == 7) {
tapsCount = 0;
GoRouter.of(context).pushNamed(DevToolsScreen.name);
}
}
}
if (currentBottomNavBarIndex != index) {
setState(() {
currentBottomNavBarIndex = index;
});
GoRouter.of(context).replace(pages[currentBottomNavBarIndex]);
}
GoRouter.of(context).replace(pages[currentBottomNavBarIndex]);
}
@override
@@ -82,7 +101,9 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
BottomNavigationBarItem(
label: '',
icon: CircleAvatar(
foregroundImage: NetworkImage(baseUrl + widget.profileModel.profilePhotoUrl),
foregroundImage: NetworkImage(
widget.profileModel.profilePhotoUrl.contains('https') ? widget.profileModel.profilePhotoUrl : baseUrl + widget.profileModel.profilePhotoUrl,
),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
radius: 16,
),

View File

@@ -7,7 +7,7 @@ class CalendarCubit extends Cubit<CalendarState> {
CalendarCubit({required this.calendarRepository}) : super(const CalendarState.loading());
loadCalendarList() async {
Future<void> loadCalendarList() async {
emit(const CalendarState.loading());
try {
final calendarList = await calendarRepository.getCalendar();

View File

@@ -89,7 +89,7 @@ class CalendarScreen extends StatelessWidget {
);
}
_buildCalendarBody(BuildContext context, CalendarStateLoaded state) {
ListView _buildCalendarBody(BuildContext context, CalendarStateLoaded state) {
return ListView.separated(
padding: const EdgeInsets.all(8),
shrinkWrap: true,

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

@@ -9,7 +9,6 @@ abstract class ChatUserModel with _$ChatUserModel {
required int id,
required String name,
required String slug,
@JsonKey(name: 'profile_photo_path') String? profilePhotoPath,
@JsonKey(name: 'profile_photo_url') String? profilePhotoUrl,
}) = _ChatUserModel;

View File

@@ -4,19 +4,23 @@ 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';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/core/websockets/ws_manager.dart';
import 'package:eatvio_app/features/chat/models/chat_message_model.dart';
import 'package:eatvio_app/features/chat/models/plan_complain_model.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
import 'package:eatvio_app/features/chat/repositories/chat_repository.dart';
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;
@@ -29,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();
@@ -45,14 +46,14 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
_timer?.cancel();
}
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(
@@ -87,17 +88,16 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
}
}
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),
@@ -156,30 +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());
});
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({required String chatId, String? data, List<File>? files}) async {
Future<bool> sendMessage({String? data, List<File>? files, required String userSlug}) async {
try {
await chatRepository.sendMessage(chatId: chatId, data: data, files: files != null ? await ImageCompressUtility.compressFilesList(files) : null);
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);
@@ -224,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());
@@ -252,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

@@ -1,17 +1,20 @@
import 'dart:io';
import 'dart:math';
import 'package:dismissible_page/dismissible_page.dart';
import 'package:easy_debounce/easy_debounce.dart';
import 'package:eatvio_app/core/const.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/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart';
import 'package:eatvio_app/features/chat/presentation/widgets/about_order_button.dart';
import 'package:eatvio_app/features/chat/presentation/widgets/chat_bubble_widget.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart';
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_state.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/chat/presentation/widgets/notification_banner.dart';
@@ -22,16 +25,19 @@ import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
import 'package:eatvio_app/features/user_details/presentation/user_details_screen.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
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();
@@ -45,6 +51,7 @@ class _ChatScreenState extends State<ChatScreen> {
final List<({int id, int index})> unreadMessagesIdsCopy = [];
final showableChatBadgeStates = ChosenPlanStateEnum.getShowableChatBadgeStates();
late AppLifecycleListener _listener;
List<File> pickedFiles = [];
void _onHide() {
chatRoomCubit.cancelTimer();
@@ -59,14 +66,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);
}
_onScroll() {
void _onScroll() {
if (scrollController.offset >= scrollController.position.maxScrollExtent && !scrollController.position.outOfRange) {
chatRoomCubit.loadNextPage(widget.chatId);
chatRoomCubit.loadNextPage(widget.uuid);
}
}
@@ -88,7 +95,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());
}
@@ -97,7 +107,7 @@ class _ChatScreenState extends State<ChatScreen> {
);
}
_buildChat(BuildContext context, ChatRoomStateLoaded state) {
Scaffold _buildChat(BuildContext context, ChatRoomStateLoaded state) {
final chosenPlan = state.chatRoom.chosenPlan;
final amIConsultant = chosenPlan?.consultantId == state.chatRoom.users.sender.id;
final showUserDetailsButton = [
@@ -155,7 +165,7 @@ class _ChatScreenState extends State<ChatScreen> {
if (state.chatRoom.users.receiver.profilePhotoUrl != null)
CircleAvatar(
radius: 20,
foregroundImage: NetworkImage(baseUrl + state.chatRoom.users.receiver.profilePhotoUrl!),
foregroundImage: NetworkImage(state.chatRoom.users.receiver.profilePhotoUrl!),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
),
if (state.chatRoom.users.receiver.profilePhotoUrl == null)
@@ -163,34 +173,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(),
@@ -246,9 +241,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();
});
}
@@ -259,6 +254,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,
),
),
],
@@ -285,6 +283,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,
),
);
},
@@ -334,100 +335,162 @@ class _ChatScreenState extends State<ChatScreen> {
),
Container(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(width: 8),
InkWell(
onTap: () async {
FilePickerResult? result = await FilePicker.platform.pickFiles(
allowMultiple: true,
type: FileType.custom,
allowedExtensions: [
'pdf',
'doc',
'docx',
'json',
'ppt',
'pptx',
'rtf',
'txt',
'xls',
'xlsx',
'csv',
'jpeg',
'jpg',
'png',
'gif',
'svg',
'xml',
'webp',
],
);
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
physics: const BouncingScrollPhysics(),
scrollDirection: Axis.horizontal,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: [
for (int i = 0; i < pickedFiles.length; i++) ...[
Stack(
clipBehavior: Clip.none,
children: [
_isImageFile(pickedFiles[i].path)
? GestureDetector(
onTap: () {
final imageFiles = pickedFiles.where((f) => _isImageFile(f.path)).toList();
final imageIndex = _imageIndexAt(pickedFiles, i);
if (result != null) {
List<File> files = result.paths.map((path) => File(path!)).toList();
await chatRoomCubit.sendMessage(chatId: widget.chatId, files: files);
}
},
customBorder: const CircleBorder(),
child: Ink(
height: 40,
width: 40,
child: Transform.rotate(angle: pi / 4, child: const Icon(Icons.attach_file_rounded, size: 30, color: Color(0xFF858E99))),
context.pushTransparentRoute(
PhotoViewScreen(imageUrls: imageFiles.map((e) => e.path).toList(), isLocal: true, index: imageIndex),
);
},
child: Hero(
tag: pickedFiles[i].path,
transitionOnUserGestures: true,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: SizedBox(height: 104, width: 104, child: Image.file(pickedFiles[i], fit: BoxFit.cover)),
),
),
)
: _buildFilePreview(pickedFiles[i]),
Positioned(
top: -8,
right: -8,
child: InkWell(
customBorder: const CircleBorder(),
onTap: () {
setState(() {
pickedFiles.remove(pickedFiles[i]);
});
},
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),
),
),
),
],
),
const SizedBox(width: 8),
],
],
),
),
const SizedBox(width: 8),
Expanded(
child: SizedBox(
height: 40,
child: TextFormField(
expands: true,
textCapitalization: TextCapitalization.sentences,
controller: textEditingController,
maxLines: null,
minLines: null,
keyboardType: TextInputType.multiline,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1),
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
hintText: 'Сообщение',
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)),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
const SizedBox(height: 12),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
const SizedBox(width: 8),
InkWell(
onTap: () => showImagePicker(state.chatRoom.users.receiver.slug),
customBorder: const CircleBorder(),
child: Ink(
height: 40,
width: 40,
child: Transform.rotate(
angle: pi / 4,
child: const Icon(Icons.attach_file_rounded, size: 30, color: Color(0xFF858E99)),
),
),
),
),
const SizedBox(width: 8),
Expanded(
child: SizedBox(
height: 40,
child: TextFormField(
expands: true,
textCapitalization: TextCapitalization.sentences,
controller: textEditingController,
maxLines: null,
minLines: null,
keyboardType: TextInputType.multiline,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1),
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
hintText: 'Сообщение',
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),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
),
),
),
),
const SizedBox(width: 8),
InkWell(
customBorder: const CircleBorder(),
onTap: () async {
// Проверяем, заблокирован ли пользователь
final blockedUsersCubit = sl<BlockedUsersCubit>();
await blockedUsersCubit.getBlockedUsers();
final blockedState = blockedUsersCubit.state;
if (blockedState is BlockedUsersStateLoaded) {
final isBlocked = blockedState.profilesList.any((profile) => profile.slug == state.chatRoom.users.receiver.slug);
if (isBlocked) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Вы не можете отправить сообщение заблокированному пользователю'),
backgroundColor: Colors.red,
),
);
}
return;
}
}
if (textEditingController.text.isNotEmpty || pickedFiles.isNotEmpty) {
chatRoomCubit.sendMessage(
data: textEditingController.text.isEmpty ? null : textEditingController.text.trim(),
files: pickedFiles.isEmpty ? null : pickedFiles,
userSlug: state.chatRoom.users.receiver.slug,
);
setState(() {
textEditingController.clear();
pickedFiles.clear();
});
}
},
child: Ink(
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
height: 40,
width: 40,
child: const Icon(Icons.arrow_upward_rounded, size: 24, color: Colors.white),
),
),
const SizedBox(width: 8),
],
),
const SizedBox(width: 8),
InkWell(
customBorder: const CircleBorder(),
onTap: () {
if (textEditingController.text.isNotEmpty) {
chatRoomCubit.sendMessage(chatId: widget.chatId, data: textEditingController.text.trim());
setState(() {
textEditingController.clear();
});
}
},
child: Ink(
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
height: 40,
width: 40,
child: const Icon(Icons.arrow_upward_rounded, size: 24, color: Colors.white),
),
),
const SizedBox(width: 8),
],
),
),
@@ -435,25 +498,152 @@ class _ChatScreenState extends State<ChatScreen> {
),
if (showAboutOrderButton)
Positioned(
top: 8,
right: 15,
left: 16,
child: AboutOrderButton(
onTap: () async {
GoRouter.of(context).push(InfoByOrderScreen.path, extra: {'planId': state.chatRoom.chosenPlan?.id});
},
)),
top: 8,
right: 15,
left: 16,
child: AboutOrderButton(
onTap: () async {
GoRouter.of(context).push(InfoByOrderScreen.path, extra: {'planId': state.chatRoom.chosenPlan?.id});
},
),
),
],
),
),
);
}
_messageDate(DateTime date) {
Widget _buildFilePreview(File file) {
final fileName = file.path.split('/').last;
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
height: 104,
width: 104,
color: const Color(0xFFE0E0E0), // Светло-серый фон
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
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)),
),
],
),
),
);
}
bool _isImageFile(String path) {
final lower = path.toLowerCase();
return lower.endsWith('.png') ||
lower.endsWith('.jpg') ||
lower.endsWith('.jpeg') ||
lower.endsWith('.gif') ||
lower.endsWith('.bmp') ||
lower.endsWith('.webp');
}
int _imageIndexAt(List<File> allFiles, int targetIndex) {
int count = 0;
for (int i = 0; i < allFiles.length; i++) {
if (_isImageFile(allFiles[i].path)) {
if (i == targetIndex) return count;
count++;
}
}
return 0;
}
void showImagePicker(String slug) {
showCupertinoModalPopup<void>(
context: context,
builder: (context) => CupertinoActionSheet(
actions: [
CupertinoActionSheetAction(
onPressed: () async {
Navigator.of(context).pop();
final picker = ImagePicker();
XFile? image = await picker.pickImage(source: ImageSource.camera);
setState(() {
if (image != null) {
pickedFiles.add(File(image.path));
}
});
// if (image != null) {
// await chatRoomCubit.sendMessage(files: [File(image.path)], userSlug: slug);
// }
},
child: const Row(children: [Icon(Icons.photo_camera), SizedBox(width: 8), Text('Фото')]),
),
CupertinoActionSheetAction(
onPressed: () async {
Navigator.of(context).pop();
final picker = ImagePicker();
List<XFile> images = await picker.pickMultiImage();
if (images != []) {
setState(() {
pickedFiles.addAll(images.map((image) => File(image.path)).toList());
});
// List<File> files = images.map((image) => File(image.path)).toList();
// await chatRoomCubit.sendMessage(files: files, userSlug: slug);
}
},
child: const Row(children: [Icon(Icons.photo_library_rounded), SizedBox(width: 8), Text('Галерея')]),
),
CupertinoActionSheetAction(
onPressed: () async {
Navigator.of(context).pop();
FilePickerResult? result = await FilePicker.platform.pickFiles(
allowMultiple: true,
type: FileType.custom,
allowedExtensions: [
'pdf',
'doc',
'docx',
'json',
'ppt',
'pptx',
'rtf',
'txt',
'xls',
'xlsx',
'csv',
'jpeg',
'jpg',
'png',
'gif',
'svg',
'xml',
'webp',
],
);
if (result != null) {
// List<File> files = result.paths.map((path) => File(path!)).toList();
setState(() {
pickedFiles.addAll(result.paths.map((path) => File(path!)).toList());
});
// await chatRoomCubit.sendMessage(files: files, userSlug: slug);
}
},
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('Отмена')),
),
);
}
Center _messageDate(DateTime date) {
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

@@ -67,18 +67,23 @@ class _ChatsScreenState extends State<ChatsScreen> {
),
];
},
body: switch (state.state) {
BaseState.error => DataErrorWidget(onReload: () => context.read<ChatsCubit>().getChatRooms()),
BaseState.success => _buildChatList(context, state),
_ => const Loader(),
},
body: RefreshIndicator(
child: switch (state.state) {
BaseState.error => DataErrorWidget(onReload: () => context.read<ChatsCubit>().getChatRooms()),
BaseState.success => _buildChatList(context, state),
_ => const Loader(),
},
onRefresh: () async {
await context.read<ChatsCubit>().getChatRooms();
},
),
),
);
},
);
}
_buildChatList(BuildContext context, ChatsState state) {
NotificationListener<ScrollNotification> _buildChatList(BuildContext context, ChatsState state) {
return NotificationListener<ScrollNotification>(
onNotification: (notification) {
loadNewContent(notification, context.read<ChatsCubit>().loadMoreChatRooms);

View File

@@ -0,0 +1,343 @@
import 'dart:io';
import 'dart:math';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/chat/repositories/chat_repository.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart';
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_state.dart';
class EmptyChatScreen extends StatefulWidget {
final Author author;
const EmptyChatScreen({
super.key,
required this.author,
});
@override
State<EmptyChatScreen> createState() => _EmptyChatScreenState();
}
class _EmptyChatScreenState extends State<EmptyChatScreen> {
final TextEditingController textEditingController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: true,
appBar: AppBar(
titleSpacing: 0,
backgroundColor: appTheme.gray100,
centerTitle: false,
title: GestureDetector(
onTap: () {
GoRouter.of(context).pushNamed(
'profileScreen',
extra: Author(
authorSlug: widget.author.authorSlug,
authorName: widget.author.authorName,
authorProfileIcon: widget.author.authorProfileIcon,
authorIsMe: false,
),
);
},
child: Row(
children: [
CircleAvatar(
radius: 20,
foregroundImage: NetworkImage(widget.author.authorProfileIcon),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
),
const SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.author.authorName,
style: CustomTextStyles.titleSmallBold.copyWith(
color: PrimaryColors().black,
),
),
],
),
],
),
),
),
body: SafeArea(
child: Column(
children: [
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
const SizedBox(width: 8),
InkWell(
onTap: () => showImagePicker(widget.author.authorSlug),
customBorder: const CircleBorder(),
child: Ink(
height: 40,
width: 40,
child: Transform.rotate(
angle: pi / 4,
child: const Icon(
Icons.attach_file_rounded,
size: 30,
color: Color(0xFF858E99),
),
),
),
),
const SizedBox(width: 8),
Expanded(
child: SizedBox(
height: 40,
child: TextFormField(
expands: true,
textCapitalization: TextCapitalization.sentences,
controller: textEditingController,
maxLines: null,
minLines: null,
keyboardType: TextInputType.multiline,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1),
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
hintText: 'Сообщение',
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)),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
),
),
),
),
const SizedBox(width: 8),
InkWell(
customBorder: const CircleBorder(),
onTap: () async {
// Проверяем, заблокирован ли пользователь
final blockedUsersCubit = sl<BlockedUsersCubit>();
await blockedUsersCubit.getBlockedUsers();
final blockedState = blockedUsersCubit.state;
if (blockedState is BlockedUsersStateLoaded) {
final isBlocked = blockedState.profilesList.any((profile) => profile.slug == widget.author.authorSlug);
if (isBlocked) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Вы не можете отправить сообщение заблокированному пользователю'),
backgroundColor: Colors.red,
),
);
}
return;
}
}
if (textEditingController.text.isNotEmpty) {
await sl<ChatRepository>().sendMessage(
data: textEditingController.text.trim(),
slug: widget.author.authorSlug,
);
if (mounted) {
setState(() {
textEditingController.clear();
});
}
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
if (context.mounted) {
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
'chatScreen',
extra: {
'uuid': chatIdOrError,
},
);
}
}
},
child: Ink(
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
height: 40,
width: 40,
child: const Icon(Icons.arrow_upward_rounded, size: 24, color: Colors.white),
),
),
const SizedBox(width: 8),
],
),
),
],
),
),
);
}
void showImagePicker(String slug) {
showCupertinoModalPopup<void>(
context: context,
builder: (context) => CupertinoActionSheet(
actions: [
CupertinoActionSheetAction(
onPressed: () async {
Navigator.of(context).pop();
// Проверяем, заблокирован ли пользователь
final blockedUsersCubit = sl<BlockedUsersCubit>();
await blockedUsersCubit.getBlockedUsers();
final blockedState = blockedUsersCubit.state;
if (blockedState is BlockedUsersStateLoaded) {
final isBlocked = blockedState.profilesList.any((profile) => profile.slug == widget.author.authorSlug);
if (isBlocked) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Вы не можете отправить сообщение заблокированному пользователю'),
backgroundColor: Colors.red,
),
);
}
return;
}
}
final picker = ImagePicker();
List<XFile>? images = await picker.pickMultiImage();
if (images.isNotEmpty) {
List<File> files = images.map((image) => File(image.path)).toList();
await sl<ChatRepository>().sendMessage(
files: files,
slug: widget.author.authorSlug,
);
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
if (mounted) {
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
'chatScreen',
extra: {
'uuid': chatIdOrError,
},
);
}
}
},
child: const Row(
children: [
Icon(Icons.photo_library_rounded),
SizedBox(width: 8),
Text('Галерея'),
],
),
),
CupertinoActionSheetAction(
onPressed: () async {
Navigator.of(context).pop();
final picker = ImagePicker();
List<XFile>? images = await picker.pickMultiImage();
if (images.isNotEmpty) {
List<File> files = images.map((image) => File(image.path)).toList();
await sl<ChatRepository>().sendMessage(
files: files,
slug: widget.author.authorSlug,
);
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
if (mounted) {
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
'chatScreen',
extra: {
'uuid': chatIdOrError,
},
);
}
}
},
child: const Row(
children: [
Icon(Icons.photo_camera),
SizedBox(width: 8),
Text('Фото'),
],
),
),
CupertinoActionSheetAction(
onPressed: () async {
Navigator.of(context).pop();
FilePickerResult? result = await FilePicker.platform.pickFiles(
allowMultiple: true,
type: FileType.custom,
allowedExtensions: [
'pdf',
'doc',
'docx',
'json',
'ppt',
'pptx',
'rtf',
'txt',
'xls',
'xlsx',
'csv',
'jpeg',
'jpg',
'png',
'gif',
'svg',
'xml',
'webp',
],
);
if (result != null) {
List<File> files = result.paths.map((path) => File(path!)).toList();
await sl<ChatRepository>().sendMessage(
files: files,
slug: widget.author.authorSlug,
);
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
if (mounted) {
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
'chatScreen',
extra: {
'uuid': chatIdOrError,
},
);
}
}
},
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('Отмена'),
),
),
);
}
}

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,11 +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/const.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 {
@@ -16,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;
@@ -23,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);
@@ -37,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'),
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(
@@ -60,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: [
@@ -81,46 +109,31 @@ class ChatBubbleWidget extends StatelessWidget {
contentPadding: const EdgeInsets.all(4),
horizontalTitleGap: 8,
onTap: () async {
if (!await launchUrl(Uri.parse('$baseUrl/storage/${mediaData[index]}'))) {
if (!await launchUrl(Uri.parse('${mediaData[index]}'))) {
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) => '$baseUrl/storage/$e').toList(),
index: index,
),
PhotoViewScreen(imageUrls: mediaData, index: index, isLocal: mediaData[index] is File ? true : false),
);
},
child: Hero(
tag: '$baseUrl/storage/${mediaData[index]}',
tag: '${mediaData[index]}',
transitionOnUserGestures: true,
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: CachedNetworkImage(
imageUrl: '$baseUrl/storage/${mediaData[index]}',
),
child: mediaData[index] is File ? Image.file(mediaData[index]) : CachedNetworkImage(imageUrl: '${mediaData[index]}'),
),
),
);
@@ -132,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,13 +154,8 @@ class ChatBubbleWidget extends StatelessWidget {
Padding(
padding: const EdgeInsets.only(left: 4, bottom: 4),
child: Text(
sendTime.format('hh:mm'),
style: const TextStyle(
color: Color(0xFF7B8794),
height: 1,
fontSize: 10,
fontWeight: FontWeight.w500,
),
sendTime.format('HH:mm', 'ru'),
style: const TextStyle(color: Color(0xFF7B8794), height: 1, fontSize: 10, fontWeight: FontWeight.w500),
),
),
],

View File

@@ -1,4 +1,3 @@
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/features/chat/models/chat_room_pagination_model.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
@@ -21,15 +20,14 @@ class ChatEntryWidget extends StatefulWidget {
class _ChatEntryWidgetState extends State<ChatEntryWidget> {
@override
Widget build(BuildContext context) {
return InkWell(
borderRadius: BorderRadius.circular(16),
return GestureDetector(
onTap: () => GoRouter.of(context).pushNamed(
'chatScreen',
extra: {
'chatId': widget.chatRoom.id.toString(),
'uuid': widget.chatRoom.id.toString(),
},
),
child: Ink(
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
@@ -42,7 +40,7 @@ class _ChatEntryWidgetState extends State<ChatEntryWidget> {
CircleAvatar(
radius: 24,
foregroundImage: NetworkImage(
baseUrl + widget.chatRoom.users.toChatUser.profilePhotoUrl,
widget.chatRoom.users.toChatUser.profilePhotoUrl,
),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
backgroundColor: Colors.white,
@@ -62,7 +60,13 @@ class _ChatEntryWidgetState extends State<ChatEntryWidget> {
SizedBox(
width: MediaQuery.sizeOf(context).width * 0.5,
child: Text(
widget.chatRoom.lastMessage!.data.toString().trim(),
widget.chatRoom.lastMessage!.mediaData != null
? widget.chatRoom.lastMessage!.isCurrentUserAuthor == true
? 'Вы: 📎 Вложение'
: '📎 Вложение'
: widget.chatRoom.lastMessage!.isCurrentUserAuthor == true
? 'Вы: ${widget.chatRoom.lastMessage!.data.toString().trim()}'
: widget.chatRoom.lastMessage!.data.toString().trim(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: CustomTextStyles.bodyMediumBluegray800,
@@ -78,7 +82,7 @@ class _ChatEntryWidgetState extends State<ChatEntryWidget> {
Padding(
padding: const EdgeInsets.only(left: 8),
child: Text(
widget.chatRoom.lastMessage!.updatedAt.format('dd MMM', 'ru'),
calculateDifference(widget.chatRoom.lastMessage!.updatedAt, 'dd MMM'),
style: theme.textTheme.bodyMedium,
),
),

View File

@@ -78,7 +78,7 @@ class ChatSearchBarWidget extends StatelessWidget {
return searchViewWidget(context);
}
searchViewWidget(BuildContext context) => SizedBox(
SizedBox searchViewWidget(BuildContext context) => SizedBox(
width: width ?? MediaQuery.of(context).size.width,
child: TextFormField(
textCapitalization: TextCapitalization.sentences,

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

@@ -1,6 +1,8 @@
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';
@@ -27,7 +29,7 @@ class ChatRepository {
}
Future<void> sendMessage({
required String chatId,
required String slug,
String? data,
List<File>? files,
}) async {
@@ -47,19 +49,22 @@ class ChatRepository {
}
await chatService.sendMessage(
id: chatId,
slug: slug,
data: data,
mediaData: multipartFiles.isEmpty ? null : multipartFiles,
socketId: (await SocketIdStorage.getSocketIdFromStorage())!,
);
} catch (e) {
} catch (e, st) {
log(e.toString(), stackTrace: st);
rethrow;
}
}
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;
}
}
@@ -72,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;
}
@@ -88,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;
}
@@ -154,4 +159,17 @@ class ChatRepository {
rethrow;
}
}
Future<String> getChatByUserSlug(String slug) async {
try {
final response = await chatService.getChatByUserSlug(slug: slug);
if (response.response.statusCode == 404) {
return '404';
} else {
return response.data['id'];
}
} catch (e) {
return 'error';
}
}
}

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/chat/{id}/message/send')
@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,
});
@@ -80,4 +81,9 @@ abstract class ChatService {
@Path() required int id,
@Body() required PlanComplainModel body,
});
@GET('/api/chat/get_by_user/{slug}')
Future<HttpResponse> getChatByUserSlug({
@Path() required String slug,
});
}

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

@@ -72,6 +72,12 @@ class ComplainingCubit extends Cubit<ComplainingState> {
emit(state.copyWith(wasSentSuccessfully: true));
return;
}
case ComplainedContent.comment:
if (id != null) {
await repository.sendCommentComplain(id: id, category: category.getValue(), body: state.complainInfo);
emit(state.copyWith(wasSentSuccessfully: true));
return;
}
}
}
} catch (e, st) {

View File

@@ -64,4 +64,16 @@ class ComplainingRepository {
rethrow;
}
}
Future<void> sendCommentComplain({
required int id,
required String category,
required ComplainModel body,
}) async {
try {
await service.sendCommentComplain(id: id, category: category, body: body);
} catch (e) {
rethrow;
}
}
}

View File

@@ -42,5 +42,12 @@ abstract class ComplainingService {
@Path() required String category,
@Body() required ComplainModel body,
});
@POST('/api/comments/{id}/complain/{category}')
Future<void> sendCommentComplain({
@Path() required int id,
@Path() required String category,
@Body() required ComplainModel body,
});
}

View File

@@ -37,7 +37,7 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
}
}
getMyRequestStatus(String slug) async {
Future<void> getMyRequestStatus(String slug) async {
_update = true;
final myRequestStatus = await consultantRepository.getMyRequestStatus(slug);
@@ -81,11 +81,11 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
);
}
disableLoading() {
void disableLoading() {
emit(state.copyWith(loading: false));
}
updateConsultantProfileModel(ConsultantProfileModel consultant, [bool acceptOffer = false]) {
void updateConsultantProfileModel(ConsultantProfileModel consultant, [bool acceptOffer = false]) {
emit(
state.copyWith(
consultant: consultant,
@@ -95,7 +95,7 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
);
}
registerConsultant() async {
Future<void> registerConsultant() async {
if (state.consultant.photos != null) {
updateConsultantProfileModel(
state.consultant.copyWith(

View File

@@ -76,7 +76,7 @@ class _AdditionalSettingsScreenState extends State<AdditionalSettingsScreen> {
);
}
_tile({
ListTile _tile({
required String title,
required Function() onTap,
required bool value,
@@ -102,7 +102,7 @@ class _AdditionalSettingsScreenState extends State<AdditionalSettingsScreen> {
);
}
_divider(BuildContext context) {
Center _divider(BuildContext context) {
return Center(
child: Container(
height: 0.4,

View File

@@ -35,7 +35,7 @@ class _BecomeConsultantScreenState extends State<BecomeConsultantScreen> {
);
}
_startRegistrationButton() {
InkWell _startRegistrationButton() {
return InkWell(
onTap: () => GoRouter.of(context).pushReplacementNamed('stepperBecomeConsultantScreen'),
borderRadius: BorderRadius.circular(20),

View File

@@ -82,7 +82,7 @@ class _CertificatesAndDiplomasScreenState extends State<CertificatesAndDiplomasS
);
}
_divider(BuildContext context) {
Container _divider(BuildContext context) {
return Container(
height: 0.4,
width: MediaQuery.of(context).size.width - 32,
@@ -93,7 +93,7 @@ class _CertificatesAndDiplomasScreenState extends State<CertificatesAndDiplomasS
);
}
_diplomaInfo({
Column _diplomaInfo({
required DiplomaEntity diploma,
}) {
return Column(
@@ -159,7 +159,7 @@ class _CertificatesAndDiplomasScreenState extends State<CertificatesAndDiplomasS
);
}
onTapCalendar(BuildContext context, DiplomaEntity diploma) {
Null Function() onTapCalendar(BuildContext context, DiplomaEntity diploma) {
return () {
_showDialog(
CupertinoDatePicker(
@@ -198,7 +198,7 @@ class _CertificatesAndDiplomasScreenState extends State<CertificatesAndDiplomasS
);
}
checkLast() {
void checkLast() {
if (diplomasList.last.textEditingController.text.isNotEmpty || diplomasList.last.date != null) {
setState(() {
diplomasList.add(
@@ -219,7 +219,7 @@ class _CertificatesAndDiplomasScreenState extends State<CertificatesAndDiplomasS
}
}
enableDeleteButton(DiplomaEntity diploma) {
void enableDeleteButton(DiplomaEntity diploma) {
if (diploma.textEditingController.text.isNotEmpty || diploma.date != null) {
setState(() {
diplomasList[diplomasList.indexOf(diploma)].enableDeleteButton = true;
@@ -228,7 +228,7 @@ class _CertificatesAndDiplomasScreenState extends State<CertificatesAndDiplomasS
_saveState();
}
_saveState() {
void _saveState() {
context.read<RegisterConsultantCubit>().updateConsultantProfileModel(
context.read<RegisterConsultantCubit>().state.consultant.copyWith(
certificates: diplomasList.map((e) => CertificatesModel(title: e.textEditingController.text, date: e.date.toString())).toList().sublist(

View File

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

View File

@@ -148,7 +148,7 @@ class _PhotoOrScansScreenState extends State<PhotoOrScansScreen> {
);
}
_saveState() {
void _saveState() {
context.read<RegisterConsultantCubit>().updateConsultantProfileModel(
context.read<RegisterConsultantCubit>().state.consultant.copyWith(
photos: imagesPaths,

View File

@@ -104,7 +104,7 @@ class _RegistrationAsScreenState extends State<RegistrationAsScreen> {
);
}
_divider(BuildContext context) {
Container _divider(BuildContext context) {
return Container(
height: 0.4,
width: MediaQuery.of(context).size.width - 32,

View File

@@ -158,7 +158,7 @@ class _SocialNetworksScreenState extends State<SocialNetworksScreen> {
);
}
setLinksState({
void setLinksState({
String? tgLink,
String? vkLink,
String? personalSiteLink,

View File

@@ -6,6 +6,7 @@ import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_cubit.dart';
import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_state.dart';
import 'package:eatvio_app/features/consultations/presentation/widgets/consultation_card_widget.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultations_archive_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
@@ -67,7 +68,7 @@ class ConsultationsContent extends StatelessWidget {
InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () {
GoRouter.of(context).pushNamed('consultantFinanceScreen');
GoRouter.of(context).pushNamed(ConsultationsArchiveScreen.name);
},
child: Ink(
width: MediaQuery.of(context).size.width - 32,

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,6 +149,9 @@ 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 &&
(day.$2.state != DiaryStateEnum.init && day.$2.state != DiaryStateEnum.started && day.$2.state != DiaryStateEnum.lastDayStarted);
return Padding(
padding: const EdgeInsets.only(right: 12),
child: Column(
@@ -192,82 +159,75 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
children: [
InkWell(
onTap: () {
if (!hasActivity) {
showErrorDialog(context,
title: s.you_can_not_open_this_day, description: '${widget.consultationsModel.userName} ${s.has_not_filled_it_yet}');
if (!hasActivity || !canConsultantOpenThisDay) {
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),
],
),
);
@@ -278,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(
@@ -291,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),
),
),
),
@@ -332,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)),
),
),
],
@@ -352,28 +290,20 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
);
}
_legendTitle(String text, Color color) {
Row _legendTitle(String text, Color color) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
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

@@ -1,7 +1,9 @@
import 'package:eatvio_app/core/enums/filters_comparison_enum.dart';
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
import 'package:eatvio_app/features/consultations/services/consultation_service.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/models/consultant_finance_model.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/models/statistic_model.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/models/user_purchases_model.dart';
class ConsultationsRepository {
final ConsultationService consultationService;
@@ -42,4 +44,20 @@ class ConsultationsRepository {
rethrow;
}
}
Future<List<ConsultantFinanceModel>> getConsultantFinances(String? date, FiltersComparisonEnum? dateComparison) async {
try {
return await consultationService.getConsultantFinances(date, dateComparison);
} catch (e) {
rethrow;
}
}
Future<List<UserPurchasesModel>> getUserPurchases(String? date, FiltersComparisonEnum? dateComparison) async {
try {
return await consultationService.getUserPurchases(date, dateComparison);
} catch (e) {
rethrow;
}
}
}

View File

@@ -1,7 +1,9 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/enums/filters_comparison_enum.dart';
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/models/consultant_finance_model.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/models/statistic_model.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/models/user_purchases_model.dart';
class ConsultationService {
final Dio dio;
@@ -48,6 +50,7 @@ class ConsultationService {
else if (state != null && stateComparison != null)
'filters[last_state][${stateComparison.value}]': state,
if (dateComparison != null && date != null) 'filters[start_date][${dateComparison.value}]': date,
'sort[]': 'created_at:desc',
},
);
return ConsultationPaginationModel.fromJson(response.data);
@@ -66,4 +69,44 @@ class ConsultationService {
rethrow;
}
}
Future<List<ConsultantFinanceModel>> getConsultantFinances(
String? date,
FiltersComparisonEnum? dateComparison,
) async {
try {
final response = await dio.get(
'/api/finances/my_finances',
queryParameters: {
if (date != null && dateComparison != null) 'filters[start_date][${dateComparison.value}]': date,
'sort[]': 'start_date:desc',
},
);
final data = response.data as Map<String, dynamic>;
final consultations = data['data'] as List<dynamic>;
return consultations.map((consultation) => ConsultantFinanceModel.fromJson(consultation as Map<String, dynamic>)).toList();
} catch (e) {
rethrow;
}
}
Future<List<UserPurchasesModel>> getUserPurchases(
String? date,
FiltersComparisonEnum? dateComparison,
) async {
try {
final response = await dio.get(
'/api/finances/my_purchases',
queryParameters: {
if (date != null && dateComparison != null) 'filters[start_date][${dateComparison.value}]': date,
'sort[]': 'start_date:desc',
},
);
final data = response.data as Map<String, dynamic>;
final purchases = data['data'] as List<dynamic>;
return purchases.map((purchase) => UserPurchasesModel.fromJson(purchase as Map<String, dynamic>)).toList();
} catch (e) {
rethrow;
}
}
}

View File

@@ -194,6 +194,7 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
isAuthor: widget.diet.author.authorIsMe,
path: 'diet/${widget.diet.slug}',
complainedContentType: ComplainedContent.diet,
userSlug: widget.diet.author.authorSlug,
),
const SizedBox(width: 16),
],
@@ -347,7 +348,7 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
);
}
_dayOfPlan(Map<int, Map<int, DailySchedule>> day, int dayNumber, S s) {
Padding _dayOfPlan(Map<int, Map<int, DailySchedule>> day, int dayNumber, S s) {
choiceChipWidget(int index) {
return ChoiceChip(
selectedColor: appTheme.blueA400,
@@ -434,7 +435,7 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
);
}
_daily(DailySchedule? daily, S s) {
SingleChildRenderObjectWidget _daily(DailySchedule? daily, S s) {
if (daily == null) return const SizedBox.shrink();
return Padding(
padding: const EdgeInsets.only(bottom: 12),

View File

@@ -289,7 +289,7 @@ class _DayWidgetState extends State<DayWidget> {
);
}
_product(int index, String product, int quantity, String portion, int dayIndex, int mealIndex) {
Material _product(int index, String product, int quantity, String portion, int dayIndex, int mealIndex) {
return Material(
child: ListTile(
onTap: () {
@@ -313,7 +313,7 @@ class _DayWidgetState extends State<DayWidget> {
);
}
_modalBottomSheet({
Future _modalBottomSheet({
required CreateDietCubit createDietCubit,
required int dayIndex,
required int mealIndex,

View File

@@ -1,6 +1,8 @@
import 'dart:io';
import 'package:dotted_border/dotted_border.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/permission_handler/permission_handler.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_cubit.dart';
@@ -11,7 +13,9 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import 'package:permission_handler/permission_handler.dart';
class DietImagePickerWidget extends StatefulWidget {
const DietImagePickerWidget({super.key});
@@ -28,11 +32,18 @@ class _DietImagePickerWidgetState extends State<DietImagePickerWidget> {
actions: [
CupertinoActionSheetAction(
onPressed: () async {
final picker = ImagePicker();
final image = await picker.pickImage(source: ImageSource.camera);
cubit.setImage(image);
if (!context.mounted) return;
Navigator.of(context).pop();
PermissionHandler.checkPermission(
permission: Permission.camera,
title: s.allowAccessToCamera,
description: s.allowAccessToCameraDescription,
onGranted: () async {
final picker = ImagePicker();
final image = await picker.pickImage(source: ImageSource.camera);
cubit.setImage(image);
AppRouter.navigatorKey.currentContext?.pop();
},
context: context,
);
},
child: Row(
children: [
@@ -44,11 +55,18 @@ class _DietImagePickerWidgetState extends State<DietImagePickerWidget> {
),
CupertinoActionSheetAction(
onPressed: () async {
final picker = ImagePicker();
final image = await picker.pickImage(source: ImageSource.gallery);
cubit.setImage(image);
if (!context.mounted) return;
Navigator.of(context).pop();
PermissionHandler.checkPermission(
permission: Permission.photos,
title: s.allowAcessToGalery,
description: s.allowAcessToGaleryDescription,
onGranted: () async {
final picker = ImagePicker();
final image = await picker.pickImage(source: ImageSource.gallery);
cubit.setImage(image);
AppRouter.navigatorKey.currentContext?.pop();
},
context: context,
);
},
child: Row(
children: [

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