Compare commits

...

118 Commits

Author SHA1 Message Date
misha
87c0d4ec2a feat(analyzer): fixed issues 2025-10-08 14:04:25 +03:00
misha
17be198a23 Merge remote-tracking branch 'upstream/main' into issue-712 2025-10-08 14:03:05 +03:00
d87d0e5e2a Merge pull request 'feat(socialAuth)' (#717) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#717
2025-10-06 13:48:47 +00:00
misha
5275273830 feat(socialAuth): added profile data fetching for social aunthentication 2025-10-06 16:46:59 +03:00
mso7nueh
584f00a1ea Merge pull request 'Issue-691 + Issue-513' (#713) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#713
2025-10-06 10:31:47 +00:00
mso7nueh
37cedb2238 fix(rating_consultant): Issue-513 2025-10-03 16:57:22 +03:00
misha
09d025252d feat(fixes): fixed analyzer issue 2025-10-02 10:25:10 +03:00
mso7nueh
086e6cf94e fix: recipe 2025-10-02 00:13:35 +03:00
mso7nueh
766d5ba248 fix: cubit 2025-10-02 00:07:28 +03:00
mso7nueh
e7ce9d8f90 fix: refactor 2025-10-02 00:05:25 +03:00
mso7nueh
7861cdce3c fix: refactor 2025-10-01 23:59:34 +03:00
mso7nueh
2865852124 fix: refactor 2025-10-01 23:59:19 +03:00
misha
d18ad86590 feat(internetConnection): fixed yandex sign in compilation 2025-10-01 17:24:48 +03:00
misha
f1945202fe feat(internetConnection): changed logic for internet connection checking and displaying state 2025-09-30 17:33:30 +03:00
mso7nueh
b65fca8116 feat(auth): Issue-691 2025-09-30 16:11:30 +03:00
mso7nueh
27376ca54d feat(auth): Issue-691 2025-09-30 00:39:21 +03:00
mso7nueh
01f1c39450 Merge branch 'main' of http://gitea.office.local:3000/mso7nueh/eatvio_app 2025-09-29 23:45:24 +03:00
mso7nueh
24fe984e6b feat(auth): Issue-691 2025-09-29 23:45:18 +03:00
mso7nueh
053cf781c3 Merge pull request 'Issue-691, 660' (#709) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#709
2025-09-29 10:31:10 +00:00
mso7nueh
46d0e31f5f fix(huawei-push): Issue-660 2025-09-29 13:29:39 +03:00
mso7nueh
a0b29f42df feat(hp): Issue-660 2025-09-26 10:31:34 +03:00
mso7nueh
96b3bd1637 Merge branch 'main' of http://gitea.office.local:3000/mso7nueh/eatvio_app 2025-09-24 23:47:19 +03:00
mso7nueh
fc09fa36b8 feat(auth): Issue-691 2025-09-24 23:47:11 +03:00
mso7nueh
68ea8da6e3 Merge pull request 'fix(profile): Issue-523 + 591 + 393 + 660' (#704) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#704
2025-09-23 20:50:00 +00:00
mso7nueh
052c4fedca feat(hms): Issue-660 2025-09-23 23:47:38 +03:00
mso7nueh
ad777c695b fix(consultations): Issue-393 2025-09-22 17:08:41 +03:00
mso7nueh
e6df89c98a fix(feed): Issue-591 2025-09-19 15:55:07 +03:00
mso7nueh
fef4451f4e fix(profile): Issue-523 2025-09-17 17:00:44 +03:00
mso7nueh
c68f86eeb7 Merge pull request 'fix: upd ver to 73' (#702) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#702
2025-09-17 13:32:52 +00:00
mso7nueh
fafc2aa1f7 fix: upd ver to 73 2025-09-17 16:32:24 +03:00
mso7nueh
ca49415ef6 Merge pull request 'Issue-372, 582, 189, 691' (#700) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#700
2025-09-17 13:28:36 +00:00
mso7nueh
2600b8cc79 fix(auth): hide social buttons 2025-09-17 16:28:13 +03:00
e1f25a63bc Merge pull request 'feat(views)' (#697) from misha/eatvio_app:issue-687 into main
Reviewed-on: eatvio/eatvio_app#697
2025-09-17 12:39:58 +00:00
misha
087436da1c changed logic of view requests sending 2025-09-17 15:13:16 +03:00
mso7nueh
c295c06565 fix(my_plans): Issue-189 2025-09-16 19:09:41 +03:00
mso7nueh
b146295996 fix(profile): Issue-582 2025-09-16 19:00:53 +03:00
mso7nueh
41f77bd373 feat(;auth): social 2025-09-16 09:28:17 +03:00
misha
6ac93d42df Merge branch 'main' into issue-687 2025-09-15 14:13:28 +03:00
misha
1a6fb39d84 gitignore 2025-09-15 14:12:05 +03:00
mso7nueh
d4a8cbc637 Merge branch 'main' of http://gitea.office.local:3000/mso7nueh/eatvio_app 2025-09-12 16:29:37 +03:00
mso7nueh
2300bc4ab6 fix(consultations): Issue-372 2025-09-12 16:29:29 +03:00
mso7nueh
b4776c3be3 Merge pull request 'Issues: 416+675' (#699) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#699
2025-09-12 13:27:29 +00:00
mso7nueh
06e07a92b7 Merge pull request 'Issues-236, 332, 256' (#698) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#698
2025-09-12 10:48:22 +00:00
mso7nueh
0fce3f9d30 fix(diary): Issue-675 2025-09-12 13:38:50 +03:00
mso7nueh
4bc8cba941 fix(profile): Issue-416 2025-09-12 13:34:16 +03:00
mso7nueh
8c348f61b1 fix(settings): Issue-256 2025-09-11 21:35:28 +03:00
mso7nueh
0446e64409 merge upstream 2025-09-11 18:25:01 +00:00
mso7nueh
74b5c6732d fix(settings): Issue-256 2025-09-11 20:11:37 +03:00
mso7nueh
b840c936bf fix(finances): Issue-332 2025-09-11 20:02:48 +03:00
mso7nueh
f3dbbf3537 fix(settings): Issue-236 2025-09-11 19:51:16 +03:00
misha
ff49562e86 feat(views): added view handling for diets, recipes, plans and posts 2025-09-11 18:04:35 +03:00
mso7nueh
8b7180c2da Merge pull request 'fix(comments): Issue-548' (#695) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#695
2025-09-10 14:37:20 +00:00
mso7nueh
5c911707a3 fix(comments): Issue-548 2025-09-10 17:36:46 +03:00
mso7nueh
aa604b667b Merge pull request 'fix(notifications): Issue-236' (#693) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#693
2025-09-10 13:45:40 +00:00
mso7nueh
0a5d78be1f fix: ver upd 2025-09-10 16:45:22 +03:00
mso7nueh
f1f5c79661 fix(notifications): Issue-236 2025-09-10 15:58:26 +03:00
mso7nueh
1c517807c4 Merge pull request 'feat(metrica): Issue-685 + 675 + 180' (#688) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#688
2025-09-10 12:58:21 +00:00
mso7nueh
cad54913d1 Merge branch 'main' of http://gitea.office.local:3000/mso7nueh/eatvio_app 2025-09-09 18:58:51 +03:00
mso7nueh
89f0a46192 fix(diet): Issue-180 2025-09-09 18:58:39 +03:00
mso7nueh
65542020a7 merge upstream 2025-09-08 14:45:58 +00:00
mso7nueh
b6b1c8b3c1 feat(diary): кбжу 2025-09-08 14:14:41 +03:00
mso7nueh
bf6e23e96a feat(metrica): Issue-685 2025-09-03 11:34:41 +03:00
mso7nueh
952cf76164 Merge pull request 'fix: Issue-645' (#684) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#684
2025-09-01 10:04:31 +00:00
mso7nueh
b665692779 fix: Issue-645 2025-09-01 13:03:43 +03:00
mso7nueh
54ac20978f Merge pull request 'feat(notifications): Issue-236' (#682) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#682
2025-09-01 09:01:48 +00:00
mso7nueh
b9dc5c0136 Удалить env/.env 2025-08-29 12:16:55 +00:00
mso7nueh
39fce4b0f9 fix: mv env 2025-08-29 15:16:40 +03:00
mso7nueh
73ff67aa13 merge upstream 2025-08-29 12:10:23 +00:00
mso7nueh
dcf2805df7 fix: analyzer 2025-08-29 14:26:28 +03:00
mso7nueh
d13eb78f0e feat(notifications): Issue-236 2025-08-29 14:25:13 +03:00
mso7nueh
5f21a65028 Merge pull request 'feat(verify_email): add dialog' (#679) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#679
2025-08-27 11:40:22 +00:00
mso7nueh
12bf9f9e5f feat(verify_email): add dialog 2025-08-27 14:39:22 +03:00
mso7nueh
1680bd2556 fix: testflight 2025-08-27 12:04:20 +03:00
mso7nueh
91d5118f60 Merge pull request 'feat(verify_email): Issue-645' (#676) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#676
2025-08-27 08:44:19 +00:00
mso7nueh
d8324c1635 feat(verify_email): Issue-645 2025-08-27 11:33:06 +03:00
mso7nueh
0f1dbe16f1 Merge pull request 'fix: testflight' (#673) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#673
2025-08-25 15:31:56 +00:00
Alexander
020682ae80 fix: testflight 2025-08-25 18:31:37 +03:00
mso7nueh
e54476656b Merge pull request 'fix: test-flight' (#671) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#671
2025-08-25 15:28:43 +00:00
Alexander
dd56924076 fix: test-flight 2025-08-25 18:28:15 +03:00
mso7nueh
b47da44cf2 Merge pull request 'fix: testflight' (#669) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#669
2025-08-25 15:19:56 +00:00
Alexander
964550a325 fix: testflight 2025-08-25 18:19:18 +03:00
mso7nueh
7ffa061f3e Merge pull request 'fix: testflight' (#667) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#667
2025-08-25 15:16:06 +00:00
Alexander
997da4f940 fix: testflight 2025-08-25 18:15:49 +03:00
mso7nueh
ca1777ece0 Merge pull request 'main' (#665) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#665
2025-08-25 15:12:32 +00:00
Alexander
1a427d6f5d Merge branch 'main' of http://gitea.office.local:3000/mso7nueh/eatvio_app 2025-08-25 18:12:05 +03:00
Alexander
f190e70f3a fix: testflight 2025-08-25 18:11:04 +03:00
mso7nueh
bc6469a6ae fix: testflight 2025-08-25 17:55:34 +03:00
mso7nueh
e3ff359e1b Merge pull request 'fix: testflight' (#662) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#662
2025-08-25 14:42:39 +00:00
Alexander
b5edf13bd9 fix: testflight 2025-08-25 17:42:05 +03:00
mso7nueh
c05d027e3d Merge pull request 'fix: add product modal and auth error, add appmetrica' (#659) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#659
2025-08-25 14:29:20 +00:00
Alexander
bae79b60fd fix: testflight 2025-08-25 17:29:07 +03:00
mso7nueh
6061e413b2 fix: upd ver to 71 2025-08-25 14:40:50 +03:00
mso7nueh
5a8fa7089c feat: appmetrica 2025-08-25 14:24:06 +03:00
mso7nueh
886cfa8f1c feat: AppMetrica 2025-08-25 13:05:53 +03:00
mso7nueh
a416912d2a fix: add product modal and auth error 2025-08-25 11:26:14 +03:00
mso7nueh
f46392ec8e Merge pull request 'fix: Issue-592' (#655) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#655
2025-08-22 11:18:00 +00:00
mso7nueh
766d06fc4d fix: Issue-592 2025-08-22 14:17:11 +03:00
mso7nueh
6f4023dd0b Merge pull request #512: Add consultant rating functionality 2025-08-22 10:32:39 +03:00
mso7nueh
f892dfed9f Merge pull request 'fix: Issue-618' (#654) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#654
2025-08-20 12:52:12 +00:00
Alexander
98803e56cf fix: Issue-618 2025-08-20 15:51:46 +03:00
mso7nueh
4391870e72 Merge pull request 'fix: build ios' (#652) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#652
2025-08-20 10:03:32 +00:00
Alexander
7fd24f57e3 fix: build ios 2025-08-20 13:03:16 +03:00
mso7nueh
384a25c4a6 Merge pull request 'fix: ios build' (#650) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#650
2025-08-20 09:59:07 +00:00
Alexander
88558d62ef fix: ios build 2025-08-20 12:58:48 +03:00
mso7nueh
09b04118aa Merge pull request 'fix: ios build' (#648) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#648
2025-08-20 09:55:07 +00:00
Alexander
f76b142117 fix: ios build 2025-08-20 12:54:54 +03:00
mso7nueh
13e66edd27 Merge pull request 'fix: Issue-613' (#647) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#647
2025-08-20 09:43:06 +00:00
Alexander
5b74ae45b3 fix: Issue-613 2025-08-20 12:42:39 +03:00
mso7nueh
3d2634b96a Merge pull request 'Добавление таймера для подтверждения почты' (#643) from misha/eatvio_app:issue-642 into main
Reviewed-on: eatvio/eatvio_app#643
2025-08-20 09:39:16 +00:00
mso7nueh
1281abaf24 Merge pull request 'Issue-191: Notifications' (#644) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#644
2025-08-20 09:39:08 +00:00
Alexander
7dfac3f792 fix: baseUrl -> prod 2025-08-20 11:57:06 +03:00
Alexander
9d4687553a fix: analyzer and token storage 2025-08-20 11:34:28 +03:00
mso7nueh
442da40622 fix: token storage 2025-08-19 14:24:14 +03:00
mso7nueh
ccf586234b fix: user charts in consultations 2025-08-15 14:13:30 +03:00
mso7nueh
4f249499a9 fix(notifications): Issue-191 2025-08-15 13:28:12 +03:00
mso7nueh
2f74387df2 fix: firebase notificatons when qr login 2025-08-14 18:13:34 +03:00
ed4db5b59e Merge pull request 'feat(menu): Added build date' (#640) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#640
2025-08-12 06:02:06 +00:00
Username1548
96e80ced38 feat(consultantRating): added api requests and state to consultant rating page 2025-06-17 13:33:41 +07:00
159 changed files with 7300 additions and 3549 deletions

View File

@@ -21,20 +21,19 @@ jobs:
- name: Copy .env file
run: |
mkdir -p env
cp env_repo/.env env/.env
cp env_repo/.env .env
- name: Load environment variables from .env
run: |
set -a
source env/.env
source .env
set +a
# Export all variables to GITHUB_ENV
while IFS= read -r line; do
if [[ $line =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then
echo "$line" >> $GITHUB_ENV
fi
done < env/.env
done < .env
- name: Unlock login keychain
run: |
@@ -59,6 +58,8 @@ jobs:
flutter pub get
flutter pub upgrade
flutter pub run build_runner build --delete-conflicting-outputs
# Configure Flutter for release mode
flutter build ios --config-only --release
cd ios
pod update
fastlane beta

View File

@@ -3,7 +3,7 @@ name: Eatvio App Android Test
on: [ pull_request ]
env:
flutter_version: "3.32.8"
flutter_version: "3.35.1"
java_version: "21.x"
flutter_channel: "stable"
my_local_cache_dir: "/opt/hostedtoolcache/"
@@ -26,20 +26,19 @@ jobs:
- name: Copy .env file
run: |
mkdir -p env
cp env_repo/.env env/.env
cp env_repo/.env .env
- name: Load environment variables from .env
run: |
set -a
source env/.env
source .env
set +a
# Export all variables to GITHUB_ENV
while IFS= read -r line; do
if [[ $line =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then
echo "$line" >> $GITHUB_ENV
fi
done < env/.env
done < .env
- name: Create key.properties file
run: |

5
.gitignore vendored
View File

@@ -16,6 +16,7 @@
*.mocks.dart
android/build/
ios/XCBuildData/
.env
env.example
env/.env
.env
android/key.properties
app/upload-keystore.jks

View File

@@ -54,6 +54,15 @@ cp env.example .env
- `GOOGLE_CHECK_URL` - URL для проверки соединения (Google)
- `YANDEX_CHECK_URL` - URL для проверки соединения (Yandex)
### VK ID Configuration
- `VK_CLIENT_ID` - ID приложения VK (числовой)
- **Примечание**: VK_CLIENT_ID - это числовой "ID приложения" из консоли VK ID, НЕ "Защищённый ключ" или "Сервисный ключ доступа"
### Yandex ID Configuration
- `YANDEX_CLIENT_ID` - ID приложения Yandex OAuth
- `YANDEX_CLIENT_SECRET` - секретный ключ приложения Yandex OAuth
- `YANDEX_REDIRECT_URI` - URI для перенаправления (должен совпадать с настройками в Yandex OAuth приложении)
## Безопасность
⚠️ **ВАЖНО**: Файл `.env` добавлен в `.gitignore` и не должен попадать в репозиторий. Все секретные данные должны храниться только в этом файле.

View File

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

View File

@@ -5,6 +5,7 @@ plugins {
// END: FlutterFire Configuration
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
id 'com.huawei.agconnect'
}
def keystoreProperties = new Properties()
@@ -13,6 +14,8 @@ if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
def yandexClientId = project.hasProperty('YANDEX_CLIENT_ID') ? project.property('YANDEX_CLIENT_ID') : (System.getenv('YANDEX_CLIENT_ID') ?: "")
android {
namespace = "com.eatvioapp.app"
compileSdk = flutter.compileSdkVersion
@@ -29,10 +32,14 @@ android {
defaultConfig {
applicationId = "com.eatvioapp.app"
minSdk = 23
minSdkVersion flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
manifestPlaceholders = [
YANDEX_CLIENT_ID: yandexClientId,
applicationName: "io.flutter.app.FlutterApplication"
]
}
signingConfigs {
@@ -47,6 +54,9 @@ android {
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
signingConfig signingConfigs.debug
@@ -57,6 +67,15 @@ android {
}
}
dependencies {
implementation 'com.huawei.agconnect:agconnect-core:1.9.1.301'
implementation 'com.huawei.hms:push:6.12.0.300'
// Huawei Analytics dependency to satisfy com.huawei.hianalytics.* references during R8
implementation 'com.huawei.hms:hianalytics:6.12.0.300'
// BouncyCastle provider required by Huawei secure utils used transitively by HMS
implementation 'org.bouncycastle:bcprov-jdk15to18:1.78.1'
}
flutter {
source = "../.."
}

21
android/app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
-keep class com.huawei.** { *; }
-dontwarn com.huawei.**
-keep class org.bouncycastle.** { *; }
-dontwarn org.bouncycastle.**
-keep class io.flutter.** { *; }
-dontwarn io.flutter.**
-keep class kotlinx.coroutines.** { *; }
-dontwarn kotlinx.coroutines.**
-keep class **.R$* { *; }
-keepclassmembers class ** { @androidx.annotation.Keep *; }
-keep @androidx.annotation.Keep class * { *; }
-keep public class * extends android.app.Application
-keep public class * extends android.content.ContentProvider
-keep class * implements android.os.Parcelable { *; }
-keepclassmembers enum * { *; }

View File

@@ -11,6 +11,16 @@
android:usesCleartextTraffic="true"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<service
android:name=".HmsPushService"
android:exported="false">
<intent-filter>
<action android:name="com.huawei.push.action.MESSAGING_EVENT" />
</intent-filter>
</service>
<meta-data
android:name="push_kit_auto_init_enabled"
android:value="true" />
<activity
android:name=".MainActivity"
android:exported="true"

View File

@@ -0,0 +1,19 @@
package com.eatvioapp.app
import android.util.Log
import com.huawei.hms.push.HmsMessageService
import com.huawei.hms.push.RemoteMessage
class HmsPushService : HmsMessageService() {
override fun onNewToken(token: String?) {
super.onNewToken(token)
Log.i("HMS", "onNewToken: ${token ?: "null"}")
}
override fun onMessageReceived(message: RemoteMessage?) {
super.onMessageReceived(message)
Log.i("HMS", "onMessageReceived: ${message?.data}")
}
}

View File

@@ -1,6 +1,71 @@
package com.eatvioapp.app
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.os.Bundle
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import com.huawei.agconnect.config.AGConnectServicesConfig
import com.huawei.hms.aaid.HmsInstanceId
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class MainActivity: FlutterActivity() {
private val channelName = "com.eatvioapp/hms_push"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channelName)
.setMethodCallHandler { call, result ->
when (call.method) {
"getHuaweiToken" -> {
CoroutineScope(Dispatchers.IO).launch {
try {
// Try to read appId from agconnect-services.json
var appId: String? = try {
AGConnectServicesConfig.fromContext(applicationContext)
.getString("client/app_id")
} catch (_: Exception) {
null
}
// Fallback to manifest meta-data if needed
if (appId.isNullOrBlank()) {
try {
val pm: PackageManager = applicationContext.packageManager
val ai: ApplicationInfo = pm.getApplicationInfo(applicationContext.packageName, PackageManager.GET_META_DATA)
val raw = ai.metaData?.getString("com.huawei.hms.client.appid")
if (!raw.isNullOrBlank()) {
appId = raw.removePrefix("appid=")
}
} catch (_: Exception) {
// ignore and handle below
}
}
if (appId.isNullOrBlank()) {
runOnUiThread {
result.error("HMS_TOKEN_ERROR", "Missing Huawei appId. Ensure agconnect-services.json or manifest meta-data is configured.", null)
}
return@launch
}
val token = HmsInstanceId.getInstance(applicationContext)
.getToken(appId, "HCM")
runOnUiThread {
result.success(token)
}
} catch (e: Exception) {
runOnUiThread {
result.error("HMS_TOKEN_ERROR", e.message, null)
}
}
}
}
else -> result.notImplemented()
}
}
}
}

View File

@@ -1,7 +1,21 @@
buildscript {
repositories {
google()
mavenCentral()
maven {url 'https://developer.huawei.com/repo/'}
gradlePluginPortal()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.12.0'
classpath 'com.huawei.agconnect:agcp:1.9.1.301'
}
}
allprojects {
repositories {
google()
mavenCentral()
maven {url 'https://developer.huawei.com/repo/'}
}
}
@@ -13,6 +27,32 @@ subprojects {
project.evaluationDependsOn(":app")
}
// Temporary safeguard: set missing namespaces for Android library modules based on their manifest package
subprojects { Project subproject ->
subproject.pluginManager.withPlugin('com.android.library') {
def androidExt = subproject.extensions.findByName('android')
if (androidExt != null) {
def hasNamespaceProp = androidExt.hasProperty('namespace')
def currentNamespace = hasNamespaceProp ? androidExt.namespace : null
if (hasNamespaceProp && !currentNamespace) {
def manifestFile = file("${subproject.projectDir}/src/main/AndroidManifest.xml")
if (manifestFile.exists()) {
try {
def manifest = new groovy.xml.XmlSlurper().parse(manifestFile)
def manifestPackage = manifest.@package?.text()
if (manifestPackage) {
androidExt.namespace = manifestPackage
subproject.logger.lifecycle("[namespace-fix] Set namespace for ${subproject.path} to '${manifestPackage}' from AndroidManifest.xml")
}
} catch (Throwable t) {
subproject.logger.warn("[namespace-fix] Failed to read manifest for ${subproject.path}: ${t.message}")
}
}
}
}
}
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

File diff suppressed because one or more lines are too long

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.12-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-all.zip

View File

@@ -12,17 +12,18 @@ pluginManagement {
repositories {
google()
mavenCentral()
maven { url 'https://developer.huawei.com/repo/' }
gradlePluginPortal()
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.7.3" apply false
id "com.android.application" version "8.12.0" apply false
// START: FlutterFire Configuration
id "com.google.gms.google-services" version "4.4.2" apply false
id "com.google.gms.google-services" version "4.4.3" apply false
// END: FlutterFire Configuration
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
id "org.jetbrains.kotlin.android" version "2.2.10" apply false
}
include ":app"

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#2F80ED"><path d="M759.23-353.08q8.92-8.92 21.19-8.92 12.27 0 21.19 8.92l63.39 64q8.92 9.07 8.92 21.15 0 12.08-8.92 21.01-8.92 8.92-21.01 8.92-12.08 0-21.14-8.92l-64-63.39q-8.93-8.92-8.73-21.38.19-12.46 9.11-21.39Zm33.85-472.22q8.92 9 8.92 21.38 0 12.38-8.92 21.3l-63.39 63.39q-8.92 8.92-21.19 8.92-12.27 0-21.19-9-8.92-9.01-8.92-21.39t8.92-21.3l63.61-63.38q9.07-8.93 21.15-8.93 12.08 0 21.01 9.01ZM167-825q9.01-8.92 21.39-8.92t21.3 8.92l63.39 63.62q8.92 9.06 8.92 21.14 0 12.09-9 21.01-9.01 8.92-21.39 8.92t-21.3-8.92l-63.39-63.39q-8.92-8.92-8.92-21.19 0-12.27 9-21.19Zm33.77 472.31q8.92 8.92 8.92 21.19 0 12.27-8.92 21.19l-63.39 63.39q-8.92 8.92-21.19 8.92-12.27 0-21.19-9-8.92-9.01-8.92-21.39t8.92-21.3l63.62-63.39q8.92-8.92 21.07-8.73 12.16.2 21.08 9.12Zm135.54 86.85L480-352.23l144.08 87.77-37.62-163.62 126.39-109.07-166.39-14.93L480-706.54l-66.08 153.46-166.38 14.54 126.38 110.46-37.61 162.24ZM480.38-486ZM480-281.31 306.69-177.08q-8.69 5.08-17.42 4.27-8.73-.81-15.81-5.88-7.07-5.08-10.92-13.27-3.85-8.19-1.23-18.12L307.23-406 154.08-538.39q-7.69-6.83-9.62-15.57-1.92-8.73 1.31-17.04 3.23-8.31 9.28-13.62 6.05-5.32 16.64-6.84l201.7-17.39 78.46-185.3q3.84-9.31 11.65-13.77 7.81-4.46 16.5-4.46 8.69 0 16.5 4.46 7.81 4.46 11.65 13.77L587-608.85l201.69 17.39q10.68 1.51 16.78 6.82 6.1 5.3 9.14 13.64 3.24 8.31 1.31 17.04-1.92 8.74-9.61 15.57L653.15-406l45.93 196.31q2.61 9.92-1.23 17.92-3.85 8-10.93 13.08-7.07 5.07-15.8 5.88-8.73.81-17.43-4.27L480-281.31Z"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,11 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1713_218)">
<path d="M7.48034 0.0116882H20.547C24.6707 0.0116882 28.0137 3.35463 28.0137 7.47836V20.545C28.0137 24.6687 24.6707 28.0117 20.547 28.0117H7.48034C3.35661 28.0117 0.0136719 24.6687 0.0136719 20.545V7.47836C0.0136717 3.35463 3.35661 0.0116882 7.48034 0.0116882Z" fill="#0077FF"/>
<path d="M14.0133 3.43393C19.8423 3.43393 24.5911 8.18276 24.5911 14.0117C24.5911 17.6432 22.8964 19.3565 20.7548 19.3565C19.5071 19.3565 18.4269 18.7047 17.8124 17.7363C16.844 18.7233 15.5031 19.3378 14.0133 19.3378C11.0709 19.3378 8.6872 16.9541 8.6872 14.0117C8.6872 11.0693 11.0709 8.68558 14.0133 8.68558C16.9557 8.68558 19.3395 11.0693 19.3395 14.0117V15.7436C19.3395 16.6189 19.954 17.2334 20.7548 17.2334C21.7046 17.2334 22.4867 16.4327 22.4867 14.0117C22.4867 9.33739 18.6876 5.53832 14.0133 5.53832C9.339 5.53832 5.53993 9.33739 5.53993 14.0117C5.53993 18.686 9.339 22.4851 14.0133 22.4851C16.7136 22.4851 18.5759 21.1443 18.5759 21.1443L19.9726 22.7458C19.9726 22.7458 17.5889 24.5895 14.0133 24.5895C8.18438 24.5895 3.43555 19.8407 3.43555 14.0117C3.43555 8.18276 8.18438 3.43393 14.0133 3.43393ZM10.7916 14.0117C10.7916 15.7809 12.2255 17.2334 14.0133 17.2334C15.7825 17.2334 17.2351 15.7809 17.2351 14.0117C17.2351 12.2239 15.7825 10.79 14.0133 10.79C12.2255 10.79 10.7916 12.2239 10.7916 14.0117Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_1713_218">
<rect width="28" height="28" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

5
assets/icons/ok_logo.svg Normal file
View File

@@ -0,0 +1,5 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 13.4393C0 7.10718 0 3.92462 1.9623 1.96231C3.9411 1.57261e-07 7.10718 0 13.4393 0H14.5607C20.8928 0 24.0754 1.57261e-07 26.0377 1.96231C28 3.92462 28 7.10718 28 13.4393V14.5607C28 20.8928 28 24.0754 26.0377 26.0377C24.0754 28 20.8928 28 14.5607 28H13.4393C7.10718 28 3.92461 28 1.9623 26.0377C0 24.0754 0 20.9093 0 14.5607V13.4393Z" fill="#FF7700"/>
<path d="M13.9997 13.934C15.3684 13.934 16.6051 13.3734 17.4956 12.4829C18.3861 11.5925 18.9467 10.3557 18.9467 8.98704C18.9467 7.61837 18.3861 6.38162 17.4956 5.49116C16.6051 4.6007 15.3684 4.04004 13.9997 4.04004C12.631 4.04004 11.3943 4.6007 10.5039 5.49116C9.61339 6.38162 9.05273 7.61837 9.05273 8.98704C9.05273 10.3557 9.61339 11.5925 10.5039 12.4829C11.3943 13.3734 12.6475 13.934 13.9997 13.934ZM12.4167 7.38751C12.8289 6.97526 13.3896 6.72791 13.9997 6.72791C14.6263 6.72791 15.187 6.97526 15.5828 7.38751C15.995 7.79976 16.2424 8.36042 16.2424 8.97055C16.2424 9.59716 15.995 10.1578 15.5828 10.5536C15.1705 10.9658 14.6098 11.2132 13.9997 11.2132C13.3731 11.2132 12.8124 10.9658 12.4167 10.5536C12.0044 10.1413 11.7571 9.58067 11.7571 8.97055C11.7571 8.36042 12.0209 7.79976 12.4167 7.38751Z" fill="white"/>
<path d="M20.6615 15.9293L19.2104 13.9505C19.1279 13.8351 18.963 13.8186 18.8641 13.9175C17.4954 15.0883 15.8134 15.8469 13.9995 15.8469C12.1856 15.8469 10.5037 15.0883 9.13498 13.9175C9.03604 13.8351 8.87114 13.8516 8.78869 13.9505L7.33758 15.9293C7.27162 16.0283 7.28811 16.1602 7.37056 16.2426C8.59081 17.232 10.009 17.9081 11.4931 18.2544L8.42591 23.6631C8.34346 23.8115 8.45889 24.0094 8.62379 24.0094H11.625C11.7239 24.0094 11.8064 23.9599 11.8393 23.861L13.983 19.0789L16.1267 23.861C16.1597 23.9434 16.2422 24.0094 16.3411 24.0094H19.3423C19.5237 24.0094 19.6226 23.828 19.5402 23.6631L16.473 18.2544C17.9571 17.9081 19.3753 17.2485 20.5955 16.2426C20.7274 16.1602 20.7439 16.0283 20.6615 15.9293Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

16
assets/icons/vk_logo.svg Normal file
View File

@@ -0,0 +1,16 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1709_298)">
<mask id="mask0_1709_298" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="28" height="28">
<path d="M27.8614 0H0.138672V28H27.8614V0Z" fill="white"/>
</mask>
<g mask="url(#mask0_1709_298)">
<path d="M0.138672 13.44C0.138672 7.10433 0.138672 3.93649 2.08743 1.96824C4.03619 0 7.17266 0 13.4456 0H14.5545C20.8275 0 23.9639 0 25.9127 1.96824C27.8614 3.93649 27.8614 7.10433 27.8614 13.44V14.56C27.8614 20.8957 27.8614 24.0635 25.9127 26.0318C23.9639 28 20.8275 28 14.5545 28H13.4456C7.17266 28 4.03619 28 2.08743 26.0318C0.138672 24.0635 0.138672 20.8957 0.138672 14.56V13.44Z" fill="#0077FF"/>
<path d="M14.89 20.1718C8.57149 20.1718 4.96756 15.7968 4.81738 8.51675H7.98241C8.08637 13.8601 10.4197 16.1234 12.2678 16.5901V8.51675H15.2481V13.125C17.0732 12.9267 18.9905 10.8268 19.6373 8.51675H22.6176C22.1209 11.3634 20.0417 13.4634 18.5632 14.3267C20.0417 15.0267 22.4098 16.8584 23.3108 20.1718H20.0302C19.3255 17.9551 17.5699 16.2401 15.2481 16.0067V20.1718H14.89Z" fill="white"/>
</g>
</g>
<defs>
<clipPath id="clip0_1709_298">
<rect width="28" height="28" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,16 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1709_319)">
<mask id="mask0_1709_319" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="28" height="28">
<path d="M27.8614 0H0.138672V28H27.8614V0Z" fill="white"/>
</mask>
<g mask="url(#mask0_1709_319)">
<path d="M0.138672 13.44C0.138672 7.10433 0.138672 3.93649 2.08743 1.96824C4.03619 0 7.17266 0 13.4456 0H14.5545C20.8275 0 23.9639 0 25.9127 1.96824C27.8614 3.93649 27.8614 7.10433 27.8614 13.44V14.56C27.8614 20.8957 27.8614 24.0635 25.9127 26.0318C23.9639 28 20.8275 28 14.5545 28H13.4456C7.17266 28 4.03619 28 2.08743 26.0318C0.138672 24.0635 0.138672 20.8957 0.138672 14.56V13.44Z" fill="#FC3F1D"/>
</g>
<path d="M17.3371 24H21V4H15.6687C10.3069 4 7.49454 6.614 7.49454 10.4755C7.49454 13.809 9.31785 15.6885 12.5476 17.73L14.51 18.9635L11.9911 16.9515L7 24H10.971L16.3029 16.423L14.4486 15.248C12.2079 13.809 11.1107 12.693 11.1107 10.27C11.1107 8.141 12.6873 6.702 15.6844 6.702H17.3224V24H17.3382H17.3371Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_1709_319">
<rect width="28" height="28" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

0
env/.env vendored
View File

View File

@@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
<string>16.0</string>
</dict>
</plist>

View File

@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
platform :ios, '14.0'
platform :ios, '16.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
@@ -39,7 +39,7 @@ post_install do |installer|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64"
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0'
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '16.0'
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
## dart: PermissionGroup.camera

View File

@@ -213,14 +213,10 @@
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";
@@ -287,14 +283,10 @@
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";
@@ -378,7 +370,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -396,7 +388,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 68;
CURRENT_PROJECT_VERSION = 74;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -418,6 +410,7 @@
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
@@ -474,7 +467,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -526,7 +519,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -546,7 +539,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 68;
CURRENT_PROJECT_VERSION = 74;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -568,6 +561,7 @@
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@@ -584,7 +578,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 68;
CURRENT_PROJECT_VERSION = 74;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -606,6 +600,7 @@
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";

View File

@@ -41,7 +41,7 @@
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
buildConfiguration = "Release"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"

View File

@@ -1,5 +1,6 @@
import UIKit
import Flutter
import UIKit
import YandexLoginSDK
@main
@objc class AppDelegate: FlutterAppDelegate {
@@ -10,4 +11,20 @@ import Flutter
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
@available(iOS 8.0, *)
func application(
_ application: UIApplication, continue userActivity: NSUserActivity,
restorationHandler: @escaping ([Any]?) -> Void
) -> Bool {
YXLSdk.shared.processUserActivity(userActivity)
return true
}
override public func application(
_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool {
return YXLSdk.shared.handleOpen(
url, sourceApplication: options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String)
}
}

View File

@@ -21,7 +21,7 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>68</string>
<string>74</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>
@@ -67,5 +67,24 @@
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>yandexauth</string>
<string>yandexauth2</string>
<string>yandexauth4</string>
</array>
<key>YAClientId</key>
<string>5399b03636104c6d920b87c1ad329254</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>YandexLoginSDK</string>
<key>CFBundleURLSchemes</key>
<array>
<string>yx5399b03636104c6d920b87c1ad329254</string>
</array>
</dict>
</array>
</dict>
</plist>

View File

@@ -8,6 +8,7 @@
<array>
<string>applinks:eatvio.ru</string>
<string>applinks:www.eatvio.ru</string>
<string>applinks:5399b03636104c6d920b87c1ad329254.oauth.yandex.ru</string>
</array>
</dict>
</plist>

View File

@@ -44,17 +44,18 @@ platform :ios do
UI.message "Using auth_key_path: #{auth_key_path}"
# Configure Flutter for release mode
UI.message "Configuring Flutter for release mode..."
sh("cd .. && flutter build ios --config-only --release")
match(type: "appstore", readonly: true)
increment_build_number(xcodeproj: "Runner.xcodeproj")
build_date = Time.now.getlocal("+03:00").strftime("%Y-%m-%d %H:%M:%S")
build_app(
workspace: "Runner.xcworkspace",
scheme: "Runner",
export_method: "app-store",
codesigning_identity: "Apple Distribution",
xcargs: "--dart-define=HIDE_PLANS=true --dart-define=BUILD_DATE='#{build_date}'"
)

37
lib/core/app_config.dart Normal file
View File

@@ -0,0 +1,37 @@
import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_cubit.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/views/cubit/views_cubit.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.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/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
/// Конфигурация приложения
class AppConfig {
/// Список глобальных BlocProvider'ов
static List<BlocProvider> get globalBlocProviders => [
BlocProvider<NewMessagesCubit>(create: (context) => sl<NewMessagesCubit>()),
BlocProvider<NotificationsCubit>(create: (context) => sl<NotificationsCubit>()..getNotifications()),
BlocProvider<FeedCubit>(create: (context) => sl<FeedCubit>()..loadFeedBlogPostList()),
BlocProvider<ProfileAvatarCubit>(create: (context) => sl<ProfileAvatarCubit>()..getMyProfile()),
BlocProvider<ChatsCubit>(create: (context) => sl<ChatsCubit>()..getChatRoomsWithLoading()),
BlocProvider<ViewsCubit>(create: (context) => sl<ViewsCubit>()),
BlocProvider<InternetConnectionCubit>(lazy: false, create: (context) => sl<InternetConnectionCubit>()),
];
/// Делегаты локализации
static const List<LocalizationsDelegate> localizationDelegates = [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
];
/// Поддерживаемые локали
static const List<Locale> supportedLocales = [Locale.fromSubtags(languageCode: 'ru')];
}

View File

@@ -0,0 +1,68 @@
import 'dart:io' as io;
import 'package:eatvio_app/core/env_config.dart';
import 'package:eatvio_app/core/locator_service.dart' as di;
import 'package:eatvio_app/firebase_options.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:appmetrica_plugin/appmetrica_plugin.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
/// Класс для инициализации приложения
class AppInitializer {
/// Инициализация всех сервисов приложения
static Future<void> initialize() async {
await _initializeSentry();
await _initializeEnvironment();
await _initializeFirebase();
await _initializeDependencyInjection();
await _initializeDebugMode();
await _initializeAnalytics();
}
/// Инициализация Sentry
static Future<void> _initializeSentry() async {
final widgetsBinding = SentryWidgetsFlutterBinding.ensureInitialized();
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
}
/// Загрузка переменных окружения
static Future<void> _initializeEnvironment() async {
await dotenv.load(fileName: ".env");
}
/// Инициализация Firebase
static Future<void> _initializeFirebase() async {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
await FirebaseMessaging.instance.requestPermission();
}
/// Инициализация системы внедрения зависимостей
static Future<void> _initializeDependencyInjection() async {
await di.init();
}
/// Настройка режима отладки
static Future<void> _initializeDebugMode() async {
if (kDebugMode) {
io.HttpClient.enableTimelineLogging = true;
}
}
/// Инициализация аналитики
static Future<void> _initializeAnalytics() async {
AppMetrica.activate(AppMetricaConfig(EnvConfig.appMetricaApiKey));
AppMetrica.reportEvent('Application started');
}
/// Запуск приложения с Sentry
static Future<void> runAppWithSentry(VoidCallback appRunner) async {
await SentryFlutter.init((options) {
options.dsn = EnvConfig.sentryDsn;
options.tracesSampleRate = 1.0;
options.debug = false;
}, appRunner: appRunner);
}
}

View File

@@ -1,508 +1,53 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/firebase_api.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/core/websockets/ws_manager.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart';
import 'package:eatvio_app/features/auth/presentation/pages/forget_password_screen.dart';
import 'package:eatvio_app/features/auth/presentation/pages/scan_qr_code_screen.dart';
import 'package:eatvio_app/features/blog_post/presentation/pages/blog_post_screen.dart';
import 'package:eatvio_app/features/blog_post_create/presentation/pages/blog_post_create_screen.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart';
import 'package:eatvio_app/features/calendar/presentation/pages/calendar_screen.dart';
import 'package:eatvio_app/features/chat/presentation/pages/chat_screen.dart';
import 'package:eatvio_app/features/chat/presentation/pages/chats_screen.dart';
import 'package:eatvio_app/features/chat/presentation/pages/empty_chat_screen.dart';
import 'package:eatvio_app/features/chat/presentation/pages/payment_screen.dart';
import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/become_consultant_screen.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/stepper_become_consultant_screen.dart';
import 'package:eatvio_app/features/consultations/presentation/pages/consultations_screen.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart';
import 'package:eatvio_app/features/menu/presentation/pages/blocked_users_screen.dart';
import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart';
import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart';
import 'package:eatvio_app/features/create_diet/presentation/diet_preview_screen.dart';
import 'package:eatvio_app/features/create_plan/presentation/create_plan_screen.dart';
import 'package:eatvio_app/features/consultant_rating/presentation/consultant_rating_screen.dart';
import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart';
import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart';
import 'package:eatvio_app/features/diets/presentation/pages/about_diet_screen.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
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/pages/feed_screen.dart';
import 'package:eatvio_app/features/glossary/presentation/pages/glossary_screen.dart';
import 'package:eatvio_app/features/menu/presentation/pages/menu_screen.dart';
import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart';
import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultations_archive_screen.dart';
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/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';
import 'package:eatvio_app/features/recipe/presentation/pages/about_recipe_screen.dart';
import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart';
import 'package:eatvio_app/features/recipe/presentation/pages/my_recipes_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/registration_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/verify_email_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/verify_screen.dart';
import 'package:eatvio_app/features/settings/entities/settings_entity.dart';
import 'package:eatvio_app/features/settings/presentation/pages/change_email_screen.dart';
import 'package:eatvio_app/features/settings/presentation/pages/change_password_screen.dart';
import 'package:eatvio_app/features/settings/presentation/pages/general_info_screen.dart';
import 'package:eatvio_app/features/settings/presentation/pages/other_info_screen.dart';
import 'package:eatvio_app/features/settings/presentation/pages/physical_activity_screen.dart';
import 'package:eatvio_app/features/settings/presentation/pages/physical_form_screen.dart';
import 'package:eatvio_app/features/settings/presentation/pages/pressure_screen.dart';
import 'package:eatvio_app/features/settings/presentation/pages/settings_screen.dart';
import 'package:eatvio_app/features/support/presentation/pages/support_screen.dart';
import 'package:eatvio_app/features/update_screen/presentation/update_screen.dart';
import 'package:eatvio_app/features/user_details/presentation/user_details_screen.dart';
import 'package:eatvio_app/features/welcome/presentation/pages/welcome_screen.dart';
import 'package:eatvio_app/core/routing/auth_routes.dart';
import 'package:eatvio_app/core/routing/content_routes.dart';
import 'package:eatvio_app/core/routing/main_navigation_routes.dart';
import 'package:eatvio_app/core/routing/other_routes.dart';
import 'package:eatvio_app/core/routing/route_constants.dart';
import 'package:eatvio_app/core/routing/settings_routes.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>();
static GoRouter appRouter = GoRouter(
initialLocation: '/update-screen',
initialLocation: RouteConstants.updateScreen,
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;
},
redirect: _handleRedirect,
routes: [
GoRoute(
path: '/auth-screen',
name: 'authScreen',
builder: (context, state) => const AuthScreen(),
routes: [
GoRoute(
path: 'registration-screen',
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(
builder: (context, state, child) {
return BlocBuilder<ProfileAvatarCubit, ProfileAvatarState>(
builder: (BuildContext context, ProfileAvatarState state) {
if (state is ProfileAvatarStateLoaded) {
return BottomNavigationBarWidget(profileModel: state.profile, child: child);
}
return const SizedBox.shrink();
},
);
},
routes: [
GoRoute(
path: '/my-profile-screen',
name: 'myProfileScreen',
pageBuilder: (context, state) {
return const NoTransitionPage(child: ProfileScreen());
},
),
GoRoute(
path: '/feed-screen',
name: 'feedScreen',
pageBuilder: (context, state) => const NoTransitionPage(child: FeedScreen()),
redirect: (context, state) async {
try {
await sl<WsManager>().initialize();
} catch (e, st) {
log(e.toString(), stackTrace: st);
}
return null;
},
),
GoRoute(
path: '/diary-screen',
name: 'diaryScreen',
pageBuilder: (context, state) => const NoTransitionPage(
child: DiaryScreen(),
),
),
GoRoute(
path: ConsultationsScreen.path,
name: ConsultationsScreen.name,
pageBuilder: (context, state) => const NoTransitionPage(child: ConsultationsScreen())),
GoRoute(path: '/chat-select-screen', name: 'chatSelectScreen', pageBuilder: (context, state) => const NoTransitionPage(child: ChatsScreen())),
GoRoute(
path: '/notifications-screen',
name: 'notificationsScreen',
pageBuilder: (context, state) => const NoTransitionPage(child: NotificationsScreen())),
GoRoute(
path: '/menu-screen',
name: 'menuScreen',
pageBuilder: (context, state) => NoTransitionPage(
child: BlocBuilder<ProfileAvatarCubit, ProfileAvatarState>(
builder: (BuildContext context, ProfileAvatarState state) {
if (state is ProfileAvatarStateLoaded) {
return MenuScreen(profileModel: state.profile);
}
return const SizedBox.shrink();
},
),
),
),
],
),
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 {
final sharedPreferences = await SharedPreferences.getInstance();
final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed') ?? false;
await sl<FirebaseApi>().registerPushToken();
if (welcomeScreenWasShowed == false) {
return null;
} else if (await TokenStorage.haveTokenInStorage()) {
return '/feed-screen';
} else {
return '/auth-screen';
}
} catch (e) {
return '/feed-screen';
}
},
),
GoRoute(
path: '/update-screen',
name: 'updateScreen',
builder: (context, state) => const UpdateScreen(),
redirect: (context, state) async {
try {
final versions = (await sl<Dio>().get('$baseUrl/api/app_versions')).data;
PackageInfo packageInfo = await PackageInfo.fromPlatform();
String version = packageInfo.version;
Version currentVersion = Version.parse(version);
Version latestVersion = Version.parse(versions['min_version']);
if (latestVersion > currentVersion) {
return null;
} else {
return '/welcome-screen';
}
} catch (e) {
return '/welcome-screen';
}
},
),
GoRoute(
path: ConsultationsArchiveScreen.path,
name: ConsultationsArchiveScreen.name,
builder: (context, state) => const ConsultationsArchiveScreen(),
),
GoRoute(
path: ConsultantFinancesScreen.path,
name: ConsultantFinancesScreen.name,
builder: (context, state) => const ConsultantFinancesScreen(),
),
GoRoute(
path: UserPurchasesScreen.path,
name: UserPurchasesScreen.name,
builder: (context, state) => const UserPurchasesScreen(),
),
GoRoute(
path: '/user-finance-screen',
name: 'userFinanceScreen',
builder: (context, state) => const UserFinanceScreen(),
),
GoRoute(
path: CardSettingsScreen.path,
name: CardSettingsScreen.name,
builder: (context, state) {
final chosenPlanId = state.extra as int?;
return CardSettingsScreen(chosenPlanId: chosenPlanId);
},
),
GoRoute(
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(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()),
GoRoute(
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(
recipeSlug: data['recipe'] as String,
index: data['index'] as int?,
navigateToComments: data['navigateToComments'] as bool,
);
},
),
GoRoute(
path: RecipeCreateScreen.path,
name: RecipeCreateScreen.name,
builder: (context, state) => RecipeCreateScreen(
recipe: state.extra != null ? state.extra as Recipe : null,
),
),
GoRoute(path: '/profile-screen', name: 'profileScreen', builder: (context, state) => ProfileScreen(author: state.extra as Author)),
GoRoute(
path: '/subs-screen/:slug',
name: 'subsScreen',
builder: (context, state) => SubsScreen(slug: state.pathParameters['slug']!, subType: state.extra as SubType),
),
GoRoute(
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(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
},
),
GoRoute(
path: '/blog-post-create-screen',
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) {
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 uuid = data['uuid'] as String;
final sendInitialMessage = data['sendInitialMessage'] as bool?;
return ChatScreen(
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',
builder: (context, state) => const SettingsScreen(),
routes: [
GoRoute(
path: 'general-info-screen',
name: 'generalInfoScreen',
builder: (context, state) => GeneralInfoScreen(settingsEntity: state.extra as SettingsEntity),
),
GoRoute(
path: 'other-info-screen',
name: 'otherInfoScreen',
builder: (context, state) => OtherInfoScreen(settingsEntity: state.extra as SettingsEntity),
),
GoRoute(
path: 'physical-activity-screen',
name: 'physicalActivityScreen',
builder: (context, state) => PhysicalActivityScreen(settingsEntity: state.extra as SettingsEntity),
),
GoRoute(
path: 'physical-form-screen',
name: 'physicalFormScreen',
builder: (context, state) => PhysicalFormScreen(settingsEntity: state.extra as SettingsEntity),
),
GoRoute(path: 'pressure-screen', name: 'pressureScreen', builder: (context, state) => PressureScreen(settingsEntity: state.extra as SettingsEntity)),
GoRoute(
path: 'change-password-screen',
name: 'changePasswordScreen',
builder: (context, state) => ChangePasswordScreen(settingsEntity: state.extra as SettingsEntity),
),
GoRoute(
path: 'change-email-screen',
name: 'changeEmailScreen',
builder: (context, state) => ChangeEmailScreen(settingsEntity: state.extra as SettingsEntity),
),
],
),
GoRoute(path: '/become-consultant-screen', name: 'becomeConsultantScreen', builder: (context, state) => const BecomeConsultantScreen()),
GoRoute(
path: '/stepper-become-consultant-screen',
name: 'stepperBecomeConsultantScreen',
builder: (context, state) => StepperBecomeConsultantScreen(
slug: state.extra == null ? null : state.extra as String,
),
),
GoRoute(path: '/calendar-screen', name: 'calendarScreen', builder: (context, state) => const CalendarScreen()),
GoRoute(path: '/support-screen', name: 'supportScreen', builder: (context, state) => const SupportScreen()),
GoRoute(
path: '/photo-view-screen/:index',
name: 'photoViewScreen',
builder: (context, state) => PhotoViewScreen(imageUrls: state.extra as List<String>, index: int.parse(state.pathParameters['index']!)),
),
GoRoute(
path: CreateDietScreen.path,
name: CreateDietScreen.name,
builder: (context, state) {
final diet = state.extra as Diet?;
return CreateDietScreen(diet: diet);
},
),
GoRoute(
path: DietPreviewScreen.path,
name: DietPreviewScreen.name,
builder: (context, state) {
final data = state.extra as Diet;
return DietPreviewScreen(diet: data);
},
),
GoRoute(path: CreatePlanScreen.path, name: CreatePlanScreen.name, builder: (context, state) => const CreatePlanScreen()),
GoRoute(path: MyDietsScreen.path, name: MyDietsScreen.name, builder: (context, state) => const MyDietsScreen()),
GoRoute(path: MyPlansScreen.path, name: MyPlansScreen.name, builder: (context, state) => const MyPlansScreen()),
GoRoute(path: UserDetailsScreen.path, name: UserDetailsScreen.name, builder: (context, state) => UserDetailsScreen(slug: state.extra as String)),
GoRoute(path: DevToolsScreen.path, name: DevToolsScreen.name, builder: (context, state) => const DevToolsScreen()),
GoRoute(path: ConsultantRatingScreen.path, name: ConsultantRatingScreen.name, builder: (context, state) => const ConsultantRatingScreen()),
GoRoute(
path: InfoByOrderScreen.path,
name: InfoByOrderScreen.name,
builder: (context, state) {
final data = state.extra as Map<String, dynamic>;
final planId = data['planId'] as int;
final successfulPayment = data['successfulPayment'] as bool?;
return InfoByOrderScreen(
planId: planId,
successfulPayment: successfulPayment,
);
},
),
GoRoute(
path: PaymentScreen.path,
name: PaymentScreen.name,
builder: (context, state) {
final data = state.extra as Map<String, dynamic>;
final planId = data['planId'] as int;
final url = data['url'] as String;
return PaymentScreen(
url: url,
planId: planId,
);
},
),
GoRoute(
path: UserDiaryScreen.path,
name: UserDiaryScreen.name,
builder: (context, state) {
final data = state.extra as Map<String, int>;
final diaryId = data['diaryId'] as int;
final currentPlanDay = data['currentDiaryDay'] as int;
return UserDiaryScreen(
diaryId: diaryId,
currentPlanDay: currentPlanDay,
);
},
),
GoRoute(
path: '/verify-screen',
name: 'verifyScreen',
builder: (context, state) => VerifyScreen(passKey: state.extra as String),
),
GoRoute(
path: '/verify-email-screen',
name: 'verifyEmailScreen',
builder: (context, state) => const VerifyEmailScreen(),
),
GoRoute(
path: MyRecipesScreen.path,
name: MyRecipesScreen.name,
builder: (context, state) => const MyRecipesScreen(),
),
...AuthRoutes.createRoutes(),
...MainNavigationRoutes.createRoutes(),
...ContentRoutes.createRoutes(),
...SettingsRoutes.createRoutes(),
...OtherRoutes.createRoutes(),
],
);
/// Обработка редиректов для deep links
static String? _handleRedirect(BuildContext context, GoRouterState 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 '${RouteConstants.aboutRecipesScreen}?slug=$contentIdentifier';
case 'blog_post':
FlutterNativeSplash.remove();
return '${RouteConstants.blogPostScreen}?id=$contentIdentifier';
case 'plan':
FlutterNativeSplash.remove();
return '${RouteConstants.aboutPlanScreen}?slug=$contentIdentifier';
case 'diet':
FlutterNativeSplash.remove();
return '${RouteConstants.aboutDietScreen}?slug=$contentIdentifier';
}
}
return null;
}
}

View File

@@ -18,25 +18,23 @@ void showCommentsActionsBottomSheet(
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,
),
));
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => Container(
margin: EdgeInsets.only(left: 8, right: 8, bottom: MediaQuery.of(context).viewInsets.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 {
@@ -82,44 +80,33 @@ class _CommentsActionsBottomSheetState extends State<CommentsActionsBottomSheet>
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(children: [
Expanded(
child: CustomTextFormField(
autofocus: true,
controller: controller,
onChanged: (value) => setState(() {}),
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,
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 SizedBox(height: 7),
const BottomSheetHandlerBar(),
if (widget.isMyComment)
BottomSheetActionWidget(
@@ -131,43 +118,22 @@ class _CommentsActionsBottomSheetState extends State<CommentsActionsBottomSheet>
}),
)
else if (widget.reply != null)
BottomSheetActionWidget(
text: s.reply,
iconPath: 'assets/icons/reply.svg',
onTap: widget.reply!,
),
BottomSheetActionWidget(text: s.reply, iconPath: 'assets/icons/reply.svg', onTap: widget.reply!),
if (widget.reply != null) const CustomDivider(),
BottomSheetActionWidget(
text: s.copy,
iconPath: 'assets/icons/copy2.svg',
onTap: widget.copy,
),
BottomSheetActionWidget(text: s.copy, iconPath: 'assets/icons/copy2.svg', onTap: widget.copy),
if (widget.isMyComment) ...[
if (widget.reply != null) ...[
const CustomDivider(),
BottomSheetActionWidget(
text: s.reply,
iconPath: 'assets/icons/reply.svg',
onTap: widget.reply!,
),
BottomSheetActionWidget(text: s.reply, iconPath: 'assets/icons/reply.svg', onTap: widget.reply!),
],
const CustomDivider(),
BottomSheetActionWidget(
text: s.deleteComment,
iconPath: 'assets/icons/red_trash.svg',
onTap: widget.delete,
),
BottomSheetActionWidget(text: s.deleteComment, iconPath: 'assets/icons/red_trash.svg', onTap: widget.delete),
] else ...[
const CustomDivider(),
BottomSheetActionWidget(
text: s.complain,
iconPath: 'assets/icons/complain.svg',
onTap: widget.complain,
),
]
BottomSheetActionWidget(text: s.complain, iconPath: 'assets/icons/complain.svg', onTap: widget.complain),
],
],
),
);
}
}

View File

@@ -1,6 +1,6 @@
import 'package:eatvio_app/core/env_config.dart';
String baseUrl = EnvConfig.baseUrl;
String baseUrl = EnvConfig.devBaseUrl;
bool hidePlans = const bool.fromEnvironment('HIDE_PLANS', defaultValue: false);
const buildDate = String.fromEnvironment('BUILD_DATE', defaultValue: '');

View File

@@ -0,0 +1,72 @@
/// Константы приложения
class AppConstants {
// Размеры
static const double defaultPadding = 16.0;
static const double smallPadding = 8.0;
static const double largePadding = 24.0;
static const double extraLargePadding = 32.0;
// Радиусы
static const double defaultBorderRadius = 8.0;
static const double smallBorderRadius = 4.0;
static const double largeBorderRadius = 16.0;
// Высоты
static const double defaultButtonHeight = 48.0;
static const double smallButtonHeight = 36.0;
static const double largeButtonHeight = 56.0;
// Ширины
static const double maxContentWidth = 400.0;
static const double minContentWidth = 280.0;
// Анимации
static const Duration defaultAnimationDuration = Duration(milliseconds: 300);
static const Duration fastAnimationDuration = Duration(milliseconds: 150);
static const Duration slowAnimationDuration = Duration(milliseconds: 500);
// Таймауты
static const Duration defaultTimeout = Duration(seconds: 30);
static const Duration shortTimeout = Duration(seconds: 10);
static const Duration longTimeout = Duration(minutes: 2);
// Лимиты
static const int maxImageSize = 5 * 1024 * 1024; // 5MB
static const int maxVideoSize = 100 * 1024 * 1024; // 100MB
static const int maxTextLength = 1000;
static const int maxTitleLength = 100;
static const int maxDescriptionLength = 500;
// Пагинация
static const int defaultPageSize = 20;
static const int smallPageSize = 10;
static const int largePageSize = 50;
// Форматы
static const List<String> supportedImageFormats = ['jpg', 'jpeg', 'png', 'webp'];
static const List<String> supportedVideoFormats = ['mp4', 'mov', 'avi'];
// Регулярные выражения
static const String emailRegex = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$';
static const String phoneRegex = r'^\+?[1-9]\d{1,14}$';
static const String urlRegex = r'^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$';
// Сообщения
static const String defaultErrorMessage = 'Произошла ошибка. Попробуйте еще раз.';
static const String networkErrorMessage = 'Проблема с подключением к интернету.';
static const String timeoutErrorMessage = 'Превышено время ожидания.';
static const String unknownErrorMessage = 'Неизвестная ошибка.';
// Ключи для SharedPreferences
static const String welcomeScreenShownKey = 'welcomeScreenWasShowed';
static const String userTokenKey = 'userToken';
static const String userDataKey = 'userData';
static const String settingsKey = 'settings';
// API endpoints
static const String apiVersion = '/api';
static const String authEndpoint = '/auth';
static const String usersEndpoint = '/users';
static const String postsEndpoint = '/posts';
static const String commentsEndpoint = '/comments';
}

View File

@@ -1,32 +0,0 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
abstract class CredentialsStorage {
static Future<bool> haveCredentialsInStorage() async {
try {
if (await const FlutterSecureStorage().read(key: 'email') == null || await const FlutterSecureStorage().read(key: 'password') == null) {
return false;
} else {
return true;
}
} catch (e) {
return false;
}
}
static Future<Map<String, String>> getCredentialsFromStorage() async {
return {
'email': (await const FlutterSecureStorage().read(key: 'email'))!,
'password': (await const FlutterSecureStorage().read(key: 'password'))!,
};
}
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 Future<void> removeCredentials() async {
await const FlutterSecureStorage().delete(key: 'email');
await const FlutterSecureStorage().delete(key: 'password');
}
}

View File

@@ -0,0 +1,42 @@
import 'dart:developer';
import 'package:flutter_bloc/flutter_bloc.dart';
/// Базовый класс для всех Cubit'ов с унифицированной обработкой ошибок
abstract class BaseCubit<State> extends Cubit<State> {
BaseCubit(super.initialState);
/// Унифицированная обработка ошибок
void handleError(Object error, [StackTrace? stackTrace]) {
log('Error in $runtimeType: $error', stackTrace: stackTrace);
// Здесь можно добавить отправку ошибок в Sentry или другую систему мониторинга
}
/// Безопасное выполнение асинхронных операций
Future<T?> safeExecute<T>(Future<T> Function() operation) async {
try {
return await operation();
} catch (error, stackTrace) {
handleError(error, stackTrace);
return null;
}
}
/// Безопасное выполнение асинхронных операций с обработкой состояния
Future<T?> safeExecuteWithState<T>(
Future<T> Function() operation, {
required State loadingState,
required State Function(T result) successState,
required State Function(Object error) errorState,
}) async {
try {
emit(loadingState);
final result = await operation();
emit(successState(result));
return result;
} catch (error, stackTrace) {
handleError(error, stackTrace);
emit(errorState(error));
return null;
}
}
}

View File

@@ -0,0 +1,28 @@
/// Утилиты для работы с состояниями Cubit'ов
class StateUtils {
/// Проверяет, является ли состояние загрузочным
static bool isLoading(dynamic state) {
return state.toString().contains('loading') || state.toString().contains('Loading');
}
/// Проверяет, является ли состояние загруженным
static bool isLoaded(dynamic state) {
return state.toString().contains('loaded') || state.toString().contains('Loaded');
}
/// Проверяет, является ли состояние ошибочным
static bool isError(dynamic state) {
return state.toString().contains('error') || state.toString().contains('Error');
}
/// Получает сообщение об ошибке из состояния
static String? getErrorMessage(dynamic state) {
if (isError(state)) {
// Попытка извлечь сообщение об ошибке из состояния
final stateString = state.toString();
final errorMatch = RegExp(r'error[:\s]+([^,}]+)').firstMatch(stateString);
return errorMatch?.group(1)?.trim();
}
return null;
}
}

View File

@@ -0,0 +1,144 @@
import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_cubit.dart';
import 'package:eatvio_app/core/views/cubit/views_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/blog_post/presentation/cubit/blog_post_cubit.dart';
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_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';
import 'package:eatvio_app/features/cancel_plan_bottom_sheet/cubit/cancel_plan_cubit.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_cubit.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart';
import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart';
import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_cubit.dart';
import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_cubit.dart';
import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart';
import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:eatvio_app/features/diets/presentation/cubit/about_diet_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';
import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/profile_cubit/profile_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/archive/consultations_archive_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/bloc/notification_settings_cubit/notification_settings_cubit.dart';
import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart';
import 'package:eatvio_app/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart';
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
import 'package:eatvio_app/features/order_info/cubit/order_info_cubit.dart';
import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_cubit.dart';
import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart' as profile_plans;
import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart';
import 'package:eatvio_app/features/profile/presentation/bloc/sub_cubit/sub_cubit.dart';
import 'package:eatvio_app/features/recipe/presentation/cubit/about_recipe_cubit.dart';
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart';
import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart';
import 'package:eatvio_app/features/user_details/presentation/cubit/user_details_cubit.dart';
import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart';
import 'package:get_it/get_it.dart';
/// Регистрация всех Cubit'ов в системе внедрения зависимостей
class CubitRegistrations {
static final sl = GetIt.instance;
/// Регистрация всех Cubit'ов
static void registerAll() {
_registerSingletonCubits();
_registerFactoryCubits();
}
/// Регистрация Cubit'ов как синглтонов
static void _registerSingletonCubits() {
sl.registerLazySingleton<NewMessagesCubit>(() => NewMessagesCubit());
sl.registerLazySingleton<NotificationsCubit>(() => NotificationsCubit(wsManager: sl(), notificationsRepository: sl()));
sl.registerLazySingleton<ProfileAvatarCubit>(() => ProfileAvatarCubit(profileRepository: sl()));
sl.registerLazySingleton<ChatsCubit>(() => ChatsCubit(chatRepository: sl(), wsManager: sl(), profileRepository: sl()));
sl.registerLazySingleton<InternetConnectionCubit>(() => InternetConnectionCubit());
}
/// Регистрация Cubit'ов как фабрик
static void _registerFactoryCubits() {
// Auth related cubits
sl.registerFactory<AuthCubit>(() => AuthCubit(authRepository: sl()));
sl.registerFactory<RegistrationCubit>(() => RegistrationCubit(registrationRepository: sl()));
sl.registerFactory<ResetPasswordCubit>(() => ResetPasswordCubit(resetPasswordRepository: sl()));
sl.registerFactory<VerifyEmailCubit>(() => VerifyEmailCubit(profileRepository: sl()));
// Feed related cubits
sl.registerFactory<FeedCubit>(() => FeedCubit(feedRepository: sl()));
sl.registerFactory<RecipesCubit>(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl(), aboutRecipeRepository: sl()));
sl.registerFactory<DietsCubit>(() => DietsCubit(feedRepository: sl(), dietsRepository: sl()));
sl.registerFactory<PlansCubit>(() => PlansCubit(feedRepository: sl(), planRepository: sl()));
sl.registerFactory<ConsultantsCubit>(() => ConsultantsCubit(feedRepository: sl()));
sl.registerFactory<CommentsCubit>(() => CommentsCubit(feedRepository: sl(), commentsRepository: sl()));
sl.registerFactory<FeedProfileCubit>(() => FeedProfileCubit(profileRepository: sl()));
// Chat related cubits
sl.registerFactory<ChatRoomCubit>(() => ChatRoomCubit(cardsRepository: sl(), chatRepository: sl(), wsManager: sl()));
// Profile related cubits
sl.registerFactory<ProfileCubit>(() => ProfileCubit(profileRepository: sl(), chatRepository: sl(), consultantRepository: sl()));
sl.registerFactory<profile_plans.PlansCubit>(() => profile_plans.PlansCubit(plansRepository: sl()));
sl.registerFactory<SubCubit>(() => SubCubit(profileRepository: sl()));
// Content creation cubits
sl.registerFactory<CreateDietCubit>(() => CreateDietCubit(profileRepository: sl(), createDietRepository: sl(), glossaryRepository: sl()));
sl.registerFactory<CreatePlanCubit>(() => CreatePlanCubit(createPlanRepository: sl()));
sl.registerFactory<CreateRecipeCubit>(() => CreateRecipeCubit(recipeRepository: sl(), glossaryRepository: sl()));
sl.registerFactory<BlogPostCreateCubit>(() => BlogPostCreateCubit(blogPostRepository: sl()));
// Content viewing cubits
sl.registerFactory<AboutDietCubit>(() => AboutDietCubit(dietsRepository: sl(), profileRepository: sl()));
sl.registerFactory<AboutPlanCubit>(() => AboutPlanCubit(planRepository: sl()));
sl.registerFactory<AboutRecipeCubit>(() => AboutRecipeCubit(recipeRepository: sl(), profileRepository: sl()));
sl.registerFactory<BlogPostCubit>(() => BlogPostCubit(sl()));
// User content cubits
sl.registerFactory<MyDietsCubit>(() => MyDietsCubit(myDietsRepository: sl(), createDietRepository: sl()));
sl.registerFactory<MyPlansCubit>(() => MyPlansCubit(myPlansRepository: sl()));
sl.registerFactory<MyRecipesCubit>(() => MyRecipesCubit(myRecipesRepository: sl(), aboutRecipeRepository: sl()));
// Other feature cubits
sl.registerFactory<CalendarCubit>(() => CalendarCubit(calendarRepository: sl()));
sl.registerFactory<DiaryCubit>(
() => DiaryCubit(
chatRepository: sl(),
diaryRepository: sl(),
glossaryRepository: sl(),
profileRepository: sl(),
dietRepository: sl(),
settingsRepository: sl(),
),
);
sl.registerFactory<SettingsCubit>(
() => SettingsCubit(profileRepository: sl(), settingsRepository: sl(), consultantRepository: sl(), consultationsRepository: sl()),
);
sl.registerFactory<RegisterConsultantCubit>(() => RegisterConsultantCubit(consultantRepository: sl()));
sl.registerFactory<ComplainingCubit>(() => ComplainingCubit(repository: sl()));
sl.registerFactory<UserCardsCubit>(() => UserCardsCubit(cardsRepository: sl()));
sl.registerFactory<OrderInfoCubit>(() => OrderInfoCubit(chatRepository: sl()));
sl.registerFactory<ConsultationsCubit>(() => ConsultationsCubit(consultationsRepository: sl()));
sl.registerFactory<ConsultantCommentsCubit>(() => ConsultantCommentsCubit(diaryRepository: sl()));
sl.registerFactory<CancelPlanCubit>(() => CancelPlanCubit(cancelPlanRepository: sl()));
sl.registerFactory<ConsultationsArchiveCubit>(() => ConsultationsArchiveCubit(consultationsRepository: sl()));
sl.registerFactory<ConsultantFinancesCubit>(() => ConsultantFinancesCubit(consultationsRepository: sl()));
sl.registerFactory<UserPurchasesCubit>(() => UserPurchasesCubit(consultationsRepository: sl()));
sl.registerFactory<BlockedUsersCubit>(() => BlockedUsersCubit(blockedUsersRepository: sl()));
sl.registerFactory<NotificationSettingsCubit>(() => NotificationSettingsCubit(notificationSettingsRepository: sl()));
sl.registerFactory<ConsultantRatingCubit>(() => ConsultantRatingCubit(feedRepository: sl(), repository: sl(), profileRepository: sl()));
sl.registerFactory<ViewsCubit>(() => ViewsCubit(viewsRepository: sl()));
sl.registerFactory<UserDetailsCubit>(() => UserDetailsCubit(profileRepository: sl()));
}
}

View File

@@ -0,0 +1,33 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/dio/phone_verify_error_interceptor.dart';
import 'package:eatvio_app/core/interceptors/token_interceptor.dart';
import 'package:get_it/get_it.dart';
/// Регистрация внешних зависимостей в системе внедрения зависимостей
class ExternalRegistrations {
static final sl = GetIt.instance;
/// Регистрация всех внешних зависимостей
static void registerAll() {
_registerDio();
}
/// Регистрация Dio HTTP клиента
static void _registerDio() {
sl.registerLazySingleton(
() => Dio(
BaseOptions(
baseUrl: baseUrl,
headers: {
'accept': 'application/json',
'content-type': 'application/json',
},
),
)..interceptors.addAll([
TokenInterceptor(),
PhoneVerifyErrorInterceptor(),
]),
);
}
}

View File

@@ -0,0 +1,89 @@
import 'package:eatvio_app/core/comments/repositories/comments_repository.dart';
import 'package:eatvio_app/core/views/repository/views_repository.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/blog_post_create/repositories/blog_post_repository.dart';
import 'package:eatvio_app/features/calendar/repositories/calendar_repository.dart';
import 'package:eatvio_app/features/cancel_plan_bottom_sheet/repository/cancel_plan_repository.dart';
import 'package:eatvio_app/features/chat/repositories/chat_repository.dart';
import 'package:eatvio_app/features/complaining/repositories/complaining_repository.dart';
import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart';
import 'package:eatvio_app/features/consultant_rating/repositories/consultant_rating_repository.dart';
import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart';
import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart';
import 'package:eatvio_app/features/create_plan/repositories/create_plan_repository.dart';
import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart';
import 'package:eatvio_app/features/diary/repositories/diary_repository.dart';
import 'package:eatvio_app/features/diets/repositories/diet_repository.dart';
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart';
import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart';
import 'package:eatvio_app/features/menu/repositories/notification_settings_repository.dart';
import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart';
import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart';
import 'package:eatvio_app/features/my_plans/repositories/my_diets_repository.dart';
import 'package:eatvio_app/features/notifications/repository/notifications_repository.dart';
import 'package:eatvio_app/features/plans/repositories/plan_repository.dart';
import 'package:eatvio_app/features/plans/repositories/plans_repository.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:eatvio_app/features/recipe/repositories/about_recipe_repository.dart';
import 'package:eatvio_app/features/recipe/repositories/my_recipes_repository.dart';
import 'package:eatvio_app/features/registration/repositories/registration_repository.dart';
import 'package:eatvio_app/features/settings/repositories/service_repository.dart';
import 'package:get_it/get_it.dart';
/// Регистрация всех Repository в системе внедрения зависимостей
class RepositoryRegistrations {
static final sl = GetIt.instance;
/// Регистрация всех Repository
static void registerAll() {
_registerSingletonRepositories();
_registerFactoryRepositories();
}
/// Регистрация Repository как синглтонов
static void _registerSingletonRepositories() {
// Core repositories
sl.registerLazySingleton<ChatRepository>(() => ChatRepository(chatService: sl()));
sl.registerLazySingleton<FeedRepository>(() => FeedRepository(feedService: sl()));
sl.registerLazySingleton<ProfileRepository>(() => ProfileRepository(profileService: sl()));
sl.registerLazySingleton<CalendarRepository>(() => CalendarRepository(calendarService: sl()));
sl.registerLazySingleton<SettingsRepository>(() => SettingsRepository(settingsService: sl()));
sl.registerLazySingleton<ConsultantRepository>(() => ConsultantRepository(consultantService: sl()));
sl.registerLazySingleton<DiaryRepository>(() => DiaryRepository(diaryService: sl()));
sl.registerLazySingleton<GlossaryRepository>(() => GlossaryRepository(glossaryService: sl()));
sl.registerLazySingleton<DietRepository>(() => DietRepository(dietsService: sl()));
sl.registerLazySingleton<PlansRepository>(() => PlansRepository(plansService: sl()));
sl.registerLazySingleton<PlanRepository>(() => PlanRepository(planService: sl()));
sl.registerLazySingleton<AboutRecipeRepository>(() => AboutRecipeRepository(recipeService: sl()));
sl.registerLazySingleton<RecipeRepository>(() => RecipeRepository(recipeService: sl()));
sl.registerLazySingleton<ComplainingRepository>(() => ComplainingRepository(service: sl()));
sl.registerLazySingleton<AuthRepository>(() => AuthRepository(authService: sl()));
sl.registerLazySingleton<CardsRepository>(() => CardsRepository(cardsService: sl()));
sl.registerLazySingleton<ConsultationsRepository>(() => ConsultationsRepository(consultationService: sl()));
sl.registerLazySingleton<CancelPlanRepository>(() => CancelPlanRepository(cancelService: sl()));
sl.registerLazySingleton<CreatePlanRepository>(() => CreatePlanRepository(createPlanService: sl()));
sl.registerLazySingleton<NotificationsRepository>(() => NotificationsRepository(notificationsService: sl()));
sl.registerLazySingleton<BlockedUsersRepository>(() => BlockedUsersRepository(blockedUserService: sl()));
sl.registerLazySingleton<CommentsRepository>(() => CommentsRepository(service: sl()));
sl.registerLazySingleton<NotificationSettingsRepository>(() => NotificationSettingsRepository(sl()));
// Content repositories
sl.registerLazySingleton<BlogPostRepository>(() => BlogPostRepository(blogPostService: sl()));
sl.registerLazySingleton<CreateDietRepository>(() => CreateDietRepository(createDietService: sl()));
sl.registerLazySingleton<MyDietsRepository>(() => MyDietsRepository(myDietsService: sl()));
sl.registerLazySingleton<MyPlansRepository>(() => MyPlansRepository(myPlansService: sl()));
// Auth repositories
sl.registerLazySingleton<RegistrationRepository>(() => RegistrationRepository(registrationService: sl()));
}
/// Регистрация Repository как фабрик
static void _registerFactoryRepositories() {
sl.registerFactory<MyRecipesRepository>(() => MyRecipesRepository(myRecipesService: sl()));
sl.registerFactory<ResetPasswordRepository>(() => ResetPasswordRepository(resetPasswordService: sl()));
sl.registerFactory<ConsultantRatingRepository>(() => ConsultantRatingRepository(sl()));
sl.registerFactory<ViewsRepository>(() => ViewsRepository(viewsService: sl()));
}
}

View File

@@ -0,0 +1,88 @@
import 'package:eatvio_app/core/comments/services/comments_service.dart';
import 'package:eatvio_app/core/firebase_api.dart';
import 'package:eatvio_app/core/views/service/views_service.dart';
import 'package:eatvio_app/core/websockets/ws_service.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_create/services/blog_post_service.dart';
import 'package:eatvio_app/features/calendar/services/calendar_service.dart';
import 'package:eatvio_app/features/cancel_plan_bottom_sheet/service/cancel_plan_service.dart';
import 'package:eatvio_app/features/chat/services/chat_service.dart';
import 'package:eatvio_app/features/complaining/services/complaining_service.dart';
import 'package:eatvio_app/features/consultant/become_consultant/services/consultant_service.dart';
import 'package:eatvio_app/features/consultant_rating/services/consultant_rating_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/services/recipe_service.dart';
import 'package:eatvio_app/features/diary/services/diary_service.dart';
import 'package:eatvio_app/features/diets/services/diet_service.dart';
import 'package:eatvio_app/features/feed/services/feed_service.dart';
import 'package:eatvio_app/features/glossary/services/glossary_service.dart';
import 'package:eatvio_app/features/menu/services/blocked_users_service.dart';
import 'package:eatvio_app/features/menu/services/notification_settings_service.dart';
import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart';
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/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:get_it/get_it.dart';
/// Регистрация всех Service в системе внедрения зависимостей
class ServiceRegistrations {
static final sl = GetIt.instance;
/// Регистрация всех Service
static void registerAll() {
_registerSingletonServices();
}
/// Регистрация Service как синглтонов
static void _registerSingletonServices() {
// Core services
sl.registerLazySingleton<ChatService>(() => ChatService(sl()));
sl.registerLazySingleton<FeedService>(() => FeedService(sl()));
sl.registerLazySingleton<ProfileService>(() => ProfileService(sl()));
sl.registerLazySingleton<CalendarService>(() => CalendarService(sl()));
sl.registerLazySingleton<SettingsService>(() => SettingsService(sl()));
sl.registerLazySingleton<ConsultantService>(() => ConsultantService(sl()));
sl.registerLazySingleton<DiaryService>(() => DiaryService(sl()));
sl.registerLazySingleton<GlossaryService>(() => GlossaryService(sl()));
sl.registerLazySingleton<DietService>(() => DietService(sl()));
sl.registerLazySingleton<PlansService>(() => PlansService(sl()));
sl.registerLazySingleton<PlanService>(() => PlanService(sl()));
sl.registerLazySingleton<MyDietsService>(() => MyDietsService(sl()));
sl.registerLazySingleton<MyPlansService>(() => MyPlansService(sl()));
sl.registerLazySingleton<WsService>(() => WsService(sl()));
sl.registerLazySingleton<AboutRecipeService>(() => AboutRecipeService(sl()));
sl.registerLazySingleton<ComplainingService>(() => ComplainingService(sl()));
sl.registerLazySingleton<AuthService>(() => AuthService(sl()));
sl.registerLazySingleton<CardsService>(() => CardsService(sl()));
sl.registerLazySingleton<ConsultationService>(() => ConsultationService(sl()));
sl.registerLazySingleton<CancelPlanService>(() => CancelPlanService(sl()));
sl.registerLazySingleton<CreatePlanService>(() => CreatePlanService(sl()));
sl.registerLazySingleton<NotificationsService>(() => NotificationsService(sl()));
sl.registerLazySingleton<FirebaseApi>(() => FirebaseApi());
sl.registerLazySingleton<ResetPasswordService>(() => ResetPasswordService(sl()));
sl.registerLazySingleton<BlockedUserService>(() => BlockedUserService(sl()));
sl.registerLazySingleton<CommentsService>(() => CommentsService(sl()));
sl.registerLazySingleton<MyRecipesService>(() => MyRecipesService(sl()));
sl.registerLazySingleton<ConsultantRatingService>(() => ConsultantRatingService(sl()));
sl.registerLazySingleton<NotificationSettingsService>(() => NotificationSettingsService(sl()));
sl.registerLazySingleton<ViewsService>(() => ViewsService(sl()));
// Content services
sl.registerLazySingleton<CreateRecipeService>(() => CreateRecipeService(sl()));
sl.registerLazySingleton<BlogPostService>(() => BlogPostService(sl()));
sl.registerLazySingleton<CreateDietService>(() => CreateDietService(sl()));
// Auth services
sl.registerLazySingleton<RegistrationService>(() => RegistrationService(sl()));
}
}

View File

@@ -7,6 +7,9 @@ class EnvConfig {
// Sentry Configuration
static String get sentryDsn => dotenv.env['SENTRY_DSN'] ?? '';
// AppMetrica Configuration
static String get appMetricaApiKey => dotenv.env['APPMETRICA_KEY'] ?? '';
// Firebase Configuration
static String get firebaseProjectId => dotenv.env['FIREBASE_PROJECT_ID'] ?? '';
static String get firebaseDatabaseUrl => dotenv.env['FIREBASE_DATABASE_URL'] ?? '';
@@ -40,4 +43,12 @@ class EnvConfig {
// Connection Check URLs
static String get googleCheckUrl => dotenv.env['GOOGLE_CHECK_URL'] ?? '';
static String get yandexCheckUrl => dotenv.env['YANDEX_CHECK_URL'] ?? '';
// VK ID Configuration
static String get vkClientId => dotenv.env['VK_CLIENT_ID'] ?? '';
// Yandex ID Configuration
static String get yandexClientId => dotenv.env['YANDEX_CLIENT_ID'] ?? '';
static String get yandexClientSecret => dotenv.env['YANDEX_CLIENT_SECRET'] ?? '';
static String get yandexRedirectUri => dotenv.env['YANDEX_REDIRECT_URI'] ?? '';
}

View File

@@ -1,28 +1,39 @@
import 'dart:developer';
import 'dart:io';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:flutter/services.dart';
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';
import 'package:flutter/services.dart' show MethodChannel, PlatformException;
class FirebaseApi {
final _firebaseMessaging = FirebaseMessaging.instance;
Future<String?> getToken() async {
await _firebaseMessaging.requestPermission();
return await _firebaseMessaging.getToken();
String? token;
if (await _isHuaweiDevice()) {
token = await _getHuaweiPushTokenNative();
} else {
await _firebaseMessaging.requestPermission();
token = await _firebaseMessaging.getToken();
}
return token;
}
Future<void> registerPushToken() async {
if (await TokenStorage.haveTokenInStorage()) {
try {
final deviceInfo = DeviceInfoPlugin();
final isHuawei = Platform.isAndroid ? await _isHuaweiDevice() : false;
await sl<Dio>().post(
'/api/profile/notification/token/register',
data: {
'platform': Platform.isIOS ? 'ios' : 'android',
'platform': Platform.isIOS ? 'ios' : (isHuawei ? 'huawei' : 'android'),
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
'token': await getToken(),
},
@@ -32,4 +43,32 @@ class FirebaseApi {
}
}
}
Future<bool> _isHuaweiDevice() async {
if (!Platform.isAndroid) return false;
try {
final info = await DeviceInfoPlugin().androidInfo;
final brand = info.brand.toLowerCase();
final manufacturer = info.manufacturer.toLowerCase();
return brand.contains('huawei') || brand.contains('honor') || manufacturer.contains('huawei') || manufacturer.contains('honor');
} catch (_) {
return false;
}
}
Future<String?> _getHuaweiPushTokenNative() async {
try {
const channel = MethodChannel('com.eatvioapp/hms_push');
final String? token = await channel.invokeMethod<String>('getHuaweiToken');
return token;
} on PlatformException catch (e, st) {
showErrorToast('$e $st');
log(e.toString(), stackTrace: st);
return null;
} catch (e, st) {
showErrorToast('$e $st');
log(e.toString(), stackTrace: st);
return null;
}
}
}

View File

@@ -0,0 +1,73 @@
import 'dart:async';
import 'dart:developer';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:eatvio_app/core/env_config.dart';
import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_state.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:http/http.dart' as http;
class InternetConnectionCubit extends Cubit<InternetConnectionState> {
InternetConnectionCubit() : super(InternetConnectionState()) {
checkConnection();
_connectivitySubscription = _connectivity.onConnectivityChanged.listen(_updateAccessStatus);
_listener = AppLifecycleListener(
onResume: () async {
_connectivitySubscription.resume();
await checkConnection();
},
onHide: _connectivitySubscription.pause,
onPause: _connectivitySubscription.pause,
);
}
final Connectivity _connectivity = Connectivity();
late StreamSubscription<List<ConnectivityResult>> _connectivitySubscription;
late final AppLifecycleListener _listener;
Future<void> checkConnection() async {
late List<ConnectivityResult> result;
try {
result = await _connectivity.checkConnectivity();
} on PlatformException catch (e) {
log('Couldn\'t check connectivity status', error: e);
return;
}
return _updateAccessStatus(result);
}
Future<void> _updateAccessStatus(List<ConnectivityResult> result) async {
final hasInternet = await _checkInternetAccess(result);
if (!hasInternet) showInternetConnectionToast();
emit(state.copyWith(isInternetAvailable: hasInternet));
}
Future<bool> _checkInternetAccess(List<ConnectivityResult> result) async {
if (result.contains(ConnectivityResult.none)) {
return false;
}
final uris = [Uri.parse(EnvConfig.googleCheckUrl), Uri.parse(EnvConfig.yandexCheckUrl), Uri.parse(EnvConfig.devBaseUrl), Uri.parse(EnvConfig.baseUrl)];
for (final uri in uris) {
try {
final response = await http.get(uri).timeout(const Duration(seconds: 3));
return response.statusCode >= 100 && response.statusCode < 600;
} catch (e) {
continue;
}
}
return false;
}
@override
Future<void> close() {
_connectivitySubscription.cancel();
_listener.dispose();
return super.close();
}
}

View File

@@ -0,0 +1,13 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'internet_connection_state.freezed.dart';
@freezed
abstract class InternetConnectionState with _$InternetConnectionState {
factory InternetConnectionState({
@Default(true) bool isInternetAvailable,
}) = _InternetConnectionState;
}

View File

@@ -1,256 +1,27 @@
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/di/cubit_registrations.dart';
import 'package:eatvio_app/core/di/external_registrations.dart';
import 'package:eatvio_app/core/di/repository_registrations.dart';
import 'package:eatvio_app/core/di/service_registrations.dart';
import 'package:eatvio_app/core/websockets/ws_manager.dart';
import 'package:eatvio_app/core/websockets/ws_service.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';
import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_cubit.dart';
import 'package:eatvio_app/features/complaining/repositories/complaining_repository.dart';
import 'package:eatvio_app/features/complaining/services/complaining_service.dart';
import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_cubit.dart';
import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart';
import 'package:eatvio_app/features/consultations/services/consultation_service.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/archive/consultations_archive_cubit.dart';
import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_cubit.dart';
import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart';
import 'package:eatvio_app/features/create_diet/services/create_diet_service.dart';
import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart';
import 'package:eatvio_app/features/create_plan/repositories/create_plan_repository.dart';
import 'package:eatvio_app/features/create_plan/services/create_plan_service.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_cubit.dart';
import 'package:eatvio_app/features/diets/repositories/diet_repository.dart';
import 'package:eatvio_app/features/diets/services/diet_service.dart';
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart';
import 'package:eatvio_app/features/blog_post_create/repositories/blog_post_repository.dart';
import 'package:eatvio_app/features/blog_post_create/services/blog_post_service.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_cubit.dart';
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart';
import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart';
import 'package:eatvio_app/features/menu/services/blocked_users_service.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
import 'package:eatvio_app/features/calendar/presentation/bloc/calendar_cubit.dart';
import 'package:eatvio_app/features/calendar/repositories/calendar_repository.dart';
import 'package:eatvio_app/features/calendar/services/calendar_service.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.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/chat/services/chat_service.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart';
import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart';
import 'package:eatvio_app/features/consultant/become_consultant/services/consultant_service.dart';
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/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';
import 'package:eatvio_app/features/feed/presentation/bloc/profile_cubit/profile_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart';
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
import 'package:eatvio_app/features/feed/services/feed_service.dart';
import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart';
import 'package:eatvio_app/features/glossary/services/glossary_service.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart';
import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart';
import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart';
import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart';
import 'package:eatvio_app/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart';
import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart';
import 'package:eatvio_app/features/my_finance/services/cards_service.dart';
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart';
import 'package:eatvio_app/features/my_plans/repositories/my_diets_repository.dart';
import 'package:eatvio_app/features/my_plans/services/my_plans_service.dart';
import 'package:eatvio_app/features/notifications/repository/notifications_repository.dart';
import 'package:eatvio_app/features/notifications/service/notifications_service.dart';
import 'package:eatvio_app/features/order_info/cubit/order_info_cubit.dart';
import 'package:eatvio_app/features/plans/repositories/plans_repository.dart';
import 'package:eatvio_app/features/plans/services/plans_service.dart';
import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_cubit.dart';
import 'package:eatvio_app/features/plans/repositories/plan_repository.dart';
import 'package:eatvio_app/features/plans/services/plan_service.dart';
import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart';
import 'package:eatvio_app/features/profile/presentation/bloc/sub_cubit/sub_cubit.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:eatvio_app/features/profile/services/profile_service.dart';
import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart';
import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart';
import 'package:eatvio_app/features/create_recipe/services/recipe_service.dart';
import 'package:eatvio_app/features/recipe/presentation/cubit/about_recipe_cubit.dart';
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart';
import 'package:eatvio_app/features/recipe/repositories/about_recipe_repository.dart';
import 'package:eatvio_app/features/recipe/repositories/my_recipes_repository.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/presentation/bloc/registration_cubit/registration_cubit.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart';
import 'package:eatvio_app/features/registration/repositories/registration_repository.dart';
import 'package:eatvio_app/features/registration/services/registration_service.dart';
import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart';
import 'package:eatvio_app/features/settings/repositories/service_repository.dart';
import 'package:eatvio_app/features/settings/services/settings_service.dart';
import 'package:eatvio_app/features/user_details/presentation/cubit/user_details_cubit.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:get_it/get_it.dart';
import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart' as profile_plans;
/// Глобальный экземпляр GetIt для внедрения зависимостей
final sl = GetIt.instance;
/// Инициализация системы внедрения зависимостей
Future<void> init() async {
// Cubit singletons
// Регистрация внешних зависимостей (Dio, etc.)
ExternalRegistrations.registerAll();
// Регистрация сервисов
ServiceRegistrations.registerAll();
// Регистрация репозиториев
RepositoryRegistrations.registerAll();
// Регистрация Cubit'ов
CubitRegistrations.registerAll();
// Регистрация WebSocket менеджера
sl.registerLazySingleton<WsManager>(() => WsManager(wsService: sl()));
sl.registerLazySingleton<NewMessagesCubit>(() => NewMessagesCubit());
sl.registerLazySingleton<NotificationsCubit>(() => NotificationsCubit(wsManager: sl(), notificationsRepository: sl()));
sl.registerLazySingleton<ProfileAvatarCubit>(() => ProfileAvatarCubit(profileRepository: sl()));
sl.registerLazySingleton<ChatsCubit>(() => ChatsCubit(chatRepository: sl(), wsManager: sl(), profileRepository: sl()));
// Cubit factories
sl.registerFactory<FeedCubit>(() => FeedCubit(feedRepository: sl()));
sl.registerFactory<RegistrationCubit>(() => RegistrationCubit(registrationRepository: sl()));
sl.registerFactory<ChatRoomCubit>(() => ChatRoomCubit(cardsRepository: sl(), chatRepository: sl(), wsManager: sl()));
sl.registerFactory<RecipesCubit>(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl(), aboutRecipeRepository: sl()));
sl.registerFactory<CalendarCubit>(() => CalendarCubit(calendarRepository: sl()));
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(), 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()));
sl.registerFactory<DiaryCubit>(() => DiaryCubit(
chatRepository: sl(),
diaryRepository: sl(),
glossaryRepository: sl(),
profileRepository: sl(),
dietRepository: sl(),
settingsRepository: sl(),
));
sl.registerFactory<AboutDietCubit>(() => AboutDietCubit(dietsRepository: sl(), profileRepository: sl()));
sl.registerFactory<BlogPostCreateCubit>(() => BlogPostCreateCubit(blogPostRepository: sl()));
sl.registerFactory<SubCubit>(() => SubCubit(profileRepository: sl()));
sl.registerFactory<CreateDietCubit>(() => CreateDietCubit(profileRepository: sl(), createDietRepository: sl(), glossaryRepository: sl()));
sl.registerFactory<CreatePlanCubit>(() => CreatePlanCubit(createPlanRepository: sl()));
sl.registerFactory<FeedProfileCubit>(() => FeedProfileCubit(profileRepository: sl()));
sl.registerFactory<AboutPlanCubit>(() => AboutPlanCubit(planRepository: sl()));
sl.registerFactory<CreateRecipeCubit>(() => CreateRecipeCubit(recipeRepository: sl(), glossaryRepository: sl()));
sl.registerFactory<MyDietsCubit>(() => MyDietsCubit(myDietsRepository: sl(), createDietRepository: sl()));
sl.registerFactory<MyPlansCubit>(() => MyPlansCubit(myPlansRepository: sl()));
sl.registerFactory<UserDetailsCubit>(() => UserDetailsCubit(profileRepository: sl()));
sl.registerFactory<ConsultantsCubit>(() => ConsultantsCubit(feedRepository: sl()));
sl.registerFactory<AboutRecipeCubit>(() => AboutRecipeCubit(recipeRepository: sl(), profileRepository: sl()));
sl.registerFactory<AuthCubit>(() => AuthCubit(authRepository: sl()));
sl.registerFactory<ComplainingCubit>(() => ComplainingCubit(repository: sl()));
sl.registerFactory<UserCardsCubit>(() => UserCardsCubit(cardsRepository: sl()));
sl.registerFactory<OrderInfoCubit>(() => OrderInfoCubit(chatRepository: sl()));
sl.registerFactory<ConsultationsCubit>(() => ConsultationsCubit(consultationsRepository: sl()));
sl.registerFactory<ConsultantCommentsCubit>(() => ConsultantCommentsCubit(diaryRepository: sl()));
sl.registerFactory<CancelPlanCubit>(() => CancelPlanCubit(cancelPlanRepository: sl()));
sl.registerFactory<VerifyEmailCubit>(() => VerifyEmailCubit(profileRepository: sl()));
sl.registerFactory<MyRecipesCubit>(() => MyRecipesCubit(myRecipesRepository: sl(), aboutRecipeRepository: sl()));
sl.registerFactory<ConsultationsArchiveCubit>(() => ConsultationsArchiveCubit(consultationsRepository: sl()));
sl.registerFactory<ConsultantFinancesCubit>(() => ConsultantFinancesCubit(consultationsRepository: sl()));
sl.registerFactory<UserPurchasesCubit>(() => UserPurchasesCubit(consultationsRepository: sl()));
sl.registerFactory<ResetPasswordCubit>(() => ResetPasswordCubit(resetPasswordRepository: sl()));
sl.registerFactory<BlogPostCubit>(() => BlogPostCubit(sl()));
sl.registerFactory<BlockedUsersCubit>(() => BlockedUsersCubit(blockedUsersRepository: sl()));
// Repository
sl.registerLazySingleton<ChatRepository>(() => ChatRepository(chatService: sl()));
sl.registerLazySingleton<FeedRepository>(() => FeedRepository(feedService: sl()));
sl.registerLazySingleton<RegistrationRepository>(() => RegistrationRepository(registrationService: sl()));
sl.registerLazySingleton<CalendarRepository>(() => CalendarRepository(calendarService: sl()));
sl.registerLazySingleton<ProfileRepository>(() => ProfileRepository(profileService: sl()));
sl.registerLazySingleton<RecipeRepository>(() => RecipeRepository(recipeService: sl()));
sl.registerLazySingleton<SettingsRepository>(() => SettingsRepository(settingsService: sl()));
sl.registerLazySingleton<ConsultantRepository>(() => ConsultantRepository(consultantService: sl()));
sl.registerLazySingleton<DiaryRepository>(() => DiaryRepository(diaryService: sl()));
sl.registerLazySingleton<GlossaryRepository>(() => GlossaryRepository(glossaryService: sl()));
sl.registerLazySingleton<DietRepository>(() => DietRepository(dietsService: sl()));
sl.registerLazySingleton<BlogPostRepository>(() => BlogPostRepository(blogPostService: sl()));
sl.registerLazySingleton<CreateDietRepository>(() => CreateDietRepository(createDietService: sl()));
sl.registerLazySingleton<PlansRepository>(() => PlansRepository(plansService: sl()));
sl.registerLazySingleton<PlanRepository>(() => PlanRepository(planService: sl()));
sl.registerLazySingleton<MyDietsRepository>(() => MyDietsRepository(myDietsService: sl()));
sl.registerLazySingleton<MyPlansRepository>(() => MyPlansRepository(myPlansService: sl()));
sl.registerLazySingleton<AboutRecipeRepository>(() => AboutRecipeRepository(recipeService: sl()));
sl.registerLazySingleton<ComplainingRepository>(() => ComplainingRepository(service: sl()));
sl.registerLazySingleton<AuthRepository>(() => AuthRepository(authService: sl()));
sl.registerLazySingleton<CardsRepository>(() => CardsRepository(cardsService: sl()));
sl.registerLazySingleton<ConsultationsRepository>(() => ConsultationsRepository(consultationService: sl()));
sl.registerLazySingleton<CancelPlanRepository>(() => CancelPlanRepository(cancelService: sl()));
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()));
sl.registerLazySingleton<FeedService>(() => FeedService(sl()));
sl.registerLazySingleton<RegistrationService>(() => RegistrationService(sl()));
sl.registerLazySingleton<CalendarService>(() => CalendarService(sl()));
sl.registerLazySingleton<ProfileService>(() => ProfileService(sl()));
sl.registerLazySingleton<CreateRecipeService>(() => CreateRecipeService(sl()));
sl.registerLazySingleton<SettingsService>(() => SettingsService(sl()));
sl.registerLazySingleton<ConsultantService>(() => ConsultantService(sl()));
sl.registerLazySingleton<DiaryService>(() => DiaryService(sl()));
sl.registerLazySingleton<GlossaryService>(() => GlossaryService(sl()));
sl.registerLazySingleton<DietService>(() => DietService(sl()));
sl.registerLazySingleton<BlogPostService>(() => BlogPostService(sl()));
sl.registerLazySingleton<CreateDietService>(() => CreateDietService(sl()));
sl.registerLazySingleton<PlansService>(() => PlansService(sl()));
sl.registerLazySingleton<PlanService>(() => PlanService(sl()));
sl.registerLazySingleton<MyDietsService>(() => MyDietsService(sl()));
sl.registerLazySingleton<MyPlansService>(() => MyPlansService(sl()));
sl.registerLazySingleton<WsService>(() => WsService(sl()));
sl.registerLazySingleton<AboutRecipeService>(() => AboutRecipeService(sl()));
sl.registerLazySingleton<ComplainingService>(() => ComplainingService(sl()));
sl.registerLazySingleton<AuthService>(() => AuthService(sl()));
sl.registerLazySingleton<CardsService>(() => CardsService(sl()));
sl.registerLazySingleton<ConsultationService>(() => ConsultationService(sl()));
sl.registerLazySingleton<CancelPlanService>(() => CancelPlanService(sl()));
sl.registerLazySingleton<CreatePlanService>(() => CreatePlanService(sl()));
sl.registerLazySingleton<NotificationsService>(() => NotificationsService(sl()));
sl.registerLazySingleton<MyRecipesService>(() => MyRecipesService(sl()));
sl.registerLazySingleton<FirebaseApi>(() => FirebaseApi());
sl.registerLazySingleton<ResetPasswordService>(() => ResetPasswordService(sl()));
sl.registerLazySingleton<BlockedUserService>(() => BlockedUserService(sl()));
sl.registerLazySingleton<CommentsService>(() => CommentsService(sl()));
// External
sl.registerLazySingleton(
() => Dio(
BaseOptions(
baseUrl: baseUrl,
headers: {
'accept': 'application/json',
'content-type': 'application/json',
},
),
)..interceptors.addAll(
[
TokenInterceptor(),
PhoneVerifyErrorInterceptor(),
],
),
);
sl.registerLazySingleton(() async => const FlutterSecureStorage());
}
}

View File

@@ -0,0 +1,37 @@
import 'package:eatvio_app/core/routing/route_constants.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/registration/presentation/pages/registration_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/verify_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/verify_email_screen.dart';
import 'package:go_router/go_router.dart';
/// Маршруты для аутентификации
class AuthRoutes {
/// Создание маршрутов аутентификации
static List<RouteBase> createRoutes() {
return [
GoRoute(
path: RouteConstants.authScreen,
name: 'authScreen',
builder: (context, state) => const AuthScreen(),
routes: [
GoRoute(path: RouteConstants.registrationScreen, name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()),
GoRoute(
path: RouteConstants.forgetPasswordScreen,
name: 'forgetPasswordScreen',
builder: (context, state) => ForgetPasswordScreen(isAuth: state.extra as bool),
),
GoRoute(path: RouteConstants.scanQRCodeScreen, name: 'scanQRCodeScreen', builder: (context, state) => const ScanQRCodeScreen()),
],
),
GoRoute(
path: RouteConstants.verifyScreen,
name: 'verifyScreen',
builder: (context, state) => VerifyScreen(passKey: state.extra as String),
),
GoRoute(path: RouteConstants.verifyEmailScreen, name: 'verifyEmailScreen', builder: (context, state) => const VerifyEmailScreen()),
];
}
}

View File

@@ -0,0 +1,73 @@
import 'package:eatvio_app/core/routing/route_constants.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/diets/presentation/pages/about_diet_screen.dart';
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
import 'package:eatvio_app/features/plans/presentation/pages/about_plan_screen.dart';
import 'package:eatvio_app/features/recipe/presentation/pages/about_recipe_screen.dart';
import 'package:go_router/go_router.dart';
/// Маршруты для контента
class ContentRoutes {
/// Создание маршрутов контента
static List<RouteBase> createRoutes() {
return [
GoRoute(
path: RouteConstants.aboutPlanScreen,
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(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
},
),
GoRoute(
path: RouteConstants.aboutRecipesScreen,
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(recipeSlug: data['recipe'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
},
),
GoRoute(
path: RouteConstants.aboutDietScreen,
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(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
},
),
GoRoute(
path: RouteConstants.blogPostCreateScreen,
name: 'blogPostCreateScreen',
builder: (context, state) => BlogPostCreateScreen(feedBlogPostModel: state.extra != null ? state.extra as FeedBlogPostModel : null),
),
GoRoute(
path: RouteConstants.blogPostScreen,
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);
},
),
];
}
}

View File

@@ -0,0 +1,94 @@
import 'package:eatvio_app/core/routing/route_constants.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart';
import 'package:eatvio_app/features/chat/presentation/pages/chats_screen.dart';
import 'package:eatvio_app/features/consultations/presentation/pages/consultations_screen.dart';
import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart';
import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart';
import 'package:eatvio_app/features/menu/presentation/pages/menu_screen.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_screen.dart';
import 'package:eatvio_app/features/profile/presentation/pages/profile_screen.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/websockets/ws_manager.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'dart:developer';
/// Маршруты для основной навигации
class MainNavigationRoutes {
/// Создание маршрутов основной навигации
static List<RouteBase> createRoutes() {
return [
ShellRoute(
builder: (context, state, child) {
return BlocBuilder<ProfileAvatarCubit, ProfileAvatarState>(
builder: (BuildContext context, ProfileAvatarState state) {
if (state is ProfileAvatarStateLoaded) {
return BottomNavigationBarWidget(profileModel: state.profile, child: child);
}
return const SizedBox.shrink();
},
);
},
routes: [
GoRoute(
path: RouteConstants.myProfileScreen,
name: 'myProfileScreen',
pageBuilder: (context, state) {
return const NoTransitionPage(child: ProfileScreen());
},
),
GoRoute(
path: RouteConstants.feedScreen,
name: 'feedScreen',
pageBuilder: (context, state) => const NoTransitionPage(child: FeedScreen()),
redirect: (context, state) async {
try {
await sl<WsManager>().initialize();
} catch (e, st) {
log(e.toString(), stackTrace: st);
}
return null;
},
),
GoRoute(
path: RouteConstants.diaryScreen,
name: 'diaryScreen',
pageBuilder: (context, state) => const NoTransitionPage(child: DiaryScreen()),
),
GoRoute(
path: ConsultationsScreen.path,
name: ConsultationsScreen.name,
pageBuilder: (context, state) => const NoTransitionPage(child: ConsultationsScreen()),
),
GoRoute(
path: RouteConstants.chatSelectScreen,
name: 'chatSelectScreen',
pageBuilder: (context, state) => const NoTransitionPage(child: ChatsScreen()),
),
GoRoute(
path: RouteConstants.notificationsScreen,
name: 'notificationsScreen',
pageBuilder: (context, state) => const NoTransitionPage(child: NotificationsScreen()),
),
GoRoute(
path: RouteConstants.menuScreen,
name: 'menuScreen',
pageBuilder: (context, state) => NoTransitionPage(
child: BlocBuilder<ProfileAvatarCubit, ProfileAvatarState>(
builder: (BuildContext context, ProfileAvatarState state) {
if (state is ProfileAvatarStateLoaded) {
return MenuScreen(profileModel: state.profile);
}
return const SizedBox.shrink();
},
),
),
),
],
),
];
}
}

View File

@@ -0,0 +1,229 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/firebase_api.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/routing/route_constants.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/features/calendar/presentation/pages/calendar_screen.dart';
import 'package:eatvio_app/features/chat/presentation/pages/chat_screen.dart';
import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart';
import 'package:eatvio_app/features/chat/presentation/pages/empty_chat_screen.dart';
import 'package:eatvio_app/features/chat/presentation/pages/payment_screen.dart';
import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/become_consultant_screen.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/stepper_become_consultant_screen.dart';
import 'package:eatvio_app/features/consultant_rating/presentation/consultant_rating_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';
import 'package:eatvio_app/features/create_plan/presentation/create_plan_screen.dart';
import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart';
import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart';
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
import 'package:eatvio_app/features/glossary/presentation/pages/glossary_screen.dart';
import 'package:eatvio_app/features/menu/presentation/pages/blocked_users_screen.dart';
import 'package:eatvio_app/features/menu/presentation/pages/notifications_settings_screen.dart';
import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart';
import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart';
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/order_info/presentation/info_by_order_screen.dart';
import 'package:eatvio_app/features/profile/presentation/pages/profile_screen.dart';
import 'package:eatvio_app/features/profile/presentation/pages/subs_screen.dart';
import 'package:eatvio_app/features/recipe/presentation/pages/my_recipes_screen.dart';
import 'package:eatvio_app/features/support/presentation/pages/support_screen.dart';
import 'package:eatvio_app/features/update_screen/presentation/update_screen.dart';
import 'package:eatvio_app/features/user_details/presentation/user_details_screen.dart';
import 'package:eatvio_app/features/welcome/presentation/pages/welcome_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/consultations_archive_screen.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.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';
/// Остальные маршруты приложения
class OtherRoutes {
/// Создание остальных маршрутов
static List<RouteBase> createRoutes() {
return [
// Welcome and update screens
GoRoute(
path: RouteConstants.welcomeScreen,
name: 'welcomeScreen',
builder: (context, state) => const WelcomeScreen(),
redirect: (context, state) async {
try {
final sharedPreferences = await SharedPreferences.getInstance();
final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed') ?? false;
await sl<FirebaseApi>().registerPushToken();
if (welcomeScreenWasShowed == false) {
return null;
} else if (await TokenStorage.haveTokenInStorage()) {
return RouteConstants.feedScreen;
} else {
return RouteConstants.authScreen;
}
} catch (e) {
return RouteConstants.feedScreen;
}
},
),
GoRoute(
path: RouteConstants.updateScreen,
name: 'updateScreen',
builder: (context, state) => const UpdateScreen(),
redirect: (context, state) async {
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 RouteConstants.welcomeScreen;
}
} catch (e) {
return RouteConstants.welcomeScreen;
}
},
),
// Archive routes
GoRoute(path: ConsultationsArchiveScreen.path, name: ConsultationsArchiveScreen.name, builder: (context, state) => const ConsultationsArchiveScreen()),
GoRoute(path: ConsultantFinancesScreen.path, name: ConsultantFinancesScreen.name, builder: (context, state) => const ConsultantFinancesScreen()),
GoRoute(path: UserPurchasesScreen.path, name: UserPurchasesScreen.name, builder: (context, state) => const UserPurchasesScreen()),
// Finance routes
GoRoute(path: RouteConstants.userFinanceScreen, name: 'userFinanceScreen', builder: (context, state) => const UserFinanceScreen()),
GoRoute(
path: CardSettingsScreen.path,
name: CardSettingsScreen.name,
builder: (context, state) {
final chosenPlanId = state.extra as int?;
return CardSettingsScreen(chosenPlanId: chosenPlanId);
},
),
// Content creation routes
GoRoute(
path: RecipeCreateScreen.path,
name: RecipeCreateScreen.name,
builder: (context, state) => RecipeCreateScreen(recipe: state.extra != null ? state.extra as Recipe : null),
),
GoRoute(
path: CreateDietScreen.path,
name: CreateDietScreen.name,
builder: (context, state) {
final diet = state.extra as Diet?;
return CreateDietScreen(diet: diet);
},
),
GoRoute(
path: DietPreviewScreen.path,
name: DietPreviewScreen.name,
builder: (context, state) {
final data = state.extra as Diet;
return DietPreviewScreen(diet: data);
},
),
GoRoute(path: RouteConstants.createPlanScreen, name: 'createPlanScreen', builder: (context, state) => const CreatePlanScreen()),
GoRoute(path: MyDietsScreen.path, name: MyDietsScreen.name, builder: (context, state) => const MyDietsScreen()),
GoRoute(path: MyPlansScreen.path, name: MyPlansScreen.name, builder: (context, state) => const MyPlansScreen()),
GoRoute(path: MyRecipesScreen.path, name: MyRecipesScreen.name, builder: (context, state) => const MyRecipesScreen()),
// Profile routes
GoRoute(
path: RouteConstants.profileScreen,
name: 'profileScreen',
builder: (context, state) => ProfileScreen(author: state.extra as Author),
),
GoRoute(
path: RouteConstants.subsScreen,
name: 'subsScreen',
builder: (context, state) => SubsScreen(slug: state.pathParameters['slug']!, subType: state.extra as SubType),
),
// Chat routes
GoRoute(
path: RouteConstants.chatScreen,
name: 'chatScreen',
builder: (context, state) {
final data = state.extra as Map<String, dynamic>;
final uuid = data['uuid'] as String;
final sendInitialMessage = data['sendInitialMessage'] as bool?;
return ChatScreen(uuid: uuid, sendInitialMessage: sendInitialMessage);
},
),
GoRoute(
path: RouteConstants.emptyChatScreen,
name: 'emptyChatScreen',
builder: (context, state) => EmptyChatScreen(author: state.extra as Author),
),
// Consultant routes
GoRoute(path: RouteConstants.becomeConsultantScreen, name: 'becomeConsultantScreen', builder: (context, state) => const BecomeConsultantScreen()),
GoRoute(
path: RouteConstants.stepperBecomeConsultantScreen,
name: 'stepperBecomeConsultantScreen',
builder: (context, state) => StepperBecomeConsultantScreen(slug: state.extra == null ? null : state.extra as String),
),
GoRoute(path: RouteConstants.calendarScreen, name: 'calendarScreen', builder: (context, state) => const CalendarScreen()),
GoRoute(path: RouteConstants.supportScreen, name: 'supportScreen', builder: (context, state) => const SupportScreen()),
GoRoute(
path: RouteConstants.photoViewScreen,
name: 'photoViewScreen',
builder: (context, state) => PhotoViewScreen(imageUrls: state.extra as List<String>, index: int.parse(state.pathParameters['index']!)),
),
// Other routes
GoRoute(path: RouteConstants.glossaryScreen, name: 'glossaryScreen', builder: (context, state) => const GlossaryScreen()),
GoRoute(path: RouteConstants.blockedUsersScreen, name: 'blockedUsersScreen', builder: (context, state) => const BlockedUsersScreen()),
GoRoute(
path: RouteConstants.notificationsSettingsScreen,
name: 'notificationsSettingsScreen',
builder: (context, state) => const NotificationsSettingsScreen(),
),
GoRoute(
path: UserDetailsScreen.path,
name: UserDetailsScreen.name,
builder: (context, state) => UserDetailsScreen(slug: state.extra as String),
),
GoRoute(path: DevToolsScreen.path, name: DevToolsScreen.name, builder: (context, state) => const DevToolsScreen()),
GoRoute(path: ConsultantRatingScreen.path, name: ConsultantRatingScreen.name, builder: (context, state) => const ConsultantRatingScreen()),
GoRoute(
path: InfoByOrderScreen.path,
name: InfoByOrderScreen.name,
builder: (context, state) {
final data = state.extra as Map<String, dynamic>;
final planId = data['planId'] as int;
final successfulPayment = data['successfulPayment'] as bool?;
return InfoByOrderScreen(planId: planId, successfulPayment: successfulPayment);
},
),
GoRoute(
path: PaymentScreen.path,
name: PaymentScreen.name,
builder: (context, state) {
final data = state.extra as Map<String, dynamic>;
final planId = data['planId'] as int;
final url = data['url'] as String;
return PaymentScreen(url: url, planId: planId);
},
),
GoRoute(
path: UserDiaryScreen.path,
name: UserDiaryScreen.name,
builder: (context, state) {
final data = state.extra as Map<String, int>;
final diaryId = data['diaryId'] as int;
final currentPlanDay = data['currentDiaryDay'] as int;
return UserDiaryScreen(diaryId: diaryId, currentPlanDay: currentPlanDay);
},
),
];
}
}

View File

@@ -0,0 +1,84 @@
/// Константы маршрутов приложения
class RouteConstants {
// Auth routes
static const String authScreen = '/auth-screen';
static const String registrationScreen = 'registration-screen';
static const String forgetPasswordScreen = 'forget-password-screen';
static const String scanQRCodeScreen = 'scan-qr-code-screen';
static const String verifyScreen = '/verify-screen';
static const String verifyEmailScreen = '/verify-email-screen';
// Main navigation routes
static const String myProfileScreen = '/my-profile-screen';
static const String feedScreen = '/feed-screen';
static const String diaryScreen = '/diary-screen';
static const String consultationsScreen = '/consultations-screen';
static const String chatSelectScreen = '/chat-select-screen';
static const String notificationsScreen = '/notifications-screen';
static const String menuScreen = '/menu-screen';
// Content routes
static const String aboutPlanScreen = '/about-plan-screen';
static const String aboutRecipesScreen = '/about-recipes-screen';
static const String aboutDietScreen = '/about-diet-screen';
static const String blogPostScreen = '/blog-post-screen';
static const String blogPostCreateScreen = '/blog-post-create-screen';
// Profile routes
static const String profileScreen = '/profile-screen';
static const String subsScreen = '/subs-screen';
// Settings routes
static const String settingsScreen = '/settings-screen';
static const String generalInfoScreen = 'general-info-screen';
static const String otherInfoScreen = 'other-info-screen';
static const String physicalActivityScreen = 'physical-activity-screen';
static const String physicalFormScreen = 'physical-form-screen';
static const String pressureScreen = 'pressure-screen';
static const String changePasswordScreen = 'change-password-screen';
static const String changeEmailScreen = 'change-email-screen';
// Consultant routes
static const String becomeConsultantScreen = '/become-consultant-screen';
static const String stepperBecomeConsultantScreen = '/stepper-become-consultant-screen';
static const String consultantRatingScreen = '/consultant-rating-screen';
// Other routes
static const String welcomeScreen = '/welcome-screen';
static const String updateScreen = '/update-screen';
static const String calendarScreen = '/calendar-screen';
static const String supportScreen = '/support-screen';
static const String photoViewScreen = '/photo-view-screen';
static const String chatScreen = '/chat-screen';
static const String emptyChatScreen = '/empty-chat-screen';
static const String glossaryScreen = '/glossary-screen';
static const String blockedUsersScreen = '/blocked-users-screen';
static const String notificationsSettingsScreen = '/notifications-settings-screen';
// Finance routes
static const String userFinanceScreen = '/user-finance-screen';
static const String cardSettingsScreen = '/card-settings-screen';
static const String orderInfoScreen = '/order-info-screen';
static const String paymentScreen = '/payment-screen';
// Content creation routes
static const String recipeCreateScreen = '/recipe-create-screen';
static const String createDietScreen = '/create-diet-screen';
static const String dietPreviewScreen = '/diet-preview-screen';
static const String createPlanScreen = '/create-plan-screen';
static const String myDietsScreen = '/my-diets-screen';
static const String myPlansScreen = '/my-plans-screen';
static const String myRecipesScreen = '/my-recipes-screen';
// User details
static const String userDetailsScreen = '/user-details-screen';
static const String userDiaryScreen = '/user-diary-screen';
// Dev tools
static const String devToolsScreen = '/dev-tools-screen';
// Archive routes
static const String consultationsArchiveScreen = '/consultations-archive-screen';
static const String consultantFinancesScreen = '/consultant-finances-screen';
static const String userPurchasesScreen = '/user-purchases-screen';
}

View File

@@ -0,0 +1,62 @@
import 'package:eatvio_app/core/routing/route_constants.dart';
import 'package:eatvio_app/features/settings/entities/settings_entity.dart';
import 'package:eatvio_app/features/settings/presentation/pages/change_email_screen.dart';
import 'package:eatvio_app/features/settings/presentation/pages/change_password_screen.dart';
import 'package:eatvio_app/features/settings/presentation/pages/general_info_screen.dart';
import 'package:eatvio_app/features/settings/presentation/pages/other_info_screen.dart';
import 'package:eatvio_app/features/settings/presentation/pages/physical_activity_screen.dart';
import 'package:eatvio_app/features/settings/presentation/pages/physical_form_screen.dart';
import 'package:eatvio_app/features/settings/presentation/pages/pressure_screen.dart';
import 'package:eatvio_app/features/settings/presentation/pages/settings_screen.dart';
import 'package:go_router/go_router.dart';
/// Маршруты для настроек
class SettingsRoutes {
/// Создание маршрутов настроек
static List<RouteBase> createRoutes() {
return [
GoRoute(
path: RouteConstants.settingsScreen,
name: 'settingsScreen',
builder: (context, state) => const SettingsScreen(),
routes: [
GoRoute(
path: RouteConstants.generalInfoScreen,
name: 'generalInfoScreen',
builder: (context, state) => GeneralInfoScreen(settingsEntity: state.extra as SettingsEntity),
),
GoRoute(
path: RouteConstants.otherInfoScreen,
name: 'otherInfoScreen',
builder: (context, state) => OtherInfoScreen(settingsEntity: state.extra as SettingsEntity),
),
GoRoute(
path: RouteConstants.physicalActivityScreen,
name: 'physicalActivityScreen',
builder: (context, state) => PhysicalActivityScreen(settingsEntity: state.extra as SettingsEntity),
),
GoRoute(
path: RouteConstants.physicalFormScreen,
name: 'physicalFormScreen',
builder: (context, state) => PhysicalFormScreen(settingsEntity: state.extra as SettingsEntity),
),
GoRoute(
path: RouteConstants.pressureScreen,
name: 'pressureScreen',
builder: (context, state) => PressureScreen(settingsEntity: state.extra as SettingsEntity),
),
GoRoute(
path: RouteConstants.changePasswordScreen,
name: 'changePasswordScreen',
builder: (context, state) => ChangePasswordScreen(settingsEntity: state.extra as SettingsEntity),
),
GoRoute(
path: RouteConstants.changeEmailScreen,
name: 'changeEmailScreen',
builder: (context, state) => ChangeEmailScreen(settingsEntity: state.extra as SettingsEntity),
),
],
),
];
}
}

View File

@@ -202,6 +202,10 @@ class PrimaryColors {
Color get darkGray => const Color(0XFFBBBCC0);
Color get commentBg => const Color(0XFFFFFDD8);
Color get cardNotificationsBG => const Color(0xFFA4A4A4);
Color get grey7B => const Color(0xFF7B7B7B);
}
PrimaryColors get appTheme => ThemeHelper().themeColor();

View File

@@ -1,9 +1,10 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:shared_preferences/shared_preferences.dart';
abstract class TokenStorage {
static Future<bool> haveTokenInStorage() async {
final sharedPreferences = await SharedPreferences.getInstance();
try {
if (await const FlutterSecureStorage().read(key: 'token') == null) {
if (sharedPreferences.getString('token') == null) {
return false;
} else {
return true;
@@ -13,19 +14,27 @@ abstract class TokenStorage {
}
}
static Future<String?> getTokenFromStorage() async => await const FlutterSecureStorage().read(key: 'token');
static Future<void> saveTokenInStorage(String token) async {
await const FlutterSecureStorage().write(key: 'token', value: token);
static Future<String?> getTokenFromStorage() async {
final sharedPreferences = await SharedPreferences.getInstance();
return sharedPreferences.getString('token');
}
static Future<void> removeTokenInStorage() async => (await const FlutterSecureStorage().delete(key: 'token'));
static Future<void> saveTokenInStorage(String token) async {
final sharedPreferences = await SharedPreferences.getInstance();
await sharedPreferences.setString('token', token);
}
static Future<void> removeTokenInStorage() async {
final sharedPreferences = await SharedPreferences.getInstance();
await sharedPreferences.remove('token');
}
}
abstract class SocketIdStorage {
static Future<bool> haveSocketIdInStorage() async {
final sharedPreferences = await SharedPreferences.getInstance();
try {
if (await const FlutterSecureStorage().read(key: 'socketId') == null) {
if (sharedPreferences.getString('socketId') == null) {
return false;
} else {
return true;
@@ -35,11 +44,18 @@ abstract class SocketIdStorage {
}
}
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<String?> getSocketIdFromStorage() async {
final sharedPreferences = await SharedPreferences.getInstance();
return sharedPreferences.getString('socketId');
}
static Future<void> removeSocketIdInStorage() async => (await const FlutterSecureStorage().delete(key: 'socketId'));
static Future<void> saveSocketIdInStorage(String socketId) async {
final sharedPreferences = await SharedPreferences.getInstance();
await sharedPreferences.setString('socketId', socketId);
}
static Future<void> removeSocketIdInStorage() async {
final sharedPreferences = await SharedPreferences.getInstance();
await sharedPreferences.remove('socketId');
}
}

View File

@@ -3,11 +3,30 @@ import 'package:intl/intl.dart';
const String dateTimeFormatPattern = 'dd/MM/yyyy';
/// Преобразует дату из формата гггг-мм-дд чч:мм:сс в дд.мм.гггг
String formatBuildDate(String dateString) {
if (dateString.isEmpty) return '';
try {
final dateTimeParts = dateString.split(' ');
if (dateTimeParts.isNotEmpty) {
final datePart = dateTimeParts[0];
final dateParts = datePart.split('-');
if (dateParts.length == 3) {
final year = dateParts[0];
final month = dateParts[1];
final day = dateParts[2];
return '$day.$month.$year';
}
}
} catch (e) {
return dateString;
}
return dateString;
}
extension DateTimeExtension on DateTime {
String format([
String pattern = dateTimeFormatPattern,
String? locale,
]) {
String format([String pattern = dateTimeFormatPattern, String? locale]) {
final localDate = isUtc ? toLocal() : this;
if (locale != null && locale.isNotEmpty) {
initializeDateFormatting(locale);
@@ -19,17 +38,13 @@ extension DateTimeExtension on DateTime {
final localDate = isUtc ? toLocal() : this;
final now = DateTime.now();
final yesterday = DateTime(now.year, now.month, now.day).subtract(const Duration(days: 1));
return localDate.year == yesterday.year &&
localDate.month == yesterday.month &&
localDate.day == yesterday.day;
return localDate.year == yesterday.year && localDate.month == yesterday.month && localDate.day == yesterday.day;
}
bool isToday() {
final localDate = isUtc ? toLocal() : this;
final now = DateTime.now();
return now.year == localDate.year &&
now.month == localDate.month &&
now.day == localDate.day;
return now.year == localDate.year && now.month == localDate.month && now.day == localDate.day;
}
}

View File

@@ -38,4 +38,16 @@ void showSuccessToast(String message) {
textColor: appTheme.whiteA700,
fontSize: 16.0,
);
}
}
void showInternetConnectionToast() {
Fluttertoast.showToast(
msg: 'Проверьте подключение к интернету',
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.TOP,
timeInSecForIosWeb: 3,
backgroundColor: appTheme.red,
textColor: Colors.white,
fontSize: 16.0,
);
}

View File

@@ -0,0 +1,87 @@
import 'package:eatvio_app/core/constants/app_constants.dart';
/// Утилиты для валидации
class ValidationUtils {
/// Валидация email
static bool isValidEmail(String email) {
if (email.isEmpty) return false;
final regex = RegExp(AppConstants.emailRegex);
return regex.hasMatch(email);
}
/// Валидация телефона
static bool isValidPhone(String phone) {
if (phone.isEmpty) return false;
final regex = RegExp(AppConstants.phoneRegex);
return regex.hasMatch(phone);
}
/// Валидация URL
static bool isValidUrl(String url) {
if (url.isEmpty) return false;
final regex = RegExp(AppConstants.urlRegex);
return regex.hasMatch(url);
}
/// Валидация длины текста
static bool isValidTextLength(String text, {int? maxLength}) {
final length = maxLength ?? AppConstants.maxTextLength;
return text.length <= length;
}
/// Валидация длины заголовка
static bool isValidTitleLength(String title) {
return title.isNotEmpty && title.length <= AppConstants.maxTitleLength;
}
/// Валидация длины описания
static bool isValidDescriptionLength(String description) {
return description.length <= AppConstants.maxDescriptionLength;
}
/// Валидация пароля
static bool isValidPassword(String password) {
// Минимум 8 символов, хотя бы одна буква и одна цифра
if (password.length < 8) return false;
final hasLetter = RegExp(r'[a-zA-Z]').hasMatch(password);
final hasDigit = RegExp(r'[0-9]').hasMatch(password);
return hasLetter && hasDigit;
}
/// Валидация имени пользователя
static bool isValidUsername(String username) {
// От 3 до 20 символов, только буквы, цифры и подчеркивания
if (username.length < 3 || username.length > 20) return false;
final regex = RegExp(r'^[a-zA-Z0-9_]+$');
return regex.hasMatch(username);
}
/// Получение сообщения об ошибке валидации
static String? getValidationErrorMessage(String? value, ValidationType type) {
if (value == null || value.isEmpty) {
return 'Поле не может быть пустым';
}
switch (type) {
case ValidationType.email:
return isValidEmail(value) ? null : 'Введите корректный email';
case ValidationType.phone:
return isValidPhone(value) ? null : 'Введите корректный номер телефона';
case ValidationType.url:
return isValidUrl(value) ? null : 'Введите корректный URL';
case ValidationType.password:
return isValidPassword(value) ? null : 'Пароль должен содержать минимум 8 символов, включая буквы и цифры';
case ValidationType.username:
return isValidUsername(value) ? null : 'Имя пользователя должно содержать от 3 до 20 символов (буквы, цифры, подчеркивания)';
case ValidationType.title:
return isValidTitleLength(value) ? null : 'Заголовок не может быть пустым и должен содержать не более ${AppConstants.maxTitleLength} символов';
case ValidationType.description:
return isValidDescriptionLength(value) ? null : 'Описание не может содержать более ${AppConstants.maxDescriptionLength} символов';
case ValidationType.text:
return isValidTextLength(value) ? null : 'Текст не может содержать более ${AppConstants.maxTextLength} символов';
}
}
}
/// Типы валидации
enum ValidationType { email, phone, url, password, username, title, description, text }

View File

@@ -0,0 +1,128 @@
import 'dart:async';
import 'package:eatvio_app/core/views/cubit/views_state.dart';
import 'package:eatvio_app/core/views/repository/views_repository.dart';
import 'package:eatvio_app/features/feed/models/view_model.dart';
import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class ViewsCubit extends Cubit<ViewsCubitState> {
final ViewsRepository viewsRepository;
ViewsCubit({required this.viewsRepository}) : super(ViewsCubitState()) {
_listener = AppLifecycleListener(onPause: _sendViews, onInactive: _sendViews);
}
late final AppLifecycleListener _listener;
final Set<int> postsIds = <int>{};
final Set<int> plansIds = <int>{};
final Set<int> dietsIds = <int>{};
final Set<int> recipesIds = <int>{};
final Set<int> _sentPosts = <int>{};
final Set<int> _sentPlans = <int>{};
final Set<int> _sentDiets = <int>{};
final Set<int> _sentRecipes = <int>{};
final Map<ViewableContentType, Map<int, Timer>> _visibilityTimers = {
ViewableContentType.posts: <int, Timer>{},
ViewableContentType.plans: <int, Timer>{},
ViewableContentType.diets: <int, Timer>{},
ViewableContentType.recipes: <int, Timer>{},
};
Timer? _debounceTimer;
Set<int> _idsForType(ViewableContentType type) {
switch (type) {
case ViewableContentType.posts:
return postsIds;
case ViewableContentType.plans:
return plansIds;
case ViewableContentType.diets:
return dietsIds;
case ViewableContentType.recipes:
return recipesIds;
}
}
Set<int> _sentIdsForType(ViewableContentType type) {
switch (type) {
case ViewableContentType.posts:
return _sentPosts;
case ViewableContentType.plans:
return _sentPlans;
case ViewableContentType.diets:
return _sentDiets;
case ViewableContentType.recipes:
return _sentRecipes;
}
}
void handleVisibility({required double visibleFraction, required int id, required ViewableContentType type}) {
final timers = _visibilityTimers[type]!;
final ids = _idsForType(type);
final sent = _sentIdsForType(type);
if (sent.contains(id)) {
timers[id]?.cancel();
timers.remove(id);
return;
}
if (ids.contains(id)) return;
if (visibleFraction >= 0.5) {
if (timers.containsKey(id)) return;
timers[id] = Timer(const Duration(seconds: 1), () {
ids.add(id);
timers.remove(id);
_scheduleDebouncedSend();
});
} else {
timers[id]?.cancel();
timers.remove(id);
}
}
Future<void> _viewContent({required ViewableContentType type, required List<int> ids}) async {
await viewsRepository.viewContent(
viewModel: ViewModel(type: type.getStringifiedVersion(), ids: ids),
);
}
void _scheduleDebouncedSend() {
_debounceTimer?.cancel();
_debounceTimer = Timer(const Duration(seconds: 4), _sendViews);
}
void _send(Set<int> ids, Set<int> sent, ViewableContentType type) {
final newIds = ids.difference(sent);
if (newIds.isNotEmpty) {
_viewContent(type: type, ids: newIds.toList());
sent.addAll(newIds);
ids.removeAll(newIds);
}
}
void _sendViews() {
_send(postsIds, _sentPosts, ViewableContentType.posts);
_send(plansIds, _sentPlans, ViewableContentType.plans);
_send(dietsIds, _sentDiets, ViewableContentType.diets);
_send(recipesIds, _sentRecipes, ViewableContentType.recipes);
}
@override
Future<void> close() async {
for (final map in _visibilityTimers.values) {
for (final t in map.values) {
t.cancel();
}
map.clear();
}
_debounceTimer?.cancel();
_listener.dispose();
_sendViews();
await super.close();
}
}

View File

@@ -0,0 +1,3 @@
class ViewsCubitState {
ViewsCubitState();
}

View File

@@ -0,0 +1,16 @@
import 'package:eatvio_app/core/views/service/views_service.dart';
import 'package:eatvio_app/features/feed/models/view_model.dart';
class ViewsRepository {
final ViewsService viewsService;
ViewsRepository({required this.viewsService});
Future<void> viewContent({required ViewModel viewModel}) async {
try {
await viewsService.viewContent(viewModel: viewModel);
} catch (e) {
rethrow;
}
}
}

View File

@@ -0,0 +1,15 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/feed/models/view_model.dart';
import 'package:retrofit/retrofit.dart';
part 'views_service.g.dart';
@RestApi()
abstract class ViewsService {
factory ViewsService(Dio dio, {String baseUrl}) = _ViewsService;
@POST('/api/views')
Future<void> viewContent({
@Body() required ViewModel viewModel,
});
}

View File

@@ -2,7 +2,8 @@ import 'package:eatvio_app/core/data/selectionPopupModel/selection_popup_model.d
import 'package:flutter/material.dart';
class CustomDropDown extends StatelessWidget {
const CustomDropDown({super.key,
const CustomDropDown({
super.key,
this.alignment,
this.width,
this.focusNode,
@@ -48,12 +49,7 @@ class CustomDropDown extends StatelessWidget {
@override
Widget build(BuildContext context) {
return alignment != null
? Align(
alignment: alignment ?? Alignment.center,
child: _buildDropDownWidget(context),
)
: _buildDropDownWidget(context);
return alignment != null ? Align(alignment: alignment ?? Alignment.center, child: _buildDropDownWidget(context)) : _buildDropDownWidget(context);
}
DropdownButtonFormField<SelectionPopupModel> _buildDropDownWidget(BuildContext context) {
@@ -61,7 +57,7 @@ class CustomDropDown extends StatelessWidget {
focusNode: focusNode ?? FocusNode(),
icon: const Icon(Icons.keyboard_arrow_down_rounded),
autofocus: autofocus!,
value: value,
initialValue: value,
isDense: true,
isExpanded: true,
items: items?.map((SelectionPopupModel item) {
@@ -69,12 +65,7 @@ class CustomDropDown extends StatelessWidget {
value: item,
child: Text(
item.title,
style: hintStyle ??
const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Colors.black,
),
style: hintStyle ?? const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black),
),
);
}).toList(),
@@ -90,19 +81,9 @@ class CustomDropDown extends StatelessWidget {
InputDecoration _buildInputDecoration(BuildContext context) {
return InputDecoration(
labelStyle: const TextStyle(
fontSize: 16,
color: Colors.black,
fontWeight: FontWeight.w400,
),
labelStyle: const TextStyle(fontSize: 16, color: Colors.black, fontWeight: FontWeight.w400),
hintText: hintText ?? "",
hintStyle: hintStyle ??
TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Colors.grey.shade400,
height: 1,
),
hintStyle: hintStyle ?? TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.grey.shade400, height: 1),
prefixIcon: prefix,
prefixIconConstraints: prefixConstraints,
suffixIcon: suffix,
@@ -111,18 +92,9 @@ class CustomDropDown extends StatelessWidget {
contentPadding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
fillColor: const Color(0xFFF6F6F6),
filled: filled,
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(20)),
borderSide: BorderSide.none,
),
enabledBorder: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(20)),
borderSide: BorderSide.none,
),
focusedBorder: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(20)),
borderSide: BorderSide.none,
),
border: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(20)), borderSide: BorderSide.none),
enabledBorder: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(20)), borderSide: BorderSide.none),
focusedBorder: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(20)), borderSide: BorderSide.none),
);
}
}

View File

@@ -3,14 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
class CustomDropDownButton<T> extends StatelessWidget {
const CustomDropDownButton({
super.key,
required this.items,
required this.onChanged,
this.value,
this.hint,
this.hasError = false,
});
const CustomDropDownButton({super.key, required this.items, required this.onChanged, this.value, this.hint, this.hasError = false});
final T? value;
final List<DropdownMenuItem<T>> items;
final void Function(T?)? onChanged;
@@ -20,11 +13,7 @@ class CustomDropDownButton<T> extends StatelessWidget {
Widget build(BuildContext context) {
return DropdownButtonFormField(
hint: hint,
icon: SvgPicture.asset(
'assets/icons/arrow_down2.svg',
width: 12,
height: 8,
),
icon: SvgPicture.asset('assets/icons/arrow_down2.svg', width: 12, height: 8),
borderRadius: BorderRadius.circular(20),
dropdownColor: appTheme.lightGray0,
decoration: InputDecoration(
@@ -43,7 +32,7 @@ class CustomDropDownButton<T> extends StatelessWidget {
fillColor: appTheme.lightGray0,
filled: true,
),
value: value,
initialValue: value,
items: items,
onChanged: onChanged,
);

View File

@@ -0,0 +1,51 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:vk_id/vk_id.dart';
part 'auth_result_entity.freezed.dart';
@freezed
abstract class AuthResultEntity with _$AuthResultEntity {
const factory AuthResultEntity({
required String accessToken,
required String userId,
String? refreshToken,
String? firstName,
String? lastName,
String? email,
String? phone,
GenderEnum? gender,
String? birthDate,
}) = _AuthResultEntity;
}
enum GenderEnum {
male,
female,
notSet;
static GenderEnum? fromVkGender(VkSex? vkGender) {
switch (vkGender) {
case VkSex.male:
return GenderEnum.male;
case VkSex.female:
return GenderEnum.female;
case VkSex.notStated:
return GenderEnum.notSet;
case null:
return null;
}
}
static GenderEnum? fromString(String? gender) {
switch (gender) {
case 'male':
return GenderEnum.male;
case 'female':
return GenderEnum.female;
case 'notSet':
return GenderEnum.notSet;
default:
return null;
}
}
}

View File

@@ -0,0 +1,12 @@
class SocialExchangeRequest {
final String provider;
final String accessToken;
SocialExchangeRequest({required this.provider, required this.accessToken});
Map<String, dynamic> toJson() {
return {'provider': provider, 'access_token': accessToken};
}
}

View File

@@ -0,0 +1,30 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'yandex_profile_model.freezed.dart';
part 'yandex_profile_model.g.dart';
@freezed
abstract class YandexProfileModel with _$YandexProfileModel {
factory YandexProfileModel({
required String id,
required String login,
required String birthday,
@JsonKey(name: 'client_id') required String clientId,
@JsonKey(name: 'display_name') required String displayName,
@JsonKey(name: 'real_name') required String realName,
@JsonKey(name: 'first_name') required String firstName,
@JsonKey(name: 'last_name') required String lastName,
@JsonKey(name: 'sex') required String sex,
@JsonKey(name: 'default_email') required String defaultEmail,
@JsonKey(name: 'default_phone') required YandexPhoneData defaultPhone,
}) = _YandexProfileModel;
factory YandexProfileModel.fromJson(Map<String, dynamic> json) => _$YandexProfileModelFromJson(json);
}
@freezed
abstract class YandexPhoneData with _$YandexPhoneData {
factory YandexPhoneData({required int id, required String number}) = _YandexPhoneData;
factory YandexPhoneData.fromJson(Map<String, dynamic> json) => _$YandexPhoneDataFromJson(json);
}

View File

@@ -6,28 +6,17 @@ import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class AuthCubit extends Cubit<AuthState> {
final AuthRepository authRepository;
AuthCubit({
required this.authRepository,
}) : super(const AuthState());
AuthCubit({required this.authRepository}) : super(const AuthState());
Future<bool> getTokenByCredentials(String email, String password) async {
emit(const AuthState.loading());
try {
final token = (await authRepository.getTokenByCredentials(
email,
password,
))
.token;
emit(
AuthState.loaded(
token: token,
),
);
await TokenStorage.saveTokenInStorage(token);
final token = (await authRepository.getTokenByCredentials(email, password)).token;
emit(AuthState.loaded(token: token));
await TokenStorage.saveTokenInStorage(token);
return true;
} catch (e) {
log(e.toString());
@@ -36,4 +25,19 @@ class AuthCubit extends Cubit<AuthState> {
return false;
}
}
Future<bool> getTokenBySocial({required String provider, required String accessToken}) async {
emit(const AuthState.loading());
try {
final token = (await authRepository.getTokenBySocial(provider: provider, accessToken: accessToken)).token;
emit(AuthState.loaded(token: token));
await TokenStorage.saveTokenInStorage(token);
return true;
} catch (e) {
log(e.toString());
showErrorToast('Не удалось войти через $provider');
emit(AuthState.error(errorMessage: '$e'));
return false;
}
}
}

View File

@@ -1,4 +1,7 @@
// ignore_for_file: unused_element, duplicate_ignore
import 'dart:async';
import 'dart:developer';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/firebase_api.dart';
@@ -7,6 +10,7 @@ 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_divider.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.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';
@@ -24,6 +28,7 @@ import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:eatvio_app/features/auth/sdk/social_auth_facade.dart';
class AuthScreen extends StatefulWidget {
const AuthScreen({super.key});
@@ -47,12 +52,20 @@ class _AuthScreenState extends State<AuthScreen> {
super.initState();
var keyboardVisibilityController = KeyboardVisibilityController();
keyboardSubscription = keyboardVisibilityController.onChange.listen((bool visible) {
setState(() {
isFocused = visible;
});
if (mounted) {
setState(() {
isFocused = visible;
});
}
});
}
@override
void dispose() {
keyboardSubscription.cancel();
super.dispose();
}
Future<void> _reconnectWebSocket(BuildContext context) async {
try {
await sl<WsManager>().clear();
@@ -108,6 +121,42 @@ class _AuthScreenState extends State<AuthScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Social auth buttons at the top (outlined)
const Text('Авторизация через:', style: TextStyle(fontWeight: FontWeight.w600)),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_SocialButton(
onTap: () => _onVkPressed(),
backgroundColor: const Color(0xFF2787F5),
tooltip: 'VK ID',
child: SvgPicture.asset('assets/icons/vk_logo.svg', height: 24, width: 24),
),
_SocialButton(
onTap: _onOkPressed,
backgroundColor: const Color(0xFFF58220),
tooltip: 'Одноклассники',
child: SvgPicture.asset('assets/icons/ok_logo.svg', height: 24, width: 24),
),
_SocialButton(
onTap: _onMailRuPressed,
backgroundColor: const Color(0xFF005FF9),
tooltip: 'Mail.ru',
child: SvgPicture.asset('assets/icons/mail_logo.svg', height: 24, width: 24),
),
_SocialButton(
onTap: _onYandexPressed,
backgroundColor: const Color(0xFFFFCC00),
tooltip: 'Яндекс ID',
child: SvgPicture.asset('assets/icons/yandex_logo.svg', height: 24, width: 24),
),
],
),
const SizedBox(height: 16),
const CustomDivider(),
const SizedBox(height: 8),
const SizedBox(height: 16),
Text(s.email),
const SizedBox(height: 4),
CustomTextFormField(
@@ -309,3 +358,88 @@ class _AuthScreenState extends State<AuthScreen> {
);
}
}
class _SocialButton extends StatelessWidget {
const _SocialButton({required this.onTap, required this.child, required this.backgroundColor, required this.tooltip});
final VoidCallback onTap;
final Widget child;
final Color backgroundColor;
final String tooltip;
@override
Widget build(BuildContext context) {
return Tooltip(
message: tooltip,
child: InkWell(
borderRadius: BorderRadius.circular(20),
onTap: onTap,
child: Ink(
height: 48,
width: (MediaQuery.of(context).size.width - 16 * 2 - 12 * 3) / 4,
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: const Color(0xFFDBDBDB), width: 1),
),
child: Center(child: child),
),
),
);
}
}
void _onVkPressed() async {
try {
final accessToken = await SocialAuthFacade.signInWithVk();
if (accessToken == null) {
return;
}
} catch (e) {
log(e.toString());
}
}
void _onOkPressed() async {
try {
final accessToken = await SocialAuthFacade.signInWithOk();
if (accessToken == null) {
return;
}
} catch (e) {
log(e.toString());
}
}
void _onMailRuPressed() async {
try {
final accessToken = await SocialAuthFacade.signInWithMailRu();
if (accessToken == null) {
return;
}
} catch (e) {
log(e.toString());
}
}
void _onYandexPressed() async {
final accessToken = await SocialAuthFacade.signInWithYandex();
if (accessToken == null) return;
}
Future<void> _reconnectAfterAuth() async {
final context = AppRouter.navigatorKey.currentContext;
if (context == null) return;
try {
await sl<WsManager>().clear();
await sl<WsManager>().initialize();
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 (_) {}
AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false});
}

View File

@@ -3,6 +3,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';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
@@ -92,6 +93,7 @@ class _ScanQRCodeScreenState extends State<ScanQRCodeScreen> {
await sl<ChatsCubit>().connectToWebSocket();
await sl<NotificationsCubit>().getNotifications();
await sl<NotificationsCubit>().connectToWebSocket();
await sl<FirebaseApi>().registerPushToken();
} catch (e) {
rethrow;
}

View File

@@ -1,6 +1,7 @@
import 'package:eatvio_app/features/auth/models/credentials_model.dart';
import 'package:eatvio_app/features/auth/models/token_model.dart';
import 'package:eatvio_app/features/auth/services/auth_service.dart';
import 'package:eatvio_app/features/auth/models/social_exchange_request.dart';
class AuthRepository {
final AuthService authService;
@@ -22,4 +23,17 @@ class AuthRepository {
rethrow;
}
}
Future<TokenModel> getTokenBySocial({
required String provider,
required String accessToken,
}) async {
try {
return await authService.getTokenBySocial(
body: SocialExchangeRequest(provider: provider, accessToken: accessToken),
);
} catch (e) {
rethrow;
}
}
}

View File

@@ -0,0 +1,283 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart';
import 'package:eatvio_app/features/auth/models/yandex_profile_model.dart';
import 'package:flutter/material.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/env_config.dart';
import 'package:http/http.dart' as http;
import 'package:vk_id/vk_id.dart';
import 'package:webview_flutter/webview_flutter.dart';
class SocialAuthFacade {
static Future<String?> signInWithVk() async {
final context = AppRouter.navigatorKey.currentContext;
if (context == null) return null;
final result = await signInWithVkAndFetchProfile(context: context);
return result?.accessToken;
}
static Future<AuthResultEntity?> signInWithVkAndFetchProfile({required BuildContext context}) async {
final clientIdStr = EnvConfig.vkClientId.trim();
if (clientIdStr.isEmpty) {
return null;
}
try {
final controller = VkIDController(clID: int.parse(clientIdStr));
final authUri = controller.generateAuthorizeLinkWithCodeVerifier();
final authUrl = authUri.value.toString();
final codeVerifier = authUri.key;
final redirectScheme = 'vk$clientIdStr://';
final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUrl, redirectSchemePrefix: redirectScheme);
if (redirectParams == null) {
return null;
}
return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier);
} catch (e) {
return null;
}
}
static Future<AuthResultEntity?> _parseVKAuthResult({
required Map<String, String> redirectParams,
required VkIDController controller,
required String codeVerifier,
}) async {
final code = redirectParams['code'];
final deviceId = redirectParams['device_id'];
final state = redirectParams['state'];
if (code == null || deviceId == null || state == null) {
return null;
}
final authRes = await controller.retrieveOAuthToken(authorizationCode: code, deviceId: deviceId, codeVerifier: codeVerifier, state: state);
final auth = authRes.result;
if (auth == null) {
var msg = "Retrieve OAuth token error";
final err = authRes.error?.vkErr;
if (err != null) {
msg += ": ${err.error} - ${err.description}";
}
log(msg);
return null;
}
final profileRes = await controller.getProfileInfo();
final profile = profileRes.result;
if (profile == null) {
var msg = "Profile info load error";
final err = profileRes.error?.vkErr;
if (err != null) {
msg += ": ${err.error} - ${err.description}";
}
log(msg);
return null;
}
return AuthResultEntity(
accessToken: auth.accessToken,
refreshToken: auth.refreshToken,
userId: profile.userId.toString(),
firstName: profile.firstName,
lastName: profile.lastName,
email: profile.email,
phone: profile.phone,
gender: GenderEnum.fromVkGender(profile.parsedSex),
birthDate: profile.bDayDt?.toString(),
);
}
Future<Map<String, dynamic>?> exchangeCodeForToken(String code, String clientId, String clientSecret) async {
final response = await http.post(
Uri.parse('https://oauth.yandex.ru/token'),
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: {'grant_type': 'authorization_code', 'code': code, 'client_id': clientId, 'client_secret': clientSecret},
);
if (response.statusCode == 200) {
return jsonDecode(response.body) as Map<String, dynamic>;
} else {
log('Error exchanging code: ${response.body}');
}
return null;
}
static Future<String?> signInWithOk() async {
final context = AppRouter.navigatorKey.currentContext;
if (context == null) return null;
final vkAuthRes = await _signInWithVkIdProvider(context: context, provider: VkProvider.okRu);
return vkAuthRes?.accessToken;
}
static Future<String?> signInWithMailRu() async {
final context = AppRouter.navigatorKey.currentContext;
if (context == null) return null;
final vkAuthRes = await _signInWithVkIdProvider(context: context, provider: VkProvider.mailRu);
return vkAuthRes?.accessToken;
}
static Future<AuthResultEntity?> signInWithYandex() async {
final clientId = EnvConfig.yandexClientId.trim();
final redirectUri = EnvConfig.yandexRedirectUri.trim();
if (clientId.isEmpty || redirectUri.isEmpty) {
return null;
}
try {
final context = AppRouter.navigatorKey.currentContext;
if (context == null) return null;
final authorizeUrl = Uri.https('oauth.yandex.ru', '/authorize', {
'response_type': 'code',
'client_id': clientId,
'redirect_uri': redirectUri,
'force_confirm': 'yes',
}).toString();
final redirectSchemePrefix = redirectUri;
final params = await _presentOAuthWebView(context: context, initialUrl: authorizeUrl, redirectSchemePrefix: redirectSchemePrefix);
if (params == null) return null;
final code = params['code'];
if (code == null || code.isEmpty) {
return null;
}
final tokens = await _getTokensFomYandex(code: code, clientId: clientId, clientSecret: EnvConfig.yandexClientSecret);
if (tokens == null) {
return null;
}
final response = await http.get(Uri.parse('https://login.yandex.ru/info?format=json'), headers: {'Authorization': 'OAuth ${tokens.accessToken}'});
final Map<String, dynamic> json = jsonDecode(response.body);
final yandexProfileData = YandexProfileModel.fromJson(json);
return AuthResultEntity(
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
userId: yandexProfileData.id,
firstName: yandexProfileData.firstName,
lastName: yandexProfileData.lastName,
email: yandexProfileData.defaultEmail,
phone: yandexProfileData.defaultPhone.number,
gender: GenderEnum.fromString(yandexProfileData.sex),
birthDate: yandexProfileData.birthday,
);
} catch (e) {
log(e.toString());
}
return null;
}
static Future<({String accessToken, String refreshToken})?> _getTokensFomYandex({
required String code,
required String clientId,
required String clientSecret,
}) async {
final response = await http.post(
Uri.parse('https://oauth.yandex.ru/token'),
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: {'grant_type': 'authorization_code', 'code': code, 'client_id': clientId, 'client_secret': clientSecret},
);
if (response.statusCode != 200) {
log('Yandex token exchange failed: ${response.body}');
return null;
}
final tokens = jsonDecode(response.body);
final accessToken = tokens['access_token'] as String;
final refreshToken = tokens['refresh_token'] as String;
return (accessToken: accessToken, refreshToken: refreshToken);
}
static Future<AuthResultEntity?> _signInWithVkIdProvider({required BuildContext context, required VkProvider provider}) async {
final clientIdStr = EnvConfig.vkClientId.trim();
if (clientIdStr.isEmpty) {
return null;
}
try {
final controller = VkIDController(clID: int.parse(clientIdStr));
final authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider);
final codeVerifier = authUri.key;
final authUrl = authUri.value.toString();
final redirectScheme = 'vk$clientIdStr://';
final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUrl, redirectSchemePrefix: redirectScheme);
if (redirectParams == null) return null;
final code = redirectParams['code'];
final deviceId = redirectParams['device_id'];
if (code == null || deviceId == null) return null;
return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier);
} catch (e) {
return null;
}
}
static Future<Map<String, String>?> _presentOAuthWebView({
required BuildContext context,
required String initialUrl,
required String redirectSchemePrefix,
}) async {
final controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onNavigationRequest: (request) {
final url = request.url;
if (url.startsWith(redirectSchemePrefix)) {
final uri = Uri.parse(url);
final params = <String, String>{}..addAll(uri.queryParameters);
if (uri.fragment.isNotEmpty) {
final fragPairs = uri.fragment.split('&');
for (final pair in fragPairs) {
final eq = pair.indexOf('=');
if (eq > 0 && eq < pair.length - 1) {
final key = Uri.decodeComponent(pair.substring(0, eq));
final value = Uri.decodeComponent(pair.substring(eq + 1));
params[key] = value;
}
}
}
Navigator.of(context, rootNavigator: true).pop(params);
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
onPageStarted: (url) {},
onPageFinished: (url) {},
onWebResourceError: (error) {},
),
);
try {
await controller.loadRequest(Uri.parse(initialUrl));
} catch (e) {
return null;
}
if (!context.mounted) return null;
return showDialog<Map<String, String>>(
context: context,
barrierDismissible: false,
builder: (_) {
const borderRadius = BorderRadius.all(Radius.circular(16));
return Dialog(
insetPadding: const EdgeInsets.all(16),
shape: const RoundedRectangleBorder(borderRadius: borderRadius),
elevation: 0,
child: ClipRRect(
borderRadius: borderRadius,
child: SizedBox(height: 560, child: WebViewWidget(controller: controller)),
),
);
},
);
}
}

View File

@@ -1,6 +1,7 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/auth/models/credentials_model.dart';
import 'package:eatvio_app/features/auth/models/token_model.dart';
import 'package:eatvio_app/features/auth/models/social_exchange_request.dart';
import 'package:retrofit/retrofit.dart';
part 'auth_service.g.dart';
@@ -13,4 +14,9 @@ abstract class AuthService {
Future<TokenModel> getTokenByCredentials({
@Body() required CredentialsModel credentials,
});
@POST('/api/social/login')
Future<TokenModel> getTokenBySocial({
@Body() required SocialExchangeRequest body,
});
}

View File

@@ -18,4 +18,26 @@ class BlogPostCubit extends Cubit<BlogPostState> {
emit(state.copyWith(isLoading: false));
}
}
Future<void> toggleLike() async {
final current = state.blogPostModel;
if (current == null) return;
// optimistic update
final wasLiked = current.wasLikedByMe;
final updated = current.copyWith(wasLikedByMe: !wasLiked, totalLikes: wasLiked ? current.totalLikes - 1 : current.totalLikes + 1);
emit(state.copyWith(blogPostModel: updated));
try {
if (wasLiked) {
await feedRepository.unlikedBlogPost(id: current.id);
} else {
await feedRepository.likeBlogPost(id: current.id);
}
} catch (e, st) {
// rollback on error
log(e.toString(), stackTrace: st);
emit(state.copyWith(blogPostModel: current));
}
}
}

View File

@@ -32,8 +32,6 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
final CommentsCubit commentsCubit = sl<CommentsCubit>();
final textEditingController = TextEditingController();
int? replyId;
bool isLiked = false;
int totalLikes = 0;
@override
void initState() {
@@ -60,26 +58,30 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
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');
},
),
appBar: AppBar(
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go('/feed-screen');
},
),
body: const Loader());
),
body: const Loader(),
);
}
isLiked = blogPostState.blogPostModel?.wasLikedByMe ?? false;
totalLikes = blogPostState.blogPostModel?.totalLikes ?? 0;
final bool isLiked = blogPostState.blogPostModel?.wasLikedByMe ?? false;
final int 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)))),
decoration: BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@@ -114,24 +116,14 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
maxLines: 5,
minLines: 1,
keyboardType: TextInputType.multiline,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Colors.black,
height: 1.2,
),
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),
),
hintStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, height: 1.2, color: Color(0xFFAEAEB2)),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
@@ -196,7 +188,7 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
id: widget.id,
feedBlogPostModel: blogPostState.blogPostModel,
userSlug: blogPostState.blogPostModel?.author.authorSlug ?? '',
),
),
const SizedBox(width: 16),
],
),
@@ -215,9 +207,7 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
children: [
CircleAvatar(
radius: 16,
foregroundImage: NetworkImage(
baseUrl + (blogPostState.blogPostModel?.author.authorProfileIcon ?? ''),
),
foregroundImage: NetworkImage(baseUrl + (blogPostState.blogPostModel?.author.authorProfileIcon ?? '')),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
),
const SizedBox(width: 8),
@@ -242,30 +232,32 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
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)}),
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 ?? ''}',
),
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,
icon: isLiked ? Icons.favorite_rounded : 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);
await context.read<BlogPostCubit>().toggleLike();
// ignore: use_build_context_synchronously
final updated = context.read<BlogPostCubit>().state.blogPostModel;
if (updated != null) {
// ignore: use_build_context_synchronously
context.read<FeedCubit>().updateFeedBlogPostList(updated, updated.wasLikedByMe);
}
},
),
const SizedBox(width: 8),
@@ -284,8 +276,10 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
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)),
Text(
blogPostState.blogPostModel?.totalViews.toString() ?? '',
style: TextStyle(fontSize: 12, color: appTheme.gray500, fontWeight: FontWeight.w400),
),
],
),
),
@@ -332,7 +326,11 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
children: [
Icon(icon, size: 20, color: iconColor ?? appTheme.gray600),
if (text != null) const SizedBox(width: 8),
if (text != null) Text(text, style: TextStyle(color: textColor ?? appTheme.gray600, fontWeight: FontWeight.w500)),
if (text != null)
Text(
text,
style: TextStyle(color: textColor ?? appTheme.gray600, fontWeight: FontWeight.w500),
),
],
),
),

View File

@@ -1,61 +1,34 @@
import 'dart:developer';
import 'package:eatvio_app/core/cubit/base_cubit.dart';
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_state.dart';
import 'package:eatvio_app/features/blog_post_create/repositories/blog_post_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
class BlogPostCreateCubit extends Cubit<BlogPostCreateState> {
class BlogPostCreateCubit extends BaseCubit<BlogPostCreateState> {
final BlogPostRepository blogPostRepository;
BlogPostCreateCubit({
required this.blogPostRepository,
}) : super(const BlogPostCreateState());
BlogPostCreateCubit({required this.blogPostRepository}) : super(const BlogPostCreateState());
Future<bool> createBlogPost({
String? title,
String? videoUrl,
String? description,
XFile? image,
}) async {
Future<bool> createBlogPost({String? title, String? videoUrl, String? description, XFile? image}) async {
try {
emit(const BlogPostCreateState.loading());
await blogPostRepository.createBlogPost(
title,
videoUrl,
description,
image,
);
await blogPostRepository.createBlogPost(title, videoUrl, description, image);
emit(const BlogPostCreateState.loaded());
return true;
} catch (e, st) {
log(e.toString(), stackTrace: st);
} catch (error, stackTrace) {
handleError(error, stackTrace);
emit(const BlogPostCreateState.loaded());
return false;
}
}
Future<bool> updateBlogPost({
required int id,
String? title,
String? videoUrl,
String? description,
XFile? image,
String? oldImageUrl,
}) async {
Future<bool> updateBlogPost({required int id, String? title, String? videoUrl, String? description, XFile? image, String? oldImageUrl}) async {
try {
emit(const BlogPostCreateState.loading());
await blogPostRepository.updateBlogPost(
id,
title,
videoUrl,
description,
image,
oldImageUrl,
);
await blogPostRepository.updateBlogPost(id, title, videoUrl, description, image, oldImageUrl);
emit(const BlogPostCreateState.loaded());
return true;
} catch (e, st) {
log(e.toString(), stackTrace: st);
} catch (error, stackTrace) {
handleError(error, stackTrace);
emit(const BlogPostCreateState.loaded());
return false;
}
@@ -65,8 +38,8 @@ class BlogPostCreateCubit extends Cubit<BlogPostCreateState> {
try {
await blogPostRepository.deleteBlogPost(id);
return true;
} catch (e, st) {
log(e.toString(), stackTrace: st);
} catch (error, stackTrace) {
handleError(error, stackTrace);
return false;
}
}

View File

@@ -1,24 +1,55 @@
import 'package:appmetrica_plugin/appmetrica_plugin.dart';
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';
import 'package:permission_handler/permission_handler.dart';
class ProfileAvatarCubit extends Cubit<ProfileAvatarState> {
final ProfileRepository profileRepository;
ProfileAvatarCubit({
required this.profileRepository,
}) : super(const ProfileAvatarState.loading());
ProfileAvatarCubit({required this.profileRepository}) : super(const ProfileAvatarState.loading());
int _calculateAge(String birthdate) {
try {
final dateParts = birthdate.split('-');
if (dateParts.length == 3) {
final birthYear = int.parse(dateParts[0]);
final birthMonth = int.parse(dateParts[1]);
final birthDay = int.parse(dateParts[2]);
final now = DateTime.now();
int age = now.year - birthYear;
if (now.month < birthMonth || (now.month == birthMonth && now.day < birthDay)) {
age--;
}
return age;
}
} catch (e) {
return 0;
}
return 0;
}
Future<void> getMyProfile() async {
emit(const ProfileAvatarState.loading());
final profile = await profileRepository.getMyProfile();
final profileDetails = await profileRepository.getMyProfileDetails();
try {
emit(
ProfileAvatarState.loaded(
profile: await profileRepository.getMyProfile(),
),
);
emit(ProfileAvatarState.loaded(profile: profile));
final notificationStatus = await Permission.notification.status;
final isNotificationEnabled = notificationStatus.isGranted;
AppMetricaUserProfile userProfile = AppMetricaUserProfile([
AppMetricaNameAttribute.withValue(profile.name),
AppMetricaGenderAttribute.withValue(profileDetails.gender == 'male' ? AppMetricaGender.male : AppMetricaGender.female),
AppMetricaBirthDateAttribute.withAge(_calculateAge(profileDetails.birthdate)),
AppMetricaNotificationEnabledAttribute.withValue(isNotificationEnabled),
]);
AppMetrica.setUserProfileID(profile.slug);
AppMetrica.reportUserProfile(userProfile);
} catch (e) {
AppRouter.navigatorKey.currentContext?.go('/auth-screen');
emit(ProfileAvatarState.error(errorMessage: e.toString()));

View File

@@ -23,81 +23,105 @@ class _RegistrationAsScreenState extends State<RegistrationAsScreen> {
children: [
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text(
'Регистрация в качестве',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w700,
height: 1.1,
),
),
child: Text('Регистрация в качестве', style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, height: 1.1)),
),
const SizedBox(height: 16),
RadioListTile<RegistrationAsType>(
value: RegistrationAsType.physical,
groupValue: registrationAsType,
title: const Text(
'Физического лица',
style: TextStyle(
fontSize: 18,
height: 1,
Column(
children: [
Row(
children: [
Radio<RegistrationAsType>(
value: RegistrationAsType.physical,
// ignore: deprecated_member_use
groupValue: registrationAsType,
activeColor: appTheme.blueA400,
// ignore: deprecated_member_use
onChanged: (value) {
setState(() {
registrationAsType = value!;
});
},
),
const Expanded(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: Text('Физического лица', style: TextStyle(fontSize: 18, height: 1)),
),
),
],
),
),
activeColor: appTheme.blueA400,
onChanged: (value) {},
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
),
RadioListTile<RegistrationAsType>(
value: RegistrationAsType.selfBusy,
groupValue: registrationAsType,
title: const Text(
'Самозанятого',
style: TextStyle(
fontSize: 18,
height: 1,
Row(
children: [
Radio<RegistrationAsType>(
value: RegistrationAsType.selfBusy,
// ignore: deprecated_member_use
groupValue: registrationAsType,
activeColor: appTheme.blueA400,
// ignore: deprecated_member_use
onChanged: (value) {
setState(() {
registrationAsType = value!;
});
},
),
const Expanded(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: Text('Самозанятого', style: TextStyle(fontSize: 18, height: 1)),
),
),
],
),
),
onChanged: null,
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
),
RadioListTile<RegistrationAsType>(
value: RegistrationAsType.iP,
groupValue: registrationAsType,
title: const Text(
'Индивидуального предпринимателя',
style: TextStyle(
fontSize: 18,
height: 1,
Row(
children: [
Radio<RegistrationAsType>(
value: RegistrationAsType.iP,
// ignore: deprecated_member_use
groupValue: registrationAsType,
activeColor: appTheme.blueA400,
// ignore: deprecated_member_use
onChanged: (value) {
setState(() {
registrationAsType = value!;
});
},
),
const Expanded(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: Text('Индивидуального предпринимателя', style: TextStyle(fontSize: 18, height: 1)),
),
),
],
),
),
onChanged: null,
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
),
RadioListTile<RegistrationAsType>(
value: RegistrationAsType.ooo,
groupValue: registrationAsType,
title: const Text(
'Общества с ограниченной ответственностью',
style: TextStyle(
fontSize: 18,
height: 1,
Row(
children: [
Radio<RegistrationAsType>(
value: RegistrationAsType.ooo,
// ignore: deprecated_member_use
groupValue: registrationAsType,
activeColor: appTheme.blueA400,
// ignore: deprecated_member_use
onChanged: (value) {
setState(() {
registrationAsType = value!;
});
},
),
const Expanded(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: Text('Общества с ограниченной ответственностью', style: TextStyle(fontSize: 18, height: 1)),
),
),
],
),
),
onChanged: null,
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
child: _divider(context),
],
),
Padding(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), child: _divider(context)),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text(
'* На данный момент регистрация доступна только в качестве физического лица. Остальные варианты скоро появятся',
),
child: Text('* На данный момент регистрация доступна только в качестве физического лица. Остальные варианты скоро появятся'),
),
],
),
@@ -108,10 +132,7 @@ class _RegistrationAsScreenState extends State<RegistrationAsScreen> {
return Container(
height: 0.4,
width: MediaQuery.of(context).size.width - 32,
decoration: BoxDecoration(
color: Colors.black26,
borderRadius: BorderRadius.circular(16),
),
decoration: BoxDecoration(color: Colors.black26, borderRadius: BorderRadius.circular(16)),
);
}
}

View File

@@ -1,16 +1,42 @@
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'rating_model.freezed.dart';
part 'rating_model.g.dart';
@freezed
abstract class RatingModel with _$RatingModel {
abstract class RatingPaginationModel with _$RatingPaginationModel {
factory RatingPaginationModel({
required List<RatingModel> data,
}) = _RatingPaginationModel;
factory RatingPaginationModel.fromJson(Map<String, dynamic> json) => _$RatingPaginationModelFromJson(json);
}
@freezed
abstract class RatingModel with _$RatingModel {
factory RatingModel({
required DateTime date,
required String title,
required int amount,
@JsonKey(name: 'created_at', fromJson: parseDate) required DateTime date,
@JsonKey(name: 'log_data') required RatingEnum type,
@JsonKey(name: 'log_scores') required int amount,
}) = _RatingModel;
factory RatingModel.fromJson(Map<String, dynamic> json) => _$RatingModelFromJson(json);
}
}
enum RatingEnum {
@JsonValue('ADDED_RECIPE')
addedRecipe,
@JsonValue('ADDED_PUBLIC_DIET')
addedPublicDiet,
@JsonValue('ADDED_BLOG_POST')
addedBlogPost,
@JsonValue('INVITED_A_NEW_USER')
invitedUser,
@JsonValue('HAS_MEDICAL_DEGREE')
hasMedicalDegree,
@JsonValue('CONSULTED')
consulted,
@JsonValue('PRODUCT_NUTRITINAL_VALUE_APPROVED')
addedGlossary;
}

View File

@@ -0,0 +1,20 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'statistics_model.freezed.dart';
part 'statistics_model.g.dart';
@freezed
abstract class StatisticsModel with _$StatisticsModel {
factory StatisticsModel({
@JsonKey(name: 'count_referal') int? countReferal,
@JsonKey(name: 'count_blog_posts') int? blogPostsCount,
@JsonKey(name: 'count_recipes') int? recipesCount,
@JsonKey(name: 'count_diets') int? dietsCount,
@JsonKey(name: 'count_consultants') int? consultationsCount,
@JsonKey(name: 'count_products_nutritional_values') int? qwe,
@JsonKey(name: 'referral_link') String? refLink,
}) = _StatisticsModel;
factory StatisticsModel.fromJson(Map<String, dynamic> json) => _$StatisticsModelFromJson(json);
}

View File

@@ -1,12 +1,18 @@
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/core/utils/toasts.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart';
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart';
import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_state.dart';
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/rating_history_widget.dart';
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/rating_widget.dart';
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/statistic_widget.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class ConsultantRatingScreen extends StatelessWidget {
static const name = 'consultantRatingScreen';
@@ -15,7 +21,10 @@ class ConsultantRatingScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return const ConsultantRatingPageContent();
return BlocProvider<ConsultantRatingCubit>(
create: (context) => sl<ConsultantRatingCubit>()..getConsultantStatistics()..getScoreLogs(),
child: const ConsultantRatingPageContent(),
);
}
}
@@ -25,52 +34,59 @@ class ConsultantRatingPageContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
final s = S.of(context);
return Scaffold(
backgroundColor: appTheme.gray100,
appBar: AppBar(
centerTitle: true,
title: Text(
s.rating_and_statistics,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
),
body: SingleChildScrollView(
child: Column(
children: [
const SizedBox(height: 16),
const RatingWidget(
rating: 1058,
return BlocBuilder<ConsultantRatingCubit, ConsultantRatingState>(
builder: (context, state) {
return Scaffold(
backgroundColor: appTheme.gray100,
appBar: AppBar(
centerTitle: true,
title: Text(
s.rating_and_statistics,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: CustomOutlinedButton(
text: s.copy_referral_link,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
),
body: state.error != null
? DataErrorWidget(onReload: () async {
await context.read<ConsultantRatingCubit>().getConsultantStatistics();
if (!context.mounted) return;
await context.read<ConsultantRatingCubit>().getScoreLogs();
})
: SingleChildScrollView(
child: Column(
children: [
const SizedBox(height: 16),
RatingWidget(
rating: state.absoluteRating!,
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: CustomOutlinedButton(
text: s.copy_referral_link,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
),
onPressed: () {
Clipboard.setData(ClipboardData(text: state.statistics?.refLink ?? ''));
showInfoToast(s.link_was_copied);
},
),
),
const SizedBox(height: 16),
StatisticWidget(isLoading: state.isStatisticsLoading, statistics: state.statistics,),
const SizedBox(height: 16),
RatingHistoryWidget(
isLoading: state.isRatingLoading,
ratingHistory: state.ratings,
),
],
),
),
onPressed: () {},
),
),
const SizedBox(height: 16),
const StatisticWidget(),
const SizedBox(height: 16),
RatingHistoryWidget(
ratingHistory: [
RatingModel(date: DateTime.now(), title: ' djkasjdasjkdkjasjkdjkaskjdjklasjkld', amount: 201300),
RatingModel(date: DateTime.now(), title: 'djkasjdasjkdkjasjkdjkaskjdjklasjkld', amount: 89323),
RatingModel(date: DateTime.now(), title: 'djkasjdasjkdkjasjkdjkaskjdjklasjkld', amount: 20),
RatingModel(date: DateTime.now(), title: 'djkasjdasjkdkjasjkdjkaskjdjklasjkld', amount: 1212),
RatingModel(date: DateTime.now(), title: 'qweqweeq', amount: 323),
RatingModel(date: DateTime.now(), title: 'ee', amount: 1),
],
),
],
),
),
);
},
);
}
}

View File

@@ -0,0 +1,38 @@
import 'dart:developer';
import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_state.dart';
import 'package:eatvio_app/features/consultant_rating/repositories/consultant_rating_repository.dart';
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class ConsultantRatingCubit extends Cubit<ConsultantRatingState> {
final ConsultantRatingRepository repository;
final FeedRepository feedRepository;
final ProfileRepository profileRepository;
ConsultantRatingCubit({required this.repository, required this.feedRepository, required this.profileRepository}) : super(ConsultantRatingState());
Future<void> getConsultantStatistics({String? date}) async {
try {
emit(state.copyWith(isStatisticsLoading: true));
final statistics = await repository.getConsultantStatistics(date: date);
final absoluteRating = await feedRepository.getConsultants((await profileRepository.getMyProfile()).name);
emit(state.copyWith(isStatisticsLoading: false, statistics: statistics, error: null, absoluteRating: absoluteRating.first.totalScores));
} catch (e, st) {
emit(state.copyWith(isStatisticsLoading: true, error: e.toString()));
log(e.toString(), stackTrace: st);
}
}
Future<void> getScoreLogs({String? date}) async {
try {
emit(state.copyWith(isRatingLoading: true));
final scoreLogs = await repository.getScoreLogs(date: date);
emit(state.copyWith(isRatingLoading: false, ratings: scoreLogs, error: null));
} catch (e, st) {
emit(state.copyWith(isRatingLoading: true, error: e.toString()));
log(e.toString(), stackTrace: st);
}
}
}

View File

@@ -0,0 +1,17 @@
import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart';
import 'package:eatvio_app/features/consultant_rating/models/statistics_model.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'consultant_rating_state.freezed.dart';
@freezed
abstract class ConsultantRatingState with _$ConsultantRatingState {
factory ConsultantRatingState({
@Default(false) bool isStatisticsLoading,
@Default(false) bool isRatingLoading,
@Default(null) StatisticsModel? statistics,
@Default(null) String? error,
@Default(<RatingModel>[]) List<RatingModel> ratings,
@Default(0) int? absoluteRating,
}) = _ConsultantRatingState;
}

View File

@@ -1,42 +1,29 @@
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/core/widgets/custom_drop_down_button.dart';
import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class PeriodFilterWidget extends StatefulWidget {
const PeriodFilterWidget({super.key});
const PeriodFilterWidget({super.key, required this.isStatistic});
final bool isStatistic;
@override
State<PeriodFilterWidget> createState() => _PeriodFilterWidgetState();
}
class _PeriodFilterWidgetState extends State<PeriodFilterWidget> {
final List<String> periodDropDownTitles = [];
int periodDropDownIndex = 0;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final s = S.of(context);
if (periodDropDownTitles.isEmpty) {
periodDropDownTitles.addAll([
s.for_whole_period_of_time,
s.for_a_year,
s.for_a_month,
s.for_a_week,
]);
}
}
PeriodFilterStatus _currentState = PeriodFilterStatus.all;
@override
Widget build(BuildContext context) {
final s = S.of(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomDropDownButton(
CustomDropDownButton<PeriodFilterStatus>(
hint: Text(
s.choose_diet_duration,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
@@ -44,24 +31,59 @@ class _PeriodFilterWidgetState extends State<PeriodFilterWidget> {
onChanged: (value) {
if (value != null) {
setState(() {
periodDropDownIndex = periodDropDownTitles.indexOf(value);
_currentState = value;
});
if (widget.isStatistic) {
if (value == PeriodFilterStatus.all) {
context.read<ConsultantRatingCubit>().getConsultantStatistics();
return;
}
context.read<ConsultantRatingCubit>().getConsultantStatistics(date: value.value);
return;
}
if (value == PeriodFilterStatus.all) {
context.read<ConsultantRatingCubit>().getScoreLogs();
return;
}
context.read<ConsultantRatingCubit>().getScoreLogs(date: value.value);
return;
}
},
items: periodDropDownTitles
items: PeriodFilterStatus.values
.mapIndexed(
(index, value) => DropdownMenuItem(
value: periodDropDownTitles[index],
value: PeriodFilterStatus.values[index],
child: Text(
periodDropDownTitles[index],
PeriodFilterStatus.values[index].name,
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
),
),
)
.toList(),
value: periodDropDownTitles[periodDropDownIndex],
value: _currentState,
),
],
);
}
}
enum PeriodFilterStatus {
all,
month,
week,
year;
String get name => switch (this) {
PeriodFilterStatus.all => 'Все периоды',
PeriodFilterStatus.month => 'За месяц',
PeriodFilterStatus.week => 'За неделю',
PeriodFilterStatus.year => 'За год',
};
String? get value => switch (this) {
PeriodFilterStatus.all => null,
PeriodFilterStatus.month => DateTime.now().subtract(const Duration(days: 30)).format('yyyy-MM-dd'),
PeriodFilterStatus.week => DateTime.now().subtract(const Duration(days: 7)).format('yyyy-MM-dd'),
PeriodFilterStatus.year => DateTime.now().subtract(const Duration(days: 365)).format('yyyy-MM-dd'),
};
}

View File

@@ -11,7 +11,6 @@ class RatingHistoryItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
height: 80,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: appTheme.gray100,
@@ -29,7 +28,7 @@ class RatingHistoryItem extends StatelessWidget {
date.format('dd.MM.yyyy'),
style: TextStyles.textXSSemibold.copyWith(color: appTheme.gray),
),
const Spacer(),
const SizedBox(height: 12),
Text(
title,
style: TextStyles.textMMedium.copyWith(color: appTheme.black),

View File

@@ -1,5 +1,6 @@
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/loader.dart';
import 'package:eatvio_app/core/widgets/main_background_bouble.dart';
import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart';
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/period_filter_widget.dart';
@@ -9,7 +10,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
class RatingHistoryWidget extends StatelessWidget {
const RatingHistoryWidget({super.key, required this.ratingHistory});
const RatingHistoryWidget({super.key, required this.ratingHistory, required this.isLoading});
final bool isLoading;
final List<RatingModel> ratingHistory;
@override
Widget build(BuildContext context) {
@@ -20,50 +22,60 @@ class RatingHistoryWidget extends StatelessWidget {
right: 16,
top: 16,
),
child: Column(
children: [
Row(
children: [
Text(
s.rating_history,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
const Spacer(),
SvgPicture.asset(
'assets/icons/question_mark.svg',
height: 22,
width: 22,
colorFilter: ColorFilter.mode(
appTheme.blueA400,
BlendMode.srcIn,
child: isLoading
? const Center(child: Loader())
: Column(
children: [
Row(
children: [
Text(
s.rating_history,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
const Spacer(),
SvgPicture.asset(
'assets/icons/question_mark.svg',
height: 22,
width: 22,
colorFilter: ColorFilter.mode(
appTheme.blueA400,
BlendMode.srcIn,
),
),
],
),
),
],
),
const SizedBox(
height: 16,
),
const PeriodFilterWidget(),
const SizedBox(
height: 12,
),
ListView.builder(
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return Padding(
padding: index == ratingHistory.length - 1 ? EdgeInsets.zero : const EdgeInsets.only(bottom: 12),
child: RatingHistoryItem(
title: ratingHistory[index].title,
amount: ratingHistory[index].amount,
date: ratingHistory[index].date,
const SizedBox(
height: 16,
),
);
},
itemCount: ratingHistory.length,
shrinkWrap: true,
),
],
),
const PeriodFilterWidget(isStatistic: false),
const SizedBox(
height: 12,
),
ListView.builder(
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return Padding(
padding: index == ratingHistory.length - 1 ? EdgeInsets.zero : const EdgeInsets.only(bottom: 12),
child: RatingHistoryItem(
title: switch (ratingHistory[index].type) {
RatingEnum.addedRecipe => 'Добавление рецепта',
RatingEnum.addedPublicDiet => 'Добавление публичной диеты',
RatingEnum.addedBlogPost => 'Добавление поста',
RatingEnum.invitedUser => 'Регистрация по реф. ссылке',
RatingEnum.hasMedicalDegree => 'Получение мед. степени',
RatingEnum.consulted => 'Консультация',
RatingEnum.addedGlossary => 'Добавление продукта в глоссарий',
},
amount: ratingHistory[index].amount,
date: ratingHistory[index].date,
),
);
},
itemCount: ratingHistory.length,
shrinkWrap: true,
),
],
),
);
}
}

View File

@@ -1,6 +1,8 @@
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/loader.dart';
import 'package:eatvio_app/core/widgets/main_background_bouble.dart';
import 'package:eatvio_app/features/consultant_rating/models/statistics_model.dart';
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/period_filter_widget.dart';
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/statistic_bouble.dart';
import 'package:eatvio_app/generated/l10n.dart';
@@ -8,14 +10,15 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
class StatisticWidget extends StatelessWidget {
const StatisticWidget({super.key});
const StatisticWidget({super.key, required this.isLoading, required this.statistics});
final bool isLoading;
final StatisticsModel? statistics;
@override
Widget build(BuildContext context) {
final s = S.of(context);
return MainBackgroundBouble(
padding: const EdgeInsets.all(16),
child: Column(
child: isLoading ? const Center(child: Loader()) : Column(
children: [
Row(
children: [
@@ -38,15 +41,17 @@ class StatisticWidget extends StatelessWidget {
const SizedBox(
height: 16,
),
const PeriodFilterWidget(),
const PeriodFilterWidget(
isStatistic: true,
),
const SizedBox(
height: 12,
),
Row(
children: [
Expanded(child: StatisticBouble(number: 12, title: s.invited_people_count)),
Expanded(child: StatisticBouble(number: statistics?.countReferal ?? 0, title: s.invited_people_count)),
const SizedBox(width: 8),
Expanded(child: StatisticBouble(number: 12, title: s.published_posts_count)),
Expanded(child: StatisticBouble(number: statistics?.blogPostsCount ?? 0, title: s.published_posts_count)),
],
),
const SizedBox(
@@ -54,9 +59,9 @@ class StatisticWidget extends StatelessWidget {
),
Row(
children: [
Expanded(child: StatisticBouble(number: 12, title: s.published_recipes_count)),
Expanded(child: StatisticBouble(number: statistics?.recipesCount ?? 0, title: s.published_recipes_count)),
const SizedBox(width: 8),
Expanded(child: StatisticBouble(number: 12, title: s.published_diets_count)),
Expanded(child: StatisticBouble(number: statistics?.dietsCount ?? 0, title: s.published_diets_count)),
],
),
const SizedBox(
@@ -64,9 +69,9 @@ class StatisticWidget extends StatelessWidget {
),
Row(
children: [
Expanded(child: StatisticBouble(number: 12, title: s.consultations_count)),
Expanded(child: StatisticBouble(number: statistics?.consultationsCount ?? 0, title: s.consultations_count)),
const SizedBox(width: 8),
Expanded(child: StatisticBouble(number: 12, title: s.active_consultations_count)),
Expanded(child: StatisticBouble(number: statistics?.qwe ?? 0, title: s.added_products_in_glossary)),
],
),
const SizedBox(

View File

@@ -0,0 +1,26 @@
import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart';
import 'package:eatvio_app/features/consultant_rating/models/statistics_model.dart';
import 'package:eatvio_app/features/consultant_rating/services/consultant_rating_service.dart';
class ConsultantRatingRepository {
final ConsultantRatingService service;
ConsultantRatingRepository(this.service);
Future<StatisticsModel> getConsultantStatistics({String? date}) async {
try {
return await service.getConsultantStatistics(date: date);
} catch (e) {
rethrow;
}
}
Future<List<RatingModel>> getScoreLogs({String? date}) async {
try {
final data = (await service.getScoreLogs(date: date)).data;
return data;
} catch (e) {
rethrow;
}
}
}

View File

@@ -0,0 +1,18 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart';
import 'package:eatvio_app/features/consultant_rating/models/statistics_model.dart';
import 'package:retrofit/retrofit.dart';
part 'consultant_rating_service.g.dart';
@RestApi()
abstract class ConsultantRatingService {
factory ConsultantRatingService(Dio dio, {String baseUrl}) = _ConsultantRatingService;
@GET('/api/consultants/rating/statistics')
Future<StatisticsModel> getConsultantStatistics({@Query('filters[duration][\$gte]') String? date});
@GET('/api/consultants/score/logs')
Future<RatingPaginationModel> getScoreLogs({@Query('filters[duration][\$gte]') String? date});
}

View File

@@ -1,11 +1,13 @@
import 'package:collection/collection.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/custom_divider.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart';
import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart';
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:flutter/material.dart';
@@ -63,7 +65,7 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
children: [
Row(
children: [
CircleAvatar(radius: 30, backgroundImage: NetworkImage(baseUrl + widget.consultationsModel.userProfilePhotoUrl)),
CircleAvatar(radius: 30, backgroundImage: NetworkImage(widget.consultationsModel.userProfilePhotoUrl)),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -284,6 +286,19 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
),
],
),
// Кнопка получения выплаты
if (widget.consultationsModel.chosenPlanState == ChosenPlanStateEnum.stateDone) ...[
const SizedBox(height: 16),
CustomOutlinedButton(
text: s.collectCharge,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.green)),
onPressed: () async {
GoRouter.of(context).push(CardSettingsScreen.path, extra: widget.consultationsModel.id);
},
),
],
],
),
),

View File

@@ -26,10 +26,7 @@ class DietPreviewScreen extends StatefulWidget {
late final List<int> selectedIndexes;
static const name = 'createDietScreenPreview';
static const path = '/create-diet-screen/preview';
DietPreviewScreen({
super.key,
required this.diet,
}) {
DietPreviewScreen({super.key, required this.diet}) {
selectedIndexes = List.generate(diet.dailySchedule.length, (_) => 0);
}
@@ -65,11 +62,7 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
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),
),
),
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
@@ -79,34 +72,16 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
Row(
children: [
Expanded(
child: SizedBox(
child: FloatingActionButton.extended(
heroTag: 'nextButtonRegistration',
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
backgroundColor: appTheme.greenA700,
onPressed: () {},
label: Text(
s.startDiet,
style: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
),
),
child: BookmarkButton(
isFavorite: widget.diet.isFavorite,
onTap: () {},
width: 52,
height: 52,
size: 24,
shape: BoxShape.circle,
color: appTheme.gray50,
),
),
const SizedBox(width: 8),
BookmarkButton(
isFavorite: widget.diet.isFavorite,
onTap: () {},
width: 52,
height: 52,
size: 24,
shape: BoxShape.circle,
color: appTheme.gray50,
),
],
),
const SizedBox(height: 8),
@@ -133,24 +108,15 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(
color: Color(0xFFD1D1D6),
width: 1,
),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(
color: Color(0xFFD1D1D6),
width: 1,
),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(
color: Color(0xFFD1D1D6),
width: 1,
),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
),
),
@@ -160,17 +126,10 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
GestureDetector(
onTap: () {},
child: Container(
decoration: BoxDecoration(
color: textEditingController.text.length > 2 ? appTheme.blueA400 : appTheme.gray50,
shape: BoxShape.circle,
),
decoration: BoxDecoration(color: textEditingController.text.length > 2 ? appTheme.blueA400 : appTheme.gray50, shape: BoxShape.circle),
height: 32,
width: 32,
child: Icon(
Icons.send_rounded,
size: 18,
color: textEditingController.text.length > 2 ? appTheme.whiteA700 : appTheme.black,
),
child: Icon(Icons.send_rounded, size: 18, color: textEditingController.text.length > 2 ? appTheme.whiteA700 : appTheme.black),
),
),
],
@@ -212,12 +171,7 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
children: [
Stack(
children: [
Image.memory(
imageData,
height: 300,
width: MediaQuery.sizeOf(context).width,
fit: BoxFit.fill,
),
Image.memory(imageData, height: 300, width: MediaQuery.sizeOf(context).width, fit: BoxFit.fill),
// CachedNetworkImage(
// imageUrl: baseUrl + '/storage/' + widget.diet.image,
// ),
@@ -233,10 +187,7 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
children: [
SvgPicture.asset('assets/icons/eye.svg', width: 18, height: 12),
const SizedBox(width: 8),
Text(
widget.diet.totalViews.toString(),
style: TextStyles.textXSSemibold.copyWith(color: appTheme.black),
),
Text(widget.diet.totalViews.toString(), style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)),
],
),
const SizedBox(width: 12),
@@ -252,10 +203,7 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
colorFilter: widget.diet.wasLikedByMe ? ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn) : null,
),
const SizedBox(width: 8),
Text(
widget.diet.totalLikes.toString(),
style: TextStyles.textXSSemibold.copyWith(color: appTheme.black),
),
Text(widget.diet.totalLikes.toString(), style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)),
],
),
),
@@ -265,10 +213,7 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
children: [
SvgPicture.asset('assets/icons/comment2.svg', width: 13, height: 12),
const SizedBox(width: 8),
Text(
widget.diet.totalComments.toString(),
style: TextStyles.textXSSemibold.copyWith(color: appTheme.black),
),
Text(widget.diet.totalComments.toString(), style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)),
],
),
],
@@ -279,23 +224,15 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
s.about_diet,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
child: Text(s.about_diet, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
),
const SizedBox(height: 12),
InfoRow(diet: widget.diet, type: ProductType.diet,),
ExpandableText(
text: widget.diet.details,
),
InfoRow(diet: widget.diet, type: ProductType.diet),
ExpandableText(text: widget.diet.details),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: AuthorCard(
author: widget.diet.author,
title: s.diet_author,
),
child: AuthorCard(author: widget.diet.author, title: s.diet_author),
),
const SizedBox(height: 16),
],
@@ -308,9 +245,7 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
child: Center(
child: Text(
'${s.diet_plan}(${widget.diet.duration.toString()} ${getDayForm(widget.diet.duration, s)})',
style: TextStyles.headingXLSemibold.copyWith(
color: appTheme.black,
),
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
),
),
@@ -320,7 +255,7 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemBuilder: (context, index) {
return _dayOfPlan(widget.diet.dailySchedule[index] ?? {}, index, s);
return _dayOfPlan(widget.diet.dailySchedule[index + 1] ?? {}, index, s);
},
itemCount: widget.diet.dailySchedule.length,
),
@@ -331,12 +266,7 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
alignment: Alignment.centerLeft,
child: Text(
'0 ${getCommentForm(widget.diet.totalComments, s)}',
style: TextStyles.textMMedium.copyWith(
color: appTheme.gray500,
),
),
child: Text('0 ${getCommentForm(widget.diet.totalComments, s)}', style: TextStyles.textMMedium.copyWith(color: appTheme.gray500)),
),
const SizedBox(height: 16),
],
@@ -355,14 +285,10 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
pressElevation: 0,
showCheckmark: false,
side: BorderSide.none,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
label: Text((index + 1).toString()),
labelStyle: TextStyles.textXSSemibold.copyWith(
color: index == widget.selectedIndexes[dayNumber] ? Colors.white : appTheme.gray600,
),
labelStyle: TextStyles.textXSSemibold.copyWith(color: index == widget.selectedIndexes[dayNumber] ? Colors.white : appTheme.gray600),
selected: widget.selectedIndexes[dayNumber] == index,
backgroundColor: appTheme.gray100,
onSelected: (bool selected) {
@@ -376,9 +302,7 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: MainBackgroundBouble(
padding: const EdgeInsets.only(
top: 16,
),
padding: const EdgeInsets.only(top: 16),
child: Column(
children: [
Padding(
@@ -386,31 +310,12 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${s.day} ${dayNumber + 1}',
style: TextStyles.headingXLSemibold.copyWith(
color: appTheme.black,
),
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 12),
child: CustomDivider(),
),
Text(
s.meal,
style: TextStyles.textMMedium.copyWith(
color: appTheme.black,
),
),
Text('${s.day} ${dayNumber + 1}', style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
const Padding(padding: EdgeInsets.symmetric(vertical: 12), child: CustomDivider()),
Text(s.meal, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
choiceChipWidget(0),
choiceChipWidget(1),
choiceChipWidget(2),
choiceChipWidget(3),
choiceChipWidget(4),
],
children: [choiceChipWidget(0), choiceChipWidget(1), choiceChipWidget(2), choiceChipWidget(3), choiceChipWidget(4)],
),
const SizedBox(height: 8),
ListView.builder(
@@ -443,20 +348,10 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
children: [
SizedBox(
width: MediaQuery.sizeOf(context).width * 0.6,
child: Text(
daily.name,
style: TextStyles.textMMedium.copyWith(
color: appTheme.black,
),
),
child: Text(daily.name, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
),
const Spacer(),
Text(
'${daily.quantity} ${getLocalizedMeasurementUnits(s, daily.portion)}',
style: TextStyles.textMSemibold.copyWith(
color: appTheme.black,
),
),
Text('${daily.quantity} ${getLocalizedMeasurementUnits(s, daily.portion)}', style: TextStyles.textMSemibold.copyWith(color: appTheme.black)),
],
),
);

View File

@@ -46,21 +46,14 @@ class _AddMealWidgetState extends State<AddMealWidget> {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16) +
EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
padding: const EdgeInsets.all(16) + EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Добавление блюда',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Color(0xFF323F4B),
),
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)),
),
const SizedBox(height: 16),
Row(
@@ -73,21 +66,17 @@ class _AddMealWidgetState extends State<AddMealWidget> {
autofocus: true,
hintText: 'Введите название продукта/блюда',
onChanged: (query) {
EasyDebounce.debounce(
'glossary-search',
const Duration(milliseconds: 250),
() async {
EasyDebounce.debounce('glossary-search', const Duration(milliseconds: 250), () async {
selectedGlossary = null;
if (query.isEmpty) {
glossariesList = [];
selectedGlossary = null;
if (query.isEmpty) {
glossariesList = [];
selectedGlossary = null;
setState(() {});
return;
}
glossariesList = await widget.createDietCubit.getGlossaryListByQuery(query);
setState(() {});
},
);
return;
}
glossariesList = await widget.createDietCubit.getGlossaryListByQuery(query);
setState(() {});
});
},
),
),
@@ -100,22 +89,20 @@ class _AddMealWidgetState extends State<AddMealWidget> {
hintText: selectedGlossary == null
? '100'
: selectedGlossary!.grInPortion == 0
? '100'
: '1',
? '100'
: '1',
suffixText: selectedGlossary == null
? 'гр'
: selectedGlossary?.grInPortion == 0
? 'гр'
: 'шт',
? 'гр'
: 'шт',
),
),
],
),
glossariesList.isEmpty ? const SizedBox(height: 16) : const SizedBox.shrink(),
Container(
constraints: const BoxConstraints(
maxHeight: 256,
),
constraints: const BoxConstraints(maxHeight: 256),
child: ListView.separated(
physics: const BouncingScrollPhysics(),
shrinkWrap: true,
@@ -130,28 +117,12 @@ class _AddMealWidgetState extends State<AddMealWidget> {
setState(() {});
FocusManager.instance.primaryFocus?.nextFocus();
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
title: Text(
glossariesList[i].name,
style: const TextStyle(
height: 1,
),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: Text(glossariesList[i].name, style: const TextStyle(height: 1)),
subtitle: glossariesList[i].grInPortion == 0
? null
: Text(
'${glossariesList[i].grInPortion} г',
style: TextStyle(
height: 1,
color: Colors.grey.shade600,
),
),
trailing: Icon(
Icons.add_rounded,
color: Colors.grey.shade400,
),
: Text('${glossariesList[i].grInPortion} г', style: TextStyle(height: 1, color: Colors.grey.shade600)),
trailing: Icon(Icons.add_rounded, color: Colors.grey.shade400),
contentPadding: const EdgeInsets.only(left: 12),
);
},
@@ -166,25 +137,15 @@ class _AddMealWidgetState extends State<AddMealWidget> {
if (widget.portionIndex != null)
InkWell(
onTap: () {
widget.createDietCubit.removePortion(
day: widget.dayIndex,
meal: widget.mealIndex,
portionIndex: widget.portionIndex!,
);
widget.createDietCubit.removePortion(day: widget.dayIndex, meal: widget.mealIndex, portionIndex: widget.portionIndex!);
Navigator.of(context).pop();
},
borderRadius: BorderRadius.circular(20),
child: Ink(
width: 52,
height: 52,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: const Color(0xFFF6F6F6),
),
child: const Icon(
Icons.delete_outline_rounded,
color: Colors.red,
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFF6F6F6)),
child: const Icon(Icons.delete_outline_rounded, color: Colors.red),
),
),
if (widget.portionIndex != null) const SizedBox(width: 8),
@@ -194,18 +155,11 @@ class _AddMealWidgetState extends State<AddMealWidget> {
child: Ink(
width: MediaQuery.of(context).size.width - (widget.portionIndex == null ? 32 : 92),
height: 52,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: appTheme.blueA400,
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
child: Center(
child: Text(
widget.portionIndex == null ? 'Добавить' : 'Сохранить',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.white,
),
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white),
),
),
),
@@ -222,8 +176,8 @@ class _AddMealWidgetState extends State<AddMealWidget> {
var measurementUnits = selectedGlossary?.grInPortion == null
? Portion.pcs
: selectedGlossary?.grInPortion == 0
? Portion.grams
: Portion.pcs;
? Portion.grams
: Portion.pcs;
final glossary = selectedGlossary;
if (glossary != null) {
final [calories, carbohydrates, proteins, fats] = _getCPFC(glossary, measurementUnits);
@@ -235,19 +189,11 @@ class _AddMealWidgetState extends State<AddMealWidget> {
portion: measurementUnits,
);
if (portionIndex == null) {
widget.createDietCubit.addPortion(
dayIndex: widget.dayIndex,
mealIndex: widget.mealIndex,
portion: portion,
);
widget.createDietCubit.addPortion(dayIndex: widget.dayIndex, mealIndex: widget.mealIndex, portion: portion);
} else {
widget.createDietCubit.updatePortion(
dayIndex: widget.dayIndex,
mealIndex: widget.mealIndex,
portionIndex: portionIndex,
portion: portion,
);
widget.createDietCubit.updatePortion(dayIndex: widget.dayIndex, mealIndex: widget.mealIndex, portionIndex: portionIndex, portion: portion);
}
Navigator.of(context).pop();
} else {
Navigator.of(context).pop();
final newGlossary = await showModalBottomSheet<GlossaryModel>(
@@ -255,13 +201,8 @@ class _AddMealWidgetState extends State<AddMealWidget> {
useRootNavigator: true,
context: context,
builder: (context) => Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: GlossaryModerationRequestModalWidget(
productName: productNameController.text,
diaryCubit: sl<DiaryCubit>(),
),
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: GlossaryModerationRequestModalWidget(productName: productNameController.text, diaryCubit: sl<DiaryCubit>()),
),
);
if (newGlossary != null) {
@@ -275,18 +216,9 @@ class _AddMealWidgetState extends State<AddMealWidget> {
portion: measurementUnits,
);
if (portionIndex == null) {
widget.createDietCubit.addPortion(
dayIndex: widget.dayIndex,
mealIndex: widget.mealIndex,
portion: portion,
);
widget.createDietCubit.addPortion(dayIndex: widget.dayIndex, mealIndex: widget.mealIndex, portion: portion);
} else {
widget.createDietCubit.updatePortion(
dayIndex: widget.dayIndex,
mealIndex: widget.mealIndex,
portion: portion,
portionIndex: portionIndex,
);
widget.createDietCubit.updatePortion(dayIndex: widget.dayIndex, mealIndex: widget.mealIndex, portion: portion, portionIndex: portionIndex);
}
}
}

View File

@@ -22,12 +22,7 @@ enum PortionState {
@freezed
abstract class DiaryDayWrapperModel with _$DiaryDayWrapperModel {
const factory DiaryDayWrapperModel({
@JsonKey(
name: "diary",
)
required DiaryDayModel diary,
}) = _DiaryDayWrapperModel;
const factory DiaryDayWrapperModel({@JsonKey(name: "diary") required DiaryDayModel diary}) = _DiaryDayWrapperModel;
factory DiaryDayWrapperModel.fromJson(Map<String, dynamic> json) => _$DiaryDayWrapperModelFromJson(json);
}
@@ -71,21 +66,13 @@ DiaryDietModel? _parseDiet(dynamic json) {
@freezed
abstract class DiaryDietModel with _$DiaryDietModel {
const factory DiaryDietModel({
required int id,
required int duration,
required String name,
}) = _DiaryDietModel;
const factory DiaryDietModel({required int id, required int duration, required String name}) = _DiaryDietModel;
factory DiaryDietModel.fromJson(Map<String, dynamic> json) => _$DiaryDietModelFromJson(json);
}
TypeOfDiaryModel _parseDayTypeForFutureDay(dynamic json) {
return TypeOfDiaryModel(
hasChosenDiet: json['has_chosen_diet'] as bool,
hasChosenPlan: json['has_chosen_plan'] as bool,
diaryState: DiaryStateEnum.init,
);
return TypeOfDiaryModel(hasChosenDiet: json['has_chosen_diet'] as bool, hasChosenPlan: json['has_chosen_plan'] as bool, diaryState: DiaryStateEnum.init);
}
@freezed
@@ -115,11 +102,30 @@ abstract class DiaryPhotoModel with _$DiaryPhotoModel {
factory DiaryPhotoModel.fromJson(Map<String, dynamic> json) => _$DiaryPhotoModelFromJson(json);
}
@freezed
abstract class ProductCaloriesInfoModel with _$ProductCaloriesInfoModel {
const factory ProductCaloriesInfoModel({
@JsonKey(name: "id") required int id,
@JsonKey(name: "product_name") required String productName,
@JsonKey(name: "energy_on_100gr") required double calories,
@JsonKey(name: "proteins_on_100gr") required double proteins,
@JsonKey(name: "fats_on_100gr") required double fats,
@JsonKey(name: "carbohydrates_on_100gr") required double carbohydrates,
@JsonKey(name: "energy_on_portion") required double energyOnPortion,
@JsonKey(name: "proteins_on_portion") required double proteinsOnPortion,
@JsonKey(name: "fats_on_portion") required double fatsOnPortion,
@JsonKey(name: "carbohydrates_on_portion") required double carbohydratesOnPortion,
}) = _ProductCaloriesInfoModel;
factory ProductCaloriesInfoModel.fromJson(Map<String, dynamic> json) => _$ProductCaloriesInfoModelFromJson(json);
}
@freezed
abstract class PortionModel with _$PortionModel {
const factory PortionModel({
@JsonKey(name: "id") required int id,
@JsonKey(name: "product_nutritional_value_id") int? productNutritionalValueId,
ProductCaloriesInfoModel? productCaloriesInfo,
@JsonKey(name: "product_name") required String productName,
@JsonKey(name: "quantity") required int quantity,
@JsonKey(name: "portion") required String portion,

View File

@@ -14,12 +14,7 @@ class AddProductModal extends StatefulWidget {
final DiaryStateLoaded state;
final DiaryCubit diaryCubit;
const AddProductModal({
super.key,
this.portion,
required this.state,
required this.diaryCubit,
});
const AddProductModal({super.key, this.portion, required this.state, required this.diaryCubit});
@override
State<AddProductModal> createState() => _AddProductModalState();
@@ -58,75 +53,62 @@ class _AddProductModalState extends State<AddProductModal> {
return StatefulBuilder(
builder: (BuildContext context, void Function(void Function()) setState) {
return Padding(
padding: const EdgeInsets.all(16) +
EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
padding: const EdgeInsets.all(16) + EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Добавление блюда',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Color(0xFF323F4B),
),
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)),
),
const SizedBox(height: 16),
Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: MediaQuery.of(context).size.width * 0.8 - 32,
width: MediaQuery.of(context).size.width * 0.76 - 32,
child: CustomTextFormField(
controller: productNameController,
autofocus: true,
hintText: 'Введите название продукта/блюда',
onChanged: (query) {
EasyDebounce.debounce(
'glossary-search',
const Duration(milliseconds: 250),
() async {
selectedGlossary = null;
if (query.isEmpty) {
glossariesList = [];
setState(() {});
return;
}
glossariesList = await widget.diaryCubit.getGlossaryListByQuery(query);
EasyDebounce.debounce('glossary-search', const Duration(milliseconds: 250), () async {
selectedGlossary = null;
if (query.isEmpty) {
glossariesList = [];
setState(() {});
},
);
return;
}
glossariesList = await widget.diaryCubit.getGlossaryListByQuery(query);
setState(() {});
});
},
),
),
const SizedBox(width: 4),
SizedBox(
width: MediaQuery.of(context).size.width * 0.2 - 4,
width: MediaQuery.of(context).size.width * 0.24 - 4,
child: CustomTextFormField(
controller: quantityController,
keyboardType: TextInputType.number,
hintText: selectedGlossary == null
? '100'
: selectedGlossary!.grInPortion == 0
? '100'
: '1',
? '100'
: '1',
suffixText: selectedGlossary == null
? 'гр'
: selectedGlossary?.grInPortion == 0
? 'гр'
: 'шт',
? 'гр'
: 'шт',
),
),
],
),
glossariesList.isEmpty ? const SizedBox(height: 16) : const SizedBox.shrink(),
Container(
constraints: const BoxConstraints(
maxHeight: 256,
),
constraints: const BoxConstraints(maxHeight: 256),
child: ListView.separated(
physics: const BouncingScrollPhysics(),
shrinkWrap: true,
@@ -141,28 +123,12 @@ class _AddProductModalState extends State<AddProductModal> {
setState(() {});
FocusManager.instance.primaryFocus?.nextFocus();
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
title: Text(
glossariesList[i].name,
style: const TextStyle(
height: 1,
),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: Text(glossariesList[i].name, style: const TextStyle(height: 1)),
subtitle: glossariesList[i].grInPortion == 0
? null
: Text(
'${glossariesList[i].grInPortion} г',
style: TextStyle(
height: 1,
color: Colors.grey.shade600,
),
),
trailing: Icon(
Icons.add_rounded,
color: Colors.grey.shade400,
),
: Text('${glossariesList[i].grInPortion} г', style: TextStyle(height: 1, color: Colors.grey.shade600)),
trailing: Icon(Icons.add_rounded, color: Colors.grey.shade400),
contentPadding: const EdgeInsets.only(left: 12),
);
},
@@ -188,14 +154,8 @@ class _AddProductModalState extends State<AddProductModal> {
child: Ink(
width: 52,
height: 52,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: const Color(0xFFF6F6F6),
),
child: const Icon(
Icons.delete_outline_rounded,
color: Colors.red,
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFF6F6F6)),
child: const Icon(Icons.delete_outline_rounded, color: Colors.red),
),
),
if (widget.portion != null) const SizedBox(width: 8),
@@ -209,8 +169,8 @@ class _AddProductModalState extends State<AddProductModal> {
portion: selectedGlossary == null
? 'gr'
: selectedGlossary!.grInPortion == 0
? 'gr'
: 'pcs',
? 'gr'
: 'pcs',
quantity: int.parse(quantityController.text),
productNutritionalValueId: selectedGlossary?.id,
);
@@ -223,8 +183,8 @@ class _AddProductModalState extends State<AddProductModal> {
portion: selectedGlossary == null
? 'gr'
: selectedGlossary!.grInPortion == 0
? 'gr'
: 'pcs',
? 'gr'
: 'pcs',
quantity: int.parse(quantityController.text),
productNutritionalValueId: selectedGlossary?.id,
portionState: widget.portion!.portionState,
@@ -237,13 +197,8 @@ class _AddProductModalState extends State<AddProductModal> {
useRootNavigator: true,
context: context,
builder: (context) => Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: GlossaryModerationRequestModalWidget(
productName: productNameController.text,
diaryCubit: widget.diaryCubit,
),
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: GlossaryModerationRequestModalWidget(productName: productNameController.text, diaryCubit: widget.diaryCubit),
),
);
}
@@ -252,18 +207,11 @@ class _AddProductModalState extends State<AddProductModal> {
child: Ink(
width: MediaQuery.of(context).size.width - (widget.portion == null ? 32 : 92),
height: 52,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: appTheme.blueA400,
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
child: Center(
child: Text(
widget.portion == null ? 'Добавить' : 'Сохранить',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.white,
),
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white),
),
),
),

View File

@@ -1,16 +1,15 @@
import 'package:collection/collection.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/meal_summary_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MealButtonWidget extends StatefulWidget {
final int mealNumber;
const MealButtonWidget({
super.key,
required this.mealNumber,
});
const MealButtonWidget({super.key, required this.mealNumber});
@override
State<MealButtonWidget> createState() => _MealButtonWidgetState();
@@ -22,9 +21,7 @@ class _MealButtonWidgetState extends State<MealButtonWidget> {
return BlocBuilder<DiaryCubit, DiaryState>(
builder: (context, state) {
if (state is DiaryStateLoaded) {
final consultantComment = state.consultantReview?.consultantComments.mealsComments.firstWhereOrNull(
(element) => element.typeNo == widget.mealNumber,
);
final consultantComment = state.consultantReview?.consultantComments.mealsComments.firstWhereOrNull((element) => element.typeNo == widget.mealNumber);
final Color bgColor;
if (state.selectedMeal == widget.mealNumber) {
@@ -34,55 +31,84 @@ class _MealButtonWidgetState extends State<MealButtonWidget> {
} else {
bgColor = const Color(0xFFF1F2F6);
}
return InkWell(
onTap: () {
context.read<DiaryCubit>().setSelectedMeal(widget.mealNumber);
},
borderRadius: BorderRadius.circular(100),
child: Ink(
width: MediaQuery.of(context).size.width / 5 - (96 / 5),
height: 32,
decoration: BoxDecoration(
final mealIndex = widget.mealNumber - 1;
final portions = mealIndex < state.diary.meals.length ? state.diary.meals[mealIndex].portions : <PortionModel>[];
final totals = MealSummaryWidget.calculateMealTotals(portions);
final calories = totals['calories']!.round();
return Column(
mainAxisSize: MainAxisSize.min,
children: [
InkWell(
onTap: () {
context.read<DiaryCubit>().setSelectedMeal(widget.mealNumber);
},
borderRadius: BorderRadius.circular(100),
color: bgColor,
),
child: Center(
child: Text(
'${widget.mealNumber}',
style: TextStyle(
color: state.selectedMeal == widget.mealNumber ? Colors.white : const Color(0xFF6D6F71),
fontSize: 16,
fontWeight: FontWeight.w600,
child: Ink(
width: MediaQuery.of(context).size.width / 5 - (96 / 5),
height: 32,
decoration: BoxDecoration(borderRadius: BorderRadius.circular(100), color: bgColor),
child: Center(
child: Text(
'${widget.mealNumber}',
style: TextStyle(
color: state.selectedMeal == widget.mealNumber ? Colors.white : const Color(0xFF6D6F71),
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
),
),
const SizedBox(height: 4),
Text(
calories > 0 ? '$calories ккал' : '— ккал',
style: const TextStyle(color: Color(0xFF6D6F71), fontSize: 12, fontWeight: FontWeight.w500),
),
],
);
}
if (state is DiaryStateLoadedTomorrowWithDiet) {
return InkWell(
onTap: () {
context.read<DiaryCubit>().setSelectedMeal(widget.mealNumber);
},
borderRadius: BorderRadius.circular(100),
child: Ink(
width: MediaQuery.of(context).size.width / 5 - (96 / 5),
height: 32,
decoration: BoxDecoration(
final mealIndex = widget.mealNumber - 1;
final portions = mealIndex < state.diary.meals.length ? state.diary.meals[mealIndex].portions : <PortionModel>[];
final totals = MealSummaryWidget.calculateMealTotals(portions);
final calories = totals['calories']!.round();
return Column(
mainAxisSize: MainAxisSize.min,
children: [
InkWell(
onTap: () {
context.read<DiaryCubit>().setSelectedMeal(widget.mealNumber);
},
borderRadius: BorderRadius.circular(100),
color: state.selectedMeal == widget.mealNumber ? appTheme.blueA400 : const Color(0xFFF1F2F6),
),
child: Center(
child: Text(
'${widget.mealNumber}',
style: TextStyle(
color: state.selectedMeal == widget.mealNumber ? Colors.white : const Color(0xFF6D6F71),
fontSize: 16,
fontWeight: FontWeight.w600,
child: Ink(
width: MediaQuery.of(context).size.width / 5 - (96 / 5),
height: 32,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(100),
color: state.selectedMeal == widget.mealNumber ? appTheme.blueA400 : const Color(0xFFF1F2F6),
),
child: Center(
child: Text(
'${widget.mealNumber}',
style: TextStyle(
color: state.selectedMeal == widget.mealNumber ? Colors.white : const Color(0xFF6D6F71),
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
),
),
const SizedBox(height: 4),
Text(
calories > 0 ? '$calories ккал' : '— ккал',
style: const TextStyle(color: Color(0xFF6D6F71), fontSize: 12, fontWeight: FontWeight.w500),
),
],
);
}
return const SizedBox.shrink();

View File

@@ -0,0 +1,117 @@
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
import 'package:flutter/material.dart';
class MealSummaryWidget extends StatelessWidget {
final List<PortionModel> portions;
final int mealNumber;
const MealSummaryWidget({super.key, required this.portions, required this.mealNumber});
@override
Widget build(BuildContext context) {
final totals = _calculateTotals();
if (totals['calories'] == 0 && totals['proteins'] == 0 && totals['fats'] == 0 && totals['carbohydrates'] == 0) {
return const SizedBox.shrink();
}
return Container(
margin: const EdgeInsets.only(top: 8),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFF2F80ED), width: 2),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Прием $mealNumber',
style: const TextStyle(color: Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w600),
),
Row(
children: [
_buildNutritionChip('К', totals['calories']!.round()),
const SizedBox(width: 4),
_buildNutritionChip('Б', totals['proteins']!.round()),
const SizedBox(width: 4),
_buildNutritionChip('Ж', totals['fats']!.round()),
const SizedBox(width: 4),
_buildNutritionChip('У', totals['carbohydrates']!.round()),
],
),
],
),
);
}
Widget _buildNutritionChip(String label, int value) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(color: const Color(0xFFF6FAFF), borderRadius: BorderRadius.circular(12)),
child: RichText(
text: TextSpan(
children: [
TextSpan(
text: '$label ',
style: const TextStyle(color: Color(0xFF2F80ED), fontSize: 12, fontWeight: FontWeight.w600),
),
TextSpan(
text: '$value',
style: const TextStyle(color: Color(0xFF6D6F71), fontSize: 12, fontWeight: FontWeight.w600),
),
],
),
),
);
}
Map<String, double> _calculateTotals() {
return calculateMealTotals(portions);
}
static Map<String, double> calculateMealTotals(List<PortionModel> portions) {
double totalCalories = 0;
double totalProteins = 0;
double totalFats = 0;
double totalCarbohydrates = 0;
for (final portion in portions) {
if (portion.portionState == PortionState.eatenDeleted) {
continue;
}
// Для продуктов из диеты учитываем только съеденные (с галочкой)
if (portion.fromDiet && portion.portionState != PortionState.eatenFromDiet) {
continue;
}
final caloriesInfo = portion.productCaloriesInfo;
if (caloriesInfo == null) {
continue;
}
double calories, proteins, fats, carbohydrates;
if (portion.portion == 'gr') {
calories = (caloriesInfo.calories * portion.quantity) / 100;
proteins = (caloriesInfo.proteins * portion.quantity) / 100;
fats = (caloriesInfo.fats * portion.quantity) / 100;
carbohydrates = (caloriesInfo.carbohydrates * portion.quantity) / 100;
} else {
calories = caloriesInfo.energyOnPortion;
proteins = caloriesInfo.proteinsOnPortion;
fats = caloriesInfo.fatsOnPortion;
carbohydrates = caloriesInfo.carbohydratesOnPortion;
}
totalCalories += calories;
totalProteins += proteins;
totalFats += fats;
totalCarbohydrates += carbohydrates;
}
return {'calories': totalCalories, 'proteins': totalProteins, 'fats': totalFats, 'carbohydrates': totalCarbohydrates};
}
}

View File

@@ -40,11 +40,7 @@ class _MealWidgetState extends State<MealWidget> {
children: [
const Text(
'Прием пищи',
style: TextStyle(
color: Color(0xFF323F4B),
fontSize: 18,
fontWeight: FontWeight.w500,
),
style: TextStyle(color: Color(0xFF323F4B), fontSize: 18, fontWeight: FontWeight.w500),
),
const SizedBox(height: 16),
const Row(
@@ -57,23 +53,17 @@ class _MealWidgetState extends State<MealWidget> {
MealButtonWidget(mealNumber: 5),
],
),
const SizedBox(height: 12),
const SizedBox(height: 16),
const TimeOfMealPickerWidget(),
const ChosenDietWidget(),
const ChosenPlanWidget(),
const ProductsListWidget(),
if (widget.showAddButton) ...[
const SizedBox(height: 12),
const AddProductButtonWidget(),
],
if (widget.showAddButton) ...[const SizedBox(height: 12), const AddProductButtonWidget()],
const SizedBox(height: 12),
const PhotoGalleryWidget(),
const SizedBox(height: 12),
const SatietyRatingWidget(),
if (widget.showMood) ...[
const SizedBox(height: 12),
const MoodRatingWidget(),
],
if (widget.showMood) ...[const SizedBox(height: 12), const MoodRatingWidget()],
BlocBuilder<DiaryCubit, DiaryState>(
builder: (context, state) {
if (state is DiaryStateLoaded && state.isConsultantView && !state.isDiaryCommented) {
@@ -91,10 +81,7 @@ class _MealWidgetState extends State<MealWidget> {
if (consultantCommentOfMeal != null) {
return Column(
children: [
Text(
'${s.mealComment} ${state.selectedMeal}',
style: TextStyles.textLSemibold.copyWith(color: appTheme.black),
),
Text('${s.mealComment} ${state.selectedMeal}', style: TextStyles.textLSemibold.copyWith(color: appTheme.black)),
const SizedBox(height: 12),
CommentContainer(comment: consultantCommentOfMeal.comment),
],
@@ -103,7 +90,7 @@ class _MealWidgetState extends State<MealWidget> {
}
return const SizedBox.shrink();
},
)
),
],
),
),

View File

@@ -23,9 +23,12 @@ class _MoodRatingWidgetState extends State<MoodRatingWidget> {
builder: (BuildContext context, DiaryState state) {
if (state is DiaryStateLoaded) {
final selectedIndex = state.selectedMeal - 1;
if (isFirstFetch[selectedIndex]) {
currentSelectedRating[selectedIndex] = state.diary.meals[selectedIndex].mood.toDouble() + 1;
isFirstFetch[selectedIndex] = false;
final meals = state.diary.meals;
if (selectedIndex >= 0 && selectedIndex < meals.length) {
if (isFirstFetch[selectedIndex]) {
currentSelectedRating[selectedIndex] = meals[selectedIndex].mood.toDouble() + 1;
isFirstFetch[selectedIndex] = false;
}
}
return Padding(
padding: const EdgeInsets.only(top: 24),
@@ -35,21 +38,17 @@ class _MoodRatingWidgetState extends State<MoodRatingWidget> {
Text(
state.isConsultantView ? 'Настроение пользователя после приема пищи' : 'Ваше настроение после приёма пищи',
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 16,
),
style: const TextStyle(fontSize: 16),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SvgPicture.asset(
'assets/icons/mood_1.svg',
height: 32,
width: 32,
),
SvgPicture.asset('assets/icons/mood_1.svg', height: 32, width: 32),
RatingBar.builder(
ignoreGestures: state.isConsultantView || state.isDiaryFinished || state.isDiaryCommented,
initialRating: state.diary.meals[selectedIndex].mood.toDouble() + 1,
initialRating: (selectedIndex >= 0 && selectedIndex < state.diary.meals.length)
? state.diary.meals[selectedIndex].mood.toDouble() + 1
: 3,
itemCount: 6,
itemSize: MediaQuery.of(context).size.width / 6 - (124 / 6),
updateOnDrag: true,
@@ -63,20 +62,11 @@ class _MoodRatingWidgetState extends State<MoodRatingWidget> {
decoration: BoxDecoration(
color: const Color(0xFFC82222),
border: currentSelectedRating[selectedIndex] == 1
? Border.all(
width: 2,
color: const Color(0xFFAA3333),
strokeAlign: BorderSide.strokeAlignOutside,
)
? Border.all(width: 2, color: const Color(0xFFAA3333), strokeAlign: BorderSide.strokeAlignOutside)
: null,
borderRadius: currentSelectedRating[selectedIndex] == 1
? const BorderRadius.horizontal(
left: Radius.circular(8),
right: Radius.circular(2),
)
: const BorderRadius.horizontal(
left: Radius.circular(8),
),
? const BorderRadius.horizontal(left: Radius.circular(8), right: Radius.circular(2))
: const BorderRadius.horizontal(left: Radius.circular(8)),
),
);
case 1:
@@ -87,11 +77,7 @@ class _MoodRatingWidgetState extends State<MoodRatingWidget> {
decoration: BoxDecoration(
color: const Color(0xFFFF3E3E),
border: currentSelectedRating[selectedIndex] == 2
? Border.all(
width: 2,
color: const Color(0xFF550000),
strokeAlign: BorderSide.strokeAlignOutside,
)
? Border.all(width: 2, color: const Color(0xFF550000), strokeAlign: BorderSide.strokeAlignOutside)
: null,
borderRadius: currentSelectedRating[selectedIndex] == 2 ? BorderRadius.circular(2) : null,
),
@@ -104,11 +90,7 @@ class _MoodRatingWidgetState extends State<MoodRatingWidget> {
decoration: BoxDecoration(
color: const Color(0xFFF8C926),
border: currentSelectedRating[selectedIndex] == 3
? Border.all(
width: 2,
color: const Color(0xFF8C5D00),
strokeAlign: BorderSide.strokeAlignOutside,
)
? Border.all(width: 2, color: const Color(0xFF8C5D00), strokeAlign: BorderSide.strokeAlignOutside)
: null,
borderRadius: currentSelectedRating[selectedIndex] == 3 ? BorderRadius.circular(2) : null,
),
@@ -121,11 +103,7 @@ class _MoodRatingWidgetState extends State<MoodRatingWidget> {
decoration: BoxDecoration(
color: const Color(0xFFFFF3BF),
border: currentSelectedRating[selectedIndex] == 4
? Border.all(
width: 2,
color: const Color(0xFFB5911B),
strokeAlign: BorderSide.strokeAlignOutside,
)
? Border.all(width: 2, color: const Color(0xFFB5911B), strokeAlign: BorderSide.strokeAlignOutside)
: null,
borderRadius: currentSelectedRating[selectedIndex] == 4 ? BorderRadius.circular(2) : null,
),
@@ -138,11 +116,7 @@ class _MoodRatingWidgetState extends State<MoodRatingWidget> {
decoration: BoxDecoration(
color: const Color(0xFF62E833),
border: currentSelectedRating[selectedIndex] == 5
? Border.all(
width: 2,
color: const Color(0xFF276918),
strokeAlign: BorderSide.strokeAlignOutside,
)
? Border.all(width: 2, color: const Color(0xFF276918), strokeAlign: BorderSide.strokeAlignOutside)
: null,
borderRadius: currentSelectedRating[selectedIndex] == 5 ? BorderRadius.circular(2) : null,
),
@@ -155,20 +129,11 @@ class _MoodRatingWidgetState extends State<MoodRatingWidget> {
decoration: BoxDecoration(
color: const Color(0xFF4DCE20),
border: currentSelectedRating[selectedIndex] == 6
? Border.all(
width: 2,
color: const Color(0xFF184D12),
strokeAlign: BorderSide.strokeAlignOutside,
)
? Border.all(width: 2, color: const Color(0xFF184D12), strokeAlign: BorderSide.strokeAlignOutside)
: null,
borderRadius: currentSelectedRating[selectedIndex] == 6
? const BorderRadius.horizontal(
left: Radius.circular(2),
right: Radius.circular(8),
)
: const BorderRadius.horizontal(
right: Radius.circular(8),
),
? const BorderRadius.horizontal(left: Radius.circular(2), right: Radius.circular(8))
: const BorderRadius.horizontal(right: Radius.circular(8)),
),
);
}
@@ -177,20 +142,12 @@ class _MoodRatingWidgetState extends State<MoodRatingWidget> {
setState(() {
currentSelectedRating[selectedIndex] = value;
});
EasyDebounce.debounce(
'change-satiety',
const Duration(milliseconds: 250),
() {
context.read<DiaryCubit>().fillMood(diaryId: state.diary.id, mood: currentSelectedRating[selectedIndex].toInt() - 1);
},
);
EasyDebounce.debounce('change-satiety', const Duration(milliseconds: 250), () {
context.read<DiaryCubit>().fillMood(diaryId: state.diary.id, mood: currentSelectedRating[selectedIndex].toInt() - 1);
});
},
),
SvgPicture.asset(
'assets/icons/mood_2.svg',
height: 32,
width: 32,
),
SvgPicture.asset('assets/icons/mood_2.svg', height: 32, width: 32),
],
),
],

View File

@@ -1,3 +1,4 @@
import 'package:collection/collection.dart';
import 'package:dotted_border/dotted_border.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
@@ -15,10 +16,7 @@ import 'package:flutter_svg/flutter_svg.dart';
class ProductWidget extends StatefulWidget {
final PortionModel portion;
const ProductWidget({
super.key,
required this.portion,
});
const ProductWidget({super.key, required this.portion});
@override
State<ProductWidget> createState() => _ProductWidgetState();
@@ -37,6 +35,60 @@ class _ProductWidgetState extends State<ProductWidget> {
isEaten = widget.portion.portionState == PortionState.eatenFromDiet ? true : false;
}
Widget _buildNutritionalInfo() {
if (widget.portion.productNutritionalValueId == null) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)),
child: const Text(
'Продукт отправлен на модерацию',
style: TextStyle(color: Color(0xFF2F80ED), fontSize: 12, fontWeight: FontWeight.w500),
),
);
}
final caloriesInfo = widget.portion.productCaloriesInfo;
if (caloriesInfo == null) {
return const SizedBox.shrink();
}
double calories, proteins, fats, carbohydrates;
if (widget.portion.portion == 'gr') {
calories = (caloriesInfo.calories * widget.portion.quantity) / 100;
proteins = (caloriesInfo.proteins * widget.portion.quantity) / 100;
fats = (caloriesInfo.fats * widget.portion.quantity) / 100;
carbohydrates = (caloriesInfo.carbohydrates * widget.portion.quantity) / 100;
} else {
calories = caloriesInfo.energyOnPortion;
proteins = caloriesInfo.proteinsOnPortion;
fats = caloriesInfo.fatsOnPortion;
carbohydrates = caloriesInfo.carbohydratesOnPortion;
}
return Row(
children: [
_buildNutritionChip('К', calories.round()),
const SizedBox(width: 4),
_buildNutritionChip('Б', proteins.round()),
const SizedBox(width: 4),
_buildNutritionChip('Ж', fats.round()),
const SizedBox(width: 4),
_buildNutritionChip('У', carbohydrates.round()),
],
);
}
Widget _buildNutritionChip(String label, int value) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8)),
child: Text(
'$label $value',
style: const TextStyle(color: Color(0xFF6D6F71), fontSize: 12, fontWeight: FontWeight.w500),
),
);
}
@override
void didUpdateWidget(ProductWidget oldWidget) {
super.didUpdateWidget(oldWidget);
@@ -48,15 +100,9 @@ class _ProductWidgetState extends State<ProductWidget> {
return showModalBottomSheet(
useRootNavigator: true,
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(12))),
isScrollControlled: true,
builder: (context) => AddProductModal(
state: state,
portion: portion,
diaryCubit: diaryCubit,
),
builder: (context) => AddProductModal(state: state, portion: portion, diaryCubit: diaryCubit),
);
}
@@ -69,15 +115,21 @@ class _ProductWidgetState extends State<ProductWidget> {
horizontalTitleGap: 0,
contentPadding: widget.portion.fromDiet ? const EdgeInsets.only(right: 24, left: 12) : null,
title: Text(widget.portion.productName),
subtitle: Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'),
const SizedBox(height: 4),
_buildNutritionalInfo(),
],
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
tileColor: const Color(0xFFF6FAFF),
);
}
if (state is DiaryStateLoaded) {
final isDiaryCommented = state.diary.typeOfDiary.diaryState == DiaryStateEnum.commented ||
final isDiaryCommented =
state.diary.typeOfDiary.diaryState == DiaryStateEnum.commented ||
state.diary.typeOfDiary.diaryState == DiaryStateEnum.lastDayCommented ||
state.diary.typeOfDiary.diaryState == DiaryStateEnum.reviewedByUser;
final isDiaryFinished =
@@ -99,21 +151,20 @@ class _ProductWidgetState extends State<ProductWidget> {
color: Color(0xFF6D6F71),
),
child: ListTile(
onTap: () => diaryCubit.restore(
diaryId: state.diary.id,
mealId: state.selectedMeal,
portionId: widget.portion.id,
),
trailing: SvgPicture.asset(
'assets/icons/revert_product.svg',
),
onTap: () => diaryCubit.restore(diaryId: state.diary.id, mealId: state.selectedMeal, portionId: widget.portion.id),
trailing: SvgPicture.asset('assets/icons/revert_product.svg'),
horizontalTitleGap: 0,
contentPadding: widget.portion.fromDiet ? const EdgeInsets.only(right: 24, left: 4) : null,
title: Text(widget.portion.productName),
subtitle: Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'),
const SizedBox(height: 4),
_buildNutritionalInfo(),
],
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
// ignore: use_full_hex_values_for_flutter_colors
tileColor: const Color(0xfff6faff03).withValues(alpha: 0.01),
),
@@ -122,12 +173,13 @@ class _ProductWidgetState extends State<ProductWidget> {
Widget? trailing;
if (state.isConsultantView && !isDiaryCommented) {
trailing = GestureDetector(
onTap: () {
setState(() {
showCommentWidget = !showCommentWidget;
});
},
child: SvgPicture.asset('assets/icons/edit.svg'));
onTap: () {
setState(() {
showCommentWidget = !showCommentWidget;
});
},
child: SvgPicture.asset('assets/icons/edit.svg'),
);
} else if (!isDiaryCommented && !widget.portion.fromDiet) {
trailing = const Icon(Icons.more_vert_rounded);
}
@@ -147,47 +199,36 @@ class _ProductWidgetState extends State<ProductWidget> {
isEaten = value!;
});
if (isEaten) {
diaryCubit.markEaten(
diaryId: state.diary.id,
mealId: state.selectedMeal,
portionId: widget.portion.id,
);
diaryCubit.markEaten(diaryId: state.diary.id, mealId: state.selectedMeal, portionId: widget.portion.id);
} else {
diaryCubit.unmarkEaten(
diaryId: state.diary.id,
mealId: state.selectedMeal,
portionId: widget.portion.id,
);
diaryCubit.unmarkEaten(diaryId: state.diary.id, mealId: state.selectedMeal, portionId: widget.portion.id);
}
}
: null,
activeColor: appTheme.blueA400,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
side: const BorderSide(
color: Color(0xFF809CB9),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
side: const BorderSide(color: Color(0xFF809CB9)),
),
)
: null,
horizontalTitleGap: 0,
contentPadding: widget.portion.fromDiet ? const EdgeInsets.only(right: 24, left: 4) : null,
title: Text(widget.portion.productName),
subtitle: Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'),
const SizedBox(height: 4),
_buildNutritionalInfo(),
],
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
tileColor: const Color(0xFFF6FAFF),
),
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: PortionCommentWidget(
enabled: showCommentWidget,
portionId: widget.portion.id,
comment: portionComment?.comment,
),
)
child: PortionCommentWidget(enabled: showCommentWidget, portionId: widget.portion.id, comment: portionComment?.comment),
),
],
);
}

View File

@@ -1,5 +1,6 @@
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/meal_summary_widget.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/product_loader_widget.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/product_widget.dart';
import 'package:flutter/material.dart';
@@ -16,33 +17,33 @@ class _ProductsListWidgetState extends State<ProductsListWidget> {
@override
Widget build(BuildContext context) {
return BlocBuilder<DiaryCubit, DiaryState>(
builder: (BuildContext context, DiaryState state) {
builder: (BuildContext context, DiaryState state) {
if (state is DiaryStateLoaded) {
int mealIndex = state.selectedMeal - 1;
final portions = state.diary.meals[mealIndex].portions;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
for (int i = 0; i < state.diary.meals[mealIndex].portions.length; i++) ...[
ProductWidget(portion: state.diary.meals[mealIndex].portions[i]),
i < state.diary.meals[mealIndex].portions.length - 1 ? const SizedBox(height: 8) : const SizedBox.shrink(),
for (int i = 0; i < portions.length; i++) ...[
ProductWidget(portion: portions[i]),
i < portions.length - 1 ? const SizedBox(height: 8) : const SizedBox.shrink(),
],
if (state.isLoadingAddingProductInMeal)
const Padding(
padding: EdgeInsets.only(top: 8),
child: ProductLoaderWidget(),
),
if (portions.isNotEmpty) MealSummaryWidget(portions: portions, mealNumber: state.selectedMeal),
if (state.isLoadingAddingProductInMeal) const Padding(padding: EdgeInsets.only(top: 8), child: ProductLoaderWidget()),
],
);
}
if (state is DiaryStateLoadedTomorrowWithDiet) {
int mealIndex = state.selectedMeal - 1;
final portions = state.diary.meals[mealIndex].portions;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
for (int i = 0; i < state.diary.meals[mealIndex].portions.length; i++) ...[
ProductWidget(portion: state.diary.meals[mealIndex].portions[i]),
i < state.diary.meals[mealIndex].portions.length - 1 ? const SizedBox(height: 8) : const SizedBox.shrink(),
for (int i = 0; i < portions.length; i++) ...[
ProductWidget(portion: portions[i]),
i < portions.length - 1 ? const SizedBox(height: 8) : const SizedBox.shrink(),
],
if (portions.isNotEmpty) MealSummaryWidget(portions: portions, mealNumber: state.selectedMeal),
],
);
}

View File

@@ -23,9 +23,12 @@ class _SatietyRatingWidgetState extends State<SatietyRatingWidget> {
builder: (BuildContext context, DiaryState state) {
if (state is DiaryStateLoaded) {
final selectedIndex = state.selectedMeal - 1;
if (isFirstFetch[selectedIndex]) {
currentSelectedRating[selectedIndex] = state.diary.meals[selectedIndex].satiety.toDouble() + 1;
isFirstFetch[selectedIndex] = false;
final meals = state.diary.meals;
if (selectedIndex >= 0 && selectedIndex < meals.length) {
if (isFirstFetch[selectedIndex]) {
currentSelectedRating[selectedIndex] = meals[selectedIndex].satiety.toDouble() + 1;
isFirstFetch[selectedIndex] = false;
}
}
return Padding(
padding: const EdgeInsets.only(top: 24),
@@ -35,21 +38,17 @@ class _SatietyRatingWidgetState extends State<SatietyRatingWidget> {
Text(
state.isConsultantView ? 'Сытость пользователя после приема пищи' : 'Ваша сытость после приёма пищи',
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 16,
),
style: const TextStyle(fontSize: 16),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SvgPicture.asset(
'assets/icons/satiety_emoji_left.svg',
height: 32,
width: 32,
),
SvgPicture.asset('assets/icons/satiety_emoji_left.svg', height: 32, width: 32),
RatingBar.builder(
ignoreGestures: state.isConsultantView || state.isDiaryFinished || state.isDiaryCommented,
initialRating: state.diary.meals[selectedIndex].satiety.toDouble() + 1,
initialRating: (selectedIndex >= 0 && selectedIndex < state.diary.meals.length)
? state.diary.meals[selectedIndex].satiety.toDouble() + 1
: 3,
itemCount: 6,
itemSize: MediaQuery.of(context).size.width / 6 - (124 / 6),
updateOnDrag: true,
@@ -63,20 +62,11 @@ class _SatietyRatingWidgetState extends State<SatietyRatingWidget> {
decoration: BoxDecoration(
color: const Color(0xFFC82222),
border: currentSelectedRating[selectedIndex] == 1
? Border.all(
width: 2,
color: const Color(0xFFAA3333),
strokeAlign: BorderSide.strokeAlignOutside,
)
? Border.all(width: 2, color: const Color(0xFFAA3333), strokeAlign: BorderSide.strokeAlignOutside)
: null,
borderRadius: currentSelectedRating[selectedIndex] == 1
? const BorderRadius.horizontal(
left: Radius.circular(8),
right: Radius.circular(2),
)
: const BorderRadius.horizontal(
left: Radius.circular(8),
),
? const BorderRadius.horizontal(left: Radius.circular(8), right: Radius.circular(2))
: const BorderRadius.horizontal(left: Radius.circular(8)),
),
);
case 1:
@@ -87,11 +77,7 @@ class _SatietyRatingWidgetState extends State<SatietyRatingWidget> {
decoration: BoxDecoration(
color: const Color(0xFFFF3E3E),
border: currentSelectedRating[selectedIndex] == 2
? Border.all(
width: 2,
color: const Color(0xFF550000),
strokeAlign: BorderSide.strokeAlignOutside,
)
? Border.all(width: 2, color: const Color(0xFF550000), strokeAlign: BorderSide.strokeAlignOutside)
: null,
borderRadius: currentSelectedRating[selectedIndex] == 2 ? BorderRadius.circular(2) : null,
),
@@ -104,11 +90,7 @@ class _SatietyRatingWidgetState extends State<SatietyRatingWidget> {
decoration: BoxDecoration(
color: const Color(0xFFF8C926),
border: currentSelectedRating[selectedIndex] == 3
? Border.all(
width: 2,
color: const Color(0xFF8C5D00),
strokeAlign: BorderSide.strokeAlignOutside,
)
? Border.all(width: 2, color: const Color(0xFF8C5D00), strokeAlign: BorderSide.strokeAlignOutside)
: null,
borderRadius: currentSelectedRating[selectedIndex] == 3 ? BorderRadius.circular(2) : null,
),
@@ -121,11 +103,7 @@ class _SatietyRatingWidgetState extends State<SatietyRatingWidget> {
decoration: BoxDecoration(
color: const Color(0xFFFFF3BF),
border: currentSelectedRating[selectedIndex] == 4
? Border.all(
width: 2,
color: const Color(0xFFB5911B),
strokeAlign: BorderSide.strokeAlignOutside,
)
? Border.all(width: 2, color: const Color(0xFFB5911B), strokeAlign: BorderSide.strokeAlignOutside)
: null,
borderRadius: currentSelectedRating[selectedIndex] == 4 ? BorderRadius.circular(2) : null,
),
@@ -138,11 +116,7 @@ class _SatietyRatingWidgetState extends State<SatietyRatingWidget> {
decoration: BoxDecoration(
color: const Color(0xFF62E833),
border: currentSelectedRating[selectedIndex] == 5
? Border.all(
width: 2,
color: const Color(0xFF276918),
strokeAlign: BorderSide.strokeAlignOutside,
)
? Border.all(width: 2, color: const Color(0xFF276918), strokeAlign: BorderSide.strokeAlignOutside)
: null,
borderRadius: currentSelectedRating[selectedIndex] == 5 ? BorderRadius.circular(2) : null,
),
@@ -155,20 +129,11 @@ class _SatietyRatingWidgetState extends State<SatietyRatingWidget> {
decoration: BoxDecoration(
color: const Color(0xFF4DCE20),
border: currentSelectedRating[selectedIndex] == 6
? Border.all(
width: 2,
color: const Color(0xFF184D12),
strokeAlign: BorderSide.strokeAlignOutside,
)
? Border.all(width: 2, color: const Color(0xFF184D12), strokeAlign: BorderSide.strokeAlignOutside)
: null,
borderRadius: currentSelectedRating[selectedIndex] == 6
? const BorderRadius.horizontal(
left: Radius.circular(2),
right: Radius.circular(8),
)
: const BorderRadius.horizontal(
right: Radius.circular(8),
),
? const BorderRadius.horizontal(left: Radius.circular(2), right: Radius.circular(8))
: const BorderRadius.horizontal(right: Radius.circular(8)),
),
);
}
@@ -178,20 +143,12 @@ class _SatietyRatingWidgetState extends State<SatietyRatingWidget> {
setState(() {
currentSelectedRating[selectedIndex] = value;
});
EasyDebounce.debounce(
'change-satiety',
const Duration(milliseconds: 250),
() {
context.read<DiaryCubit>().fillSatiety(diaryId: state.diary.id, satiety: currentSelectedRating[selectedIndex].toInt() - 1);
},
);
EasyDebounce.debounce('change-satiety', const Duration(milliseconds: 250), () {
context.read<DiaryCubit>().fillSatiety(diaryId: state.diary.id, satiety: currentSelectedRating[selectedIndex].toInt() - 1);
});
},
),
SvgPicture.asset(
'assets/icons/satiety_emoji_right.svg',
height: 32,
width: 32,
),
SvgPicture.asset('assets/icons/satiety_emoji_right.svg', height: 32, width: 32),
],
),
],

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