Compare commits

...

148 Commits

Author SHA1 Message Date
Username1548
33151b7dae changed lable of the app and hid user purchases and finaces 2025-08-06 16:55:42 +07: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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
197 changed files with 5698 additions and 4388 deletions

View File

@@ -20,6 +20,7 @@ jobs:
- name: Flutter build
run: |
echo "${API_APPSTORE_KEY}" | base64 --decode > ios/AuthKey.p8
flutter clean
flutter pub get
flutter pub upgrade
@@ -31,6 +32,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/"
@@ -119,7 +119,7 @@ jobs:
run: flutter test
- name: Build apk
run: flutter build apk --release
run: flutter build apk --release --dart-define=HIDE_PLANS=false
- name: Rename and move apk
run: |

8
.gitignore vendored
View File

@@ -11,4 +11,10 @@
/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

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

0
env/.env vendored Normal file
View File

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 = 27;
CURRENT_PROJECT_VERSION = 64;
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 = 27;
CURRENT_PROJECT_VERSION = 64;
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 = 27;
CURRENT_PROJECT_VERSION = 64;
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>27</string>
<string>64</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>
@@ -31,6 +31,8 @@
<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>
<key>NSLocationWhenInUseUsageDescription</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,16 @@ default_platform(:ios)
platform :ios do
desc "Push a new beta build to TestFlight"
lane :beta do
match(type: "appstore")
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")
build_app(
workspace: "Runner.xcworkspace",
scheme: "Runner",
export_method: "app-store",
codesigning_identity: "Apple Distribution",
build_args: "--dart-define=HIDE_PLANS=true"
)
app_store_connect_api_key(key_id: "325JS9JCZH", issuer_id: "dfa27d1d-2bb8-4d4d-847e-0b32f9b880b3", key_filepath: "./AuthKey.p8")
upload_to_testflight
end
end

View File

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

View File

@@ -1,5 +1,3 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/firebase_api.dart';
@@ -7,6 +5,8 @@ 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';
@@ -15,6 +15,7 @@ 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';
@@ -22,6 +23,7 @@ import 'package:eatvio_app/features/consultant/become_consultant/presentation/pa
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';
@@ -42,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';
@@ -67,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>();
@@ -78,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',
@@ -89,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(
@@ -117,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(
@@ -155,34 +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 {
try {
if (await TokenStorage.haveTokenInStorage()) {
final firebaseApi = sl<FirebaseApi>();
final fbToken = await firebaseApi.getToken();
final deviceInfo = DeviceInfoPlugin();
await sl<Dio>().post(
'/api/profile/notification/token/register',
data: {
'platform': Platform.isIOS ? 'ios': 'android',
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
'token': fbToken,
},
);
final sharedPreferences = await SharedPreferences.getInstance();
final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed') ?? false;
await sl<FirebaseApi>().registerPushToken();
if (welcomeScreenWasShowed == false) {
return null;
} else if (await TokenStorage.haveTokenInStorage()) {
return '/feed-screen';
} else {
return '/auth-screen';
}
} catch (e) {
rethrow;
}
final sharedPreferences = await SharedPreferences.getInstance();
final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed');
if (welcomeScreenWasShowed == true) {
return '/feed-screen';
} else {
return null;
}
},
),
@@ -191,14 +225,18 @@ 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';
}
},
@@ -235,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()),
@@ -244,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,
);
@@ -269,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(
@@ -278,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,204 @@
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart';
import 'package:eatvio_app/core/widgets/custom_divider.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:go_router/go_router.dart';
void showCommentsActionsBottomSheet(
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)
_ActionWidget(
text: s.edit,
iconPath: 'assets/icons/edit_paint.svg',
needToPop: false,
onTap: () => setState(() {
isUserEditingComment = true;
}),
)
else if (widget.reply != null)
_ActionWidget(
text: s.reply,
iconPath: 'assets/icons/reply.svg',
onTap: widget.reply!,
),
if (widget.reply != null) const CustomDivider(),
_ActionWidget(
text: s.copy,
iconPath: 'assets/icons/copy2.svg',
onTap: widget.copy,
),
if (widget.isMyComment) ...[
if (widget.reply != null) ...[
const CustomDivider(),
_ActionWidget(
text: s.reply,
iconPath: 'assets/icons/reply.svg',
onTap: widget.reply!,
),
],
const CustomDivider(),
_ActionWidget(
text: s.deleteComment,
iconPath: 'assets/icons/red_trash.svg',
onTap: widget.delete,
),
] else ...[
const CustomDivider(),
_ActionWidget(
text: s.complain,
iconPath: 'assets/icons/complain.svg',
onTap: widget.complain,
),
]
],
),
);
}
}
class _ActionWidget extends StatelessWidget {
const _ActionWidget({required this.text, required this.iconPath, required this.onTap, this.needToPop = true});
final String text;
final String iconPath;
final void Function() onTap;
final bool needToPop;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
if (needToPop) Navigator.pop(context);
onTap();
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Row(
children: [
Expanded(
child: Text(
text,
style: TextStyles.textLMedium.copyWith(color: appTheme.black),
),
),
SvgPicture.asset(iconPath)
],
),
),
);
}
}

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

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'] ?? 'https://eatvio.ru';
static String get devBaseUrl => dotenv.env['DEV_BASE_URL'] ?? 'https://dev2.eatvio.com';
// Sentry Configuration
static String get sentryDsn => dotenv.env['SENTRY_DSN'] ?? 'http://ba7f2653067776f3034e6e89850e85a1@sentry.md-desk.ru:9000/2';
// Firebase Configuration
static String get firebaseProjectId => dotenv.env['FIREBASE_PROJECT_ID'] ?? 'eatvio-3467b';
static String get firebaseDatabaseUrl => dotenv.env['FIREBASE_DATABASE_URL'] ?? 'https://eatvio-3467b-default-rtdb.europe-west1.firebasedatabase.app';
static String get firebaseStorageBucket => dotenv.env['FIREBASE_STORAGE_BUCKET'] ?? 'eatvio-3467b.firebasestorage.app';
static String get firebaseMessagingSenderId => dotenv.env['FIREBASE_MESSAGING_SENDER_ID'] ?? '613980912922';
// Firebase Android Configuration
static String get firebaseAndroidApiKey => dotenv.env['FIREBASE_ANDROID_API_KEY'] ?? 'AIzaSyBUe1UqAAMfgMcd4hWdRoLF1Os-yn-HE9k';
static String get firebaseAndroidAppId => dotenv.env['FIREBASE_ANDROID_APP_ID'] ?? '1:613980912922:android:4de95e5f2d50ba69ecc0a3';
// Firebase iOS Configuration
static String get firebaseIosApiKey => dotenv.env['FIREBASE_IOS_API_KEY'] ?? 'AIzaSyBnE8tQjL9_1HuXrTnqNy6qbXpoenO36yQ';
static String get firebaseIosAppId => dotenv.env['FIREBASE_IOS_APP_ID'] ?? '1:613980912922:ios:c557d140a1212e63ecc0a3';
static String get firebaseIosBundleId => dotenv.env['FIREBASE_IOS_BUNDLE_ID'] ?? 'com.eatvioapp.app';
// App Store URLs
static String get appStoreUrl => dotenv.env['APP_STORE_URL'] ?? 'https://apps.apple.com/app/idYOUR_APP_ID';
static String get rustoreUrl => dotenv.env['RUSTORE_URL'] ?? 'https://www.rustore.ru/catalog/app/com.eatvioapp.app';
// Telegram Support
static String get telegramSupportUrl => dotenv.env['TELEGRAM_SUPPORT_URL'] ?? 'https://t.me/eatvio';
static String get supportEmail => dotenv.env['SUPPORT_EMAIL'] ?? 'help@eatv.io';
// Test Credentials (for development only)
static String get testEmail => dotenv.env['TEST_EMAIL'] ?? 'reshetnick.mike+user1@gmail.com';
static String get testPassword => dotenv.env['TEST_PASSWORD'] ?? '1234567890';
// Email Validation API
static String get emailValidationApiUrl => dotenv.env['EMAIL_VALIDATION_API_URL'] ?? 'https://api.validate.email/validate';
// Connection Check URLs
static String get googleCheckUrl => dotenv.env['GOOGLE_CHECK_URL'] ?? 'https://google.com';
static String get yandexCheckUrl => dotenv.env['YANDEX_CHECK_URL'] ?? 'https://yandex.ru';
}

View File

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

View File

@@ -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,13 +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';
@@ -33,6 +39,9 @@ import 'package:eatvio_app/features/blog_post_create/services/blog_post_service.
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';
@@ -49,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';
@@ -121,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()));
@@ -158,6 +167,9 @@ Future<void> init() async {
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()));
@@ -186,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()));
@@ -216,6 +231,9 @@ Future<void> init() async {
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

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

View File

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

@@ -33,7 +33,7 @@ extension DateTimeExtension on DateTime {
}
}
String calculateDifference(DateTime dateTime) {
String calculateDifference(DateTime dateTime, [String? format]) {
final localDateTime = dateTime.isUtc ? dateTime.toLocal() : dateTime;
if (localDateTime.isToday()) {
@@ -41,6 +41,6 @@ String calculateDifference(DateTime dateTime) {
} else if (localDateTime.isYesterday()) {
return 'Вчера';
} else {
return localDateTime.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

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

@@ -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,13 +1,15 @@
import 'dart:async';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/firebase_api.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/image_constant.dart';
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_state.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';
@@ -52,21 +54,36 @@ 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>(
@@ -80,10 +97,10 @@ class _AuthScreenState extends State<AuthScreen> {
title: GestureDetector(
onTap: () {
tapsCount++;
if (tapsCount == 6) {
if (tapsCount == 7) {
tapsCount = 0;
GoRouter.of(context).pushNamed(DevToolsScreen.name);
}
GoRouter.of(context).pushNamed(DevToolsScreen.name);
},
child: SvgPicture.asset(
ImageConstant.imgLogoEatvio,
@@ -127,6 +144,20 @@ class _AuthScreenState extends State<AuthScreen> {
),
),
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,
),
),
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@@ -182,38 +213,137 @@ class _AuthScreenState extends State<AuthScreen> {
),
),
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,263 +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),
if (widget.blogPost.image.isNotEmpty)
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

@@ -80,7 +80,7 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
);
}
_showImagePicker() {
void _showImagePicker() {
showCupertinoModalPopup<void>(
context: context,
builder: (context) {
@@ -145,7 +145,7 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
);
}
_buildBlogPostCreate(BuildContext context) {
Scaffold _buildBlogPostCreate(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: true,
appBar: AppBar(),
@@ -253,7 +253,7 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
);
}
_buildAddPhoto() {
GestureDetector _buildAddPhoto() {
return GestureDetector(
onTap: () {
_showImagePicker();

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

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

@@ -7,11 +7,13 @@ import 'package:audioplayers/audioplayers.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';
@@ -45,14 +47,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 +89,19 @@ 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 +160,70 @@ 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 compressedFiles = files != null ? await ImageCompressUtility.compressFilesList(List<File>.from(files)) : null;
AudioPlayer().play((AssetSource('audio/send_message.wav')));
emit(
(state as ChatRoomStateLoaded).copyWith(
chatMessages: [
ChatMessageModel(
id: 1,
data: data ?? '',
mediaData: compressedFiles,
authorId: 1,
updatedAt: DateTime.now(),
isRead: true,
isCurrentUserAuthor: true,
roomId: '',
),
...(state as ChatRoomStateLoaded).chatMessages,
],
),
);
await chatRepository.sendMessage(
data: data,
files: compressedFiles,
slug: userSlug,
);
sl<ChatsCubit>().getChatRooms();
return true;
} catch (e, st) {
log(e.toString(), stackTrace: st);

View File

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

View File

@@ -1,6 +1,7 @@
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/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
@@ -8,6 +9,7 @@ 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';
@@ -21,16 +23,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();
@@ -44,6 +49,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();
@@ -58,14 +64,14 @@ class _ChatScreenState extends State<ChatScreen> {
void initState() {
super.initState();
scrollController.addListener(_onScroll);
chatRoomCubit.initChat(widget.chatId, sendInitialMessage: widget.sendInitialMessage ?? false);
chatRoomCubit.initChat(widget.uuid, sendInitialMessage: widget.sendInitialMessage ?? false);
chatRoomCubit.setTimer();
_listener = AppLifecycleListener(onHide: _onHide, onResume: _onResume);
}
_onScroll() {
void _onScroll() {
if (scrollController.offset >= scrollController.position.maxScrollExtent && !scrollController.position.outOfRange) {
chatRoomCubit.loadNextPage(widget.chatId);
chatRoomCubit.loadNextPage(widget.uuid);
}
}
@@ -87,7 +93,7 @@ class _ChatScreenState extends State<ChatScreen> {
if (state is ChatRoomStateLoaded) {
return _buildChat(context, state);
} else if (state is ChatRoomStateError) {
return Scaffold(appBar: AppBar(), body: DataErrorWidget(onReload: () => chatRoomCubit.initChat(widget.chatId)));
return Scaffold(appBar: AppBar(), body: DataErrorWidget(onReload: () => chatRoomCubit.initChat(widget.uuid)));
} else {
return const Scaffold(body: Loader());
}
@@ -96,7 +102,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 = [
@@ -333,100 +339,155 @@ 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: () {
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),
],
),
),
@@ -434,21 +495,173 @@ 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),

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,289 @@
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';
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 {
if (textEditingController.text.isNotEmpty) {
await sl<ChatRepository>().sendMessage(
data: textEditingController.text.trim(),
slug: widget.author.authorSlug,
);
setState(() {
textEditingController.clear();
});
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
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 picker = ImagePicker();
XFile? image = await picker.pickImage(source: ImageSource.camera);
if (image != null) {
await sl<ChatRepository>().sendMessage(
files: [File(image.path)],
slug: widget.author.authorSlug,
);
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
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);
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);
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,3 +1,5 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:dismissible_page/dismissible_page.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
@@ -59,8 +61,9 @@ class ChatBubbleWidget extends StatelessWidget {
),
),
child: Padding(
padding:
mediaData.isNotEmpty ? const EdgeInsets.all(8) : EdgeInsets.only(top: 8, bottom: 8, left: isCurrentUser ? 16 : 8, right: isCurrentUser ? 8 : 16),
padding: mediaData.isNotEmpty
? const EdgeInsets.all(8)
: EdgeInsets.only(top: 8, bottom: 8, left: isCurrentUser ? 16 : 8, right: isCurrentUser ? 8 : 16),
child: Column(
crossAxisAlignment: isCurrentUser ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [
@@ -86,17 +89,17 @@ class ChatBubbleWidget extends StatelessWidget {
},
leading: const Icon(
Icons.file_copy_outlined,
color: Colors.white,
color: Colors.black,
),
trailing: const Icon(
Icons.download_outlined,
color: Colors.white,
color: Colors.black,
),
title: Text(
mediaData[index].toString().replaceAll('chat_files/', ''),
maxLines: 2,
style: const TextStyle(
color: Colors.white,
color: Colors.black,
fontSize: 12,
overflow: TextOverflow.ellipsis,
),
@@ -107,8 +110,9 @@ class ChatBubbleWidget extends StatelessWidget {
onTap: () {
context.pushTransparentRoute(
PhotoViewScreen(
imageUrls: mediaData.map((e) => '$e').toList(),
imageUrls: mediaData,
index: index,
isLocal: mediaData[index] is File ? true : false,
),
);
},
@@ -117,9 +121,11 @@ class ChatBubbleWidget extends StatelessWidget {
transitionOnUserGestures: true,
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: CachedNetworkImage(
imageUrl: '${mediaData[index]}',
),
child: mediaData[index] is File
? Image.file(mediaData[index])
: CachedNetworkImage(
imageUrl: '${mediaData[index]}',
),
),
),
);

View File

@@ -20,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),
@@ -61,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,
@@ -77,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

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

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

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

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

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

@@ -32,7 +32,7 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
),
);
addStep() {
void addStep() {
emit(
state.copyWith.entity(
steps: [
@@ -43,13 +43,13 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
);
}
removeStep(StepEntity step) {
void removeStep(StepEntity step) {
List<StepEntity> steps = [...state.entity.steps];
steps.remove(step);
emit(state.copyWith.entity(steps: steps));
}
addIngredient(i.Ingredient entity) {
void addIngredient(i.Ingredient entity) {
emit(
state.copyWith.entity(
ingredients: [
@@ -60,7 +60,7 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
);
}
removeIngredient(int index) {
void removeIngredient(int index) {
List<i.Ingredient> ingredients = [...state.entity.ingredients];
ingredients.removeAt(index);
@@ -73,26 +73,26 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
emit(state.copyWith.entity(ingredients: ingredients));
}
replaceIngredientPosition(int oldIndex, int newIndex) {
void replaceIngredientPosition(int oldIndex, int newIndex) {
List<i.Ingredient> ingredients = [...state.entity.ingredients];
final item = ingredients.removeAt(oldIndex);
ingredients.insert(newIndex, item);
emit(state.copyWith.entity(ingredients: ingredients));
}
replaceTitle(String title) {
void replaceTitle(String title) {
emit(state.copyWith.entity(title: title == '' ? null : title));
}
replaceDescription(String description) {
void replaceDescription(String description) {
emit(state.copyWith.entity(description: description == '' ? null : description));
}
changeType(TypeOfRecipe typeOfRecipe) {
void changeType(TypeOfRecipe typeOfRecipe) {
emit(state.copyWith.entity(typeOfRecipe: typeOfRecipe));
}
addTag(String tag) {
void addTag(String tag) {
emit(
state.copyWith.entity(
tags: [
@@ -103,46 +103,46 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
);
}
removeTag(String tag) {
void removeTag(String tag) {
List<String> tags = [...state.entity.tags];
tags.remove(tag);
emit(state.copyWith.entity(tags: tags));
}
replacePortionsCount(int count) {
void replacePortionsCount(int count) {
emit(state.copyWith.entity(countOfPortions: count));
}
replaceEnegryInPortion(double energy) {
void replaceEnegryInPortion(double energy) {
emit(state.copyWith.entity(energyInOnePortion: energy));
}
setTotalCookingMinutes(int minutes) {
void setTotalCookingMinutes(int minutes) {
emit(state.copyWith.entity(totalCookMinutes: minutes));
}
setTotalCookingHours(int hours) {
void setTotalCookingHours(int hours) {
emit(state.copyWith.entity(totalCookHours: hours));
}
setActiveCookingMinutes(int minutes) {
void setActiveCookingMinutes(int minutes) {
emit(state.copyWith.entity(activeCookMinutes: minutes));
}
setActiveCookingHours(int hours) {
void setActiveCookingHours(int hours) {
emit(state.copyWith.entity(activeCookHours: hours));
}
replaceVideoUrl(String videoUrl) {
void replaceVideoUrl(String videoUrl) {
emit(state.copyWith.entity(videoUrl: videoUrl));
}
replaceImage(File? image) {
void replaceImage(File? image) {
emit(state.copyWith.entity(image: image));
}
replaceDescriptionInStep(String description, int index) {
void replaceDescriptionInStep(String description, int index) {
List<StepEntity> steps = [...state.entity.steps];
steps[index] = steps[index].copyWith(description: description);
@@ -150,7 +150,7 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
emit(state.copyWith.entity(steps: steps));
}
replaceImageInStep(File? image, int index) {
void replaceImageInStep(File? image, int index) {
List<StepEntity> steps = [...state.entity.steps];
steps[index] = steps[index].copyWith(image: image);
@@ -182,7 +182,7 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
}
}
disableLoading() {
void disableLoading() {
emit(state.copyWith(loading: false));
}

View File

@@ -69,7 +69,7 @@ class _ImageWidgetState extends State<ImageWidget> {
);
}
_showImagePicker() {
void _showImagePicker() {
showCupertinoModalPopup<void>(
context: context,
builder: (context) {

View File

@@ -112,7 +112,7 @@ class _IngredientsWidgetState extends State<IngredientsWidget> {
);
}
_modalBottomSheet({
Future _modalBottomSheet({
required CreateRecipeCubit createRecipeCubit,
int? portionIndex,
String? productNameControllerText,

View File

@@ -82,7 +82,7 @@ class _StepImageWidgetState extends State<StepImageWidget> {
);
}
_showImagePicker() {
void _showImagePicker() {
showCupertinoModalPopup<void>(
context: context,
builder: (context) {

View File

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

View File

@@ -1,6 +1,7 @@
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/firebase_api.dart';
import 'package:eatvio_app/core/locator_service.dart';
@@ -8,6 +9,7 @@ import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/features/dev_tools/test_cases.dart';
import 'package:eatvio_app/core/env_config.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -51,6 +53,29 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
GetPlansComments(),
SendPlansComment(),
GetRecipesComments(),
AuthServiceTest(),
DiaryServiceTest(),
BlockedUserServiceTest(),
PlanServiceTest(),
AboutRecipeServiceTest(),
BlogPostServiceTest(),
CalendarServiceTest(),
CardsServiceTest(),
ConsultantServiceTest(),
ConsultationServiceTest(),
CreateDietServiceTest(),
CreatePlanServiceTest(),
GlossaryServiceTest(),
MyDietsServiceTest(),
MyPlansServiceTest(),
MyRecipesServiceTest(),
NotificationsServiceTest(),
PlansServiceTest(),
ProfileServiceTest(),
RegistrationServiceTest(),
ResetPasswordServiceTest(),
SettingsServiceTest(),
WsServiceTest(),
];
final controller = TextEditingController();
late final List<TestState> testsStates;
@@ -73,14 +98,21 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Column(
children: [
Padding(padding: const EdgeInsets.all(8.0), child: Row(children: [Text('BaseUrl: $baseUrlForTest')])),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(children: [Text('BaseUrl: $baseUrlForTest')]),
),
Row(
children: [
Expanded(child: CustomTextFormField(controller: controller, hintText: 'type new base url')),
Expanded(
child: CustomTextFormField(controller: controller, hintText: 'type new base url'),
),
IconButton(
onPressed: () {
setState(() {
baseUrlForTest = controller.text;
baseUrl = baseUrlForTest.toLowerCase();
sl<Dio>().options.baseUrl = baseUrl;
controller.clear();
});
},
@@ -89,43 +121,50 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
],
),
const SizedBox(height: 10),
if (baseUrlForTest != baseUrl)
CustomOutlinedButton(
text: 'Set dev2 base url',
onPressed: () {
setState(() {
baseUrlForTest = baseUrl;
});
},
buttonStyle: const ButtonStyle(),
),
CustomOutlinedButton(
text: baseUrl == EnvConfig.devBaseUrl ? 'Set production base url' : 'Set dev2 base url',
onPressed: () {
setState(() {
if (baseUrl == EnvConfig.baseUrl) {
baseUrlForTest = EnvConfig.devBaseUrl;
} else {
baseUrlForTest = EnvConfig.baseUrl;
}
baseUrl = baseUrlForTest.toLowerCase();
sl<Dio>().options.baseUrl = baseUrl;
controller.clear();
});
},
buttonStyle: const ButtonStyle(),
),
SizedBox(height: baseUrlForTest != baseUrl ? 40 : 10),
ListView.builder(
shrinkWrap: true,
itemCount: testCases.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder:
(context, index) => Padding(
padding: const EdgeInsets.only(bottom: 10.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(child: Text('${index + 1}. ${testCases[index].name}')),
Row(
mainAxisSize: MainAxisSize.min,
children: [
CustomOutlinedButton(
text: 'run',
height: 20,
width: 60,
onPressed: () async {
if (testsStates[index] == TestState.loading) return;
if (baseUrl.toLowerCase() != EnvConfig.baseUrl.toLowerCase())
ListView.builder(
shrinkWrap: true,
itemCount: testCases.length,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => Padding(
padding: const EdgeInsets.only(bottom: 10.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(child: Text('${index + 1}. ${testCases[index].name}')),
Row(
mainAxisSize: MainAxisSize.min,
children: [
CustomOutlinedButton(
text: 'run',
height: 20,
width: 60,
onPressed: () async {
if (testsStates[index] == TestState.loading) return;
setState(() {
testsStates[index] = TestState.loading;
});
setState(() {
testsStates[index] = TestState.loading;
});
try {
final result = await testCases[index].run(baseUrl: baseUrlForTest);
if (result) {
if (result.success) {
setState(() {
testsStates[index] = TestState.success;
});
@@ -135,27 +174,55 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
testsStates[index] = TestState.fail;
});
showErrorToast('${testCases[index].name} failed');
// Show AlertDialog with error description
if (context.mounted) {
showDialog(
context: AppRouter.navigatorKey.currentContext!,
builder: (context) => AlertDialog(
title: const Text('Test Failed'),
content: SingleChildScrollView(child: Text(result.error ?? 'No error details.')),
actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('OK'))],
),
);
}
}
},
buttonStyle: const ButtonStyle(),
),
const SizedBox(width: 5),
switch (testsStates[index]) {
TestState.success => Container(width: 20, height: 20, color: Colors.green),
TestState.fail => Container(width: 20, height: 20, color: Colors.red),
TestState.notRun => Container(width: 20, height: 20, color: Colors.grey),
TestState.loading => const SizedBox(
width: 20,
height: 20,
child: SizedBox(height: 20, width: 20, child: Center(child: CircularProgressIndicator())),
),
} catch (e, stack) {
setState(() {
testsStates[index] = TestState.fail;
});
showErrorToast('${testCases[index].name} failed');
// Show AlertDialog with error description
if (context.mounted) {
showDialog(
context: AppRouter.navigatorKey.currentContext!,
builder: (context) => AlertDialog(
title: const Text('Test Failed'),
content: SingleChildScrollView(child: Text('$e\n$stack')),
actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('OK'))],
),
);
}
}
},
],
),
],
),
buttonStyle: const ButtonStyle(),
),
const SizedBox(width: 5),
switch (testsStates[index]) {
TestState.success => Container(width: 20, height: 20, color: Colors.green),
TestState.fail => Container(width: 20, height: 20, color: Colors.red),
TestState.notRun => Container(width: 20, height: 20, color: Colors.grey),
TestState.loading => const SizedBox(
width: 20,
height: 20,
child: SizedBox(height: 20, width: 20, child: Center(child: CircularProgressIndicator())),
),
},
],
),
],
),
),
),
),
const SizedBox(height: 10),
CustomOutlinedButton(
text: 'add token to server',

View File

@@ -4,19 +4,49 @@ import 'dart:developer';
import 'package:dio/dio.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_service.dart';
import 'package:eatvio_app/features/auth/models/credentials_model.dart';
import 'package:eatvio_app/features/auth/services/reset_password_service.dart';
import 'package:eatvio_app/features/blog_post_create/services/blog_post_service.dart';
import 'package:eatvio_app/features/calendar/services/calendar_service.dart';
import 'package:eatvio_app/features/chat/services/chat_service.dart';
import 'package:eatvio_app/features/consultant/become_consultant/services/consultant_service.dart';
import 'package:eatvio_app/features/consultations/services/consultation_service.dart';
import 'package:eatvio_app/features/create_diet/services/create_diet_service.dart';
import 'package:eatvio_app/features/create_plan/services/create_plan_service.dart';
import 'package:eatvio_app/features/create_recipe/models/recipe_model.dart';
import 'package:eatvio_app/features/create_recipe/models/step.dart';
import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart';
import 'package:eatvio_app/features/diets/services/diet_service.dart';
import 'package:eatvio_app/features/feed/services/feed_service.dart';
import 'package:eatvio_app/features/glossary/services/glossary_service.dart';
import 'package:eatvio_app/features/menu/services/blocked_users_service.dart';
import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart';
import 'package:eatvio_app/features/my_finance/services/cards_service.dart';
import 'package:eatvio_app/features/my_plans/services/my_plans_service.dart';
import 'package:eatvio_app/features/notifications/service/notifications_service.dart';
import 'package:eatvio_app/features/plans/services/plan_service.dart';
import 'package:eatvio_app/features/plans/services/plans_service.dart';
import 'package:eatvio_app/features/profile/services/profile_service.dart';
import 'package:eatvio_app/features/create_recipe/services/recipe_service.dart';
import 'package:eatvio_app/features/auth/services/auth_service.dart';
import 'package:eatvio_app/features/diary/services/diary_service.dart';
import 'package:eatvio_app/features/recipe/services/about_recipe_service.dart';
import 'package:eatvio_app/features/recipe/services/my_recipes_service.dart';
import 'package:eatvio_app/features/registration/services/registration_service.dart';
import 'package:eatvio_app/features/settings/services/settings_service.dart';
import 'package:eatvio_app/core/env_config.dart';
class TestResult {
final bool success;
final String? error;
TestResult(this.success, [this.error]);
}
sealed class TestCases {
String get name => throw UnimplementedError();
Future<bool> run({required String baseUrl}) async => throw UnimplementedError();
Future<TestResult> run({required String baseUrl}) async => throw UnimplementedError();
}
class CreateRecipeTask implements TestCases {
@@ -24,7 +54,7 @@ class CreateRecipeTask implements TestCases {
String get name => 'api/recipes/create';
@override
Future<bool> run({required String baseUrl}) async {
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final recipeRepository = RecipeRepository(recipeService: CreateRecipeService(dio, baseUrl: baseUrl));
@@ -41,10 +71,16 @@ class CreateRecipeTask implements TestCases {
steps: [Step(description: 'desc')],
),
);
} catch (e) {
return false;
} on DioException catch (e) {
final res = e.response;
if (res != null && (res.statusCode == null || res.statusCode! < 200 || res.statusCode! >= 300)) {
return TestResult(false, 'DioError: statusCode=${res.statusCode}\n${res.data}');
}
return TestResult(false, e.toString());
} catch (e, stack) {
return TestResult(false, '$e\n$stack');
}
return true;
return TestResult(true);
}
}
@@ -53,69 +89,54 @@ class Test850task implements TestCases {
String get name => 'task #850';
@override
Future<bool> run({required String baseUrl}) async {
Future<TestResult> run({required String baseUrl}) async {
try {
// get all of needed services
final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl);
final profileService = ProfileService(dio, baseUrl: baseUrl);
final dietsService = DietService(dio, baseUrl: baseUrl);
// get all of needed data
final dietsPaginationModel = await feedService.getDiets(page: 1);
final chosenDietsPaginationModel = await profileService.getChosenDiets();
final lastChosenDiet = chosenDietsPaginationModel.diets.firstOrNull;
final today = DateTime.now();
final yesterday = today.subtract(const Duration(days: 1));
final tomorrow = today.add(const Duration(days: 1));
// run test case
for (final diet in dietsPaginationModel.data) {
if (!diet.author.authorIsMe) {
if (lastChosenDiet?.state == 'started' || lastChosenDiet?.state == 'inited') {
try {
await dietsService.cancelDiet(id: lastChosenDiet?.dietId ?? 0);
} catch (e) {
} catch (e, stack) {
log('unexpected error7: $e');
return false;
return TestResult(false, '$e\n$stack');
}
await Future.delayed(const Duration(seconds: 3));
}
try {
await dietsService.startDiet(id: diet.id, body: {'date': yesterday.format('yyyy-MM-dd')});
return false;
return TestResult(false, 'Expected error but got success');
} catch (e) {
log('expect ot have error: $e');
}
// test start diet with correct date
try {
await dietsService.startDiet(id: diet.id, body: {'date': today.format('yyyy-MM-dd')});
} catch (e) {
} catch (e, stack) {
log('unexpected error2: $e');
return false;
return TestResult(false, '$e\n$stack');
}
// test cancelation of diet
try {
await dietsService.cancelDiet(id: diet.id);
} catch (e) {
} catch (e, stack) {
log('unexpected error3: $e');
return false;
return TestResult(false, '$e\n$stack');
}
await Future.delayed(const Duration(seconds: 3));
// test start diet with correct date
try {
await dietsService.startDiet(id: diet.id, body: {'date': tomorrow.format('yyyy-MM-dd')});
} catch (e) {
} catch (e, stack) {
log('unexpected error4: $e');
return false;
return TestResult(false, '$e\n$stack');
}
// test cancelation of diet
try {
await dietsService.cancelDiet(id: diet.id);
} catch (e) {
@@ -124,11 +145,10 @@ class Test850task implements TestCases {
break;
}
}
return true;
} catch (e) {
return TestResult(true);
} catch (e, stack) {
log('unexpected error6: $e');
return false;
return TestResult(false, '$e\n$stack');
}
}
}
@@ -139,16 +159,16 @@ class GetChatRoomPreviews implements TestCases {
String get name => '/api/chat/index';
@override
Future<bool> run({required String baseUrl}) async {
Future<TestResult> run({required String baseUrl}) async {
try {
// get all of needed services
// final dio = sl<Dio>();
// final chatService = ChatService(dio, baseUrl: baseUrl);
// await chatService.getChatRooms();
return true;
return TestResult(true);
} catch (e) {
return false;
return TestResult(false, e.toString());
}
}
}
@@ -158,16 +178,16 @@ class GetChatRoom implements TestCases {
String get name => '/api/chat/show/{id}';
@override
Future<bool> run({required String baseUrl}) async {
Future<TestResult> run({required String baseUrl}) async {
try {
// final dio = sl<Dio>();
// final chatService = ChatService(dio, baseUrl: baseUrl);
// final chatRoomsModels = await chatService.getChatRoomPreviews();
// await chatService.getChatRoom(id: chatRoomsModels.chatRooms.firstOrNull?.id ?? '');
return true;
return TestResult(true);
} catch (e) {
return false;
return TestResult(false, e.toString());
}
}
}
@@ -177,16 +197,16 @@ class SendMessage implements TestCases {
String get name => '/api/chat/{id}/message/send';
@override
Future<bool> run({required String baseUrl}) async {
Future<TestResult> run({required String baseUrl}) async {
try {
// final dio = sl<Dio>();
// final chatService = ChatService(dio, baseUrl: baseUrl);
// final chatRoomsModels = await chatService.getChatRoomPreviews();
// await chatService.sendMessage(id: chatRoomsModels.chatRooms.firstOrNull?.id ?? '', data: 'test message');
return true;
return TestResult(true);
} catch (e) {
return false;
return TestResult(false, e.toString());
}
}
}
@@ -196,16 +216,16 @@ class GetChatMessages implements TestCases {
String get name => '/api/chat/show/{id}/messages?page={page}';
@override
Future<bool> run({required String baseUrl}) async {
Future<TestResult> run({required String baseUrl}) async {
try {
// final dio = sl<Dio>();
// final chatService = ChatService(dio, baseUrl: baseUrl);
// final chatRoomsModels = await chatService.getChatRoomPreviews();
// await chatService.getChatMessages(id: chatRoomsModels.chatRooms.firstOrNull?.id ?? '', page: 1);
return true;
return TestResult(true);
} catch (e) {
return false;
return TestResult(false, e.toString());
}
}
}
@@ -215,7 +235,7 @@ class GetChatRoomIdByUserSlug implements TestCases {
String get name => '/api/chat/get_by_user/{user}';
@override
Future<bool> run({required String baseUrl}) async {
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final chatService = ChatService(dio, baseUrl: baseUrl);
@@ -230,9 +250,9 @@ class GetChatRoomIdByUserSlug implements TestCases {
break;
}
}
return true;
return TestResult(true);
} catch (e) {
return false;
return TestResult(false, e.toString());
}
}
}
@@ -243,14 +263,14 @@ class GetBlogPostList implements TestCases {
String get name => '/api/blogPosts/index';
@override
Future<bool> run({required String baseUrl}) async {
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl);
await feedService.getBlogPostList(page: 1);
return true;
return TestResult(true);
} catch (e) {
return false;
return TestResult(false, e.toString());
}
}
}
@@ -260,7 +280,7 @@ class GetRecipes implements TestCases {
String get name => '/api/recipes/index';
@override
Future<bool> run({required String baseUrl}) async {
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl);
@@ -269,9 +289,9 @@ class GetRecipes implements TestCases {
final tags = tagsModel.data;
final tagsIds = tags.map((e) => e.id).toList();
await feedService.getRecipes(page: 1, tags: tagsIds, query: null);
return true;
return TestResult(true);
} catch (e) {
return false;
return TestResult(false, e.toString());
}
}
}
@@ -281,14 +301,14 @@ class GetDiets implements TestCases {
String get name => '/api/diets/index';
@override
Future<bool> run({required String baseUrl}) async {
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl);
await feedService.getDiets(page: 1, query: null);
return true;
return TestResult(true);
} catch (e) {
return false;
return TestResult(false, e.toString());
}
}
}
@@ -298,14 +318,14 @@ class GetPlans implements TestCases {
String get name => '/api/plans/index';
@override
Future<bool> run({required String baseUrl}) async {
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl);
await feedService.getPlans(page: 1, query: null);
return true;
return TestResult(true);
} catch (e) {
return false;
return TestResult(false, e.toString());
}
}
}
@@ -315,16 +335,16 @@ class GetBlogPostComments implements TestCases {
String get name => '/api/blogPosts/{id}/comments/show';
@override
Future<bool> run({required String baseUrl}) async {
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl);
final blogPostsModel = await feedService.getBlogPostList(page: 1, query: null);
final blogPosts = blogPostsModel.feedBlogPostList;
await feedService.getBlogPostComments(id: blogPosts.first.id.toString());
return true;
return TestResult(true);
} catch (e) {
return false;
return TestResult(false, e.toString());
}
}
}
@@ -334,16 +354,16 @@ class SendBlogPostComment implements TestCases {
String get name => '/api/blogPosts/{id}/comment/create';
@override
Future<bool> run({required String baseUrl}) async {
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl);
final blogPostsModel = await feedService.getBlogPostList(page: 1, query: null);
final blogPosts = blogPostsModel.feedBlogPostList;
await feedService.sendBlogPostComment(id: blogPosts.first.id.toString(), body: 'auto test message');
return true;
return TestResult(true);
} catch (e) {
return false;
return TestResult(false, e.toString());
}
}
}
@@ -353,16 +373,16 @@ class GetDietsComments implements TestCases {
String get name => '/api/diets/{id}/comments/show';
@override
Future<bool> run({required String baseUrl}) async {
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl);
final dietsModel = await feedService.getDiets(page: 1, query: null);
final diets = dietsModel.data;
await feedService.getDietsComments(id: diets.first.id.toString());
return true;
return TestResult(true);
} catch (e) {
return false;
return TestResult(false, e.toString());
}
}
}
@@ -372,16 +392,16 @@ class SendDietsComment implements TestCases {
String get name => '/api/diets/{id}/comment/create';
@override
Future<bool> run({required String baseUrl}) async {
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl);
final dietsModel = await feedService.getDiets(page: 1, query: null);
final diets = dietsModel.data;
await feedService.sendDietsComment(id: diets.first.id.toString(), body: 'auto test message');
return true;
return TestResult(true);
} catch (e) {
return false;
return TestResult(false, e.toString());
}
}
}
@@ -391,16 +411,16 @@ class GetPlansComments implements TestCases {
String get name => '/api/plans/{slug}/comments/show';
@override
Future<bool> run({required String baseUrl}) async {
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl);
final plansModel = await feedService.getPlans(page: 1, query: null);
final plans = plansModel.data;
await feedService.getPlansComments(slug: plans.first.slug);
return true;
return TestResult(true);
} catch (e) {
return false;
return TestResult(false, e.toString());
}
}
}
@@ -410,16 +430,16 @@ class SendPlansComment implements TestCases {
String get name => '/api/plans/{slug}/comment/create';
@override
Future<bool> run({required String baseUrl}) async {
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl);
final plansModel = await feedService.getPlans(page: 1, query: null);
final plans = plansModel.data;
await feedService.sendPlansComment(slug: plans.first.slug, body: 'auto test message');
return true;
return TestResult(true);
} catch (e) {
return false;
return TestResult(false, e.toString());
}
}
}
@@ -429,7 +449,7 @@ class GetRecipesComments implements TestCases {
String get name => '/api/recipes/{id}/comments/show';
@override
Future<bool> run({required String baseUrl}) async {
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final feedService = FeedService(dio, baseUrl: baseUrl);
@@ -440,9 +460,428 @@ class GetRecipesComments implements TestCases {
final recipesModel = await feedService.getRecipes(page: 1, tags: tagsIds, query: null);
final recipes = recipesModel.recipesList;
await feedService.getRecipesComments(id: recipes.first.id.toString());
return true;
return TestResult(true);
} catch (e) {
return false;
return TestResult(false, e.toString());
}
}
}
// AuthService
class AuthServiceTest implements TestCases {
@override
String get name => 'AuthService: getTokenByCredentials';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final authService = AuthService(dio, baseUrl: baseUrl);
// Use dummy credentials, expecting failure but no crash
await authService.getTokenByCredentials(
credentials: CredentialsModel(email: EnvConfig.testEmail, password: EnvConfig.testPassword),
);
return TestResult(false, 'Should not succeed');
} catch (e) {
return TestResult(true, 'Exception expected for dummy credentials');
}
}
}
// DiaryService
class DiaryServiceTest implements TestCases {
@override
String get name => 'DiaryService: getDiaryIndexByDate';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final diaryService = DiaryService(dio, baseUrl: baseUrl);
await diaryService.getDiaryIndexByDate(date: '2023-01-01');
return TestResult(true);
} catch (e) {
return TestResult(false, e.toString());
}
}
}
// BlockedUserService
class BlockedUserServiceTest implements TestCases {
@override
String get name => 'BlockedUserService: getBlockedUsers';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final blockedUserService = BlockedUserService(dio, baseUrl: baseUrl);
await blockedUserService.getBlockedUsers();
return TestResult(true);
} catch (e) {
return TestResult(false, e.toString());
}
}
}
// PlanService
class PlanServiceTest implements TestCases {
@override
String get name => 'PlanService: getPlan';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final planService = PlanService(dio, baseUrl: baseUrl);
// Use a dummy slug, expecting failure but no crash
await planService.getPlan('dummy-slug');
return TestResult(false, 'Should not succeed');
} catch (e) {
return TestResult(true, 'Exception expected for dummy slug');
}
}
}
// AboutRecipeService
class AboutRecipeServiceTest implements TestCases {
@override
String get name => 'AboutRecipeService: getRecipe';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final aboutRecipeService = AboutRecipeService(dio, baseUrl: baseUrl);
await aboutRecipeService.getRecipe('dummy-id');
return TestResult(false, 'Expected failure but got success');
} catch (e) {
return TestResult(true, 'Exception expected for dummy id');
}
}
}
// BlogPostService
class BlogPostServiceTest implements TestCases {
@override
String get name => 'BlogPostService: createBlogPost';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final blogPostService = BlogPostService(dio, baseUrl: baseUrl);
// Use empty FormData, expecting failure but no crash
await blogPostService.createBlogPost(formData: FormData());
return TestResult(false, 'Expected failure but got success');
} catch (e) {
return TestResult(true, 'Exception expected for empty FormData');
}
}
}
// CalendarService
class CalendarServiceTest implements TestCases {
@override
String get name => 'CalendarService: getCalendar';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final calendarService = CalendarService(dio, baseUrl: baseUrl);
await calendarService.getCalendar();
return TestResult(true);
} catch (e) {
return TestResult(false, e.toString());
}
}
}
// CardsService
class CardsServiceTest implements TestCases {
@override
String get name => 'CardsService: getCards';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final cardsService = CardsService(dio, baseUrl: baseUrl);
await cardsService.getCards();
return TestResult(true);
} catch (e) {
return TestResult(false, e.toString());
}
}
}
// ConsultantService
class ConsultantServiceTest implements TestCases {
@override
String get name => 'ConsultantService: getMyRequestStatus';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final consultantService = ConsultantService(dio, baseUrl: baseUrl);
await consultantService.getMyRequestStatus(slug: 'dummy-slug');
return TestResult(false, 'Expected failure but got success');
} catch (e) {
return TestResult(true, 'Exception expected for dummy slug');
}
}
}
// ConsultationService
class ConsultationServiceTest implements TestCases {
@override
String get name => 'ConsultationService: getConsultations';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final consultationService = ConsultationService(dio);
await consultationService.getConsultations();
return TestResult(true);
} catch (e) {
return TestResult(false, e.toString());
}
}
}
// CreateDietService
class CreateDietServiceTest implements TestCases {
@override
String get name => 'CreateDietService: getDietPortionsNutrition';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final createDietService = CreateDietService(dio, baseUrl: baseUrl);
await createDietService.getDietPortionsNutrition(diet: 0);
return TestResult(false, 'Expected failure but got success');
} catch (e) {
return TestResult(true, 'Exception expected for diet: 0');
}
}
}
// CreatePlanService
class CreatePlanServiceTest implements TestCases {
@override
String get name => 'CreatePlanService: createPlan';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final createPlanService = CreatePlanService(dio, baseUrl: baseUrl);
await createPlanService.createPlan(formData: FormData());
return TestResult(false, 'Expected failure but got success');
} catch (e) {
return TestResult(true, 'Exception expected for empty FormData');
}
}
}
// GlossaryService
class GlossaryServiceTest implements TestCases {
@override
String get name => 'GlossaryService: getGlossaryByQuery';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final glossaryService = GlossaryService(dio, baseUrl: baseUrl);
await glossaryService.getGlossaryByQuery(query: 'test');
return TestResult(true);
} catch (e) {
return TestResult(false, e.toString());
}
}
}
// MyDietsService
class MyDietsServiceTest implements TestCases {
@override
String get name => 'MyDietsService: getDiets';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final myDietsService = MyDietsService(dio, baseUrl: baseUrl);
await myDietsService.getDiets(page: 1);
return TestResult(true);
} catch (e) {
return TestResult(false, e.toString());
}
}
}
// MyPlansService
class MyPlansServiceTest implements TestCases {
@override
String get name => 'MyPlansService: getPlans';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final myPlansService = MyPlansService(dio, baseUrl: baseUrl);
await myPlansService.getPlans(page: 1);
return TestResult(true);
} catch (e) {
return TestResult(false, e.toString());
}
}
}
// MyRecipesService
class MyRecipesServiceTest implements TestCases {
@override
String get name => 'MyRecipesService: getMyRecipes';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final myRecipesService = MyRecipesService(dio, baseUrl: baseUrl);
await myRecipesService.getMyRecipes();
return TestResult(true);
} catch (e) {
return TestResult(false, e.toString());
}
}
}
// NotificationsService
class NotificationsServiceTest implements TestCases {
@override
String get name => 'NotificationsService: getNotifications';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final notificationsService = NotificationsService(dio);
await notificationsService.getNotifications(page: 1);
return TestResult(true);
} catch (e) {
return TestResult(false, e.toString());
}
}
}
// PlansService
class PlansServiceTest implements TestCases {
@override
String get name => 'PlansService: getPlansBySlug';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final plansService = PlansService(dio, baseUrl: baseUrl);
await plansService.getPlansBySlug(author: 'dummy-author');
return TestResult(false, 'Expected failure but got success');
} catch (e) {
return TestResult(true, 'Exception expected for dummy author');
}
}
}
// ProfileService
class ProfileServiceTest implements TestCases {
@override
String get name => 'ProfileService: getMyProfile';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final profileService = ProfileService(dio, baseUrl: baseUrl);
await profileService.getMyProfile();
return TestResult(true);
} catch (e) {
return TestResult(false, e.toString());
}
}
}
// RegistrationService
class RegistrationServiceTest implements TestCases {
@override
String get name => 'RegistrationService: getLocationsByAddress';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final registrationService = RegistrationService(dio, baseUrl: baseUrl);
await registrationService.getLocationsByAddress(query: 'test');
return TestResult(true);
} catch (e) {
return TestResult(false, e.toString());
}
}
}
// ResetPasswordService
class ResetPasswordServiceTest implements TestCases {
@override
String get name => 'ResetPasswordService: sendEmailCodeForPasswordChange';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final resetPasswordService = ResetPasswordService(dio, baseUrl: baseUrl);
await resetPasswordService.sendEmailCodeForPasswordChange(data: {'email': 'test@example.com'});
return TestResult(false, 'Expected failure but got success');
} catch (e) {
return TestResult(true, 'Exception expected for test@example.com');
}
}
}
// SettingsService
class SettingsServiceTest implements TestCases {
@override
String get name => 'SettingsService: getProfileSettings';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final settingsService = SettingsService(dio, baseUrl: baseUrl);
await settingsService.getProfileSettings();
return TestResult(true);
} catch (e) {
return TestResult(false, e.toString());
}
}
}
// WsService
class WsServiceTest implements TestCases {
@override
String get name => 'WsService: getWebsocket';
@override
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final wsService = WsService(dio, baseUrl: baseUrl);
await wsService.getWebsocket();
return TestResult(true);
} catch (e) {
return TestResult(false, e.toString());
}
}
}

View File

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

View File

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

View File

@@ -64,7 +64,7 @@ abstract class DiaryFutureDietDayModel with _$DiaryFutureDietDayModel {
factory DiaryFutureDietDayModel.fromJson(Map<String, dynamic> json) => _$DiaryFutureDietDayModelFromJson(json);
}
DiaryDietModel? _parseDiet(json) {
DiaryDietModel? _parseDiet(dynamic json) {
if (json == null) return null;
return DiaryDietModel.fromJson(json['diet']);
}
@@ -80,7 +80,7 @@ abstract class DiaryDietModel with _$DiaryDietModel {
factory DiaryDietModel.fromJson(Map<String, dynamic> json) => _$DiaryDietModelFromJson(json);
}
TypeOfDiaryModel _parseDayTypeForFutureDay(json) {
TypeOfDiaryModel _parseDayTypeForFutureDay(dynamic json) {
return TypeOfDiaryModel(
hasChosenDiet: json['has_chosen_diet'] as bool,
hasChosenPlan: json['has_chosen_plan'] as bool,

View File

@@ -55,7 +55,7 @@ class DiaryCubit extends Cubit<DiaryState> {
return null;
}
setSelectedMeal(int meal) {
void setSelectedMeal(int meal) {
selectedMeal = meal;
final currentState = state;
if (currentState is DiaryStateLoaded) {
@@ -154,7 +154,7 @@ class DiaryCubit extends Cubit<DiaryState> {
return false;
}
fetchDiary([bool needLoading = false, int? diaryId, int? currentDay]) async {
Future<void> fetchDiary([bool needLoading = false, int? diaryId, int? currentDay]) async {
this.diaryId ??= diaryId;
if (needLoading) {
@@ -179,11 +179,14 @@ class DiaryCubit extends Cubit<DiaryState> {
int? currentPlanDay;
String? planName;
if (diaryDay.chosenPlanId != null) {
consultantReview = await diaryRepository.getDiaryComments(planId: diaryDay.chosenPlanId!, diaryId: diaryDay.id);
final chosenPlanModel = await chatRepository.getChosenPlan(id: diaryDay.chosenPlanId!);
if (diaryDay.typeOfDiary.diaryState == DiaryStateEnum.lastDayCommented || diaryDay.typeOfDiary.diaryState == DiaryStateEnum.commented) {
await diaryRepository.viewCommentOfDiaryDayByConsultant(diaryId: diaryDay.id);
}
consultantReview = await diaryRepository.getDiaryComments(planId: diaryDay.chosenPlanId!, diaryId: diaryDay.id);
planDuration = chosenPlanModel.plan.duration;
planName = chosenPlanModel.plan.name;
currentPlanDay = currentDay ?? DateTime.now().difference(chosenPlanModel.startDate).inDays + 1;
currentPlanDay = consultantReview.currentPlanDay;
}
final nutrition = await getNutrition(diaryDay.id);
@@ -194,20 +197,19 @@ class DiaryCubit extends Cubit<DiaryState> {
diaryDay.typeOfDiary.diaryState == DiaryStateEnum.reviewedByUser;
emit(
DiaryState.loaded(
diary: diaryDay,
nutrition: nutrition,
selectedDay: selectedDay,
selectedMeal: selectedMeal,
isConsultantView: this.diaryId != null,
consultantReview: consultantReview,
isDiaryFinished: isDiaryFinished,
isDiaryCommented: isDiaryCommented,
chosenPlanCurrentDay: currentPlanDay,
chosenPlanDuration: planDuration,
chosenPlanName: planName,
isMoodEnabled: isMoodEnabled,
isWakeUpAndRetireEnabled: isWakeUpAndRetireTimeEnabled
),
diary: diaryDay,
nutrition: nutrition,
selectedDay: selectedDay,
selectedMeal: selectedMeal,
isConsultantView: this.diaryId != null,
consultantReview: consultantReview,
isDiaryFinished: isDiaryFinished,
isDiaryCommented: isDiaryCommented,
chosenPlanCurrentDay: currentPlanDay,
chosenPlanDuration: planDuration,
chosenPlanName: planName,
isMoodEnabled: isMoodEnabled,
isWakeUpAndRetireEnabled: isWakeUpAndRetireTimeEnabled),
);
} catch (e, st) {
log(e.toString(), stackTrace: st);
@@ -215,7 +217,7 @@ class DiaryCubit extends Cubit<DiaryState> {
}
}
finishDiaryDay({
Future<void> finishDiaryDay({
required int diaryId,
required String wakeUp,
required String retire,
@@ -233,7 +235,7 @@ class DiaryCubit extends Cubit<DiaryState> {
return (await diaryRepository.getNutritionData(index));
}
addProductToMeal({
Future<void> addProductToMeal({
required int diaryId,
required int mealId,
required String productName,
@@ -241,6 +243,7 @@ class DiaryCubit extends Cubit<DiaryState> {
required int quantity,
int? productNutritionalValueId,
}) async {
emit((state as DiaryStateLoaded).copyWith(isLoadingAddingProductInMeal: true));
await diaryRepository.addPortionToMeal(
diaryId: diaryId,
mealId: mealId,
@@ -252,7 +255,7 @@ class DiaryCubit extends Cubit<DiaryState> {
await fetchDiary();
}
updateProductInMeal({
Future<void> updateProductInMeal({
required int diaryId,
required int mealId,
required int portionId,
@@ -286,22 +289,22 @@ class DiaryCubit extends Cubit<DiaryState> {
await fetchDiary();
}
markEaten({required int diaryId, required int mealId, required int portionId}) async {
Future<void> markEaten({required int diaryId, required int mealId, required int portionId}) async {
await diaryRepository.markEaten(diaryId: diaryId, mealId: mealId, portionId: portionId);
await fetchDiary();
}
unmarkEaten({required int diaryId, required int mealId, required int portionId}) async {
Future<void> unmarkEaten({required int diaryId, required int mealId, required int portionId}) async {
await diaryRepository.unmarkEaten(diaryId: diaryId, mealId: mealId, portionId: portionId);
await fetchDiary();
}
restore({required int diaryId, required int mealId, required int portionId}) async {
Future<void> restore({required int diaryId, required int mealId, required int portionId}) async {
await diaryRepository.restore(diaryId: diaryId, mealId: mealId, portionId: portionId);
await fetchDiary();
}
deleteProductFromMeal({required int diaryId, required int mealId, required int portionId}) async {
Future<void> deleteProductFromMeal({required int diaryId, required int mealId, required int portionId}) async {
await diaryRepository.deleteProductFromMeal(diaryId: diaryId, mealId: mealId, portionId: portionId);
await fetchDiary();
}
@@ -310,12 +313,12 @@ class DiaryCubit extends Cubit<DiaryState> {
return await glossaryRepository.getGlossaryListByQuery(query);
}
uploadPhotos({required List<File> files, required int diaryId}) async {
Future<void> uploadPhotos({required List<File> files, required int diaryId}) async {
await diaryRepository.uploadPhotos(files: await ImageCompressUtility.compressFilesList(files), diaryId: diaryId, mealId: selectedMeal);
await fetchDiary();
}
deletePhoto({required DiaryPhotoModel photo, required int diaryId}) async {
Future<void> deletePhoto({required DiaryPhotoModel photo, required int diaryId}) async {
await diaryRepository.deletePhoto(photo: photo, diaryId: diaryId, mealId: selectedMeal);
await fetchDiary();
}
@@ -335,32 +338,32 @@ class DiaryCubit extends Cubit<DiaryState> {
return null;
}
fillTime({required int diaryId, required String time}) async {
Future<void> fillTime({required int diaryId, required String time}) async {
await diaryRepository.fillTime(diaryId: diaryId, mealId: selectedMeal, time: time);
await fetchDiary();
}
fillWakeup({required int diaryId, required String time}) async {
Future<void> fillWakeup({required int diaryId, required String time}) async {
await diaryRepository.fillWakeup(diaryId: diaryId, time: time);
await fetchDiary();
}
fillRetire({required int diaryId, required String time}) async {
Future<void> fillRetire({required int diaryId, required String time}) async {
await diaryRepository.fillRetire(diaryId: diaryId, time: time);
await fetchDiary();
}
fillSatiety({required int diaryId, required int satiety}) async {
Future<void> fillSatiety({required int diaryId, required int satiety}) async {
await diaryRepository.fillSatiety(diaryId: diaryId, mealId: selectedMeal, satiety: satiety);
await fetchDiary();
}
fillMood({required int diaryId, required int mood}) async {
Future<void> fillMood({required int diaryId, required int mood}) async {
await diaryRepository.fillMood(diaryId: diaryId, mealId: selectedMeal, mood: mood);
await fetchDiary();
}
fillComment(String comment) {
void fillComment(String comment) {
this.comment = comment;
}
}

View File

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

View File

@@ -21,7 +21,7 @@ class _AddProductButtonWidgetState extends State<AddProductButtonWidget> {
diaryCubit = context.read<DiaryCubit>();
}
addProductInMeal(DiaryStateLoaded state, [int? portionId]) {
Future addProductInMeal(DiaryStateLoaded state, [int? portionId]) {
return showModalBottomSheet(
useRootNavigator: true,
context: context,

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