Compare commits

...

54 Commits

Author SHA1 Message Date
misha
54c27c28cd feat(test): updated tests to increase tests coverage 2026-05-06 14:12:34 +03:00
misha
fe243965e3 feat(test): 70% tests coverage 2026-05-06 05:27:25 +03:00
misha
56313a4fcc Merge branch 'issue-974' of ssh://10.55.5.123:222/misha/eatvio_app into issue-974 2026-05-05 10:52:00 +03:00
misha
cb36ef8e07 Merge remote-tracking branch 'upstream/main' into issue-974 2026-05-05 10:51:20 +03:00
misha
b2072ab626 feat(video_calls): fixed falling test 2026-05-05 10:40:11 +03:00
5b72684c95 merge upstream 2026-05-05 05:17:22 +00:00
misha
91ad51d340 feat(video_calls): first impl for main video calls screen 2026-05-05 08:09:24 +03:00
5d86b7c71b Merge pull request 'version changed' (#971) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#971
2026-04-17 14:17:46 +00:00
misha
50aefde842 version changed 2026-04-17 17:17:04 +03:00
5fbd578463 Merge pull request 'ci/cd: fastlane version' (#970) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#970
2026-04-17 11:13:08 +00:00
misha
426bd49aa1 ci/cd: fastlane version 2026-04-17 14:12:10 +03:00
c6fc20d217 Merge pull request 'version changed' (#968) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#968
Reviewed-by: Анатолий Полоз <t0ly.od@ya.ru>
2026-04-17 08:30:02 +00:00
misha
17e18dc68b version changed 2026-04-17 11:08:18 +03:00
d483295aab Merge pull request 'feat(pushNotifications): fixed push notifications navigation' (#948) from misha/eatvio_app:push-notifications-navigation into main
Reviewed-on: eatvio/eatvio_app#948
Reviewed-by: Анатолий Полоз <t0ly.od@ya.ru>
2026-04-16 09:38:30 +00:00
misha
c8284338aa feat(image_compress_utility): adde test for corupted images 2026-04-15 16:27:27 +03:00
misha
03ec91a595 feat(image_compress_utility): improved compressing logic 2026-04-15 15:50:09 +03:00
misha
e814683984 version changed 2026-04-14 09:44:12 +03:00
misha
684415ef5b feat(setting): added profile data apdating for sttings screen, added validation for general info form 2026-04-14 09:28:02 +03:00
misha
631ceb2b7a version changed 2026-04-08 12:47:07 +03:00
misha
71a405c63f feat(notifications): changed navigation flow from Cancelled diet and started diet notifications 2026-04-08 12:43:15 +03:00
misha
4ca90eb2df feat(push_navigation): fixed navigation to chat screen 2026-04-08 12:14:43 +03:00
misha
f17f1ecb57 version changed 2026-04-07 10:32:10 +03:00
misha
a71bcc752d feat(fixes): fixed analyzer issue 2026-04-07 10:06:04 +03:00
misha
aac085def4 feat(app): refactoring of sentry using, feat(become_consultant): fixed possible infinite loading, feat(push_notifications): fixed push navigation from paid plan push 2026-04-07 10:05:11 +03:00
misha
b6b13b1db5 version update 2026-04-01 13:21:19 +03:00
misha
6629286bf1 feat(CI/CD): changed gitea user token name 2026-04-01 10:59:54 +03:00
misha
966bc23f0b feat(CI/CD): removed proxy server 2026-04-01 10:49:50 +03:00
misha
a13b623ca2 feat(feed): fixed comments state updating in feed, feat(consultantRegistration): fixed registration flow for users with unverified phone, feat(tests): fixed tests coverage 2026-04-01 10:37:59 +03:00
misha
ee2dd75a94 feat(ci/cd): added test coverage verification 2026-03-31 10:20:43 +03:00
misha
c450a41715 feat(ci/cd): added token for repo 2026-03-30 10:33:46 +03:00
misha
55b6b75134 feat(ci/cd): env repo changed 2026-03-30 10:16:13 +03:00
misha
335e36dd18 feat(pushNotifications): fixed push notifications navigation 2026-03-26 18:06:20 +03:00
7106bde4ae Merge pull request 'fix 939, 927' (#940) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#940
Reviewed-by: misha <misha@noreply.localhost>
2026-03-26 08:58:17 +00:00
mso7nueh
4f241f7db6 feat: try push handling 2026-03-18 15:39:13 +03:00
mso7nueh
3d7755a9a2 feat: Introduce AppRouter for centralized application routing and deep link handling, and create push_navigation_handler for push notification navigation. 2026-03-17 16:33:54 +03:00
mso7nueh
decc8c63e6 Merge branch 'main' of http://gitea.office.local:3000/mso7nueh/eatvio_app 2026-03-17 12:32:31 +03:00
mso7nueh
ad5b48ef1b Fix merge conflicts with upstream main by retaining list refresh logic 2026-03-17 12:32:02 +03:00
mso7nueh
e8dc0138e2 Merge pull request 'feat(recipes, diets)' (#938) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#938
Reviewed-by: mso7nueh <mso7nueh@mail.ru>
2026-03-17 09:19:25 +00:00
mso7nueh
96896671b6 merge upstream 2026-03-17 09:18:48 +00:00
mso7nueh
5e2f0430d9 Merge branch 'main' of http://gitea.office.local:3000/mso7nueh/eatvio_app 2026-03-17 12:18:25 +03:00
mso7nueh
2da3b10e63 feat: Implement 'My Recipes' feature, add diary moderation request modal, and introduce diary repository. 2026-03-17 12:18:17 +03:00
misha
5cb121d733 fat(recipes, diets): added list refreshing 2026-03-16 10:32:12 +03:00
mso7nueh
109013c25c Merge pull request 'feat(diary)' (#931) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#931
Reviewed-by: mso7nueh <mso7nueh@mail.ru>
2026-03-12 13:49:32 +00:00
misha
f5d14d213f Merge remote-tracking branch 'upstream/main' 2026-03-12 16:04:58 +03:00
misha
26ae28a705 feat(tests): fixed test errors 2026-03-12 14:41:16 +03:00
mso7nueh
7672477c03 Merge pull request 'Обновить pubspec.yaml' (#932) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#932
2026-03-11 12:15:49 +00:00
mso7nueh
07277d6e48 Обновить pubspec.yaml 2026-03-11 12:15:33 +00:00
misha
9d1bedff26 feat(diary): fixed consultant portion comments and day finishing errors handling 2026-03-11 11:55:10 +03:00
misha
e39638be7f feat(notifications): removed unknown notification from layoute 2026-03-05 13:44:33 +03:00
442fc0ec14 Merge pull request 'feat(consultant_registration)' (#924) from misha/eatvio_app:issue-914 into main
Reviewed-on: eatvio/eatvio_app#924
2026-03-04 07:56:21 +00:00
a8f8cc8eb0 Merge pull request 'feat(notifications)' (#921) from misha/eatvio_app:issue-911 into main
Reviewed-on: eatvio/eatvio_app#921
2026-03-04 07:55:58 +00:00
misha
a492c2d964 Merge remote-tracking branch 'upstream/main' into issue-914 2026-03-04 09:21:45 +03:00
misha
be0a99b164 fix: ci/cd 2026-02-25 13:02:28 +03:00
misha
6dab0d159b feat(consultant_registration): added new consultant registration flow 2026-02-25 07:08:05 +03:00
251 changed files with 8432 additions and 3551 deletions

View File

@@ -8,6 +8,9 @@ jobs:
build:
runs-on: [macos-arm64]
steps:
- name: Fastlane version
run: fastlane --version
- name: Checkout
uses: https://github.com/actions/checkout@v2

View File

@@ -1,37 +1,39 @@
name: Eatvio App Android Test
on: [ pull_request ]
on: [pull_request]
env:
flutter_version: "3.41.2"
java_version: "21.x"
flutter_channel: "stable"
my_local_cache_dir: "/opt/hostedtoolcache/"
ANDROID_SDK_ROOT: "/opt/hostedtoolcache/Android/Sdk"
flutter_version: '3.41.2'
java_version: '21.x'
flutter_channel: 'stable'
my_local_cache_dir: '/opt/hostedtoolcache/'
ANDROID_SDK_ROOT: '/opt/hostedtoolcache/Android/Sdk'
jobs:
build:
runs-on: [ ubuntu-latest ]
runs-on: [ubuntu-latest]
steps:
- uses: https://github.com/actions/checkout@v3
- name: Set proxy server
env:
PROXY_USER: ${{ secrets.PROXY_USER }}
PROXY_PASSWORD: ${{ secrets.PROXY_PASSWORD }}
run: |
echo "Setting up proxy..."
cat <<EOF >/etc/apt/apt.conf.d/proxy.conf
Acquire::http::Proxy "socks5h://${PROXY_USER}:${PROXY_PASSWORD}@10.55.5.202:19843/";
EOF
# Пока что не нужно проксирование
# - name: Set proxy server
# env:
# PROXY_USER: ${{ secrets.PROXY_USER }}
# PROXY_PASSWORD: ${{ secrets.PROXY_PASSWORD }}
# run: |
# echo "Setting up proxy..."
# cat <<EOF >/etc/apt/apt.conf.d/proxy.conf
# Acquire::http::Proxy "socks5h://${PROXY_USER}:${PROXY_PASSWORD}@10.55.5.202:19843/";
# EOF
- name: Checkout env repository
uses: https://github.com/actions/checkout@v3
with:
repository: mso7nueh/eatvio_app_env
repository: eatvio/eatvio_app_env
path: env_repo
token: ${{ vars.MSO7NUEH_TOKEN }}
# Здесь может быть токен любого юзера с доступом в gitea
token: ${{ vars.READ_USER_GITEA_TOKEN }}
ref: main
- name: Copy .env file
@@ -66,7 +68,7 @@ jobs:
- name: Cache Android SDK
id: cache-sdk
uses: "https://github.com/actions/cache@v4"
uses: 'https://github.com/actions/cache@v4'
with:
path: ${{ env.my_local_cache_dir }}
key: ${{ runner.os }}-cache-sdk
@@ -74,14 +76,14 @@ jobs:
- name: Set up JDK
uses: https://github.com/actions/setup-java@v3
with:
distribution: "zulu"
distribution: 'zulu'
java-version: ${{ env.java_version }}
cache: gradle
- name: Install utils for Android SDK
run: |
apt-get update
apt-get install -y openjdk-17-jdk wget jq clang cmake ninja-build libgtk-3-dev
apt-get install -y openjdk-17-jdk wget jq clang cmake ninja-build libgtk-3-dev
- name: Install Android SDK
if: steps.cache-sdk.outputs.cache-hit != 'true'
@@ -100,15 +102,14 @@ jobs:
sdkmanager "platform-tools" "platforms;android-35" "platforms;android-34" "platforms;android-33" "platforms;android-31"
sdkmanager "build-tools;34.0.0"
sdkmanager "ndk;25.1.8937393"
sdkmanager --install "cmake;3.22.1"
sdkmanager --install "cmake;3.22.1"
- name: Set up Android environment variables
id: "android-env-setup"
id: 'android-env-setup'
run: |
echo "ANDROID_SDK_ROOT=${{ env.my_local_cache_dir }}Android/Sdk" >> $GITHUB_ENV
echo "PATH=$PATH:${{ env.my_local_cache_dir }}Android/Sdk/cmdline-tools/latest/bin:${{ env.my_local_cache_dir }}Android/Sdk/platform-tools:${{ env.my_local_cache_dir }}Android/Sdk/build-tools/30.0.3:${{ env.my_local_cache_dir }}Android/Sdk/build-tools/34.0.0" >> $GITHUB_ENV
echo "ANDROID_HOME=${{ env.my_local_cache_dir }}Android/Sdk" >> $GITHUB_ENV
echo "ANDROID_HOME=${{ env.my_local_cache_dir }}Android/Sdk" >> $GITHUB_ENV
- name: Set up Flutter
run: |
@@ -147,7 +148,7 @@ jobs:
if [ "$ARCH" = "x86_64" ]; then ARCH="x64"; fi
git config --global --add safe.directory "${{ env.my_local_cache_dir }}flutter"
flutter config --android-sdk="$ANDROID_HOME"
flutter doctor -v
flutter doctor -v
- name: Get dependencies
run: |
@@ -212,12 +213,11 @@ jobs:
percentage = (covered / total) * 100
printf "✅ Coverage: %.2f%%\n", percentage
print ""
if (percentage < 60) {
printf "⚠️ Warning: Coverage is below 60%%\n"
} else if (percentage < 80) {
printf "💛 Good: Coverage is above 60%%\n"
if (percentage < 70) {
printf "❌ FAIL: Coverage is below 70%%\n"
exit 1
} else {
printf "💚 Excellent: Coverage is above 80%%\n"
printf "💚 Excellent: Coverage is above 70%%\n"
}
}
}
@@ -236,7 +236,7 @@ jobs:
APK_NAME="eatvio_app_${TIMESTAMP}.apk"
mv build/app/outputs/flutter-apk/app-release.apk $APK_NAME
echo "APK build finished $APK_NAME"
echo "APK_NAME=$APK_NAME" >> $GITHUB_ENV
echo "APK_NAME=$APK_NAME" >> $GITHUB_ENV
- name: Upload APK artifact
uses: https://github.com/actions/upload-artifact@v3

View File

@@ -1,6 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
@@ -55,32 +57,19 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<!-- Main domains -->
<data android:host="eatvio.ru" />
<data android:host="www.eatvio.ru" />
<data android:host="eatvio.com" />
<data android:host="www.eatvio.com" />
<!-- Development/Subdomains -->
<data android:host="*.eatvio.ru" />
<data android:host="*.eatvio.com" />
<data android:pathPattern="/ru/recipe/.*" />
<data android:pathPattern="/en/recipe/.*" />
<data android:pathPattern="/ru/blog_post/.*" />
<data android:pathPattern="/en/blog_post/.*" />
<data android:pathPattern="/ru/diet/.*" />
<data android:pathPattern="/en/diet/.*" />
<data android:pathPattern="/ru/plan/.*" />
<data android:pathPattern="/en/plan/.*" />
<data android:pathPattern="/ru/chat/.*" />
<data android:pathPattern="/en/chat/.*" />
<data android:pathPattern="/ru/chat" />
<data android:pathPattern="/en/chat" />
<data android:pathPattern="/ru/profile/.*" />
<data android:pathPattern="/en/profile/.*" />
<data android:pathPattern="/ru/diary/.*" />
<data android:pathPattern="/en/diary/.*" />
<!-- Simplify patterns to allow navigation within the app for all matching hosts -->
<data android:pathPattern="/.*" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.

View File

@@ -0,0 +1,3 @@
<svg width="10" height="9" viewBox="0 0 10 9" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.53669 8.89177L6.8122 7.15382C6.52317 7.26749 6.22614 7.34957 5.92111 7.40005C5.61615 7.45053 5.3063 7.47578 4.99155 7.47578C3.95998 7.47578 3.01527 7.19683 2.15742 6.63893C1.29964 6.08096 0.619788 5.34562 0.117849 4.43293C0.0770142 4.36144 0.047164 4.28862 0.0282984 4.21447C0.00943278 4.14041 0 4.06506 0 3.98842C0 3.91178 0.0102087 3.8356 0.030626 3.75987C0.0510433 3.68423 0.0816693 3.61224 0.122504 3.5439C0.317857 3.21368 0.525501 2.89417 0.745437 2.58538C0.96529 2.2765 1.22096 2.00328 1.51243 1.76572L0.406101 0.633204C0.338315 0.564287 0.303606 0.478889 0.301972 0.377008C0.300421 0.275127 0.337662 0.185536 0.413696 0.108233C0.484667 0.0360776 0.570746 0 0.671934 0C0.773041 0 0.85908 0.0360776 0.93005 0.108233L9.05304 8.3668C9.12091 8.43579 9.15689 8.52119 9.16097 8.62299C9.16505 8.72487 9.12908 8.81446 9.05304 8.89177C8.98207 8.96392 8.89599 9 8.79481 9C8.6937 9 8.60766 8.96392 8.53669 8.89177ZM4.99155 6.01955C5.10025 6.01955 5.20405 6.01299 5.30295 5.99988C5.40194 5.98676 5.49855 5.95815 5.5928 5.91406L3.09751 3.37713C3.05725 3.47295 3.02989 3.57118 3.01544 3.67181C3.00098 3.77237 2.99375 3.8779 2.99375 3.98842C2.99375 4.5537 3.18772 5.03355 3.57565 5.42795C3.96358 5.82235 4.43554 6.01955 4.99155 6.01955ZM4.99155 0.501058C6.02622 0.501058 6.97529 0.781293 7.83878 1.34176C8.70227 1.90223 9.38344 2.64201 9.88227 3.56109C9.91992 3.62436 9.94896 3.69224 9.96937 3.76473C9.98979 3.83721 10 3.91178 10 3.98842C10 4.06506 9.99106 4.13962 9.97317 4.21211C9.95529 4.28459 9.92748 4.35247 9.88975 4.41574C9.74086 4.70378 9.57442 4.97521 9.39042 5.23004C9.20634 5.48487 9.00057 5.72471 8.77312 5.94956C8.67136 6.05302 8.55074 6.10059 8.41125 6.09229C8.27184 6.08399 8.15121 6.02811 8.04937 5.92465L7.0223 4.88517C6.97142 4.83344 6.93781 4.77369 6.92147 4.70594C6.90514 4.63827 6.90608 4.56769 6.92429 4.49421C6.94749 4.40868 6.96411 4.32565 6.97415 4.24511C6.98428 4.16465 6.98934 4.07909 6.98934 3.98842C6.98934 3.42313 6.79538 2.94329 6.40745 2.54888C6.01952 2.15448 5.54755 1.95728 4.99155 1.95728C4.90236 1.95728 4.81898 1.96322 4.74139 1.97509C4.66381 1.98688 4.58447 2.00461 4.50337 2.02827C4.43117 2.04994 4.35894 2.05248 4.28666 2.03587C4.21446 2.01926 4.15293 1.9851 4.10205 1.93337L3.64584 1.46967C3.50643 1.32785 3.46012 1.16274 3.50692 0.974343C3.55372 0.785942 3.67136 0.665878 3.85985 0.614149C4.04516 0.575788 4.23174 0.547349 4.41958 0.528832C4.60742 0.510316 4.79807 0.501058 4.99155 0.501058ZM5.94046 3.04758C6.03977 3.15876 6.1216 3.28347 6.18596 3.42172C6.25032 3.55997 6.29066 3.70449 6.30699 3.85527C6.31516 3.91531 6.29303 3.95699 6.2406 3.98032C6.18817 4.00357 6.139 3.9919 6.0931 3.94532L5.04153 2.88093C4.99563 2.83427 4.98542 2.78428 5.0109 2.73097C5.0363 2.67767 5.07852 2.65101 5.13757 2.65101C5.29593 2.66438 5.44199 2.70478 5.57577 2.7722C5.70962 2.83954 5.83119 2.93133 5.94046 3.04758Z" fill="#2F80ED"/>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,3 @@
<svg width="18" height="14" viewBox="0 0 18 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 12.0833V7.91666C0 7.56944 0.121528 7.2743 0.364583 7.03125C0.607639 6.78819 0.902778 6.66666 1.25 6.66666C1.59722 6.66666 1.89236 6.78819 2.13542 7.03125C2.37847 7.2743 2.5 7.56944 2.5 7.91666V12.0833C2.5 12.4306 2.37847 12.7257 2.13542 12.9687C1.89236 13.2118 1.59722 13.3333 1.25 13.3333C0.902778 13.3333 0.607639 13.2118 0.364583 12.9687C0.121528 12.7257 0 12.4306 0 12.0833ZM5 12.0833V5.83333C5 5.48611 5.12153 5.19097 5.36458 4.94791C5.60764 4.70486 5.90278 4.58333 6.25 4.58333C6.59722 4.58333 6.89236 4.70486 7.13542 4.94791C7.37847 5.19097 7.5 5.48611 7.5 5.83333V12.0833C7.5 12.4306 7.37847 12.7257 7.13542 12.9687C6.89236 13.2118 6.59722 13.3333 6.25 13.3333C5.90278 13.3333 5.60764 13.2118 5.36458 12.9687C5.12153 12.7257 5 12.4306 5 12.0833ZM10 12.0833V3.75C10 3.40278 10.1215 3.10764 10.3646 2.86458C10.6076 2.62153 10.9028 2.5 11.25 2.5C11.5972 2.5 11.8924 2.62153 12.1354 2.86458C12.3785 3.10764 12.5 3.40278 12.5 3.75V12.0833C12.5 12.4306 12.3785 12.7257 12.1354 12.9687C11.8924 13.2118 11.5972 13.3333 11.25 13.3333C10.9028 13.3333 10.6076 13.2118 10.3646 12.9687C10.1215 12.7257 10 12.4306 10 12.0833ZM15 6.8125V1.25C15 0.902777 15.1215 0.607639 15.3646 0.364583C15.6076 0.121528 15.9028 0 16.25 0C16.5972 0 16.8924 0.121528 17.1354 0.364583C17.3785 0.607639 17.5 0.902777 17.5 1.25V6.54166C17.3611 6.51389 17.2257 6.49305 17.0938 6.47916C16.9618 6.46528 16.8194 6.45833 16.6667 6.45833C16.375 6.45833 16.0868 6.48958 15.8021 6.55208C15.5174 6.61458 15.25 6.70139 15 6.8125ZM16.0729 13.0937C15.9132 12.934 15.8333 12.7361 15.8333 12.5C15.8333 12.2639 15.9132 12.066 16.0729 11.9062C16.2326 11.7465 16.4306 11.6667 16.6667 11.6667C16.9028 11.6667 17.1007 11.7465 17.2604 11.9062C17.4201 12.066 17.5 12.2639 17.5 12.5C17.5 12.7361 17.4201 12.934 17.2604 13.0937C17.1007 13.2535 16.9028 13.3333 16.6667 13.3333C16.4306 13.3333 16.2326 13.2535 16.0729 13.0937ZM15.8333 10V8.75C15.8333 8.51389 15.9132 8.31597 16.0729 8.15625C16.2326 7.99652 16.4306 7.91666 16.6667 7.91666C16.9028 7.91666 17.1007 7.99652 17.2604 8.15625C17.4201 8.31597 17.5 8.51389 17.5 8.75V10C17.5 10.2361 17.4201 10.434 17.2604 10.5937C17.1007 10.7535 16.9028 10.8333 16.6667 10.8333C16.4306 10.8333 16.2326 10.7535 16.0729 10.5937C15.9132 10.434 15.8333 10.2361 15.8333 10Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.292969 2.29289C0.683493 1.90237 1.31651 1.90237 1.70703 2.29289L3.41406 3.99992H11.4424C12.3865 3.99991 13.1558 3.99969 13.7803 4.05071C14.4257 4.10345 15.0042 4.21564 15.543 4.49016C16.3897 4.92159 17.0783 5.61023 17.5098 6.45696C17.7843 6.99573 17.8965 7.57424 17.9492 8.21965C17.9518 8.25175 17.9517 8.28444 17.9541 8.31731L22.4189 5.12981C23.0808 4.65704 24 5.12991 24 5.94328V18.0566C24 18.8699 23.0808 19.3428 22.4189 18.87L18 15.7138V18.5859L19.707 20.2929C20.0976 20.6834 20.0976 21.3164 19.707 21.707C19.3165 22.0975 18.6835 22.0975 18.293 21.707L0.292969 3.70696C-0.097555 3.31643 -0.0975551 2.68342 0.292969 2.29289ZM1 7.99895C1.55212 7.99895 1.99974 8.44689 2 8.99895V13.3993C2 14.3959 2.00036 15.0836 2.04395 15.6171C2.08658 16.1387 2.16554 16.4249 2.27246 16.6347C2.51215 17.105 2.89491 17.4868 3.36523 17.7265C3.57511 17.8334 3.86102 17.9124 4.38281 17.955C4.91619 17.9986 5.60333 17.9989 6.59961 17.9989H11C11.5521 17.9989 11.9997 18.4469 12 18.9989C12 19.5512 11.5523 19.9989 11 19.9989H6.55762C5.61342 19.999 4.84421 19.9992 4.21973 19.9482C3.57428 19.8954 2.99583 19.7832 2.45703 19.5087C1.61035 19.0773 0.921646 18.3886 0.490235 17.5419C0.215871 17.0032 0.103504 16.4245 0.0507818 15.7792C-0.000207456 15.1548 -1.10191e-05 14.3854 5.0477e-07 13.4413V8.99895C0.00026378 8.44689 0.447878 7.99895 1 7.99895ZM16 16.5859V10.5995C16 9.60324 15.9986 8.91611 15.9551 8.38274C15.9125 7.86113 15.8344 7.57502 15.7275 7.36516C15.4879 6.89475 15.1052 6.51207 14.6348 6.27239C14.4249 6.16547 14.1389 6.0865 13.6172 6.04387C13.0837 6.00029 12.396 5.99992 11.3994 5.99992H5.41406L16 16.5859Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 694 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 855 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.83398 9.03418C6.1464 8.72177 6.6534 8.72178 6.96582 9.03418C7.27824 9.3466 7.27824 9.8536 6.96582 10.166L2.73047 14.4004H5.59961C6.04144 14.4004 6.40039 14.7584 6.40039 15.2002C6.40032 15.642 6.04139 16 5.59961 16H0.799805C0.750353 16 0.702243 15.994 0.655273 15.9854C0.650431 15.9845 0.645453 15.9844 0.640625 15.9834C0.632394 15.9817 0.624348 15.9795 0.616211 15.9775C0.599845 15.9737 0.583526 15.9697 0.567383 15.9648C0.525709 15.9522 0.485753 15.936 0.447266 15.917C0.370564 15.8792 0.298188 15.8294 0.234375 15.7656C0.170233 15.7015 0.119834 15.6289 0.0820312 15.5518C0.0522722 15.4911 0.0292924 15.4265 0.015625 15.3584C0.0146554 15.3536 0.0145519 15.3486 0.0136719 15.3438C0.0052094 15.2971 7.59671e-06 15.2493 0 15.2002V10.4004C0 9.95863 0.358067 9.59972 0.799805 9.59961C1.24163 9.59961 1.59961 9.95856 1.59961 10.4004V13.2686L5.83398 9.03418ZM15.2002 0C15.2493 1.2747e-05 15.2971 0.0052045 15.3438 0.0136719C15.3486 0.0145519 15.3536 0.0146554 15.3584 0.015625C15.3666 0.017277 15.3747 0.0195822 15.3828 0.0214844C15.3992 0.0253083 15.4155 0.0293148 15.4316 0.0341797C15.4733 0.0467652 15.5133 0.0631386 15.5518 0.0820312C15.6289 0.119834 15.7015 0.170233 15.7656 0.234375C15.8294 0.298188 15.8792 0.370564 15.917 0.447266C15.9252 0.463964 15.9334 0.480716 15.9404 0.498047C15.9612 0.549032 15.9755 0.601773 15.9854 0.655273C15.994 0.702239 16 0.750358 16 0.799805V5.59961C16 6.04137 15.6419 6.40028 15.2002 6.40039C14.7584 6.40039 14.4004 6.04144 14.4004 5.59961V2.73047L10.166 6.96582C9.8536 7.27824 9.3466 7.27824 9.03418 6.96582C8.72178 6.6534 8.72177 6.1464 9.03418 5.83398L13.2695 1.59961H10.4004C9.95856 1.59961 9.59961 1.24163 9.59961 0.799805C9.59972 0.358067 9.95863 0 10.4004 0H15.2002Z" fill="#EFF0FA"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,9 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 6.3V6C16 3.79 14.21 2 12 2C9.79 2 8 3.79 8 6V11" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.04004 14.19C9.77004 15 10.83 15.5 12 15.5C14.21 15.5 16 13.71 16 11.5V11" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.77979 16.9499C8.14979 18.2199 9.97978 18.9999 11.9998 18.9999C16.2198 18.9999 19.6498 15.5699 19.6498 11.3499V9.6499" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.3501 9.6499V11.3499C4.3501 12.4099 4.5601 13.4099 4.9501 14.3299" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M20.0702 2.83984L3.93018 18.9898" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11 3V6" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 19V22" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 15.5C14.21 15.5 16 13.71 16 11.5V6C16 3.79 14.21 2 12 2C9.79 2 8 3.79 8 6V11.5C8 13.71 9.79 15.5 12 15.5Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.35059 9.6499V11.3499C4.35059 15.5699 7.78059 18.9999 12.0006 18.9999C16.2206 18.9999 19.6506 15.5699 19.6506 11.3499V9.6499" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 19V22" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 629 B

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.40023 8.79965C6.44894 8.79967 6.49651 8.80496 6.54281 8.81332C6.54796 8.81425 6.5533 8.81424 6.55844 8.81527C6.56666 8.81692 6.57472 8.81923 6.58285 8.82113C6.5992 8.82496 6.61555 8.82896 6.63168 8.83383C6.64056 8.83651 6.64929 8.83963 6.65805 8.84262C6.69881 8.85648 6.73865 8.87374 6.77719 8.89437C6.80436 8.90894 6.83012 8.9257 6.85531 8.9432C6.89387 8.97001 6.93129 8.99965 6.96566 9.03402C6.9997 9.06805 7.02889 9.10526 7.05551 9.1434C7.05897 9.14836 7.06289 9.153 7.06625 9.15804C7.12722 9.24979 7.16631 9.35128 7.18539 9.4559C7.19389 9.50255 7.20001 9.55035 7.20004 9.59945V14.4002C7.19984 14.8418 6.84178 15.1998 6.40023 15.2C5.95852 15.2 5.59964 14.8419 5.59945 14.4002V11.5311L1.36508 15.7655C1.05264 16.0777 0.546577 16.0778 0.234217 15.7655C-0.0781257 15.4531 -0.0780189 14.947 0.234217 14.6346L4.46762 10.4002H1.59945C1.15782 10.4 0.799647 10.0411 0.799647 9.59945C0.799876 9.15796 1.15796 8.79988 1.59945 8.79965H6.40023ZM14.6346 0.234217C14.947 -0.0780189 15.4531 -0.0781257 15.7655 0.234217C16.0778 0.546577 16.0777 1.05264 15.7655 1.36508L11.5321 5.59945H14.4002C14.8419 5.59964 15.2 5.95852 15.2 6.40023C15.1998 6.84178 14.8418 7.19984 14.4002 7.20004H9.59945C9.55035 7.20001 9.50255 7.19389 9.4559 7.18539C9.45074 7.18445 9.44541 7.18448 9.44027 7.18344C9.43204 7.18177 9.42399 7.17949 9.41586 7.17758C9.3995 7.17372 9.38317 7.16978 9.36703 7.16488C9.35943 7.16257 9.35207 7.1596 9.34457 7.15707C9.30221 7.14282 9.2605 7.12587 9.22054 7.10433C9.19365 7.08982 9.16833 7.0729 9.1434 7.05551C9.10526 7.02889 9.06805 6.9997 9.03402 6.96566C8.99965 6.93129 8.97001 6.89387 8.9432 6.85531C8.9257 6.83012 8.90894 6.80436 8.89437 6.77719C8.85465 6.70298 8.82796 6.62378 8.81332 6.54281C8.80496 6.49651 8.79967 6.44894 8.79965 6.40023V1.59945C8.79988 1.15796 9.15796 0.799876 9.59945 0.799647C10.0411 0.799647 10.4 1.15782 10.4002 1.59945V4.46762L14.6346 0.234217Z" fill="#EFF0FA"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 0C15.523 0 20 4.477 20 10C20 15.523 15.523 20 10 20C4.477 20 0 15.523 0 10C0 4.477 4.477 0 10 0ZM10 2C7.87827 2 5.84344 2.84285 4.34315 4.34315C2.84285 5.84344 2 7.87827 2 10C2 12.1217 2.84285 14.1566 4.34315 15.6569C5.84344 17.1571 7.87827 18 10 18C12.1217 18 14.1566 17.1571 15.6569 15.6569C17.1571 14.1566 18 12.1217 18 10C18 7.87827 17.1571 5.84344 15.6569 4.34315C14.1566 2.84285 12.1217 2 10 2ZM10 13C10.2652 13 10.5196 13.1054 10.7071 13.2929C10.8946 13.4804 11 13.7348 11 14C11 14.2652 10.8946 14.5196 10.7071 14.7071C10.5196 14.8946 10.2652 15 10 15C9.73478 15 9.48043 14.8946 9.29289 14.7071C9.10536 14.5196 9 14.2652 9 14C9 13.7348 9.10536 13.4804 9.29289 13.2929C9.48043 13.1054 9.73478 13 10 13ZM10 4C10.2652 4 10.5196 4.10536 10.7071 4.29289C10.8946 4.48043 11 4.73478 11 5V11C11 11.2652 10.8946 11.5196 10.7071 11.7071C10.5196 11.8946 10.2652 12 10 12C9.73478 12 9.48043 11.8946 9.29289 11.7071C9.10536 11.5196 9 11.2652 9 11V5C9 4.73478 9.10536 4.48043 9.29289 4.29289C9.48043 4.10536 9.73478 4 10 4Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -35,6 +35,8 @@
<string>This permission is not needed by the app, but it is required by an underlying API. If you see this dialog, contact us.</string>
<key>NSCameraUsageDescription</key>
<string>Предоставление доступа позволит Вам делать фотографии и добавлять к записям</string>
<key>NSMicrophoneUsageDescription</key>
<string>Нужен для звука в видеозвонках с консультантом</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string></string>
<key>NSPhotoLibraryUsageDescription</key>

View File

@@ -3,6 +3,7 @@ import 'dart:developer';
import 'dart:io' as io;
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/env_config.dart';
import 'package:eatvio_app/core/error_reporting/error_report_policy.dart';
import 'package:eatvio_app/core/locator_service.dart' as di;
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/firebase_options.dart';
@@ -16,6 +17,7 @@ import 'package:sentry_flutter/sentry_flutter.dart';
/// Класс для инициализации приложения
class AppInitializer {
/// Инициализация всех сервисов приложения
static Future<void> initialize({required AppBuildType buildType}) async {
await _initializeEnvironment();
@@ -79,11 +81,12 @@ class AppInitializer {
options.dsn = EnvConfig.sentryDsn;
options.tracesSampleRate = 1.0;
options.debug = false;
options.beforeSend = (event, hint) {
if (kDebugMode) return null;
if (!ErrorReportPolicy.allowSentryEvent(event, hint)) return null;
return event;
};
}, appRunner: appRunner);
FlutterError.onError = (details) async {
if (kDebugMode) return;
await Sentry.captureException(details.exception, stackTrace: details.stack);
};
}
}

View File

@@ -1,4 +1,5 @@
import 'dart:developer' as developer;
import 'package:eatvio_app/core/const.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';
@@ -6,6 +7,7 @@ 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:eatvio_app/features/auth/entities/auth_result_entity.dart';
import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:go_router/go_router.dart';
@@ -14,8 +16,8 @@ import 'package:go_router/go_router.dart';
class AppRouter {
static GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
// Домены для deep links (в т.ч. ссылки из пушей dev2.eatvio.com)
static const List<String> _deepLinkHosts = ['eatvio.ru', 'www.eatvio.ru', 'dev2.eatvio.com', 'www.dev2.eatvio.com'];
static const List<String> deepLinkHosts = ['eatvio.ru', 'eatvio.com', 'dev2.eatvio.com'];
static const List<String> metaSegments = ['ru', 'en', 'api', 'v2'];
static GoRouter appRouter = GoRouter(
initialLocation: RouteConstants.updateScreen,
@@ -30,11 +32,102 @@ class AppRouter {
],
);
static String? _resolveRecipeDeepLink(List<String> effectiveSegments) {
if (effectiveSegments.length != 2) return null;
final contentIdentifier = effectiveSegments[1];
developer.log('Deep link: recipe/$contentIdentifier', name: 'AppRouter');
return '/about-recipes-screen?slug=$contentIdentifier';
}
static String? _resolveBlogPostDeepLink(List<String> effectiveSegments) {
if (effectiveSegments.length != 2) return null;
final contentIdentifier = effectiveSegments[1];
developer.log('Deep link: blog_post/$contentIdentifier', name: 'AppRouter');
return '/blog-post-screen?id=$contentIdentifier';
}
static String? _resolvePlanDeepLink(List<String> effectiveSegments) {
if (effectiveSegments.length != 2) return null;
final contentIdentifier = effectiveSegments[1];
final planId = int.tryParse(contentIdentifier);
if (planId != null) {
developer.log('Deep link: diary', name: 'AppRouter');
return RouteConstants.diaryScreen;
}
developer.log('Deep link: plan/$contentIdentifier', name: 'AppRouter');
return '/about-plan-screen?slug=$contentIdentifier';
}
static String? _resolveDietDeepLink(List<String> effectiveSegments) {
if (effectiveSegments.length == 2) {
final contentIdentifier = effectiveSegments[1];
final dietId = int.tryParse(contentIdentifier);
if (dietId != null) {
developer.log('Deep link: diet/id=$dietId', name: 'AppRouter');
return '/about-diet-screen?id=$dietId';
}
developer.log('Deep link: diet/slug=$contentIdentifier', name: 'AppRouter');
return '/about-diet-screen?slug=$contentIdentifier';
}
const dietTabIndexWithHiddenPlansTab = 3;
const dietTabIndexWithoutHiddenPlansTab = 4;
final initialTabIndex = hidePlans ? dietTabIndexWithHiddenPlansTab : dietTabIndexWithoutHiddenPlansTab;
return '${RouteConstants.feedScreen}?initialTabIndex=$initialTabIndex';
}
static String? _resolveChatDeepLink(List<String> effectiveSegments) {
if (effectiveSegments.length == 2) {
final contentIdentifier = effectiveSegments[1];
developer.log('Deep link: chat/$contentIdentifier', name: 'AppRouter');
return '${RouteConstants.chatScreen}?uuid=$contentIdentifier';
} else if (effectiveSegments.length == 3 && effectiveSegments[1] == 'show') {
final contentIdentifier = effectiveSegments[2];
developer.log('Deep link: chat/$contentIdentifier', name: 'AppRouter');
return '${RouteConstants.chatScreen}?uuid=$contentIdentifier';
}
developer.log('Deep link: chat', name: 'AppRouter');
return RouteConstants.chatSelectScreen;
}
static String? _resolveProfileDeepLink(List<String> effectiveSegments) {
if (effectiveSegments.length == 2) {
final contentIdentifier = effectiveSegments[1];
developer.log('Deep link: profile/$contentIdentifier', name: 'AppRouter');
return '/profile-screen?slug=$contentIdentifier';
}
return RouteConstants.myProfileScreen;
}
static String? _resolveBadgeDeepLink(List<String> effectiveSegments) {
return RouteConstants.myProfileScreen;
}
static String? _resolveDiaryDeepLink(List<String> effectiveSegments) {
developer.log('Deep link: diary', name: 'AppRouter');
return RouteConstants.diaryScreen;
}
static String? _resolveConsultantDiaryCommentDeepLink(List<String> effectiveSegments) {
if (effectiveSegments.length != 2) return null;
final contentIdentifier = effectiveSegments[1];
developer.log('Deep link: consultant_diary_comment/$contentIdentifier → diary', name: 'AppRouter');
return RouteConstants.diaryScreen;
}
static String? _resolveConsultationsDeepLink(List<String> effectiveSegments) {
if (effectiveSegments.length != 3 || effectiveSegments[1] != 'status') return RouteConstants.consultationsScreen;
final consultationId = effectiveSegments[2];
developer.log('Deep link: consultations/status/$consultationId', name: 'AppRouter');
return '${InfoByOrderScreen.path}?planId=$consultationId';
}
static String? _resolveDeepLinkUri(Uri uri) {
final host = uri.host.toLowerCase();
final segments = uri.pathSegments;
final isDeepLink = _deepLinkHosts.contains(host);
final isDeepLink = deepLinkHosts.any((allowedHost) => host == allowedHost || host.endsWith('.$allowedHost'));
if (!isDeepLink) {
return null;
@@ -44,120 +137,61 @@ class AppRouter {
if (segments.isEmpty) {
developer.log('Empty segments in deep link, redirecting to feed', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.feedScreen;
}
if (segments.length >= 4 &&
segments[0] == 'api' &&
segments[1] == 'v2' &&
segments[2] == 'chat' &&
segments[3] == 'show') {
developer.log('Deep link: api/v2/chat/show → chat-select-screen', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.chatSelectScreen;
}
if (segments[0] != 'ru' && segments[0] != 'en') {
developer.log('Invalid language segment: ${segments[0]}, redirecting to feed', name: 'AppRouter');
FlutterNativeSplash.remove();
final effectiveSegments = segments.where((segment) => !metaSegments.contains(segment)).toList();
if (effectiveSegments.isEmpty) {
developer.log('No effective segments after language check, redirecting to feed', name: 'AppRouter');
return RouteConstants.feedScreen;
}
if (segments.length == 4) {
final type = segments[1];
final type = effectiveSegments[0];
if (type == 'diary') {
developer.log('Deep link: diary, redirecting to diary-screen', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.diaryScreen;
}
if (type == 'consultations' && segments[2] == 'status') {
final consultationId = segments[3];
developer.log('Deep link: consultations/status/$consultationId', name: 'AppRouter');
FlutterNativeSplash.remove();
return '${RouteConstants.consultationsScreen}?id=$consultationId';
}
developer.log('Unknown deep link type for 4 segments: $type, redirecting to feed', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.feedScreen;
} else if (segments.length == 3) {
final type = segments[1];
final contentIdentifier = segments[2];
switch (type) {
case 'recipe':
developer.log('Deep link: recipe/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/about-recipes-screen?slug=$contentIdentifier';
case 'blog_post':
developer.log('Deep link: blog_post/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/blog-post-screen?id=$contentIdentifier';
case 'plan':
developer.log('Deep link: plan/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/about-plan-screen?slug=$contentIdentifier';
case 'diet': {
final dietId = int.tryParse(contentIdentifier);
if (dietId != null) {
developer.log('Deep link: diet/id=$dietId', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/about-diet-screen?id=$dietId';
}
developer.log('Deep link: diet/slug=$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/about-diet-screen?slug=$contentIdentifier';
}
case 'chat':
developer.log('Deep link: chat/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.chatSelectScreen;
case 'profile':
// В диплинке передан slug — открываем профиль пользователя по slug.
developer.log('Deep link: profile/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return '/profile-screen?slug=$contentIdentifier';
case 'badge':
// В badge нет slug, открываем вкладку «Мой профиль».
developer.log('Deep link: badge → my-profile tab', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.myProfileScreen;
case 'diary':
developer.log('Deep link: diary/$contentIdentifier', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.diaryScreen;
case 'consultant_diary_comment':
developer.log('Deep link: consultant_diary_comment/$contentIdentifier → diary', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.diaryScreen;
default:
developer.log('Unknown deep link type: $type, redirecting to feed', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.feedScreen;
}
} else if (segments.length == 2) {
final type = segments[1];
if (type == 'chat') {
developer.log('Deep link: chat', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.chatSelectScreen;
}
if (type == 'diary') {
developer.log('Deep link: diary', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.diaryScreen;
}
developer.log('Unknown deep link type for 2 segments: $type, redirecting to feed', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.feedScreen;
switch (type) {
case 'recipe':
final target = _resolveRecipeDeepLink(effectiveSegments);
if (target != null) return target;
break;
case 'blog_post':
final target = _resolveBlogPostDeepLink(effectiveSegments);
if (target != null) return target;
break;
case 'plan':
final target = _resolvePlanDeepLink(effectiveSegments);
if (target != null) return target;
break;
case 'diet':
final target = _resolveDietDeepLink(effectiveSegments);
if (target != null) return target;
break;
case 'chat':
final target = _resolveChatDeepLink(effectiveSegments);
if (target != null) return target;
break;
case 'profile':
final target = _resolveProfileDeepLink(effectiveSegments);
if (target != null) return target;
break;
case 'badge':
final target = _resolveBadgeDeepLink(effectiveSegments);
if (target != null) return target;
break;
case 'diary':
final target = _resolveDiaryDeepLink(effectiveSegments);
if (target != null) return target;
break;
case 'consultant_diary_comment':
final target = _resolveConsultantDiaryCommentDeepLink(effectiveSegments);
if (target != null) return target;
break;
case 'consultations':
final target = _resolveConsultationsDeepLink(effectiveSegments);
if (target != null) return target;
break;
}
developer.log('Unknown deep link format: ${segments.length} segments, redirecting to feed', name: 'AppRouter');
FlutterNativeSplash.remove();
developer.log('Unknown deep link format: ${effectiveSegments.length} effective segments, type=$type, redirecting to feed', name: 'AppRouter');
return RouteConstants.feedScreen;
}
@@ -190,20 +224,20 @@ class AppRouter {
if (uri.host.isEmpty && uri.pathSegments.isNotEmpty) {
uri = Uri(
scheme: 'https',
host: _deepLinkHosts.first,
host: deepLinkHosts.first,
pathSegments: uri.pathSegments,
queryParameters: uri.queryParameters.isEmpty ? null : uri.queryParameters,
);
}
final target = _resolveDeepLinkUri(uri);
FlutterNativeSplash.remove();
if (target != null) {
appRouter.go(target);
} else {
developer.log('navigateByDeepLinkUrl: unresolved url=$url (normalized=${uri.toString()}), redirecting to feed', name: 'AppRouter');
appRouter.go(RouteConstants.feedScreen);
return;
}
developer.log('navigateByDeepLinkUrl: unresolved url=$url (normalized=${uri.toString()}), redirecting to feed', name: 'AppRouter');
appRouter.go(RouteConstants.feedScreen);
}
static void navigateToRegisterViaSocial({required AuthResultEntity authResult, required String provider}) {

View File

@@ -5,15 +5,13 @@ import 'package:eatvio_app/core/comments/repositories/comments_repository.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart';
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class CommentsCubit extends Cubit<CommentsState> {
final FeedRepository feedRepository;
final CommentsRepository commentsRepository;
CommentsCubit({
required this.feedRepository,
required this.commentsRepository,
}) : super(const CommentsState());
CommentsCubit({required this.feedRepository, required this.commentsRepository}) : super(const CommentsState());
void setReplyId(int? replyId) {
emit((state as CommentsStateLoaded).copyWith(replyId: replyId));
@@ -36,51 +34,34 @@ class CommentsCubit extends Cubit<CommentsState> {
emit(CommentsState.loaded(comment: await feedRepository.getPlansComments(slug: id)));
break;
}
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
emit(CommentsState.error(errorMessage: e.toString()));
}
}
Future<void> sendComment({
required String id,
required String commentText,
required CommentSourceType commentSourceType,
int? parentCommentId,
}) async {
Future<bool> sendComment({required String id, required String commentText, required CommentSourceType commentSourceType, int? parentCommentId}) async {
try {
switch (commentSourceType) {
case CommentSourceType.blogPost:
await feedRepository.sendBlogPostComment(
id: id,
commentText: commentText,
parentCommentId: parentCommentId,
);
await feedRepository.sendBlogPostComment(id: id, commentText: commentText, parentCommentId: parentCommentId);
break;
case CommentSourceType.diet:
await feedRepository.sendDietsComment(
id: id,
commentText: commentText,
parentCommentId: parentCommentId,
);
await feedRepository.sendDietsComment(id: id, commentText: commentText, parentCommentId: parentCommentId);
break;
case CommentSourceType.recipe:
await feedRepository.sendRecipesComment(
id: id,
commentText: commentText,
parentCommentId: parentCommentId,
);
await feedRepository.sendRecipesComment(id: id, commentText: commentText, parentCommentId: parentCommentId);
break;
case CommentSourceType.plan:
await feedRepository.sendPlansComment(
slug: id,
commentText: commentText,
parentCommentId: parentCommentId,
);
await feedRepository.sendPlansComment(slug: id, commentText: commentText, parentCommentId: parentCommentId);
break;
}
getComments(id, commentSourceType);
} catch (e) {
return true;
} catch (e, st) {
await reportFrom(e, st, this);
emit(CommentsState.error(errorMessage: e.toString()));
return false;
}
}
@@ -93,6 +74,7 @@ class CommentsCubit extends Cubit<CommentsState> {
emit(CommentsState.loaded(comment: updatedCommentModel));
return true;
} catch (e, st) {
await reportFrom(e, st, this);
emit(CommentsState.loaded(comment: commentModel));
log(e.toString(), stackTrace: st);
}
@@ -101,13 +83,15 @@ class CommentsCubit extends Cubit<CommentsState> {
Future<bool> updateComment({required int id, required String body}) async {
final commentModel = (state as CommentsStateLoaded).comment;
final updatedCommentModel =
commentModel.copyWith(data: commentModel.data.map((comment) => comment.id == id ? comment.copyWith(body: body) : comment).toList());
final updatedCommentModel = commentModel.copyWith(
data: commentModel.data.map((comment) => comment.id == id ? comment.copyWith(body: body) : comment).toList(),
);
try {
await commentsRepository.updateComment(id: id, body: body);
emit(CommentsState.loaded(comment: updatedCommentModel));
return true;
} catch (e, st) {
await reportFrom(e, st, this);
emit(CommentsState.loaded(comment: commentModel));
log(e.toString(), stackTrace: st);
}

View File

@@ -1,6 +1,5 @@
import 'package:eatvio_app/core/comments/models/comment_update_model.dart';
import 'package:eatvio_app/core/comments/services/comments_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class CommentsRepository {
final CommentsService service;
@@ -8,20 +7,12 @@ class CommentsRepository {
CommentsRepository({required this.service});
Future<void> deleteComment({required int id}) async {
try {
return service.deleteComment(id);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return service.deleteComment(id);
}
Future<void> updateComment({required int id, required String body}) async {
try {
return service.updateComment(id, CommentUpdateModel(body: body));
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return service.updateComment(id, CommentUpdateModel(body: body));
}
}

View File

@@ -2,6 +2,7 @@ import 'package:eatvio_app/core/cubit/marketing/marketing_cubit_state.dart';
import 'package:eatvio_app/core/prefs.dart';
import 'package:eatvio_app/core/prefs_keys.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class MarketingCubit extends Cubit<MarketingCubitState> {
final Prefs prefs;
@@ -22,8 +23,9 @@ class MarketingCubit extends Cubit<MarketingCubitState> {
await prefs.setBool(PrefsKeys.marketingBottomSheetHasBeenShown, true);
}
return needToShowMarketingBottomSheet;
} catch (e) {
rethrow;
} catch (e, st) {
await reportFrom(e, st, this);
return false;
}
}
}

View File

@@ -1,10 +1,13 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/constants/app_constants.dart';
import 'package:eatvio_app/core/error_reporting/error_reporter.dart';
import 'package:eatvio_app/core/error_reporting/sentry_error_reporter.dart';
import 'package:eatvio_app/core/interceptors/phone_verify_error_interceptor.dart';
import 'package:eatvio_app/core/interceptors/token_interceptor.dart';
import 'package:eatvio_app/core/prefs.dart';
import 'package:get_it/get_it.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:sentry_dio/sentry_dio.dart';
@@ -14,6 +17,7 @@ class ExternalRegistrations {
/// Регистрация всех внешних зависимостей
static void registerAll() {
sl.registerLazySingleton<ErrorReporter>(SentryErrorReporter.new);
_registerDio();
_registerPrefs();
}
@@ -30,7 +34,9 @@ class ExternalRegistrations {
sendTimeout: AppConstants.pendingDelay,
),
)..interceptors.addAll([TokenInterceptor(), PhoneVerifyErrorInterceptor()])
..addSentry(),
..addSentry(
failedRequestStatusCodes: const [SentryStatusCode.defaultRange()],
),
);
}

View File

@@ -33,6 +33,7 @@ import 'package:eatvio_app/features/recipe/repositories/my_recipes_repository.da
import 'package:eatvio_app/features/registration/repositories/registration_repository.dart';
import 'package:eatvio_app/features/registration_v2/repositories/registration_repository_v2.dart';
import 'package:eatvio_app/features/settings/repositories/service_repository.dart';
import 'package:eatvio_app/features/video_calls/repositories/video_call_repository.dart';
import 'package:get_it/get_it.dart';
/// Регистрация всех Repository в системе внедрения зависимостей
@@ -49,6 +50,7 @@ class RepositoryRegistrations {
static void _registerSingletonRepositories() {
// Core repositories
sl.registerLazySingleton<ChatRepository>(() => ChatRepository(chatService: sl()));
sl.registerLazySingleton<VideoCallRepository>(() => VideoCallRepository(videoCallService: sl()));
sl.registerLazySingleton<FeedRepository>(() => FeedRepository(feedService: sl()));
sl.registerLazySingleton<ProfileRepository>(() => ProfileRepository(profileService: sl()));
sl.registerLazySingleton<CalendarRepository>(() => CalendarRepository(calendarService: sl()));

View File

@@ -35,6 +35,7 @@ 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/registration_v2/services/registration_service_v2.dart';
import 'package:eatvio_app/features/settings/services/settings_service.dart';
import 'package:eatvio_app/features/video_calls/services/video_call_service.dart';
import 'package:get_it/get_it.dart';
/// Регистрация всех Service в системе внедрения зависимостей
@@ -50,6 +51,7 @@ class ServiceRegistrations {
static void _registerSingletonServices() {
// Core services
sl.registerLazySingleton<ChatService>(() => ChatService(sl()));
sl.registerLazySingleton<VideoCallService>(() => VideoCallService(sl()));
sl.registerLazySingleton<FeedService>(() => FeedService(sl()));
sl.registerLazySingleton<ProfileService>(() => ProfileService(sl()));
sl.registerLazySingleton<CalendarService>(() => CalendarService(sl()));
@@ -72,7 +74,7 @@ class ServiceRegistrations {
sl.registerLazySingleton<CancelPlanService>(() => CancelPlanService(sl()));
sl.registerLazySingleton<CreatePlanService>(() => CreatePlanService(sl()));
sl.registerLazySingleton<NotificationsService>(() => NotificationsService(sl()));
sl.registerLazySingleton<FirebaseApi>(() => FirebaseApi());
sl.registerLazySingleton<FirebaseApi>(() => FirebaseApi(errorReporter: sl()));
sl.registerLazySingleton<ResetPasswordService>(() => ResetPasswordService(sl()));
sl.registerLazySingleton<BlockedUserService>(() => BlockedUserService(sl()));
sl.registerLazySingleton<CommentsService>(() => CommentsService(sl()));

View File

@@ -0,0 +1,80 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
/// Rules for which errors are sent to crash reporting.
///
/// [shouldReport] is used by [ErrorReporter]. [allowSentryEvent] is used in
/// [SentryFlutter.init] as a last-line filter (Dio failed-request capture, etc.).
class ErrorReportPolicy {
ErrorReportPolicy._();
/// When `false`, the error is not sent via [ErrorReporter].
static bool shouldReport(Object? error) {
if (error == null) return false;
if (error is String) return false;
if (error is SocketException) return false;
if (error is DioException) {
return _shouldReportDio(error);
}
return true;
}
/// Used in SDK [SentryOptions.beforeSend]. Return `false` to drop the event.
///
/// For [DioException], rules differ from [shouldReport]: events from `sentry_dio`
/// (e.g. 5xx) must pass through here, while [shouldReport] returns false for 5xx
/// so [SentryErrorReporter] does not duplicate them after `catch` in cubits / manual [report] calls.
static bool allowSentryEvent(SentryEvent event, Hint hint) {
final t = event.throwable;
if (t == null) return true;
if (t is DioException) {
return _allowSentrySdkDio(t);
}
return shouldReport(t);
}
/// Cancel, timeouts, socket / connection errors — do not send (noisy, often user environment).
static bool _isTransientDioFailure(DioException e) {
switch (e.type) {
case DioExceptionType.cancel:
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
case DioExceptionType.connectionError:
return true;
case DioExceptionType.unknown:
case DioExceptionType.badResponse:
case DioExceptionType.badCertificate:
break;
}
return e.error is SocketException;
}
/// Noise to drop for any Dio-originated SDK event; do not use for manual [report].
static bool _allowSentrySdkDio(DioException e) {
if (_isTransientDioFailure(e)) return false;
final code = e.response?.statusCode;
if (code != null && code >= 400 && code < 500) return false;
return true;
}
static bool _shouldReportDio(DioException e) {
if (_isTransientDioFailure(e)) return false;
final code = e.response?.statusCode;
if (code != null) {
if (code >= 500 && code <= 599) {
return false;
}
if (code >= 400 && code < 500) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,9 @@
/// Application-wide error reporting (e.g. Sentry).
abstract class ErrorReporter {
Future<void> report(
Object error,
StackTrace? stackTrace, {
String? origin,
Map<String, Object?>? extras,
});
}

View File

@@ -0,0 +1,14 @@
import 'package:eatvio_app/core/error_reporting/error_reporter.dart';
/// Reporting sink that does nothing (dev tooling, tests, diagnostics).
class NoOpErrorReporter implements ErrorReporter {
const NoOpErrorReporter();
@override
Future<void> report(
Object error,
StackTrace? stackTrace, {
String? origin,
Map<String, Object?>? extras,
}) async {}
}

View File

@@ -0,0 +1,17 @@
import 'package:eatvio_app/core/error_reporting/error_reporter.dart';
import 'package:eatvio_app/core/error_reporting/no_op_error_reporter.dart';
import 'package:eatvio_app/core/locator_service.dart';
/// Отправка в [ErrorReporter] с тегом origin = [context.runtimeType] (имя кубита/виджета и т.д.).
///
/// Если [ErrorReporter] не зарегистрирован (например, после [GetIt.reset] в тестах), используется [NoOpErrorReporter].
Future<void> reportFrom(Object error, StackTrace? stackTrace, Object context) {
final reporter = sl.isRegistered<ErrorReporter>()
? sl<ErrorReporter>()
: const NoOpErrorReporter();
return reporter.report(
error,
stackTrace,
origin: context.runtimeType.toString(),
);
}

View File

@@ -0,0 +1,33 @@
import 'package:eatvio_app/core/error_reporting/error_report_policy.dart';
import 'package:eatvio_app/core/error_reporting/error_reporter.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class SentryErrorReporter implements ErrorReporter {
SentryErrorReporter();
@override
Future<void> report(
Object error,
StackTrace? stackTrace, {
String? origin,
Map<String, Object?>? extras,
}) async {
if (!ErrorReportPolicy.shouldReport(error)) return;
await Sentry.captureException(
error,
stackTrace: stackTrace,
withScope: (scope) {
if (origin != null && origin.isNotEmpty) {
scope.setTag('origin', origin);
}
if (extras != null && extras.isNotEmpty) {
scope.setContexts(
'report_extras',
extras.map((k, v) => MapEntry(k, v?.toString())),
);
}
},
);
}
}

View File

@@ -11,11 +11,14 @@ 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;
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:eatvio_app/core/error_reporting/error_reporter.dart';
class FirebaseApi {
final ErrorReporter _errorReporter;
final _firebaseMessaging = FirebaseMessaging.instance;
FirebaseApi({required ErrorReporter errorReporter}) : _errorReporter = errorReporter;
Future<String?> getToken() async {
String? token;
@@ -61,7 +64,7 @@ class FirebaseApi {
);
} catch (e, st) {
log(e.toString(), stackTrace: st);
await Sentry.captureException(e, stackTrace: st);
await _errorReporter.report(e, st, origin: 'FirebaseApi');
// Не пробрасываем ошибку дальше, так как это не критичная операция
}
}
@@ -86,7 +89,7 @@ class FirebaseApi {
);
} catch (e, st) {
log(e.toString(), stackTrace: st);
await Sentry.captureException(e, stackTrace: st);
await _errorReporter.report(e, st, origin: 'FirebaseApi');
// Не пробрасываем ошибку дальше, так как это не критичная операция
}
}
@@ -112,12 +115,12 @@ class FirebaseApi {
} on PlatformException catch (e, st) {
showErrorToast('$e $st');
log(e.toString(), stackTrace: st);
await Sentry.captureException(e, stackTrace: st);
await _errorReporter.report(e, st, origin: 'FirebaseApi');
return null;
} catch (e, st) {
showErrorToast('$e $st');
log(e.toString(), stackTrace: st);
await Sentry.captureException(e, stackTrace: st);
await _errorReporter.report(e, st, origin: 'FirebaseApi');
return null;
}
}

View File

@@ -3,7 +3,6 @@ import 'package:eatvio_app/core/app_initializer.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:image/image.dart' as img;
import 'package:path/path.dart' as p;
abstract class ImageCompressUtility {
@@ -30,44 +29,21 @@ abstract class ImageCompressUtility {
}
static Future<File> compressFile(File file) async {
// Skip compression in test environment to avoid hangs
try {
final appConfig = sl<AppConfig>();
if (appConfig.buildType == AppBuildType.tests) {
// In tests, return the original file without compression
return file;
}
} catch (_) {
// If AppConfig is not available, proceed with compression
} catch (_) {}
final compressedPath = '${p.withoutExtension(file.path)}_compressed.jpg';
final result = await FlutterImageCompress.compressAndGetFile(file.absolute.path, compressedPath, quality: 90, minHeight: 1080, minWidth: 1080);
if (result == null) {
return file;
}
final image = img.decodeImage(file.readAsBytesSync())!;
int width = image.width;
int height = image.height;
if (width > height) {
if (width > 1080) {
height = (height * 1080) ~/ width;
width = 1080;
}
} else {
if (height > 1080) {
width = (width * 1080) ~/ height;
height = 1080;
}
}
String filePathWithoutExtension = p.withoutExtension(file.path);
String compressedFilePath = '${filePathWithoutExtension}_compressed.jpg';
final result = await FlutterImageCompress.compressAndGetFile(
file.absolute.path,
compressedFilePath,
quality: 90,
minHeight: height,
minWidth: width,
);
return File(result!.path);
return File(result.path);
}
}

View File

@@ -1,6 +1,7 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/routing/route_constants.dart';
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:go_router/go_router.dart';
@@ -12,18 +13,21 @@ class PhoneVerifyErrorInterceptor extends Interceptor {
final data = err.response?.data;
if (data is Map<String, dynamic> && data['type'] == 'unverificated_phone') {
final String phone = await sl<ProfileRepository>().getMyPhone();
final String phone = await sl<ProfileRepository>().getMyPhone();
final String passKey = await sl<ProfileRepository>().getPassKey(routeName: 'user_detailz.change-phone-verify', routeParams: {"phone": phone});
await showErrorDialog(AppRouter.navigatorKey.currentContext!,
title: 'Привяжите номер телефона', description: 'Для того, что бы выполнить это действие необходимо привязать номер телефона');
await showErrorDialog(
AppRouter.navigatorKey.currentContext!,
title: 'Привяжите номер телефона',
description: 'Для того, что бы выполнить это действие необходимо привязать номер телефона',
);
final currentContext = AppRouter.navigatorKey.currentContext;
if (currentContext != null && currentContext.mounted && GoRouter.of(currentContext).state.path == RouteConstants.stepperBecomeConsultantScreen) {
GoRouter.of(currentContext).pop();
}
AppRouter.navigatorKey.currentContext?.pushNamed('verifyScreen', extra: passKey);
return handler.reject(
DioException(
requestOptions: err.requestOptions,
response: err.response,
error: 'Phone number not verified',
type: DioExceptionType.badResponse,
),
DioException(requestOptions: err.requestOptions, response: err.response, error: 'Phone number not verified', type: DioExceptionType.badResponse),
);
}
}

View File

@@ -8,6 +8,7 @@ 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:eatvio_app/core/error_reporting/report_from.dart';
import 'package:http/http.dart' as http;
class InternetConnectionCubit extends Cubit<InternetConnectionState> {
@@ -33,7 +34,8 @@ class InternetConnectionCubit extends Cubit<InternetConnectionState> {
try {
result = await _connectivity.checkConnectivity();
} on PlatformException catch (e) {
} on PlatformException catch (e, st) {
await reportFrom(e, st, this);
log('Couldn\'t check connectivity status', error: e);
return;
}
@@ -42,9 +44,13 @@ class InternetConnectionCubit extends Cubit<InternetConnectionState> {
}
Future<void> _updateAccessStatus(List<ConnectivityResult> result) async {
final hasInternet = await _checkInternetAccess(result);
if (!hasInternet) showInternetConnectionToast();
emit(state.copyWith(isInternetAvailable: hasInternet));
try {
final hasInternet = await _checkInternetAccess(result);
if (!hasInternet) showInternetConnectionToast();
emit(state.copyWith(isInternetAvailable: hasInternet));
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<bool> _checkInternetAccess(List<ConnectivityResult> result) async {
@@ -57,7 +63,8 @@ class InternetConnectionCubit extends Cubit<InternetConnectionState> {
try {
final response = await http.get(uri).timeout(const Duration(seconds: 3));
return response.statusCode >= 100 && response.statusCode < 600;
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
continue;
}
}

View File

@@ -32,5 +32,5 @@ Future<void> init({required AppBuildType buildType}) async {
CubitRegistrations.registerAll();
// Регистрация WebSocket менеджера
sl.registerLazySingleton<WsManager>(() => WsManager(wsService: sl()));
sl.registerLazySingleton<WsManager>(() => WsManager(wsService: sl(), errorReporter: sl()));
}

View File

@@ -5,18 +5,14 @@ import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
/// Отвечает за навигацию по диплинкам, пришедшим в пуш-уведомлениях.
class PushNavigationHandler {
static bool _initialized = false;
static const MethodChannel _hmsChannel = MethodChannel('com.eatvioapp/hms_push');
/// Инициализация обработчиков Firebase Messaging.
/// Должна вызываться один раз после старта приложения.
static Future<void> initialize() async {
if (_initialized) return;
_initialized = true;
// Сообщение, по которому запустили приложение из "убитого" состояния.
try {
final initialMessage = await FirebaseMessaging.instance.getInitialMessage();
if (initialMessage != null) {
@@ -25,18 +21,11 @@ class PushNavigationHandler {
});
}
} catch (e) {
developer.log(
'PushNavigationHandler: error while getting initialMessage: $e',
name: 'PushNavigationHandler',
);
developer.log('PushNavigationHandler: error while getting initialMessage: $e', name: 'PushNavigationHandler');
}
// Тап по уведомлению, когда приложение было в фоне.
FirebaseMessaging.onMessageOpenedApp.listen(
(message) => _handleMessageInternal(message, source: 'onMessageOpenedApp'),
);
FirebaseMessaging.onMessageOpenedApp.listen((message) => _handleMessageInternal(message, source: 'onMessageOpenedApp'));
// Подписка на события от native-кода (HMS через MethodChannel).
_hmsChannel.setMethodCallHandler((call) async {
if (call.method == 'openActionUrl') {
final url = call.arguments as String? ?? '';
@@ -45,25 +34,17 @@ class PushNavigationHandler {
});
}
/// Входная точка для навигации из native-кода (например, HMS через MethodChannel).
static void handleActionUrlFromNative(String url) {
if (url.isEmpty) {
developer.log(
'PushNavigationHandler.handleActionUrlFromNative: empty url',
name: 'PushNavigationHandler',
);
developer.log('PushNavigationHandler.handleActionUrlFromNative: empty url', name: 'PushNavigationHandler');
return;
}
developer.log(
'PushNavigationHandler.handleActionUrlFromNative: navigating by url=$url',
name: 'PushNavigationHandler',
);
developer.log('PushNavigationHandler.handleActionUrlFromNative: navigating by url=$url', name: 'PushNavigationHandler');
AppRouter.navigateByDeepLinkUrl(url);
}
/// Публичный метод для единичной обработки сообщения (если понадобится где-то ещё).
static void handleMessage(RemoteMessage message) {
_handleMessageInternal(message, source: 'manualHandle');
}
@@ -71,21 +52,13 @@ class PushNavigationHandler {
static void _handleMessageInternal(RemoteMessage message, {required String source}) {
final data = message.data;
final actionUrl = data['action_url'] as String?;
if (actionUrl == null || actionUrl.isEmpty) {
developer.log(
'PushNavigationHandler: no action_url in message data, source=$source, data=$data',
name: 'PushNavigationHandler',
);
developer.log('PushNavigationHandler: no action_url in message data, source=$source, data=$data', name: 'PushNavigationHandler');
return;
}
developer.log(
'PushNavigationHandler: navigating by action_url=$actionUrl, source=$source',
name: 'PushNavigationHandler',
);
developer.log('PushNavigationHandler: navigating by action_url=$actionUrl, source=$source', name: 'PushNavigationHandler');
AppRouter.navigateByDeepLinkUrl(actionUrl);
}
}

View File

@@ -10,17 +10,14 @@ import 'package:eatvio_app/features/registration/presentation/pages/verify_email
import 'package:eatvio_app/features/registration_v2/presentation/pages/registration_v2_page.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(),
// registerAnonymousPushToken вызывается в AuthCubit.initializeUser()
// и не должен блокировать навигацию
routes: [
GoRoute(path: RouteConstants.registrationScreen, name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()),
GoRoute(path: RouteConstants.registrationScreenV2, name: 'registrationScreenV2', builder: (context, state) => const RegistrationV2Page()),

View File

@@ -33,10 +33,7 @@ class MainNavigationRoutes {
});
}
return _MainShellWithNavScope(
currentLocation: state.matchedLocation,
child: child,
);
return _MainShellWithNavScope(currentLocation: state.matchedLocation, child: child);
},
routes: [
GoRoute(
@@ -49,7 +46,14 @@ class MainNavigationRoutes {
GoRoute(
path: RouteConstants.feedScreen,
name: 'feedScreen',
pageBuilder: (context, state) => const NoTransitionPage(child: FeedScreen()),
pageBuilder: (context, state) {
final initialTabIndexFromQuery = int.tryParse(state.uri.queryParameters['initialTabIndex'] ?? '');
final extra = state.extra;
if (extra is int) {
return NoTransitionPage(child: FeedScreen(initialTabIndex: extra));
}
return NoTransitionPage(child: FeedScreen(initialTabIndex: initialTabIndexFromQuery));
},
redirect: (context, state) async {
try {
await sl<WsManager>().initialize();
@@ -85,16 +89,7 @@ class MainNavigationRoutes {
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();
},
),
),
pageBuilder: (context, state) => const NoTransitionPage(child: MenuScreen()),
),
],
),
@@ -105,10 +100,7 @@ class MainNavigationRoutes {
/// Wraps the shell with [BottomNavScope] so descendants (e.g. notifications)
/// can switch tabs via [BottomNavController.switchToRoute] instead of pushing.
class _MainShellWithNavScope extends StatefulWidget {
const _MainShellWithNavScope({
required this.child,
required this.currentLocation,
});
const _MainShellWithNavScope({required this.child, required this.currentLocation});
final Widget child;
final String currentLocation;
@@ -124,10 +116,7 @@ class _MainShellWithNavScopeState extends State<_MainShellWithNavScope> {
Widget build(BuildContext context) {
return BottomNavScope(
controller: _navController,
child: BottomNavigationBarWidget(
currentLocation: widget.currentLocation,
child: widget.child,
),
child: BottomNavigationBarWidget(currentLocation: widget.currentLocation, child: widget.child),
);
}
}

View File

@@ -14,6 +14,7 @@ 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/video_calls/presentation/pages/video_call_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';
@@ -156,10 +157,7 @@ class OtherRoutes {
builder: (context, state) {
if (state.extra is Map<String, dynamic>) {
final data = state.extra as Map<String, dynamic>;
return RecipeCreateScreen(
recipe: data['recipe'] as Recipe?,
autoValidate: data['autoValidate'] as bool? ?? false,
);
return RecipeCreateScreen(recipe: data['recipe'] as Recipe?, autoValidate: data['autoValidate'] as bool? ?? false);
}
return RecipeCreateScreen(recipe: state.extra != null ? state.extra as Recipe : null);
},
@@ -212,6 +210,10 @@ class OtherRoutes {
path: RouteConstants.chatScreen,
name: 'chatScreen',
builder: (context, state) {
final uuidFromQuery = state.uri.queryParameters['uuid'];
if (uuidFromQuery != null) {
return ChatScreen(uuid: uuidFromQuery, sendInitialMessage: false);
}
final data = state.extra as Map<String, dynamic>;
final uuid = data['uuid'] as String;
final sendInitialMessage = data['sendInitialMessage'] as bool?;
@@ -223,13 +225,39 @@ class OtherRoutes {
name: 'emptyChatScreen',
builder: (context, state) => EmptyChatScreen(user: state.extra as ProfileModel),
),
GoRoute(
path: RouteConstants.videoCallScreen,
name: VideoCallScreen.name,
builder: (context, state) {
final extra = state.extra;
if (extra is Map<String, dynamic>) {
return VideoCallScreen(
conversationUuid: extra['conversationUuid'] as String,
counterpartName: extra['counterpartName'] as String?,
localParticipantName: extra['localParticipantName'] as String?,
onBackToChat: extra['onBackToChat'] as void Function()?,
onHangUp: extra['onHangUp'] as void Function()?,
);
}
final uuid = state.uri.queryParameters['uuid'];
if (uuid != null) {
return VideoCallScreen(conversationUuid: uuid);
}
throw ArgumentError('VideoCallScreen: pass extra Map with conversationUuid or query uuid');
},
),
// 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),
builder: (context, state) {
final data = state.extra as Map<String, dynamic>;
final slug = data['slug'] as String?;
final isCanceled = data['isCanceled'] as bool?;
return StepperBecomeConsultantScreen(slug: slug, isCanceled: isCanceled);
},
),
GoRoute(path: RouteConstants.calendarScreen, name: 'calendarScreen', builder: (context, state) => const CalendarScreen()),
GoRoute(path: RouteConstants.supportScreen, name: 'supportScreen', builder: (context, state) => const SupportScreen()),
@@ -262,6 +290,11 @@ class OtherRoutes {
path: InfoByOrderScreen.path,
name: InfoByOrderScreen.name,
builder: (context, state) {
final queryParams = state.uri.queryParameters;
final planIdFromQuery = int.tryParse(queryParams['planId'] ?? '');
if (planIdFromQuery != null) {
return InfoByOrderScreen(planId: planIdFromQuery, successfulPayment: null, customerSlug: null, isConsultantView: true);
}
final data = state.extra as Map<String, dynamic>;
final planId = data['planId'] as int;
final successfulPayment = data['successfulPayment'] as bool?;

View File

@@ -53,6 +53,7 @@ class RouteConstants {
static const String photoViewScreen = '/photo-view-screen';
static const String chatScreen = '/chat-screen';
static const String emptyChatScreen = '/empty-chat-screen';
static const String videoCallScreen = '/video-call-screen';
static const String glossaryScreen = '/glossary-screen';
static const String blockedUsersScreen = '/blocked-users-screen';
static const String notificationsSettingsScreen = '/notifications-settings-screen';

View File

@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
/// Helper class for managing themes and colors.
class ThemeHelper {
// The current app theme
@@ -200,6 +199,12 @@ class PrimaryColors {
Color get grey7B => const Color(0xFF7B7B7B);
Color get greyD9 => const Color(0xFFD9D9D9);
Color get videoCallControlOffBackground => const Color(0xFFED2F2F);
Color get videoCallPipIconBackground => const Color(0xFF565656);
Color get videoCallNoticeBannerBackground => const Color(0xFF9D9D9D);
}
PrimaryColors get appTheme => ThemeHelper().themeColor();

View File

@@ -1,4 +1,6 @@
import 'dart:async';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
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';
@@ -86,9 +88,13 @@ class ViewsCubit extends Cubit<ViewsCubitState> {
}
Future<void> _viewContent({required ViewableContentType type, required List<int> ids}) async {
await viewsRepository.viewContent(
viewModel: ViewModel(type: type.getStringifiedVersion(), ids: ids),
);
try {
await viewsRepository.viewContent(
viewModel: ViewModel(type: type.getStringifiedVersion(), ids: ids),
);
} catch (e, st) {
await reportFrom(e, st, this);
}
}
void _scheduleDebouncedSend() {

View File

@@ -1,6 +1,5 @@
import 'package:eatvio_app/core/views/service/views_service.dart';
import 'package:eatvio_app/features/feed/models/view_model.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class ViewsRepository {
final ViewsService viewsService;
@@ -8,11 +7,7 @@ class ViewsRepository {
ViewsRepository({required this.viewsService});
Future<void> viewContent({required ViewModel viewModel}) async {
try {
await viewsService.viewContent(viewModel: viewModel);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await viewsService.viewContent(viewModel: viewModel);
}
}

View File

@@ -9,14 +9,16 @@ import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/websockets/ws_service.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:eatvio_app/core/error_reporting/error_reporter.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
class WsManager {
final WsService wsService;
final ErrorReporter _errorReporter;
WsManager({required this.wsService});
WsManager({required this.wsService, required ErrorReporter errorReporter})
: _errorReporter = errorReporter;
PusherChannelsClient? client;
int? userId;
@@ -114,7 +116,7 @@ class WsManager {
_subscribe();
await _connectCubitsToStreams();
} catch (e, st) {
await Sentry.captureException(e, stackTrace: st);
await _errorReporter.report(e, st, origin: 'WsManager');
log(e.toString(), stackTrace: st);
}
}
@@ -130,7 +132,7 @@ class WsManager {
await sl<NotificationsCubit>().connectToWebSocket();
} catch (e, st) {
log('WsManager._connectCubitsToStreams: $e', stackTrace: st);
await Sentry.captureException(e, stackTrace: st);
await _errorReporter.report(e, st, origin: 'WsManager');
}
}

View File

@@ -9,6 +9,7 @@ import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
class AuthCubit extends Cubit<AuthState> {
@@ -26,7 +27,8 @@ class AuthCubit extends Cubit<AuthState> {
emit(AuthState.loaded(token: token));
await TokenStorage.saveTokenInStorage(token);
return true;
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
showErrorToast('Неверный адрес электронной почты или пароль');
emit(AuthState.error(errorMessage: '$e'));
@@ -37,22 +39,30 @@ class AuthCubit extends Cubit<AuthState> {
Future<void> initializeUser() async {
try {
await registerAnonymousPushToken();
FlutterNativeSplash.remove();
} catch (e) {
log('Error in initializeUser: $e');
FlutterNativeSplash.remove();
FlutterNativeSplash.remove();
} catch (e, st) {
log('Error in initializeUser: $e', stackTrace: st);
FlutterNativeSplash.remove();
}
}
Future<void> registerAnonymousPushToken() async {
final hasAnonymousTokenBeenSent = prefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent) ?? false;
if (!hasAnonymousTokenBeenSent) {
await firebaseApi.registerAnonymousPushToken();
prefs.setBool(PrefsKeys.hasAnonymousTokenBeenSent, true);
try {
final hasAnonymousTokenBeenSent = prefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent) ?? false;
if (!hasAnonymousTokenBeenSent) {
await firebaseApi.registerAnonymousPushToken();
prefs.setBool(PrefsKeys.hasAnonymousTokenBeenSent, true);
}
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<void> registerPushToken() async {
await firebaseApi.registerPushToken();
try {
await firebaseApi.registerPushToken();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
}

View File

@@ -7,6 +7,7 @@ import 'package:eatvio_app/features/auth/models/oauth_verification_request_respo
import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart';
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
final AuthRepository authRepository;
@@ -29,7 +30,8 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
log('OAuthCubit: Got token model: ${tokenModel.token}');
TokenStorage.saveTokenInStorage(tokenModel.token);
emit(OAuthState.loaded(token: tokenModel.token));
} on DioException catch (e) {
} on DioException catch (e, st) {
await reportFrom(e, st, this);
log('OAuthCubit: Error occurred: $e');
// Проверяем, является ли ошибка ошибкой валидации email/phone
final errors = e.response?.data['errors'] as Map<String, dynamic>?;
@@ -62,7 +64,8 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
} else {
emit(OAuthState.error(errorMessage: e.toString()));
}
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
log('OAuthCubit: Error occurred: $e');
emit(OAuthState.error(errorMessage: e.toString()));
}
@@ -103,7 +106,8 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
TokenStorage.saveTokenInStorage(token);
cacheRegistrationDate(DateTime.now());
emit(OAuthState.loaded(token: token));
} on DioException catch (e) {
} on DioException catch (e, st) {
await reportFrom(e, st, this);
emit(OAuthState.error(errorMessage: e.toString()));
final errors = e.response?.data['errors'] as Map<String, dynamic>?;
final emailErrors = errors?['email'] as List<dynamic>?;
@@ -153,7 +157,8 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
// Сбрасываем состояние после успешного запроса кода
emit(const OAuthState());
return response;
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
log('OAuthCubit: Error requesting verification code: $e');
emit(OAuthState.error(errorMessage: e.toString()));
return null;
@@ -200,10 +205,10 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
log('OAuthCubit: OAuth verification confirmed successfully, token received');
TokenStorage.saveTokenInStorage(response.token);
emit(OAuthState.loaded(token: response.token));
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
log('OAuthCubit: Error confirming OAuth verification: $e');
emit(OAuthState.error(errorMessage: e.toString()));
rethrow; // Пробрасываем ошибку дальше, чтобы UI мог её обработать
}
}
}

View File

@@ -1,6 +1,7 @@
import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_state.dart';
import 'package:eatvio_app/features/auth/repositories/reset_password_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class ResetPasswordCubit extends Cubit<ResetPasswordState> {
final ResetPasswordRepository resetPasswordRepository;
@@ -15,7 +16,8 @@ class ResetPasswordCubit extends Cubit<ResetPasswordState> {
} else {
emit(const ResetPasswordState.codeSendError());
}
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
emit(const ResetPasswordState.codeSendError());
}
}
@@ -28,7 +30,8 @@ class ResetPasswordCubit extends Cubit<ResetPasswordState> {
} else {
emit(const ResetPasswordState.resetPasswordError());
}
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
emit(const ResetPasswordState.resetPasswordError());
}
}

View File

@@ -7,7 +7,6 @@ import 'package:eatvio_app/features/auth/models/oauth_verification_confirm_respo
import 'package:eatvio_app/features/auth/models/oauth_verification_request_response.dart';
import 'package:eatvio_app/features/auth/models/token_model.dart';
import 'package:eatvio_app/features/auth/services/auth_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class AuthRepository {
final AuthService authService;
@@ -15,14 +14,10 @@ class AuthRepository {
AuthRepository({required this.authService});
Future<TokenModel> getTokenByCredentials(String email, String password) async {
try {
return await authService.getTokenByCredentials(
credentials: CredentialsModel(email: email, password: password),
);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return await authService.getTokenByCredentials(
credentials: CredentialsModel(email: email, password: password),
);
}
Future<TokenModel> signInViaSocialMedia({
@@ -31,14 +26,10 @@ class AuthRepository {
required String refreshToken,
required String clientId,
}) async {
try {
return await authService.signInViaSocialMedia(
body: OAuthLoginRequest(provider: provider, accessToken: accessToken, refreshToken: refreshToken, clientId: clientId),
);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return await authService.signInViaSocialMedia(
body: OAuthLoginRequest(provider: provider, accessToken: accessToken, refreshToken: refreshToken, clientId: clientId),
);
}
Future<String> signUpViaSocialMedia({
@@ -56,47 +47,39 @@ class AuthRepository {
required int height,
required String clientId,
}) async {
try {
return (await authService.signUpViaSocialMedia(
body: OAuthModel(
provider: provider,
accessToken: accessToken,
id: id,
refreshToken: refreshToken,
firstName: firstName,
lastName: lastName,
email: email,
phone: phone,
dateOfBirth: dateOfBirth,
gender: gender == 'М' ? 'male' : 'female',
weight: weight,
height: height,
clientId: clientId,
),
))['token']!
as String;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return (await authService.signUpViaSocialMedia(
body: OAuthModel(
provider: provider,
accessToken: accessToken,
id: id,
refreshToken: refreshToken,
firstName: firstName,
lastName: lastName,
email: email,
phone: phone,
dateOfBirth: dateOfBirth,
gender: gender == 'М' ? 'male' : 'female',
weight: weight,
height: height,
clientId: clientId,
),
))['token']!
as String;
}
Future<OAuthVerificationRequestResponse?> requestOAuthVerification({String? email, String? phone}) async {
try {
Map<String, dynamic>? body;
if (email != null) {
body = {'email': email};
} else if (phone != null) {
body = {'phone': phone};
}
if (body != null) {
return await authService.requestOAuthVerification(body: body);
}
return null;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
Map<String, dynamic>? body;
if (email != null) {
body = {'email': email};
} else if (phone != null) {
body = {'phone': phone};
}
if (body != null) {
return await authService.requestOAuthVerification(body: body);
}
return null;
}
Future<OAuthVerificationConfirmResponse> confirmOAuthVerification({
@@ -116,33 +99,28 @@ class AuthRepository {
required int weight,
required int height,
}) async {
try {
final request = OAuthVerificationConfirmRequest(
provider: provider,
accessToken: accessToken,
refreshToken: refreshToken,
clientId: clientId,
id: id,
email: email,
phone: phone,
code: code,
verificationType: verificationType,
firstName: firstName,
lastName: lastName,
dateOfBirth: dateOfBirth,
gender: gender,
weight: weight,
height: height,
);
// Логируем запрос перед отправкой
log(
'AuthRepository: Sending OAuth verification confirm request: provider=$provider, verificationType=$verificationType, code=$code, email=$email, phone=$phone',
);
return await authService.confirmOAuthVerification(body: request);
} catch (e, s) {
log('AuthRepository: Error in confirmOAuthVerification: $e');
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
final request = OAuthVerificationConfirmRequest(
provider: provider,
accessToken: accessToken,
refreshToken: refreshToken,
clientId: clientId,
id: id,
email: email,
phone: phone,
code: code,
verificationType: verificationType,
firstName: firstName,
lastName: lastName,
dateOfBirth: dateOfBirth,
gender: gender,
weight: weight,
height: height,
);
// Логируем запрос перед отправкой
log(
'AuthRepository: Sending OAuth verification confirm request: provider=$provider, verificationType=$verificationType, code=$code, email=$email, phone=$phone',
);
return await authService.confirmOAuthVerification(body: request);
}
}

View File

@@ -1,5 +1,4 @@
import 'package:eatvio_app/features/auth/services/reset_password_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class ResetPasswordRepository {
final ResetPasswordService resetPasswordService;
@@ -9,22 +8,18 @@ class ResetPasswordRepository {
});
Future<bool> sendEmailCodeForPasswordChange(String email) async {
try {
final response = await resetPasswordService.sendEmailCodeForPasswordChange(
data: {
'email': email,
},
);
int statusCode = response.response.statusCode ?? 0;
if (statusCode >= 200 && statusCode < 300) {
return true;
} else {
return false;
}
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
final response = await resetPasswordService.sendEmailCodeForPasswordChange(
data: {
'email': email,
},
);
int statusCode = response.response.statusCode ?? 0;
if (statusCode >= 200 && statusCode < 300) {
return true;
} else {
return false;
}
}
Future<bool> confirmPasswordChange(
@@ -33,24 +28,20 @@ class ResetPasswordRepository {
String passwordConfirmation,
String email,
) async {
try {
final response = await resetPasswordService.confirmPasswordChange(
data: {
'code': code,
'password': password,
'password_confirmation': passwordConfirmation,
'email': email,
},
);
int statusCode = response.response.statusCode ?? 0;
if (statusCode >= 200 && statusCode < 300) {
return true;
} else {
return false;
}
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
final response = await resetPasswordService.confirmPasswordChange(
data: {
'code': code,
'password': password,
'password_confirmation': passwordConfirmation,
'email': email,
},
);
int statusCode = response.response.statusCode ?? 0;
if (statusCode >= 200 && statusCode < 300) {
return true;
} else {
return false;
}
}
}

View File

@@ -8,10 +8,11 @@ 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:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/error_reporting/error_reporter.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:http/http.dart' as http;
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:vk_id/vk_id.dart';
import 'package:webview_flutter/webview_flutter.dart';
@@ -43,7 +44,7 @@ class SocialAuthFacade {
}
return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier, clientId: clientIdStr);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
await sl<ErrorReporter>().report(e, s, origin: 'SocialAuthFacade');
return null;
}
}
@@ -161,7 +162,7 @@ class SocialAuthFacade {
);
} catch (e, s) {
log(e.toString());
await Sentry.captureException(e, stackTrace: s);
await sl<ErrorReporter>().report(e, s, origin: 'SocialAuthFacade');
}
return null;
}
@@ -209,7 +210,7 @@ class SocialAuthFacade {
return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier, clientId: clientIdStr);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
await sl<ErrorReporter>().report(e, s, origin: 'SocialAuthFacade');
return null;
}
}

View File

@@ -1,7 +1,6 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/barcode/models/barcode_product_model.dart';
import 'package:eatvio_app/features/barcode/services/barcode_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class BarcodeRepository {
final BarcodeService barcodeService;
@@ -15,49 +14,46 @@ class BarcodeRepository {
/// Получить информацию о продукте по штрихкоду
/// Возвращает либо данные продукта, либо статус задачи обработки
Future<BarcodeResult> getProductByBarcode(String barcode) async {
try {
final response = await dio.get('/api/gsone/barcode/$barcode');
if (response.statusCode == 200) {
// Данные найдены в БД
final product = BarcodeProductModel.fromJson(response.data as Map<String, dynamic>);
return BarcodeResult.success(product);
} else if (response.statusCode == 202) {
// Запущена задача обработки
final taskStatus = BarcodeTaskStatusModel.fromJson(response.data as Map<String, dynamic>);
return BarcodeResult.processing(taskStatus.taskId ?? '');
}
throw Exception('Unexpected status code: ${response.statusCode}');
} on DioException catch (e, s) {
if (e.response?.statusCode == 202) {
// Если получен статус 202, значит запущена задача обработки
final taskStatus = BarcodeTaskStatusModel.fromJson(e.response!.data as Map<String, dynamic>);
return BarcodeResult.processing(taskStatus.taskId ?? '');
}
await Sentry.captureException(e, stackTrace: s);
rethrow;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
final response = await dio.get<Map<String, dynamic>>(
'/api/gsone/barcode/$barcode',
options: Options(validateStatus: (_) => true),
);
final code = response.statusCode ?? 0;
final data = response.data;
if (data is! Map<String, dynamic>) {
throw DioException(
requestOptions: response.requestOptions,
response: response,
message: 'Invalid barcode response',
);
}
if (code == 200) {
final product = BarcodeProductModel.fromJson(data);
return BarcodeResult.success(product);
}
if (code == 202) {
final taskStatus = BarcodeTaskStatusModel.fromJson(data);
return BarcodeResult.processing(taskStatus.taskId ?? '');
}
throw DioException(
requestOptions: response.requestOptions,
response: response,
message: 'Unexpected status code: $code',
);
}
/// Получить статус задачи обработки
Future<BarcodeTaskStatus> getTaskStatus(String taskId) async {
try {
final statusModel = await barcodeService.getTaskStatus(taskId: taskId);
if (statusModel.status == 'completed' && statusModel.data != null) {
return BarcodeTaskStatus.completed(statusModel.data!);
} else if (statusModel.status == 'failed') {
return BarcodeTaskStatus.failed();
} else {
return BarcodeTaskStatus.processing();
}
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
final statusModel = await barcodeService.getTaskStatus(taskId: taskId);
if (statusModel.status == 'completed' && statusModel.data != null) {
return BarcodeTaskStatus.completed(statusModel.data!);
} else if (statusModel.status == 'failed') {
return BarcodeTaskStatus.failed();
} else {
return BarcodeTaskStatus.processing();
}
}
}
@@ -109,4 +105,3 @@ enum BarcodeTaskStatusType {
processing,
failed,
}

View File

@@ -3,6 +3,7 @@ import 'dart:developer';
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_state.dart';
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class BlogPostCubit extends Cubit<BlogPostState> {
final FeedRepository feedRepository;
@@ -16,6 +17,7 @@ class BlogPostCubit extends Cubit<BlogPostState> {
emit(state.copyWith(blogPostModel: postModel, isLoading: false));
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
emit(state.copyWith(isLoading: false));
}
@@ -37,6 +39,7 @@ class BlogPostCubit extends Cubit<BlogPostState> {
await feedRepository.likeBlogPost(id: current.id);
}
} catch (e, st) {
await reportFrom(e, st, this);
// rollback on error
log(e.toString(), stackTrace: st);
emit(state.copyWith(blogPostModel: current));

View File

@@ -78,9 +78,7 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
return Scaffold(
resizeToAvoidBottomInset: true,
bottomSheet: Container(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom + 8,
),
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom + 8),
decoration: BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
@@ -147,14 +145,18 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
),
const SizedBox(width: 8),
GestureDetector(
onTap: () {
onTap: () async {
if (textEditingController.text.length > 2) {
commentsCubit.sendComment(
final isSent = await commentsCubit.sendComment(
id: widget.id.toString(),
commentText: textEditingController.text,
parentCommentId: replyId,
commentSourceType: CommentSourceType.blogPost,
);
if (!context.mounted) return;
if (isSent) context.read<FeedCubit>().incrementCommentsState(widget.id);
context.read<CommentsCubit>().setReplyId(null);
setState(() {
textEditingController.clear();
@@ -237,9 +239,7 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
if (blogPostState.blogPostModel?.description.isNotEmpty ?? false)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: MarkdownHtmlWidget(
markdownText: blogPostState.blogPostModel!.description,
),
child: MarkdownHtmlWidget(markdownText: blogPostState.blogPostModel!.description),
),
const SizedBox(height: 8),
if (blogPostState.blogPostModel?.image.isNotEmpty ?? false)

View File

@@ -1,9 +1,12 @@
import 'package:eatvio_app/core/cubit/base_cubit.dart';
import 'dart:developer';
import 'package:eatvio_app/core/error_reporting/report_from.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 BaseCubit<BlogPostCreateState> {
class BlogPostCreateCubit extends Cubit<BlogPostCreateState> {
final BlogPostRepository blogPostRepository;
BlogPostCreateCubit({required this.blogPostRepository}) : super(const BlogPostCreateState());
@@ -15,7 +18,8 @@ class BlogPostCreateCubit extends BaseCubit<BlogPostCreateState> {
emit(const BlogPostCreateState.loaded());
return true;
} catch (error, stackTrace) {
handleError(error, stackTrace);
log('BlogPostCreateCubit: $error', stackTrace: stackTrace);
await reportFrom(error, stackTrace, this);
emit(BlogPostCreateState.error(errorMessage: error.toString()));
return false;
}
@@ -28,7 +32,8 @@ class BlogPostCreateCubit extends BaseCubit<BlogPostCreateState> {
emit(const BlogPostCreateState.loaded());
return true;
} catch (error, stackTrace) {
handleError(error, stackTrace);
log('BlogPostCreateCubit: $error', stackTrace: stackTrace);
await reportFrom(error, stackTrace, this);
emit(BlogPostCreateState.error(errorMessage: error.toString()));
return false;
}
@@ -39,7 +44,8 @@ class BlogPostCreateCubit extends BaseCubit<BlogPostCreateState> {
await blogPostRepository.deleteBlogPost(id);
return true;
} catch (error, stackTrace) {
handleError(error, stackTrace);
log('BlogPostCreateCubit: $error', stackTrace: stackTrace);
await reportFrom(error, stackTrace, this);
return false;
}
}

View File

@@ -1,5 +1,5 @@
import 'dart:developer';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/locator_service.dart';
@@ -8,7 +8,6 @@ import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
import 'package:eatvio_app/features/blog_post_create/services/blog_post_service.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class BlogPostRepository {
final BlogPostService blogPostService;
@@ -23,48 +22,30 @@ class BlogPostRepository {
String? description,
XFile? image,
) async {
FormData formData = FormData.fromMap({
final formData = FormData.fromMap({
if (title != null && title.isNotEmpty) 'title': title,
if (description != null && description.isNotEmpty) 'description_md': description,
if (videoUrl != null && videoUrl.isNotEmpty) 'video_url': videoUrl,
if (image != null) 'image': await MultipartFile.fromFile((await ImageCompressUtility.compressFile(File(image.path))).path, filename: image.name),
});
try {
return await blogPostService.createBlogPost(
formData: formData,
);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return blogPostService.createBlogPost(
formData: formData,
);
}
Future<XFile?> downloadImage(String imageUrl) async {
try {
// Загрузка изображения
final response = await http.get(Uri.parse('${sl<AppConfig>().baseUrl}/storage/$imageUrl'));
final response = await http.get(Uri.parse('${sl<AppConfig>().baseUrl}/storage/$imageUrl'));
if (response.statusCode == 200) {
// Получение временной директории
final directory = await getTemporaryDirectory();
// Создание файла в временной директории
final filePath = '${directory.path}/image.jpg'; // Вы можете изменить название файла
final file = File(filePath);
// Запись данных в файл
await file.writeAsBytes(response.bodyBytes);
if (response.statusCode == 200) {
final directory = await getTemporaryDirectory();
final filePath = '${directory.path}/image.jpg';
final file = File(filePath);
await file.writeAsBytes(response.bodyBytes);
// Возвращаем объект XFile
return XFile(file.path);
} else {
log('Ошибка загрузки изображения: ${response.statusCode}');
return null;
}
} catch (e, s) {
log('Произошла ошибка: $e');
await Sentry.captureException(e, stackTrace: s);
return null;
return XFile(file.path);
}
return null;
}
Future<void> updateBlogPost(
@@ -75,9 +56,9 @@ class BlogPostRepository {
XFile? image,
String? oldImageUrl,
) async {
FormData formData = FormData.fromMap({
final formData = FormData.fromMap({
'title': title,
'description_md': description,
'description_md': description,
'video_url': videoUrl,
'image': image != null
? await MultipartFile.fromFile((await ImageCompressUtility.compressFile(File(image.path))).path, filename: image.name)
@@ -86,23 +67,13 @@ class BlogPostRepository {
: null,
});
try {
return await blogPostService.updateBlogPost(
id: id,
formData: formData,
);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return blogPostService.updateBlogPost(
id: id,
formData: formData,
);
}
Future<void> deleteBlogPost(int id) async {
try {
await blogPostService.deleteBlogPost(id: id);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await blogPostService.deleteBlogPost(id: id);
}
}

View File

@@ -1,8 +1,11 @@
import 'dart:async';
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:eatvio_app/core/error_reporting/report_from.dart';
import 'package:go_router/go_router.dart';
import 'package:permission_handler/permission_handler.dart';
@@ -28,7 +31,8 @@ class ProfileAvatarCubit extends Cubit<ProfileAvatarState> {
return age;
}
} catch (e) {
} catch (e, st) {
unawaited(reportFrom(e, st, this));
return 0;
}
return 0;
@@ -51,10 +55,12 @@ class ProfileAvatarCubit extends Cubit<ProfileAvatarState> {
]);
AppMetrica.setUserProfileID(profile.slug);
AppMetrica.reportUserProfile(userProfile);
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
// Игнорируем ошибки плагинов, профиль уже загружен
}
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
AppRouter.navigatorKey.currentContext?.go('/auth-screen');
emit(ProfileAvatarState.error(errorMessage: e.toString()));
}

View File

@@ -1,6 +1,7 @@
import 'package:eatvio_app/features/calendar/presentation/bloc/calendar_state.dart';
import 'package:eatvio_app/features/calendar/repositories/calendar_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class CalendarCubit extends Cubit<CalendarState> {
final CalendarRepository calendarRepository;
@@ -12,7 +13,8 @@ class CalendarCubit extends Cubit<CalendarState> {
try {
final calendarList = await calendarRepository.getCalendar();
emit(CalendarState.loaded(calendarList: calendarList));
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
emit(CalendarState.error(errorMessage: e.toString()));
}
}

View File

@@ -1,6 +1,5 @@
import 'package:eatvio_app/features/calendar/models/calendar_model.dart';
import 'package:eatvio_app/features/calendar/services/calendar_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class CalendarRepository {
final CalendarService calendarService;
@@ -8,11 +7,7 @@ class CalendarRepository {
CalendarRepository({required this.calendarService});
Future<List<CalendarModel>> getCalendar() async {
try {
return await calendarService.getCalendar();
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return await calendarService.getCalendar();
}
}

View File

@@ -4,6 +4,7 @@ import 'package:eatvio_app/features/cancel_plan_bottom_sheet/cubit/cancel_plan_s
import 'package:eatvio_app/features/cancel_plan_bottom_sheet/models/cancelation_model.dart';
import 'package:eatvio_app/features/cancel_plan_bottom_sheet/repository/cancel_plan_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class CancelPlanCubit extends Cubit<CancelPlanState> {
final CancelPlanRepository cancelPlanRepository;
@@ -18,6 +19,7 @@ class CancelPlanCubit extends Cubit<CancelPlanState> {
await cancelPlanRepository.cancelChosenPlan(id: id, body: body);
return true;
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
}
return false;

View File

@@ -1,6 +1,5 @@
import 'package:eatvio_app/features/cancel_plan_bottom_sheet/models/cancelation_model.dart';
import 'package:eatvio_app/features/cancel_plan_bottom_sheet/service/cancel_plan_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class CancelPlanRepository {
final CancelPlanService cancelService;
@@ -9,11 +8,7 @@ class CancelPlanRepository {
required int id,
CancelationModel? body
}) async {
try {
await cancelService.cancelChosenPlan(id: id, body: body);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await cancelService.cancelChosenPlan(id: id, body: body);
}
}

View File

@@ -20,6 +20,7 @@ import 'package:eatvio_app/features/my_finance/models/card_model.dart';
import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart';
import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
import 'package:uuid/uuid.dart';
class ChatRoomCubit extends Cubit<ChatRoomState> {
@@ -64,7 +65,8 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
);
currentPage++;
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
emit(const ChatRoomState.error(errorMessage: 'Ошибка получеия данных'));
}
}
@@ -75,6 +77,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
final cards = await cardsRepository.getCards();
_setCard(cards: cards);
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
}
}
@@ -90,6 +93,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
Future<void> initChat(String uuid, {bool sendInitialMessage = false}) async {
try {
emit(const ChatRoomState.loading());
final chatRoom = await chatRepository.getChatRoom(uuid);
ChosenPlanModel? chosenPlan;
if (chatRoom.chosenPlanId != null) {
@@ -111,7 +115,8 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
if (chosenPlan?.consultantId == chatRoom.users.sender.id) {
await checkCards();
}
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
typingSubscription?.cancel();
messagesSubscription?.cancel();
@@ -121,13 +126,17 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
}
Future<void> updateChosenPlanState() async {
final currentState = state;
if (currentState is ChatRoomStateLoaded) {
final chosenPlanId = currentState.chatRoom.chosenPlanId;
if (chosenPlanId != null) {
final chosenPlan = await chatRepository.getChosenPlan(id: chosenPlanId);
emit(currentState.copyWith.chatRoom(chosenPlan: chosenPlan));
try {
final currentState = state;
if (currentState is ChatRoomStateLoaded) {
final chosenPlanId = currentState.chatRoom.chosenPlanId;
if (chosenPlanId != null) {
final chosenPlan = await chatRepository.getChosenPlan(id: chosenPlanId);
emit(currentState.copyWith.chatRoom(chosenPlan: chosenPlan));
}
}
} catch (e, st) {
await reportFrom(e, st, this);
}
}
@@ -142,6 +151,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
emit((state as ChatRoomStateLoaded).copyWith.chatRoom(chosenPlan: chosenPlan));
return true;
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
return false;
}
@@ -151,7 +161,8 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
try {
await chatRepository.cancelChosenPlan(id: planId);
emit((state as ChatRoomStateLoaded).copyWith.chatRoom(chosenPlan: null));
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
emit(const ChatRoomState.error(errorMessage: 'Ошибка получеия данных'));
}
}
@@ -165,12 +176,12 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
AudioPlayer().play((AssetSource('audio/recive_message.wav')));
final message = ChatMessageModel.fromJson(jsonDecode(event.data));
final currentState = state as ChatRoomStateLoaded;
// Сообщения от текущего пользователя не приходят через WebSocket,
// они обрабатываются через ответ API в sendMessage
// Здесь обрабатываем только входящие сообщения от других пользователей
List<ChatMessageModel> messagesList = [message, ...currentState.chatMessages];
chatRepository.emitNewMessageToChats(message);
emit(currentState.copyWith(chatMessages: messagesList, isLoading: isLoading));
EasyDebounce.debounce('searchChatRoom', const Duration(milliseconds: 300), () => sl<ChatsCubit>().getChatRooms());
@@ -191,7 +202,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
final messageUuid = uuid.v4();
final compressedFiles = files != null ? await ImageCompressUtility.compressFilesList(List<File>.from(files)) : null;
AudioPlayer().play((AssetSource('audio/send_message.wav')));
// Создаем временное сообщение с состоянием loading
emit(
(state as ChatRoomStateLoaded).copyWith(
@@ -216,7 +227,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
try {
// Отправляем сообщение и получаем ответ от сервера с правильными данными
final serverMessage = await chatRepository.sendMessage(data: data, files: compressedFiles, slug: userSlug);
// Заменяем локальное сообщение данными с сервера (с правильными URL файлов)
emit(
(state as ChatRoomStateLoaded).copyWith(
@@ -238,7 +249,8 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
}).toList(),
),
);
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
emit(
(state as ChatRoomStateLoaded).copyWith(
chatMessages: (state as ChatRoomStateLoaded).chatMessages.map((elem) {
@@ -253,6 +265,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
sl<ChatsCubit>().getChatRooms();
return true;
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
return false;
}
@@ -274,6 +287,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
}
return false;
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
return false;
}
@@ -286,6 +300,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
final paymentModel = await chatRepository.getPaymentLink(id: planId);
return paymentModel.paymentUrl;
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
return null;
}
@@ -300,7 +315,8 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
body: PlanComplainModel(description: description),
);
return true;
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
return false;
}
@@ -312,7 +328,8 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
if (planId == null) return false;
await chatRepository.finishChosenPlan(id: planId);
return true;
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
return false;
}
@@ -341,7 +358,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
if (message != null) {
try {
AudioPlayer().play((AssetSource('audio/send_message.wav')));
// Определяем, есть ли файлы для повторной отправки
List<File>? filesToResend;
if (message.mediaData != null && message.mediaData!.isNotEmpty) {
@@ -352,7 +369,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
filesToResend = message.mediaData!.cast<File>();
}
}
emit(
(state as ChatRoomStateLoaded).copyWith(
chatMessages: [
@@ -375,12 +392,8 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
try {
// Отправляем сообщение и получаем ответ от сервера
final serverMessage = await chatRepository.sendMessage(
data: message.data,
files: filesToResend,
slug: userSlug,
);
final serverMessage = await chatRepository.sendMessage(data: message.data, files: filesToResend, slug: userSlug);
// Заменяем локальное сообщение данными с сервера
emit(
(state as ChatRoomStateLoaded).copyWith(
@@ -400,7 +413,8 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
}).toList(),
),
);
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
emit(
(state as ChatRoomStateLoaded).copyWith(
chatMessages: (state as ChatRoomStateLoaded).chatMessages.map((elem) {
@@ -414,6 +428,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
}
sl<ChatsCubit>().getChatRooms();
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
}
}

View File

@@ -9,6 +9,7 @@ import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_sta
import 'package:eatvio_app/features/chat/repositories/chat_repository.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class ChatsCubit extends Cubit<ChatsState> {
final ChatRepository chatRepository;
@@ -31,10 +32,14 @@ class ChatsCubit extends Cubit<ChatsState> {
late int lastPage;
Future<void> connectToWebSocket() async {
await newIncomingMessagesSubscription?.cancel();
newIncomingMessagesSubscription = wsManager.newMessagesStream?.listen((event) {
_handleNewMessageViaSocket(event.data);
});
try {
await newIncomingMessagesSubscription?.cancel();
newIncomingMessagesSubscription = wsManager.newMessagesStream?.listen((event) {
_handleNewMessageViaSocket(event.data);
});
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<void> getChatRoomsWithLoading({String? query}) async {
@@ -53,7 +58,8 @@ class ChatsCubit extends Cubit<ChatsState> {
),
);
page++;
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
emit(
state.copyWith(errorMessage: e.toString(), state: BaseState.error),
);
@@ -76,6 +82,7 @@ class ChatsCubit extends Cubit<ChatsState> {
);
page++;
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
emit(
state.copyWith(errorMessage: e.toString(), state: BaseState.error),
@@ -99,6 +106,7 @@ class ChatsCubit extends Cubit<ChatsState> {
);
page++;
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
emit(
state.copyWith(
@@ -132,33 +140,37 @@ class ChatsCubit extends Cubit<ChatsState> {
}
Future<void> _handleNewMessageViaSocket(dynamic data) async {
final json = jsonDecode(data);
final message = ChatMessageModel.fromJson(json['message']);
try {
final json = jsonDecode(data);
final message = ChatMessageModel.fromJson(json['message']);
final currentChatRooms = [...state.chatRooms];
final currentChatRooms = [...state.chatRooms];
bool wasChanged = false;
bool willIncrementUnreadChats = false;
final updatedChatRooms = currentChatRooms.map((chatRoom) {
if (chatRoom.id == message.roomId) {
wasChanged = true;
if (chatRoom.unreadMessagesCount == 0) {
willIncrementUnreadChats = true;
bool wasChanged = false;
bool willIncrementUnreadChats = false;
final updatedChatRooms = currentChatRooms.map((chatRoom) {
if (chatRoom.id == message.roomId) {
wasChanged = true;
if (chatRoom.unreadMessagesCount == 0) {
willIncrementUnreadChats = true;
}
return chatRoom.copyWith(lastMessage: message, unreadMessagesCount: chatRoom.unreadMessagesCount + 1);
}
return chatRoom.copyWith(lastMessage: message, unreadMessagesCount: chatRoom.unreadMessagesCount + 1);
}
return chatRoom;
}).toList();
return chatRoom;
}).toList();
if (!wasChanged && _query == null) {
getChatRooms();
} else {
emit(
state.copyWith(
chatRooms: updatedChatRooms,
chatsWithUnreadMessagesCount: willIncrementUnreadChats ? state.chatsWithUnreadMessagesCount + 1 : state.chatsWithUnreadMessagesCount,
),
);
if (!wasChanged && _query == null) {
getChatRooms();
} else {
emit(
state.copyWith(
chatRooms: updatedChatRooms,
chatsWithUnreadMessagesCount: willIncrementUnreadChats ? state.chatsWithUnreadMessagesCount + 1 : state.chatsWithUnreadMessagesCount,
),
);
}
} catch (e, st) {
await reportFrom(e, st, this);
}
}

View File

@@ -3,6 +3,7 @@ import 'dart:math';
import 'package:dismissible_page/dismissible_page.dart';
import 'package:easy_debounce/easy_debounce.dart';
import 'package:eatvio_app/core/routing/route_constants.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
@@ -23,6 +24,7 @@ import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.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/features/video_calls/presentation/pages/video_call_screen.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart';
@@ -62,6 +64,14 @@ class _ChatScreenState extends State<ChatScreen> {
chatRoomCubit.setTimer();
}
@override
void didUpdateWidget(ChatScreen oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.uuid != widget.uuid) {
chatRoomCubit.initChat(widget.uuid, sendInitialMessage: widget.sendInitialMessage ?? false);
}
}
@override
void initState() {
super.initState();
@@ -96,11 +106,34 @@ class _ChatScreenState extends State<ChatScreen> {
return _buildChat(context, state);
} else if (state is ChatRoomStateError) {
return Scaffold(
appBar: AppBar(),
appBar: AppBar(
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go(RouteConstants.chatSelectScreen);
},
),
),
body: DataErrorWidget(onReload: () => chatRoomCubit.initChat(widget.uuid)),
);
} else {
return const Scaffold(body: Loader());
return Scaffold(
body: const Loader(),
appBar: AppBar(
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go(RouteConstants.chatSelectScreen);
},
),
),
);
}
},
),
@@ -148,8 +181,18 @@ class _ChatScreenState extends State<ChatScreen> {
resizeToAvoidBottomInset: true,
appBar: AppBar(
titleSpacing: 0,
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go(RouteConstants.chatSelectScreen);
},
),
backgroundColor: appTheme.gray100,
centerTitle: false,
title: GestureDetector(
onTap: () {
GoRouter.of(context).pushNamed('profileScreen', extra: state.chatRoom.users.receiver.slug);
@@ -191,6 +234,23 @@ class _ChatScreenState extends State<ChatScreen> {
],
),
),
actions: [
Padding(
padding: const EdgeInsetsDirectional.only(end: 8),
child: _ChatHeaderVideoCallButton(
onPressed: () {
GoRouter.of(context).pushNamed(
VideoCallScreen.name,
extra: <String, dynamic>{
'conversationUuid': widget.uuid,
'counterpartName': state.chatRoom.users.receiver.name,
'localParticipantName': state.chatRoom.users.sender.name,
},
);
},
),
),
],
),
body: SafeArea(
child: Stack(
@@ -224,7 +284,7 @@ class _ChatScreenState extends State<ChatScreen> {
final isCurrentUserAuthor = state.chatRoom.users.receiver.id != state.chatMessages[index].authorId;
final message = state.chatMessages[index];
final isPlanOrderMessage = message.type == 'plan_order';
PacketOptions? messageOption;
if (isPlanOrderMessage && chosenPlan != null) {
if (chosenPlan.optionType == 'options1') {
@@ -235,7 +295,7 @@ class _ChatScreenState extends State<ChatScreen> {
messageOption = chosenPlan.plan.packet3Options;
}
}
return Column(
children: [
_messageDate(message.updatedAt),
@@ -284,7 +344,7 @@ class _ChatScreenState extends State<ChatScreen> {
final isCurrentUserAuthor = state.chatRoom.users.receiver.id != state.chatMessages[index].authorId;
final message = state.chatMessages[index];
final isPlanOrderMessage = message.type == 'plan_order';
PacketOptions? messageOption;
if (isPlanOrderMessage && chosenPlan != null) {
if (chosenPlan.optionType == 'options1') {
@@ -295,7 +355,7 @@ class _ChatScreenState extends State<ChatScreen> {
messageOption = chosenPlan.plan.packet3Options;
}
}
return Column(
children: [
if (isPlanOrderMessage && chosenPlan != null && messageOption != null)
@@ -699,3 +759,35 @@ class _ChatScreenState extends State<ChatScreen> {
);
}
}
class _ChatHeaderVideoCallButton extends StatelessWidget {
const _ChatHeaderVideoCallButton({required this.onPressed});
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final tooltipMessage = S.maybeOf(context)?.video_call;
final button = Material(
color: theme.colorScheme.onPrimary,
borderRadius: BorderRadius.circular(20),
elevation: 0,
child: InkWell(
onTap: onPressed,
customBorder: const CircleBorder(),
child: SizedBox(
width: 40,
height: 40,
child: Center(
child: Icon(Icons.videocam_rounded, color: appTheme.blueA400, size: 18),
),
),
),
);
if (tooltipMessage == null || tooltipMessage.isEmpty) {
return button;
}
return Tooltip(message: tooltipMessage, child: button);
}
}

View File

@@ -1,5 +1,4 @@
import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/token_storage.dart';
@@ -12,7 +11,6 @@ import 'package:eatvio_app/features/chat/models/plan_complain_model.dart';
import 'package:eatvio_app/features/chat/services/chat_service.dart';
import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart';
import 'package:http_parser/http_parser.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class ChatRepository {
final ChatService chatService;
@@ -34,160 +32,104 @@ class ChatRepository {
String? data,
List<File>? files,
}) async {
try {
final multipartFiles = <MultipartFile>[];
final multipartFiles = <MultipartFile>[];
if (files != null) {
for (final file in files) {
final fileBytes = await file.readAsBytes();
final multipartFile = MultipartFile.fromBytes(
fileBytes,
filename: file.path.split('/').last,
contentType: MediaType('application', 'octet-stream'),
);
multipartFiles.add(multipartFile);
}
if (files != null) {
for (final file in files) {
final fileBytes = await file.readAsBytes();
final multipartFile = MultipartFile.fromBytes(
fileBytes,
filename: file.path.split('/').last,
contentType: MediaType('application', 'octet-stream'),
);
multipartFiles.add(multipartFile);
}
// Если data пустая строка или null, не передаем её вообще
// Сервер требует либо непустую строку, либо отсутствие поля
final dataToSend = (data == null || data.trim().isEmpty) ? null : data;
return await chatService.sendMessage(
slug: slug,
data: dataToSend,
mediaData: multipartFiles.isEmpty ? null : multipartFiles,
socketId: (await SocketIdStorage.getSocketIdFromStorage())!,
);
} catch (e, st) {
log(e.toString(), stackTrace: st);
await Sentry.captureException(e, stackTrace: st);
rethrow;
}
// Если data пустая строка или null, не передаем её вообще
// Сервер требует либо непустую строку, либо отсутствие поля
final dataToSend = (data == null || data.trim().isEmpty) ? null : data;
return await chatService.sendMessage(
slug: slug,
data: dataToSend,
mediaData: multipartFiles.isEmpty ? null : multipartFiles,
socketId: (await SocketIdStorage.getSocketIdFromStorage())!,
);
}
Future<ChatRoomModel> getChatRoom(String uuid) async {
try {
return (await chatService.getChatRoom(uuid: uuid));
} catch (e, st) {
log(e.toString(), stackTrace: st);
await Sentry.captureException(e, stackTrace: st);
rethrow;
}
return (await chatService.getChatRoom(uuid: uuid));
}
Future<ChatRoomPaginationModel> getChatRooms({String? query, int page = 1}) async {
try {
return (await chatService.getChatRooms(query: query, page: page));
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return (await chatService.getChatRooms(query: query, page: page));
}
Future<ChatMessagesModel> getChatMessages(String id, int page) async {
try {
return (await chatService.getChatMessages(id: id, page: page));
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return (await chatService.getChatMessages(id: id, page: page));
}
Future<ChatRoomModel> getChatRoomIdByUserSlug({required String slug}) async {
try {
return (await chatService.getChatRoomIdByUserSlug(user: slug));
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return (await chatService.getChatRoomIdByUserSlug(user: slug));
}
Future<void> markMessagesAsRead(String id, List<int> messageIds) async {
try {
await chatService.markMessagesAsRead(id: id, body: {'messages_ids': messageIds});
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await chatService.markMessagesAsRead(id: id, body: {'messages_ids': messageIds});
}
Future<void> cancelChosenPlan({
required int id,
}) async {
try {
await chatService.cancelChosenPlan(id: id);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await chatService.cancelChosenPlan(id: id);
}
Future<void> choseStartPlanDate({required int id, required Map<String, String> date}) async {
try {
await chatService.choseStartPlanDate(id: id, date: date);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await chatService.choseStartPlanDate(id: id, date: date);
}
Future<PaymentModel> getPaymentLink({
required int id,
}) async {
try {
return await chatService.getPaymentLink(id: id);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return await chatService.getPaymentLink(id: id);
}
Future<ChosenPlanModel> getChosenPlan({
required int id,
}) async {
try {
return await chatService.getChosenPlan(id: id);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return await chatService.getChosenPlan(id: id);
}
Future<void> finishChosenPlan({
required int id,
}) async {
try {
await chatService.finishChosenPlan(id: id);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await chatService.finishChosenPlan(id: id);
}
Future<void> complainAboutConsultation({
required int id,
required PlanComplainModel body,
}) async {
try {
await chatService.complainAboutConsultation(id: id, body: body);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await chatService.complainAboutConsultation(id: id, body: body);
}
Future<String> getChatByUserSlug(String slug) async {
try {
final response = await chatService.getChatByUserSlug(slug: slug);
if (response.response.statusCode == 404) {
return '404';
} else {
return response.data['id'];
}
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
return 'error';
final response = await chatService.getChatByUserSlug(slug: slug);
if (response.response.statusCode == 404) {
return '404';
}
return response.data['id'];
}
}

View File

@@ -5,6 +5,7 @@ import 'package:eatvio_app/features/complaining/presentation/complaining_bottom_
import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_state.dart';
import 'package:eatvio_app/features/complaining/repositories/complaining_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class ComplainingCubit extends Cubit<ComplainingState> {
final ComplainingRepository repository;
@@ -81,6 +82,7 @@ class ComplainingCubit extends Cubit<ComplainingState> {
}
}
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
}
emit(state.copyWith(wasSentSuccessfully: false));

View File

@@ -1,6 +1,5 @@
import 'package:eatvio_app/features/complaining/models/complain_model.dart';
import 'package:eatvio_app/features/complaining/services/complaining_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class ComplainingRepository {
final ComplainingService service;
@@ -11,12 +10,8 @@ class ComplainingRepository {
required String category,
required ComplainModel body,
}) async {
try {
await service.sendPlanComplain(slug: slug, category: category, body: body);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await service.sendPlanComplain(slug: slug, category: category, body: body);
}
Future<void> sendRecipeComplain({
@@ -24,12 +19,8 @@ class ComplainingRepository {
required String category,
required ComplainModel body,
}) async {
try {
await service.sendRecipeComplain(slug: slug, category: category, body: body);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await service.sendRecipeComplain(slug: slug, category: category, body: body);
}
Future<void> sendBlogPostsComplain({
@@ -37,12 +28,8 @@ class ComplainingRepository {
required String category,
required ComplainModel body,
}) async {
try {
await service.sendBlogPostsComplain(id: id, category: category, body: body);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await service.sendBlogPostsComplain(id: id, category: category, body: body);
}
Future<void> sendProfileComplain({
@@ -50,12 +37,8 @@ class ComplainingRepository {
required String category,
required ComplainModel body,
}) async {
try {
await service.sendProfileComplain(slug: slug, category: category, body: body);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await service.sendProfileComplain(slug: slug, category: category, body: body);
}
Future<void> sendDietComplain({
@@ -63,12 +46,8 @@ class ComplainingRepository {
required String category,
required ComplainModel body,
}) async {
try {
await service.sendDietComplain(id: id, category: category, body: body);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await service.sendDietComplain(id: id, category: category, body: body);
}
Future<void> sendCommentComplain({
@@ -76,11 +55,7 @@ class ComplainingRepository {
required String category,
required ComplainModel body,
}) async {
try {
await service.sendCommentComplain(id: id, category: category, body: body);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await service.sendCommentComplain(id: id, category: category, body: body);
}
}

View File

@@ -22,6 +22,9 @@ abstract class ConsultantCertPhotoModel with _$ConsultantCertPhotoModel {
static const String visibleGroup = 'user_certs';
static const String hiddenGroup = 'hidden_user_certs';
static const String pendingGroup = 'pending_user_certs';
bool get isHidden => group == hiddenGroup;
bool get isActive => group == visibleGroup || group == hiddenGroup;
}

View File

@@ -1,10 +1,11 @@
import 'dart:developer';
import 'dart:io';
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_cert_photo_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_profile_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_state.dart';
import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
final ConsultantRepository consultantRepository;
@@ -16,82 +17,120 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
consultant: ConsultantProfileModel(socialLinks: SocialLinksModel(), hasMedicalEducation: true),
loading: true,
acceptOffer: false,
isUpdating: false,
),
);
Future<void> getMyRequestStatus(String slug) async {
_update = true;
final myRequestStatus = await consultantRepository.getMyRequestStatus(slug);
final certPhotos = await _fetchVisibleCertPhotos();
try {
_update = true;
final myRequestStatus = await consultantRepository.getMyRequestStatus(slug);
final certPhotos = await consultantRepository.getConsultantCertPhotos();
List<CertificatesModel>? certificates = myRequestStatus.educationModel.completedCourses?.map((courseMap) {
final course = courseMap.values.first;
List<CertificatesModel>? certificates = myRequestStatus.educationModel.completedCourses?.map((courseMap) {
final course = courseMap.values.first;
return CertificatesModel(title: course.title, date: course.date);
}).toList();
return CertificatesModel(title: course.title, date: course.date);
}).toList();
String? sanitizeLink(String? link) {
return (link != null && link.isNotEmpty) ? link : null;
}
String? sanitizeLink(String? link) {
return (link != null && link.isNotEmpty) ? link : null;
}
emit(
RegisterConsultantState(
consultant: ConsultantProfileModel(
certificates: certificates,
socialLinks: SocialLinksModel(
vkLink: sanitizeLink(myRequestStatus.educationModel.vkLink),
tgLink: sanitizeLink(myRequestStatus.educationModel.tgLink),
fbLink: sanitizeLink(myRequestStatus.educationModel.fbLink),
instaLink: sanitizeLink(myRequestStatus.educationModel.instaLink),
personalSiteLink: sanitizeLink(myRequestStatus.educationModel.personalSiteLink),
emit(
RegisterConsultantState(
consultant: ConsultantProfileModel(
certificates: certificates,
socialLinks: SocialLinksModel(
vkLink: sanitizeLink(myRequestStatus.educationModel.vkLink),
tgLink: sanitizeLink(myRequestStatus.educationModel.tgLink),
fbLink: sanitizeLink(myRequestStatus.educationModel.fbLink),
instaLink: sanitizeLink(myRequestStatus.educationModel.instaLink),
personalSiteLink: sanitizeLink(myRequestStatus.educationModel.personalSiteLink),
),
hasMedicalEducation: myRequestStatus.educationModel.hasMedicalEducation == 1,
),
hasMedicalEducation: myRequestStatus.educationModel.hasMedicalEducation == 1,
acceptOffer: false,
loading: false,
requestModel: myRequestStatus,
certPhotos: certPhotos,
isUpdating: _update,
error: null,
),
acceptOffer: false,
loading: false,
requestModel: myRequestStatus,
certPhotos: certPhotos,
),
);
);
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
emit(state.copyWith(loading: false, error: e.toString()));
}
}
Future<void> disableLoading() async {
final certPhotos = await _fetchVisibleCertPhotos();
emit(state.copyWith(loading: false, certPhotos: certPhotos));
try {
final certPhotos = await consultantRepository.getConsultantCertPhotos();
emit(state.copyWith(loading: false, certPhotos: certPhotos, error: null));
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
emit(state.copyWith(loading: false, error: e.toString()));
}
}
/// Повторная загрузка после [DataErrorWidget], аналогично [AboutDietCubit.getDiet].
Future<void> reloadData(String? slug) async {
emit(state.copyWith(loading: true, error: null));
if (slug != null) {
await getMyRequestStatus(slug);
} else {
await disableLoading();
}
}
void updateConsultantProfileModel(ConsultantProfileModel consultant, [bool acceptOffer = false]) {
emit(state.copyWith(consultant: consultant, acceptOffer: acceptOffer, loading: false));
emit(state.copyWith(consultant: consultant, acceptOffer: acceptOffer, loading: false, error: null));
}
Future<void> registerConsultant() async {
await consultantRepository.registerConsultant(consultant: state.consultant, update: _update);
emit(state.copyWith(error: null));
try {
if (state.newAddedPhotos.isNotEmpty) {
await consultantRepository.storeConsultantCertPhotos(files: state.newAddedPhotos);
}
if (state.deletedPhotoIds.isNotEmpty) {
for (final int id in state.deletedPhotoIds) {
await consultantRepository.deletePhoto(id);
}
}
await consultantRepository.registerConsultant(consultant: state.consultant, update: _update);
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
emit(state.copyWith(error: e.toString()));
}
}
Future<void> uploadCertPhotos(List<File> files) async {
Future<void> addCertPhotos(List<File> files) async {
if (files.isEmpty) {
return;
}
final compressedFiles = await ImageCompressUtility.compressFilesList(files);
final uploaded = await consultantRepository.storeConsultantCertPhotos(files: compressedFiles);
emit(state.copyWith(certPhotos: _onlyVisible(uploaded)));
}
Future<void> removeCertPhoto(int id) async {
await consultantRepository.hideConsultantCertPhoto(id);
emit(state.copyWith(certPhotos: state.certPhotos.where((photo) => photo.id != id).toList()));
}
Future<List<ConsultantCertPhotoModel>> _fetchVisibleCertPhotos() async {
try {
final photos = await consultantRepository.getConsultantCertPhotos();
return _onlyVisible(photos);
} catch (e) {
return [];
final compressedFiles = await ImageCompressUtility.compressFilesList(files);
emit(state.copyWith(newAddedPhotos: [...state.newAddedPhotos, ...compressedFiles], error: null));
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
emit(state.copyWith(error: e.toString()));
}
}
List<ConsultantCertPhotoModel> _onlyVisible(List<ConsultantCertPhotoModel> photos) {
return photos.where((photo) => !photo.isHidden).toList();
Future<void> removeCertPhoto(int? id, String? path) async {
if (id != null) {
emit(state.copyWith(deletedPhotoIds: [...state.deletedPhotoIds, id]));
return;
}
if (path != null) {
emit(state.copyWith(newAddedPhotos: state.newAddedPhotos.where((file) => file.uri.path != path).toList()));
}
}
}

View File

@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_cert_photo_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_profile_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
@@ -11,7 +13,11 @@ abstract class RegisterConsultantState with _$RegisterConsultantState {
required ConsultantProfileModel consultant,
required bool acceptOffer,
required bool loading,
required bool isUpdating,
ConsultationRequestModel? requestModel,
@Default(<ConsultantCertPhotoModel>[]) List<ConsultantCertPhotoModel> certPhotos,
@Default(<File>[]) List<File> newAddedPhotos,
@Default(<int>[]) List<int> deletedPhotoIds,
String? error,
}) = _RegisterConsultantState;
}

View File

@@ -17,18 +17,13 @@ class _BecomeConsultantScreenState extends State<BecomeConsultantScreen> {
title: const Text('Стать консультантом'),
automaticallyImplyLeading: false,
centerTitle: false,
leading: IconButton(
onPressed: () => GoRouter.of(context).pop(),
icon: const Icon(Icons.close),
),
leading: IconButton(onPressed: () => GoRouter.of(context).pop(), icon: const Icon(Icons.close)),
),
body: const Padding(
padding: EdgeInsets.all(16),
child: Text(
'Чтобы стать консультантом необходимо пройти короткую регистрацию. Подготовьте свои дипломы и/или сертификаты в формате jpg, они Вам пригодятся для проверки и подтверждения. Также необходимо будет указать ссылки на Ваши соц. сети и указать тип лица (физ. лицо или юр. лицо). На данный момент доступна регистрация в качестве физ. лица.\n\nВнутри Итвио разница между обычным пользователем и консультантом заключается в дополнительных возможностях последнего создавать публичные диеты, рецепты и планы питания; участвовать в ежемесячных рейтингах.',
style: TextStyle(
fontSize: 16,
),
style: TextStyle(fontSize: 16),
),
),
bottomSheet: _startRegistrationButton(),
@@ -37,24 +32,16 @@ class _BecomeConsultantScreenState extends State<BecomeConsultantScreen> {
InkWell _startRegistrationButton() {
return InkWell(
onTap: () => GoRouter.of(context).pushReplacementNamed('stepperBecomeConsultantScreen'),
onTap: () => GoRouter.of(context).pushReplacementNamed('stepperBecomeConsultantScreen', extra: {'slug': null}),
borderRadius: BorderRadius.circular(20),
child: Ink(
height: 52,
width: MediaQuery.of(context).size.width - 32,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: appTheme.blueA400,
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
child: const Center(
child: Text(
'Начать регистрацию',
style: TextStyle(
color: Colors.white,
height: 1,
fontWeight: FontWeight.w600,
fontSize: 18,
),
style: TextStyle(color: Colors.white, height: 1, fontWeight: FontWeight.w600, fontSize: 18),
),
),
),

View File

@@ -33,7 +33,9 @@ class _PhotoOrScansScreenState extends State<PhotoOrScansScreen> {
const SizedBox(height: 16),
BlocBuilder<RegisterConsultantCubit, RegisterConsultantState>(
builder: (context, state) {
final photos = state.certPhotos;
final photos = state.certPhotos.where((photo) => !state.deletedPhotoIds.contains(photo.id)).toList();
final addedPhotos = state.newAddedPhotos;
final totalPhotosCount = photos.length + addedPhotos.length;
return Column(
children: [
if (isUploading) ...[const LinearProgressIndicator(), const SizedBox(height: 16)],
@@ -47,7 +49,7 @@ class _PhotoOrScansScreenState extends State<PhotoOrScansScreen> {
mainAxisSpacing: 8,
),
itemBuilder: (BuildContext context, int index) {
if (index == photos.length) {
if (index == totalPhotosCount) {
return InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () => _pickAndUploadPhotos(),
@@ -57,10 +59,25 @@ class _PhotoOrScansScreenState extends State<PhotoOrScansScreen> {
),
);
} else {
return _CertPhotoCard(photo: photos[index], allPhotos: photos, onRemove: () => _removePhoto(photos[index].id));
if (index < photos.length) {
final photo = photos[index];
return _CertPhotoCard(
photo: photo,
allPhotos: photos,
onRemove: () => _removePhoto(id: photo.id),
);
} else {
final localIndex = index - photos.length;
final localPhoto = addedPhotos[localIndex];
return _LocalCertPhotoCard(
photo: localPhoto,
allPhotos: addedPhotos,
onRemove: () => _removePhoto(path: localPhoto.uri.path),
);
}
}
},
itemCount: photos.length + 1,
itemCount: totalPhotosCount + 1,
),
],
);
@@ -82,7 +99,7 @@ class _PhotoOrScansScreenState extends State<PhotoOrScansScreen> {
});
try {
// ignore: use_build_context_synchronously
await context.read<RegisterConsultantCubit>().uploadCertPhotos(newImages.map((file) => File(file.path)).toList());
await context.read<RegisterConsultantCubit>().addCertPhotos(newImages.map((file) => File(file.path)).toList());
} finally {
if (mounted) {
setState(() {
@@ -92,8 +109,8 @@ class _PhotoOrScansScreenState extends State<PhotoOrScansScreen> {
}
}
Future<void> _removePhoto(int id) async {
await context.read<RegisterConsultantCubit>().removeCertPhoto(id);
Future<void> _removePhoto({int? id, String? path}) async {
await context.read<RegisterConsultantCubit>().removeCertPhoto(id, path);
}
}
@@ -147,3 +164,61 @@ class _CertPhotoCard extends StatelessWidget {
);
}
}
class _LocalCertPhotoCard extends StatelessWidget {
final File photo;
final List<File> allPhotos;
final VoidCallback onRemove;
const _LocalCertPhotoCard({required this.photo, required this.allPhotos, required this.onRemove});
@override
Widget build(BuildContext context) {
return Hero(
tag: photo.path,
child: Material(
borderRadius: BorderRadius.circular(20),
child: InkWell(
onTap: () {
context.pushTransparentRoute(
rootNavigator: true,
PhotoViewScreen(
imageUrls: allPhotos,
isLocal: true,
index: allPhotos.indexOf(photo),
),
);
},
borderRadius: BorderRadius.circular(20),
child: Stack(
clipBehavior: Clip.none,
children: [
Ink(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(20)),
image: DecorationImage(
fit: BoxFit.cover,
image: FileImage(photo),
),
),
),
Positioned(
right: -12,
top: -12,
child: InkWell(
onTap: onRemove,
customBorder: const CircleBorder(),
child: Ink(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(color: Color(0xFFF1F2F6), shape: BoxShape.circle),
child: const Icon(size: 20, Icons.close, color: Colors.black),
),
),
),
],
),
),
),
);
}
}

View File

@@ -1,6 +1,6 @@
import 'package:eatvio_app/core/utils/image_constant.dart';
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.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/presentation/bloc/register_consultant_cubit/register_consultant_state.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/additional_settings_screen.dart';
@@ -10,6 +10,7 @@ import 'package:eatvio_app/features/consultant/become_consultant/presentation/pa
import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/registration_as_screen.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/social_networks_screen.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -19,8 +20,8 @@ import 'package:lottie/lottie.dart';
class StepperBecomeConsultantScreen extends StatefulWidget {
final String? slug;
const StepperBecomeConsultantScreen({super.key, this.slug});
final bool? isCanceled;
const StepperBecomeConsultantScreen({super.key, this.slug, this.isCanceled});
@override
State<StepperBecomeConsultantScreen> createState() => _StepperBecomeConsultantScreenState();
@@ -49,8 +50,37 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
create: (context) => registrationConsultantCubit,
child: BlocBuilder<RegisterConsultantCubit, RegisterConsultantState>(
builder: (context, state) {
if (state.loading == false) {
if (state.loading) {
return const Scaffold(body: Loader());
}
if (state.error != null) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go('/feed-screen');
},
),
title: Text(
'',
maxLines: 2,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
),
body: DataErrorWidget(
onReload: () => context.read<RegisterConsultantCubit>().reloadData(widget.slug),
),
);
}
return Scaffold(
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: Row(
children: [
@@ -63,25 +93,17 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
highlightElevation: 0,
focusElevation: 0,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
backgroundColor: const Color(0xFFEAF2FD),
onPressed: () async {
setState(() {
pageController.previousPage(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
);
pageController.previousPage(duration: const Duration(milliseconds: 350), curve: Curves.easeInOut);
});
},
label: Icon(
Icons.arrow_back_ios_new_rounded,
color: appTheme.blueA400,
),
label: Icon(Icons.arrow_back_ios_new_rounded, color: appTheme.blueA400),
),
),
if (state.requestModel?.educationModel.requestState == ModerationStatus.canceled)
if (widget.isCanceled == true)
InkWell(
onTap: () {
showDialog(
@@ -94,15 +116,10 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
children: [
for (int i = 0; i < state.requestModel!.canceledModeration!.additional!.length; i++) ...[
Text('• ${state.requestModel!.canceledModeration!.additional![i]}'),
]
],
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
),
);
},
@@ -110,26 +127,18 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
child: Ink(
height: 48,
width: MediaQuery.of(context).size.width * 0.6,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: const Color(0x33FFCC00),
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0x33FFCC00)),
child: const Center(
child: Text(
'Комментарий',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF323F4B),
height: 1,
),
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF323F4B), height: 1),
),
),
),
),
Container(
height: 48,
width: state.requestModel?.educationModel.requestState == ModerationStatus.canceled
width: widget.isCanceled == true
? MediaQuery.of(context).size.width * 0.2
: MediaQuery.of(context).size.width * 0.8,
padding: const EdgeInsets.only(left: 4, right: 16),
@@ -138,18 +147,19 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
backgroundColor: isLoading
? Colors.grey
: state.requestModel?.educationModel.requestState == ModerationStatus.canceled
? const Color(0xFFEAF2FD)
: appTheme.blueA400,
: widget.isCanceled == true
? const Color(0xFFEAF2FD)
: appTheme.blueA400,
onPressed: isLoading
? null
: () async {
if (step == 6) {
if (widget.isCanceled == true) {
return;
}
if (registrationConsultantCubit.state.acceptOffer) {
setState(() {
isLoading = true;
@@ -157,6 +167,9 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
try {
await registrationConsultantCubit.registerConsultant();
if (!context.mounted) return;
if (registrationConsultantCubit.state.error != null) {
return;
}
await showModalBottomSheet(
useRootNavigator: true,
showDragHandle: true,
@@ -170,27 +183,15 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
const Text(
'Спасибо!\nВаши данные отправлены на модерацию',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20,
height: 1,
fontWeight: FontWeight.w600,
),
style: TextStyle(fontSize: 20, height: 1, fontWeight: FontWeight.w600),
),
const SizedBox(height: 16),
Lottie.asset(
'assets/lottie_animations/success.json',
width: 160,
height: 160,
),
Lottie.asset('assets/lottie_animations/success.json', width: 160, height: 160),
const SizedBox(height: 16),
const Text(
'В течении трех рабочих дней Вам придет уведомление о смене статуса',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
height: 1,
fontWeight: FontWeight.w500,
),
style: TextStyle(fontSize: 16, height: 1, fontWeight: FontWeight.w500),
),
const SizedBox(height: 16),
InkWell(
@@ -201,18 +202,11 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
child: Ink(
width: MediaQuery.of(context).size.width - 32,
height: 52,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: appTheme.blueA400,
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
child: const Center(
child: Text(
'Закрыть',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w600,
),
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600),
),
),
),
@@ -223,7 +217,7 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
);
} catch (e) {
if (!context.mounted) return;
showDialog(
showDialog<void>(
context: context,
builder: (context) => AlertDialog(
actionsPadding: const EdgeInsets.symmetric(horizontal: 4),
@@ -231,9 +225,7 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
actions: [
TextButton(
child: const Text('Ок'),
onPressed: () {
Navigator.of(context).pop();
},
onPressed: () => Navigator.of(context).pop(),
),
],
),
@@ -263,24 +255,15 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
} else {
FocusManager.instance.primaryFocus?.unfocus();
setState(() {
pageController.nextPage(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
);
pageController.nextPage(duration: const Duration(milliseconds: 350), curve: Curves.easeInOut);
});
}
},
label: isLoading
? const CircularProgressIndicator()
: state.requestModel?.educationModel.requestState == ModerationStatus.canceled
? Icon(
Icons.arrow_forward_ios_outlined,
color: appTheme.blueA400,
)
: Text(
'Продолжить',
style: theme.textTheme.titleSmall!.copyWith(color: Colors.white),
),
: widget.isCanceled == true
? Icon(Icons.arrow_forward_ios_outlined, color: appTheme.blueA400)
: Text('Продолжить', style: theme.textTheme.titleSmall!.copyWith(color: Colors.white)),
),
),
],
@@ -288,23 +271,11 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
appBar: AppBar(
automaticallyImplyLeading: false,
leading: Builder(
builder: (context) => IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () => GoRouter.of(context).pop(),
),
builder: (context) => IconButton(icon: const Icon(Icons.close_rounded), onPressed: () => GoRouter.of(context).pop()),
),
centerTitle: true,
title: SvgPicture.asset(
ImageConstant.imgLogoEatvio,
height: 48,
fit: BoxFit.contain,
),
actions: [
Padding(
padding: const EdgeInsets.only(right: 16),
child: Text('шаг $step из 6'),
),
],
title: SvgPicture.asset(ImageConstant.imgLogoEatvio, height: 48, fit: BoxFit.contain),
actions: [Padding(padding: const EdgeInsets.only(right: 16), child: Text('шаг $step из 6'))],
bottom: PreferredSize(
preferredSize: Size(MediaQuery.of(context).size.width, 32),
child: Padding(
@@ -312,10 +283,7 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
child: TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
tween: Tween<double>(
begin: 1 / 6,
end: progressValue,
),
tween: Tween<double>(begin: 1 / 6, end: progressValue),
builder: (context, value, _) => LinearProgressIndicator(
value: value,
borderRadius: BorderRadius.circular(16),
@@ -346,11 +314,6 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
],
),
);
} else {
return const Scaffold(
body: Loader(),
);
}
},
),
);

View File

@@ -5,7 +5,6 @@ import 'package:eatvio_app/features/consultant/become_consultant/models/consulta
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/services/consultant_service.dart';
import 'package:http_parser/http_parser.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class ConsultantRepository {
final ConsultantService consultantService;
@@ -13,81 +12,58 @@ class ConsultantRepository {
const ConsultantRepository({required this.consultantService});
Future<void> registerConsultant({required ConsultantProfileModel consultant, bool update = false}) async {
try {
if (update) {
return await consultantService.consultantUpdate(consultantModel: consultant);
} else {
return await consultantService.consultantRegistration(consultantModel: consultant);
}
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
if (update) {
return await consultantService.consultantUpdate(consultantModel: consultant);
} else {
return await consultantService.consultantRegistration(consultantModel: consultant);
}
}
Future<ConsultationRequestModel> getConsultantRequestStatus(String slug) async {
try {
return await consultantService.getConsultantRequestStatus(slug: slug);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return await consultantService.getConsultantRequestStatus(slug: slug);
}
Future<ConsultationRequestModel> getMyRequestStatus(String slug) async {
try {
return await consultantService.getMyRequestStatus(slug: slug);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return await consultantService.getMyRequestStatus(slug: slug);
}
Future<List<ConsultantCertPhotoModel>> getConsultantCertPhotos() async {
try {
return await consultantService.getConsultantCertPhotos();
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return await consultantService.getConsultantCertPhotos();
}
Future<List<ConsultantCertPhotoModel>> storeConsultantCertPhotos({required List<File> files}) async {
try {
final multipartFiles = <MultipartFile>[];
final multipartFiles = <MultipartFile>[];
for (final file in files) {
final fileBytes = await file.readAsBytes();
final multipartFile = MultipartFile.fromBytes(fileBytes, filename: file.path.split('/').last, contentType: MediaType('application', 'octet-stream'));
multipartFiles.add(multipartFile);
}
if (multipartFiles.isEmpty) {
return [];
}
return await consultantService.storeConsultantCertPhotos(photos: multipartFiles);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
for (final file in files) {
final fileBytes = await file.readAsBytes();
final multipartFile = MultipartFile.fromBytes(fileBytes, filename: file.path.split('/').last, contentType: MediaType('application', 'octet-stream'));
multipartFiles.add(multipartFile);
}
if (multipartFiles.isEmpty) {
return [];
}
return await consultantService.storeConsultantCertPhotos(photos: multipartFiles);
}
Future<void> hideConsultantCertPhoto(int id) async {
try {
await consultantService.hideConsultantCertPhoto(payload: {'cert_photo_id': id});
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await consultantService.hideConsultantCertPhoto(payload: {'cert_photo_id': id});
}
Future<void> unHideConsultantCertPhoto(int id) async {
try {
await consultantService.unHideConsultantCertPhoto(payload: {'cert_photo_id': id});
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await consultantService.unHideConsultantCertPhoto(payload: {'cert_photo_id': id});
}
Future<void> deletePhoto(int id) async {
await consultantService.deletePhoto(id: id);
}
}

View File

@@ -40,4 +40,7 @@ abstract class ConsultantService {
@POST('/api/v2/consultant_request/cert_photos/un_hide')
Future<void> unHideConsultantCertPhoto({@Body() required Map<String, dynamic> payload});
@POST('/api/v2/consultant_request/cert_photos/{id}/queue-for-deletion')
Future<void> deletePhoto({@Path() required int id});
}

View File

@@ -5,6 +5,7 @@ import 'package:eatvio_app/features/consultant_rating/repositories/consultant_ra
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';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class ConsultantRatingCubit extends Cubit<ConsultantRatingState> {
final ConsultantRatingRepository repository;
@@ -20,6 +21,7 @@ class ConsultantRatingCubit extends Cubit<ConsultantRatingState> {
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) {
await reportFrom(e, st, this);
emit(state.copyWith(isStatisticsLoading: true, error: e.toString()));
log(e.toString(), stackTrace: st);
}
@@ -31,6 +33,7 @@ class ConsultantRatingCubit extends Cubit<ConsultantRatingState> {
final scoreLogs = await repository.getScoreLogs(date: date);
emit(state.copyWith(isRatingLoading: false, ratings: scoreLogs, error: null));
} catch (e, st) {
await reportFrom(e, st, this);
emit(state.copyWith(isRatingLoading: true, error: e.toString()));
log(e.toString(), stackTrace: st);
}

View File

@@ -1,7 +1,6 @@
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';
import 'package:sentry_flutter/sentry_flutter.dart';
class ConsultantRatingRepository {
final ConsultantRatingService service;
@@ -9,21 +8,13 @@ class ConsultantRatingRepository {
ConsultantRatingRepository(this.service);
Future<StatisticsModel> getConsultantStatistics({String? date}) async {
try {
return await service.getConsultantStatistics(date: date);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return await service.getConsultantStatistics(date: date);
}
Future<List<RatingModel>> getScoreLogs({String? date}) async {
try {
final data = (await service.getScoreLogs(date: date)).data;
return data;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
final data = (await service.getScoreLogs(date: date)).data;
return data;
}
}

View File

@@ -4,6 +4,7 @@ import 'package:eatvio_app/core/enums/base_state.dart';
import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_state.dart';
import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class ConsultationsCubit extends Cubit<ConsultationsState> {
final ConsultationsRepository consultationsRepository;
@@ -14,6 +15,7 @@ class ConsultationsCubit extends Cubit<ConsultationsState> {
final consultations = await consultationsRepository.getConsultations();
emit(state.copyWith(consultations: consultations, state: BaseState.success));
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
emit(state.copyWith(state: BaseState.error));
}

View File

@@ -1,7 +1,6 @@
import 'package:eatvio_app/core/enums/filters_comparison_enum.dart';
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
import 'package:eatvio_app/features/consultations/services/consultation_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/models/consultant_finance_model.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/models/statistic_model.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/models/user_purchases_model.dart';
@@ -11,13 +10,8 @@ class ConsultationsRepository {
ConsultationsRepository({required this.consultationService});
Future<List<ConsultationModel>> getConsultations() async {
try {
final consultations = await consultationService.getConsultations();
return consultations.data;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
final consultations = await consultationService.getConsultations();
return consultations.data;
}
Future<List<ConsultationModel>> getFilteredConsultations({
@@ -26,44 +20,24 @@ class ConsultationsRepository {
FiltersComparisonEnum? stateComparison,
FiltersComparisonEnum? dateComparison,
}) async {
try {
final model = await consultationService.getFilteredConsultations(
state: state,
date: date,
stateComparison: stateComparison,
dateComparison: dateComparison,
);
return model.data;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
final model = await consultationService.getFilteredConsultations(
state: state,
date: date,
stateComparison: stateComparison,
dateComparison: dateComparison,
);
return model.data;
}
Future<StatisticModel> getStatistic() async {
try {
return await consultationService.getStatistics();
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return consultationService.getStatistics();
}
Future<List<ConsultantFinanceModel>> getConsultantFinances(String? date, FiltersComparisonEnum? dateComparison) async {
try {
return await consultationService.getConsultantFinances(date, dateComparison);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return consultationService.getConsultantFinances(date, dateComparison);
}
Future<List<UserPurchasesModel>> getUserPurchases(String? date, FiltersComparisonEnum? dateComparison) async {
try {
return await consultationService.getUserPurchases(date, dateComparison);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return consultationService.getUserPurchases(date, dateComparison);
}
}

View File

@@ -103,7 +103,7 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
if (state.published) {
Future.delayed(const Duration(milliseconds: 500), () {
if (!context.mounted) return;
context.pop();
context.pop(true);
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
@@ -113,7 +113,7 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
} else if (state.drafted) {
Future.delayed(const Duration(milliseconds: 500), () {
if (!context.mounted) return;
context.pop();
context.pop(true);
showBottomSheetWithBlur(
context: context,
child: SuccessPublishedDietOrPlanBottomSheet(title: state.dietData.name ?? 'Диета', isDrafted: true),
@@ -231,7 +231,7 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
if (dietState == DietState.draft) {
Future.delayed(const Duration(milliseconds: 500), () {
if (!context.mounted) return;
context.pop();
context.pop(true);
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
@@ -241,7 +241,7 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
} else {
Future.delayed(const Duration(milliseconds: 500), () {
if (!context.mounted) return;
context.pop();
context.pop(true);
showBottomSheetWithBlur(
context: context,
child: SuccessPublishedDietOrPlanBottomSheet(title: state.dietData.name ?? 'Диета', isDrafted: true),
@@ -304,7 +304,7 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
} else {
Future.delayed(const Duration(milliseconds: 500), () {
if (!context.mounted) return;
context.pop();
context.pop(true);
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,

View File

@@ -13,9 +13,9 @@ import 'package:eatvio_app/features/glossary/models/glossary_model.dart';
import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:http/http.dart' as http;
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:path_provider/path_provider.dart';
class CreateDietCubit extends Cubit<CreateDietState> {
@@ -29,9 +29,10 @@ class CreateDietCubit extends Cubit<CreateDietState> {
Future<void> setInitialData(Diet? diet) async {
if (diet == null) return;
emit(state.copyWith(isLoading: true, isDietBeingEdited: true));
dietId = diet.id;
final dailyScheduleWithNutrition = await createDietRepository.getDietPortionsNutrition(diet.id);
try {
emit(state.copyWith(isLoading: true, isDietBeingEdited: true));
dietId = diet.id;
final dailyScheduleWithNutrition = await createDietRepository.getDietPortionsNutrition(diet.id);
final countOfDays = diet.duration;
final List<List<List<DailySchedule>>> dailySchedule = List.generate(countOfDays, (index) => [[], [], [], [], []]);
@@ -66,7 +67,11 @@ class CreateDietCubit extends Cubit<CreateDietState> {
imageUrl: appConfig.buildType == AppBuildType.tests ? null : imageUrl,
),
);
emit(state.copyWith(isLoading: false, isDietBeingEdited: false, dietState: diet.state, durationIndex: durationValues.indexOf(diet.duration)));
emit(state.copyWith(isLoading: false, isDietBeingEdited: false, dietState: diet.state, durationIndex: durationValues.indexOf(diet.duration)));
} catch (e, st) {
await reportFrom(e, st, this);
emit(state.copyWith(isLoading: false, isDietBeingEdited: false));
}
}
void setDetails(String text) {
@@ -112,7 +117,12 @@ class CreateDietCubit extends Cubit<CreateDietState> {
}
Future<List<GlossaryModel>> getGlossaryListByQuery(String query) async {
return await glossaryRepository.getGlossaryListByQuery(query);
try {
return await glossaryRepository.getGlossaryListByQuery(query);
} catch (e, st) {
await reportFrom(e, st, this);
return [];
}
}
void setImage(XFile? image, {String? imageUrl}) {
@@ -204,7 +214,8 @@ class CreateDietCubit extends Cubit<CreateDietState> {
Future<Diet?> getDietForPreview() async {
if (!_isFormValidForDraft()) return null;
final authorProfile = await profileRepository.getMyProfile();
try {
final authorProfile = await profileRepository.getMyProfile();
final imageBytes = (await state.dietData.image?.readAsBytes()) ?? <int>[];
String image = '';
if (imageBytes.isNotEmpty && imageBytes.length <= _maxImageBytesForBase64) {
@@ -297,19 +308,26 @@ class CreateDietCubit extends Cubit<CreateDietState> {
isFavorite: false,
dailyNutrition: dailyNutrition,
);
return diet;
return diet;
} catch (e, st) {
await reportFrom(e, st, this);
return null;
}
}
Future<File> downloadImageAsFile(String imageUrl) async {
final response = await http.get(Uri.parse(imageUrl));
if (response.statusCode == 200) {
Future<File?> downloadImageAsFile(String imageUrl) async {
try {
final response = await http.get(Uri.parse(imageUrl));
if (response.statusCode != 200) {
throw Exception('Не удалось загрузить изображение');
}
final tempDir = await getTemporaryDirectory();
final filePath = '${tempDir.path}/${imageUrl.split('/').last}';
final file = File(filePath);
return await file.writeAsBytes(response.bodyBytes);
} else {
throw Exception('Не удалось загрузить изображение');
return file.writeAsBytes(response.bodyBytes);
} catch (e, st) {
await reportFrom(e, st, this);
return null;
}
}
@@ -353,7 +371,7 @@ class CreateDietCubit extends Cubit<CreateDietState> {
emit(state.copyWith(published: dietState != DietState.draft, drafted: dietState == DietState.draft));
return null;
} catch (e, st) {
await Sentry.captureException(e, stackTrace: st);
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
emit(state.copyWith(isDietPublishing: false));
}
@@ -391,7 +409,7 @@ class CreateDietCubit extends Cubit<CreateDietState> {
emit(state.copyWith(isDietBeingEdited: false));
return null;
} catch (e, st) {
await Sentry.captureException(e, stackTrace: st);
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
emit(state.copyWith(isDietBeingEdited: false));
return 'error';

View File

@@ -2,7 +2,6 @@ import 'dart:io';
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/create_diet/services/create_diet_service.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class CreateDietRepository {
final CreateDietService createDietService;
@@ -62,48 +61,40 @@ class CreateDietRepository {
dietMap.addAll({'name': name, 'duration': duration});
final formData = FormData.fromMap(dietMap);
try {
switch (state) {
case DietState.active:
return await createDietService.createPublishedDiet(formData: formData);
case DietState.draft:
return await createDietService.createDraftedDiet(formData: formData);
case DietState.personal:
return await createDietService.createPersonalDiet(formData: formData);
}
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
switch (state) {
case DietState.active:
return await createDietService.createPublishedDiet(formData: formData);
case DietState.draft:
return await createDietService.createDraftedDiet(formData: formData);
case DietState.personal:
return await createDietService.createPersonalDiet(formData: formData);
}
}
Future<Map<int, Map<int, Map<int, DailySchedule>>>> getDietPortionsNutrition(int dietId) async {
try {
final response = await createDietService.getDietPortionsNutrition(diet: dietId);
final data = response.data;
final dailySchedule = (data as Map<String, dynamic>).map((key, value) {
return MapEntry(
int.parse(key),
(value as Map<String, dynamic>).map((key, value) {
return MapEntry(
int.parse(key),
(value as Map<String, dynamic>).map((key, value) {
final portionMap = Map<String, dynamic>.from(value as Map<String, dynamic>);
final pnvid = portionMap['product_nutritional_value_id'];
if (pnvid is String) {
portionMap['product_nutritional_value_id'] = int.tryParse(pnvid);
}
return MapEntry(int.parse(key), DailySchedule.fromJson(portionMap));
}),
);
}),
);
});
return dailySchedule;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
final response = await createDietService.getDietPortionsNutrition(diet: dietId);
final data = response.data;
final dailySchedule = (data as Map<String, dynamic>).map((key, value) {
return MapEntry(
int.parse(key),
(value as Map<String, dynamic>).map((key, value) {
return MapEntry(
int.parse(key),
(value as Map<String, dynamic>).map((key, value) {
final portionMap = Map<String, dynamic>.from(value as Map<String, dynamic>);
final pnvid = portionMap['product_nutritional_value_id'];
if (pnvid is String) {
portionMap['product_nutritional_value_id'] = int.tryParse(pnvid);
}
return MapEntry(int.parse(key), DailySchedule.fromJson(portionMap));
}),
);
}),
);
});
return dailySchedule;
}
Future<int> updateDiet({
@@ -161,23 +152,16 @@ class CreateDietRepository {
}
final formData = FormData.fromMap(dietMap);
try {
switch (state) {
case DietState.active:
final response = await createDietService.updatePublished(diet: dietId, formData: formData);
return response.response.statusCode ?? 0;
case DietState.draft:
final response = await createDietService.updateDrafted(diet: dietId, formData: formData);
return response.response.statusCode ?? 0;
case DietState.personal:
final response = await createDietService.updatePersonal(diet: dietId, formData: formData);
return response.response.statusCode ?? 0;
}
} on DioException catch (e) {
return e.response?.statusCode ?? 0;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
switch (state) {
case DietState.active:
final response = await createDietService.updatePublished(diet: dietId, formData: formData);
return response.response.statusCode ?? 0;
case DietState.draft:
final response = await createDietService.updateDrafted(diet: dietId, formData: formData);
return response.response.statusCode ?? 0;
case DietState.personal:
final response = await createDietService.updatePersonal(diet: dietId, formData: formData);
return response.response.statusCode ?? 0;
}
}
}

View File

@@ -7,6 +7,7 @@ import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_state.dart';
import 'package:eatvio_app/features/create_plan/repositories/create_plan_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
class CreatePlanCubit extends Cubit<CreatePlanState> {
@@ -457,7 +458,8 @@ class CreatePlanCubit extends Cubit<CreatePlanState> {
await createPlanRepository.createPlan(model: planMap);
emit(state.copyWith(isLoading: false, planCreated: true));
}
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
emit(state.copyWith(isLoading: false, planCreated: true));
}

View File

@@ -1,6 +1,5 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/create_plan/services/create_plan_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class CreatePlanRepository {
final CreatePlanService createPlanService;
@@ -10,13 +9,9 @@ class CreatePlanRepository {
Future<void> createPlan({
required Map<String, dynamic> model,
}) async {
try {
final formData = FormData.fromMap(model);
return await createPlanService.createPlan(formData: formData);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
final formData = FormData.fromMap(model);
return await createPlanService.createPlan(formData: formData);
}
}

View File

@@ -11,7 +11,7 @@ import 'package:eatvio_app/features/feed/models/recipes_model.dart';
import 'package:eatvio_app/features/glossary/models/glossary_model.dart';
import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
import 'package:http/http.dart' as http;
import 'package:path_provider/path_provider.dart';
@@ -166,7 +166,7 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
await recipeRepository.publishRecipe(model, update, slug);
return true;
} catch (e, st) {
await Sentry.captureException(e, stackTrace: st);
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
return false;
}
@@ -179,7 +179,7 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
emit(state.copyWith(drafted: true));
return true;
} catch (e, st) {
await Sentry.captureException(e, stackTrace: st);
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
return false;
}
@@ -204,16 +204,19 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
));
}
Future<File> downloadImageAsFile(String imageUrl) async {
final response = await http.get(Uri.parse(imageUrl));
if (response.statusCode == 200) {
Future<File?> downloadImageAsFile(String imageUrl) async {
try {
final response = await http.get(Uri.parse(imageUrl));
if (response.statusCode != 200) {
throw Exception('Не удалось загрузить изображение');
}
final tempDir = await getTemporaryDirectory();
final filePath = '${tempDir.path}/${imageUrl.split('/').last}';
final file = File(filePath);
return await file.writeAsBytes(response.bodyBytes);
} else {
throw Exception('Не удалось загрузить изображение');
return file.writeAsBytes(response.bodyBytes);
} catch (e, st) {
await reportFrom(e, st, this);
return null;
}
}
@@ -222,44 +225,53 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
}
Future<void> recipeToEntity(Recipe recipe) async {
final file = await downloadImageAsFile('${sl<AppConfig>().baseUrl}/storage/${recipe.image}');
try {
final file = await downloadImageAsFile('${sl<AppConfig>().baseUrl}/storage/${recipe.image}');
if (file == null) {
disableLoading();
return;
}
final List<StepEntity> steps = await Future.wait(
recipe.steps!.map(
(e) async => StepEntity(
description: e.description,
image: e.image.isEmpty ? null : await downloadImageAsFile('${sl<AppConfig>().baseUrl}/storage/${e.image}'),
final List<StepEntity> steps = await Future.wait(
recipe.steps!.map(
(e) async => StepEntity(
description: e.description,
image: e.image.isEmpty ? null : await downloadImageAsFile('${sl<AppConfig>().baseUrl}/storage/${e.image}'),
),
),
),
);
emit(
CreateRecipeState(
entity: RecipeCreateEntity(
title: recipe.title,
typeOfRecipe: TypeOfRecipe.fromApiType(recipe.type!),
countOfPortions: recipe.servings,
energyInOnePortion: recipe.calories,
totalCookHours: recipe.cookingTime ~/ 60,
totalCookMinutes: recipe.cookingTime % 60,
activeCookHours: recipe.activeCookingTime ~/ 60,
activeCookMinutes: recipe.activeCookingTime % 60,
description: recipe.description,
image: file,
videoUrl: recipe.videoUrl,
ingredients: recipe.ingredients!
.map((e) => i.Ingredient(
name: e.name,
unit: e.unit!,
portion: e.portion,
productId: e.productId,
))
.toList(),
steps: steps,
tags: recipe.tags.map((e) => (e.name)).toList(),
);
emit(
CreateRecipeState(
entity: RecipeCreateEntity(
title: recipe.title,
typeOfRecipe: TypeOfRecipe.fromApiType(recipe.type!),
countOfPortions: recipe.servings,
energyInOnePortion: recipe.calories,
totalCookHours: recipe.cookingTime ~/ 60,
totalCookMinutes: recipe.cookingTime % 60,
activeCookHours: recipe.activeCookingTime ~/ 60,
activeCookMinutes: recipe.activeCookingTime % 60,
description: recipe.description,
image: file,
videoUrl: recipe.videoUrl,
ingredients: recipe.ingredients!
.map((e) => i.Ingredient(
name: e.name,
unit: e.unit!,
portion: e.portion,
productId: e.productId,
))
.toList(),
steps: steps,
tags: recipe.tags.map((e) => (e.name)).toList(),
),
loading: false,
),
loading: false,
),
);
);
} catch (e, st) {
await reportFrom(e, st, this);
disableLoading();
}
}
bool validateAll() {
@@ -376,13 +388,18 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
await recipeRepository.deleteRecipe(slug);
return true;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
await reportFrom(e, s, this);
return false;
}
}
Future<List<GlossaryModel>> getGlossaryListByQuery(String query) async {
return await glossaryRepository.getGlossaryListByQuery(query);
try {
return await glossaryRepository.getGlossaryListByQuery(query);
} catch (e, st) {
await reportFrom(e, st, this);
return [];
}
}
bool validateSteps() {

View File

@@ -109,7 +109,7 @@ class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
if (isSuccess) {
Future.delayed(const Duration(milliseconds: 500), () {
if (!context.mounted) return;
context.pop();
context.pop('active');
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
@@ -157,7 +157,7 @@ class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
});
if (isSuccess) {
if (!context.mounted) return;
context.pop();
context.pop('drafted');
} else {
if (!context.mounted) return;
showErrorDialog(context, title: 'Не удалось сохранить черновик');

View File

@@ -7,7 +7,6 @@ import 'package:eatvio_app/features/create_recipe/models/recipe_model.dart';
import 'package:eatvio_app/features/create_recipe/models/recipe_tag_model.dart';
import 'package:eatvio_app/features/create_recipe/models/step.dart';
import 'package:eatvio_app/features/create_recipe/services/recipe_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:eatvio_app/features/create_recipe/models/product.dart';
class RecipeRepository {
@@ -16,12 +15,8 @@ class RecipeRepository {
RecipeRepository({required this.recipeService});
Future<Map<String, List<Product>>> inputProduct(String name) async {
try {
return await recipeService.getInputProduct(name);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return await recipeService.getInputProduct(name);
}
Future<List> _buildStepsMap(List<Step?>? steps) async {
@@ -110,67 +105,43 @@ class RecipeRepository {
}
Future<void> publishRecipe(RecipeModel recipe, [bool update = false, String? slug]) async {
try {
final formData = await _buildPublishFormData(recipe);
if (update) {
await recipeService.updateRecipe(slug: slug!, formData: formData);
} else {
await recipeService.publishRecipe(formData: formData);
}
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
final formData = await _buildPublishFormData(recipe);
if (update) {
await recipeService.updateRecipe(slug: slug!, formData: formData);
} else {
await recipeService.publishRecipe(formData: formData);
}
}
Future<void> saveAsDraft(RecipeModel recipe, [bool update = false, String? slug]) async {
try {
final formData = await _buildDraftFormData(recipe);
log(formData.fields.toString());
if (update) {
await recipeService.updateRecipe(slug: slug!, formData: formData);
} else {
await recipeService.createDraftedRecipe(formData: formData);
}
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
final formData = await _buildDraftFormData(recipe);
log(formData.fields.toString());
if (update) {
await recipeService.updateRecipe(slug: slug!, formData: formData);
} else {
await recipeService.createDraftedRecipe(formData: formData);
}
}
Future<void> draftExistingRecipe(int id) async {
try {
await recipeService.draftRecipe(id: id);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await recipeService.draftRecipe(id: id);
}
Future<RecipeModel> getRecipe(String recipeID) async {
try {
return await recipeService.getRecipe(recipeID);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return await recipeService.getRecipe(recipeID);
}
Future<List<RecipeTag>> getRecipesTags() async {
try {
return (await recipeService.getTags()).data;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return (await recipeService.getTags()).data;
}
Future<void> deleteRecipe(String slug) async {
try {
return await recipeService.deleteRecipe(slug: slug);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return await recipeService.deleteRecipe(slug: slug);
}
}

View File

@@ -240,7 +240,7 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
} else {
platform = 'ios';
}
await dio.post('/api/profile/notification/token/register', data: {'token': await FirebaseApi().getToken(), 'platform': platform});
await dio.post('/api/profile/notification/token/register', data: {'token': await sl<FirebaseApi>().getToken(), 'platform': platform});
showInfoToast('Токен добавлен');
} catch (e) {
showInfoToast('Не удалось добавть токен');
@@ -253,7 +253,7 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
buttonStyle: const ButtonStyle(),
onPressed: () async {
try {
final token = await FirebaseApi().getToken();
final token = await sl<FirebaseApi>().getToken();
if (token != null) {
Clipboard.setData(ClipboardData(text: token));
showInfoToast('Токен скопирован');
@@ -278,7 +278,7 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
} else {
platform = 'ios';
}
await dio.post('/api/profile/notification/token/unregister', data: {'token': await FirebaseApi().getToken(), 'platform': platform});
await dio.post('/api/profile/notification/token/unregister', data: {'token': await sl<FirebaseApi>().getToken(), 'platform': platform});
showInfoToast('Токен удалён');
} catch (e) {
showInfoToast('Не удалось удалить токен');

View File

@@ -36,7 +36,6 @@ import 'package:eatvio_app/features/recipe/services/my_recipes_service.dart';
import 'package:eatvio_app/features/registration/services/registration_service.dart';
import 'package:eatvio_app/features/settings/services/settings_service.dart';
import 'package:eatvio_app/core/env_config.dart';
class TestResult {
final bool success;
final String? error;
@@ -57,7 +56,9 @@ class CreateRecipeTask implements TestCases {
Future<TestResult> run({required String baseUrl}) async {
try {
final dio = sl<Dio>();
final recipeRepository = RecipeRepository(recipeService: CreateRecipeService(dio, baseUrl: baseUrl));
final recipeRepository = RecipeRepository(
recipeService: CreateRecipeService(dio, baseUrl: baseUrl),
);
await recipeRepository.publishRecipe(
const RecipeModel(
title: 'Title',

View File

@@ -3,6 +3,7 @@ import 'package:eatvio_app/features/diary/models/consultant_comment_model.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_state.dart';
import 'package:eatvio_app/features/diary/repositories/diary_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class ConsultantCommentsCubit extends Cubit<ConsultantCommentsState> {
final DiaryRepository diaryRepository;
@@ -74,6 +75,7 @@ class ConsultantCommentsCubit extends Cubit<ConsultantCommentsState> {
}
await diaryRepository.commentUserDay(diaryId: diaryId, commentModel: state.commentsModel);
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
}
}

View File

@@ -19,8 +19,15 @@ import 'package:eatvio_app/features/glossary/repositories/glossary_repository.da
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:eatvio_app/features/settings/repositories/service_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class DiaryCubit extends Cubit<DiaryState> {
static const NutritionModel _zeroNutritionModel = NutritionModel(energy: 0, proteins: 0, fats: 0, carbohydrates: 0);
static const NutritionDataModel _fallbackNutrition = NutritionDataModel(
dailyNutritionIntake: _zeroNutritionModel,
dailyNutritionRecommendation: _zeroNutritionModel,
);
late final DateTime today;
late DateTime selectedDay;
int selectedMeal = 1;
@@ -47,6 +54,7 @@ class DiaryCubit extends Cubit<DiaryState> {
}
String comment = '';
bool commentNeedsToBeFilled = false;
void setInitialDate(DateTime? date) {
if (date == null) return;
@@ -68,7 +76,7 @@ class DiaryCubit extends Cubit<DiaryState> {
return canSelect;
}
Future<bool> fetchDiaryV2({bool needLoading = false, int? diaryId, DateTime? date}) async {
Future<bool> fetchDiaryV2({bool needLoading = false, int? diaryId}) async {
final currentState = state;
try {
if (needLoading) {
@@ -101,6 +109,7 @@ class DiaryCubit extends Cubit<DiaryState> {
emit(currentState);
return false;
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
emit(DiaryState.error(errorMessage: e.toString()));
return false;
@@ -114,7 +123,8 @@ class DiaryCubit extends Cubit<DiaryState> {
if (diaryEntity != null) {
setDiary(diaryDay: diaryEntity, diaryId: diaryId);
}
} catch (e) {
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
}
}
@@ -125,6 +135,7 @@ class DiaryCubit extends Cubit<DiaryState> {
final diaryDay = await diaryRepository.getDiaryDayV2(day: selectedDay.toISO8601WithTimeZoneOffset());
return diaryDay;
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
if (e is DioException) {
if (e.response?.statusCode == 422) {
@@ -151,8 +162,9 @@ class DiaryCubit extends Cubit<DiaryState> {
return false;
}
emit(const DiaryState.loading());
if (diaryDay.diet?.id != null && diaryDay.dayOfChosenDiet != null) {
final diet = await dietRepository.getDiet((diaryDay.diet?.id)!);
try {
if (diaryDay.diet?.id != null && diaryDay.dayOfChosenDiet != null) {
final diet = await dietRepository.getDiet((diaryDay.diet?.id)!);
final meals = diet.dailySchedule[diaryDay.dayOfChosenDiet];
final firstMealPortions = meals?[1]?.values.map((elem) {
return PortionModel(
@@ -220,9 +232,14 @@ class DiaryCubit extends Cubit<DiaryState> {
selectedMeal: selectedMeal,
),
);
return true;
return true;
}
return false;
} catch (e, st) {
await reportFrom(e, st, this);
emit(DiaryState.error(errorMessage: e.toString()));
return false;
}
return false;
}
Future<void> setDiary({int? diaryId, required DiaryDayEntity diaryDay}) async {
@@ -278,18 +295,31 @@ class DiaryCubit extends Cubit<DiaryState> {
),
);
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
emit(DiaryState.error(errorMessage: e.toString()));
}
}
Future<void> finishDiaryDay({required int diaryId, required String wakeUp, required String retire}) async {
await diaryRepository.finishDiaryDay(diaryId: diaryId, wakeUp: wakeUp, retire: retire, comment: comment.isEmpty ? null : comment);
await updateCurrentDiary();
Future<bool> finishDiaryDay({required int diaryId, required String wakeUp, required String retire}) async {
try {
await diaryRepository.finishDiaryDay(diaryId: diaryId, wakeUp: wakeUp, retire: retire, comment: comment.isEmpty ? null : comment);
await updateCurrentDiary();
return true;
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
return false;
}
}
Future<NutritionDataModel> getNutrition(int index) async {
return (await diaryRepository.getNutritionData(index));
try {
return (await diaryRepository.getNutritionData(index));
} catch (e, st) {
await reportFrom(e, st, this);
return _fallbackNutrition;
}
}
Future<void> addProductToMeal({
@@ -301,15 +331,23 @@ class DiaryCubit extends Cubit<DiaryState> {
int? productNutritionalValueId,
}) async {
emit((state as DiaryStateLoaded).copyWith(isLoadingAddingProductInMeal: true));
await diaryRepository.addPortionToMeal(
diaryId: diaryId,
mealId: mealId,
productName: productName,
portion: portion,
quantity: quantity,
productNutritionalValueId: productNutritionalValueId,
);
await updateCurrentDiary();
try {
await diaryRepository.addPortionToMeal(
diaryId: diaryId,
mealId: mealId,
productName: productName,
portion: portion,
quantity: quantity,
productNutritionalValueId: productNutritionalValueId,
);
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
final s = state;
if (s is DiaryStateLoaded) {
emit(s.copyWith(isLoadingAddingProductInMeal: false));
}
}
}
Future<void> updateProductInMeal({
@@ -322,62 +360,95 @@ class DiaryCubit extends Cubit<DiaryState> {
int? productNutritionalValueId,
required PortionState portionState,
}) async {
if (portionState == PortionState.filledDiet || portionState == PortionState.eatenFromDiet) {
await diaryRepository.addPortionToMeal(
diaryId: diaryId,
mealId: mealId,
productName: productName,
portion: portion,
quantity: quantity,
productNutritionalValueId: productNutritionalValueId,
);
} else {
await diaryRepository.updateProductInMeal(
diaryId: diaryId,
mealId: mealId,
portionId: portionId,
productName: productName,
portion: portion,
quantity: quantity,
productNutritionalValueId: productNutritionalValueId,
portionState: portionState,
);
try {
if (portionState == PortionState.filledDiet || portionState == PortionState.eatenFromDiet) {
await diaryRepository.addPortionToMeal(
diaryId: diaryId,
mealId: mealId,
productName: productName,
portion: portion,
quantity: quantity,
productNutritionalValueId: productNutritionalValueId,
);
} else {
await diaryRepository.updateProductInMeal(
diaryId: diaryId,
mealId: mealId,
portionId: portionId,
productName: productName,
portion: portion,
quantity: quantity,
productNutritionalValueId: productNutritionalValueId,
portionState: portionState,
);
}
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
}
await updateCurrentDiary();
}
Future<void> markEaten({required int diaryId, required int mealId, required int portionId}) async {
await diaryRepository.markEaten(diaryId: diaryId, mealId: mealId, portionId: portionId);
await updateCurrentDiary();
try {
await diaryRepository.markEaten(diaryId: diaryId, mealId: mealId, portionId: portionId);
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<void> unmarkEaten({required int diaryId, required int mealId, required int portionId}) async {
await diaryRepository.unmarkEaten(diaryId: diaryId, mealId: mealId, portionId: portionId);
await updateCurrentDiary();
try {
await diaryRepository.unmarkEaten(diaryId: diaryId, mealId: mealId, portionId: portionId);
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<void> restore({required int diaryId, required int mealId, required int portionId}) async {
await diaryRepository.restore(diaryId: diaryId, mealId: mealId, portionId: portionId);
await updateCurrentDiary();
try {
await diaryRepository.restore(diaryId: diaryId, mealId: mealId, portionId: portionId);
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<void> deleteProductFromMeal({required int diaryId, required int mealId, required int portionId}) async {
await diaryRepository.deleteProductFromMeal(diaryId: diaryId, mealId: mealId, portionId: portionId);
await updateCurrentDiary();
try {
await diaryRepository.deleteProductFromMeal(diaryId: diaryId, mealId: mealId, portionId: portionId);
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<List<GlossaryModel>> getGlossaryListByQuery(String query) async {
return await glossaryRepository.getGlossaryListByQuery(query);
try {
return await glossaryRepository.getGlossaryListByQuery(query);
} catch (e, st) {
await reportFrom(e, st, this);
return [];
}
}
Future<void> uploadPhotos({required List<File> files, required int diaryId}) async {
await diaryRepository.uploadPhotos(files: await ImageCompressUtility.compressFilesList(files), diaryId: diaryId, mealId: selectedMeal);
await updateCurrentDiary();
try {
await diaryRepository.uploadPhotos(files: await ImageCompressUtility.compressFilesList(files), diaryId: diaryId, mealId: selectedMeal);
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<void> deletePhoto({required DiaryPhotoModel photo, required int diaryId}) async {
await diaryRepository.deletePhoto(photo: photo, diaryId: diaryId, mealId: selectedMeal);
await updateCurrentDiary();
try {
await diaryRepository.deletePhoto(photo: photo, diaryId: diaryId, mealId: selectedMeal);
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<GlossaryModerationModel?> sendProductToModeration({
@@ -390,38 +461,60 @@ class DiaryCubit extends Cubit<DiaryState> {
try {
return await diaryRepository.sendProductToModeration(name: name, energy: energy, proteins: proteins, fats: fats, carbohydrates: carbohydrates);
} catch (e, stackTrace) {
await reportFrom(e, stackTrace, this);
log(e.toString(), stackTrace: stackTrace);
}
return null;
}
Future<void> fillTime({required int diaryId, required String time}) async {
await diaryRepository.fillTime(diaryId: diaryId, mealId: selectedMeal, time: time);
await updateCurrentDiary();
try {
await diaryRepository.fillTime(diaryId: diaryId, mealId: selectedMeal, time: time);
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<void> fillWakeup({required int diaryId, required String time}) async {
await diaryRepository.fillWakeup(diaryId: diaryId, time: time);
await updateCurrentDiary();
try {
await diaryRepository.fillWakeup(diaryId: diaryId, time: time);
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<void> fillRetire({required int diaryId, required String time}) async {
await diaryRepository.fillRetire(diaryId: diaryId, time: time);
await updateCurrentDiary();
try {
await diaryRepository.fillRetire(diaryId: diaryId, time: time);
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<void> fillSatiety({required int diaryId, required int satiety}) async {
await diaryRepository.fillSatiety(diaryId: diaryId, mealId: selectedMeal, satiety: satiety);
await updateCurrentDiary();
try {
await diaryRepository.fillSatiety(diaryId: diaryId, mealId: selectedMeal, satiety: satiety);
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
Future<void> fillMood({required int diaryId, required int mood}) async {
await diaryRepository.fillMood(diaryId: diaryId, mealId: selectedMeal, mood: mood);
await updateCurrentDiary();
try {
await diaryRepository.fillMood(diaryId: diaryId, mealId: selectedMeal, mood: mood);
await updateCurrentDiary();
} catch (e, st) {
await reportFrom(e, st, this);
}
}
void fillComment(String comment) {
this.comment = comment;
commentNeedsToBeFilled = comment.isNotEmpty && comment.length < 3;
}
}

View File

@@ -7,6 +7,7 @@ import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/core/widgets/main_background_bouble.dart';
import 'package:eatvio_app/core/widgets/show_error_dialog.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/diary/presentation/bloc/diary_state.dart';
@@ -39,12 +40,12 @@ class UserDiaryScreen extends StatelessWidget {
final int diaryId;
@override
Widget build(BuildContext context) {
return DiaryScreenContent(diaryId: diaryId,);
return DiaryScreenContent(diaryId: diaryId);
}
}
class DiaryScreenContent extends StatefulWidget {
const DiaryScreenContent({super.key, this.diaryId,this.date});
const DiaryScreenContent({super.key, this.diaryId, this.date});
final int? diaryId;
final DateTime? date;
@override
@@ -134,11 +135,14 @@ class _DiaryScreenContentState extends State<DiaryScreenContent> {
setState(() {
_isDiaryFinishing = true;
});
await context.read<DiaryCubit>().finishDiaryDay(
final isFinished = await context.read<DiaryCubit>().finishDiaryDay(
diaryId: state.diary.id,
wakeUp: state.diary.wakeup.toString(),
retire: state.diary.retire.toString(),
);
if (!isFinished && context.mounted) {
showErrorDialog(context);
}
setState(() {
_isDiaryFinishing = false;
});

View File

@@ -2,6 +2,7 @@ import 'package:easy_debounce/easy_debounce.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_text_form_field.dart';
import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -15,10 +16,14 @@ class CommentForConsultantWidget extends StatefulWidget {
class _CommentForConsultantWidgetState extends State<CommentForConsultantWidget> {
final controller = TextEditingController();
bool _commentNeedsToBeFilled = false;
@override
void initState() {
super.initState();
if (widget.comment != null) controller.text = widget.comment ?? '';
if (widget.comment != null) {
controller.text = widget.comment ?? '';
}
}
@override
@@ -41,13 +46,27 @@ class _CommentForConsultantWidgetState extends State<CommentForConsultantWidget>
CustomTextFormField(
controller: controller,
onChanged: (value) {
EasyDebounce.debounce('client-typing', const Duration(milliseconds: 500), () => context.read<DiaryCubit>().fillComment(value));
EasyDebounce.debounce(
'client-typing',
const Duration(milliseconds: 500),
() => context.read<DiaryCubit>().fillComment(value),
);
if (widget.comment == null) {
setState(() {
_commentNeedsToBeFilled = value.isNotEmpty && value.length < 3;
});
}
},
hasError: _commentNeedsToBeFilled,
hintText: 'Укажите дополнительную информацию',
minLines: 4,
maxLines: 4,
readOnly: widget.comment != null,
),
if (_commentNeedsToBeFilled) ...[
const SizedBox(height: 4),
const ValidationErrorWidget(error: 'Минимум 3 символа'),
],
],
),
),

View File

@@ -4,7 +4,9 @@ import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class FinishDayButtonWidget extends StatefulWidget {
final Future<void> Function() onPressed;
@@ -18,6 +20,14 @@ class FinishDayButtonWidget extends StatefulWidget {
class _FinishDayButtonWidgetState extends State<FinishDayButtonWidget> {
void _onTap() {
if (context.read<DiaryCubit>().commentNeedsToBeFilled) {
showErrorDialog(
context,
title: 'Комментарий слишком короткий',
description: 'Минимум 3 символа в комментарии для консультанта.',
);
return;
}
if (!widget.hasProducts) {
showErrorDialog(
context,

View File

@@ -257,12 +257,17 @@ class _GlossaryModerationRequestModalWidgetState extends State<GlossaryModeratio
const SizedBox(height: 12),
InkWell(
onTap: () async {
final energy = double.tryParse(energyController.text.replaceAll(',', '.')) ?? 0;
final proteins = double.tryParse(proteinsController.text.replaceAll(',', '.')) ?? 0;
final fats = double.tryParse(fatsController.text.replaceAll(',', '.')) ?? 0;
final carbohydrates = double.tryParse(carbohydratesController.text.replaceAll(',', '.')) ?? 0;
glossary = (await widget.diaryCubit.sendProductToModeration(
name: productNameController.text,
energy: double.parse(energyController.text.replaceAll(',', '.')),
proteins: double.parse(proteinsController.text.replaceAll(',', '.')),
fats: double.parse(fatsController.text.replaceAll(',', '.')),
carbohydrates: double.parse(carbohydratesController.text.replaceAll(',', '.')),
energy: energy,
proteins: proteins,
fats: fats,
carbohydrates: carbohydrates,
))?.glossary;
setState(() {
glossaryState = GlossaryState.success;

View File

@@ -22,6 +22,11 @@ class PortionCommentWidget extends StatefulWidget {
class _PortionCommentWidgetState extends State<PortionCommentWidget> {
final controller = TextEditingController();
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
void didUpdateWidget(PortionCommentWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.portionId != oldWidget.portionId) {
@@ -57,17 +62,19 @@ class _PortionCommentWidgetState extends State<PortionCommentWidget> {
hintText: s.typePortionComment,
hasError: hasError,
onChanged: (value) {
final cubit = context.read<ConsultantCommentsCubit>();
EasyDebounce.debounce(
'client-typing',
'portion-comment-${widget.portionId}',
const Duration(milliseconds: 500),
() {
final hasPortionComment = state.commentsModel.portions.any((portion) => portion.id == widget.portionId);
final currentState = cubit.state;
final hasPortionComment = currentState.commentsModel.portions.any((portion) => portion.id == widget.portionId);
if (hasPortionComment) {
final portion = state.commentsModel.portions.firstWhere((portion) => portion.id == widget.portionId);
context.read<ConsultantCommentsCubit>().updatePortionComment(portion.copyWith(comment: value));
final portion = currentState.commentsModel.portions.firstWhere((portion) => portion.id == widget.portionId);
cubit.updatePortionComment(portion.copyWith(comment: value));
return;
}
context.read<ConsultantCommentsCubit>().addPortionComment(PortionCommentModel(id: widget.portionId, comment: value));
cubit.addPortionComment(PortionCommentModel(id: widget.portionId, comment: value));
},
);
},

View File

@@ -1,5 +1,7 @@
import 'dart:async';
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:eatvio_app/features/diary/models/consultant_comment_model.dart';
import 'package:eatvio_app/features/diary/models/consultant_review_model.dart';
import 'package:eatvio_app/features/diary/models/create_day_model.dart';
@@ -9,7 +11,6 @@ import 'package:eatvio_app/features/diary/models/nutrition_model.dart';
import 'package:eatvio_app/features/diary/models/product_moderation_model.dart';
import 'package:eatvio_app/features/diary/services/diary_service.dart';
import 'package:eatvio_app/features/glossary/models/glossary_model.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class DiaryRepository {
final DiaryService diaryService;
@@ -19,134 +20,82 @@ class DiaryRepository {
DiaryRepository({required this.diaryService});
Future<DiaryDayModel> getDiaryByDate(String date) async {
try {
Set<int> ids = {};
final diaryDays = await getDiaryIndexByDate(date);
if (diaryDays.isEmpty) {
throw Exception('No diary entry found for $date');
}
final DiaryDayModel diaryDay = diaryDays.last;
for (var meal in diaryDay.meals) {
for (var portion in meal.portions) {
if (portion.productNutritionalValueId != null) {
ids.add(portion.productNutritionalValueId!);
}
}
}
if (ids.isNotEmpty) {
final glossary = await getGlossariesIndex(ids: ids.toList());
final List<MealModel> updatedMeals = List.from(diaryDay.meals);
for (var mealIndex = 0; mealIndex < updatedMeals.length; mealIndex++) {
final meal = updatedMeals[mealIndex];
final updatedPortions = meal.portions.map((portion) {
if (portion.productNutritionalValueId != null) {
try {
final glossaryItem = glossary.glossariesList.firstWhere((g) => g.id == portion.productNutritionalValueId);
return portion.copyWith(
productCaloriesInfo: ProductCaloriesInfoModel(
id: glossaryItem.id,
productName: glossaryItem.name,
calories: glossaryItem.energyOn100Gr,
proteins: glossaryItem.proteinsOn100Gr,
fats: glossaryItem.fatsOn100Gr,
carbohydrates: glossaryItem.carbohydratesOn100Gr,
energyOnPortion: glossaryItem.energyOnPortion,
proteinsOnPortion: glossaryItem.proteinsOnPortion,
fatsOnPortion: glossaryItem.fatsOnPortion,
carbohydratesOnPortion: glossaryItem.carbohydratesOnPortion,
),
);
} catch (e) {
return portion;
}
}
return portion;
}).toList();
PortionModel _portionWithGlossary(PortionModel portion, List<GlossaryModel> glossaries) {
final id = portion.productNutritionalValueId;
if (id == null) return portion;
final glossaryItem = glossaries.firstWhereOrNull((g) => g.id == id);
if (glossaryItem == null) return portion;
return portion.copyWith(
productCaloriesInfo: ProductCaloriesInfoModel(
id: glossaryItem.id,
productName: glossaryItem.name,
calories: glossaryItem.energyOn100Gr,
proteins: glossaryItem.proteinsOn100Gr,
fats: glossaryItem.fatsOn100Gr,
carbohydrates: glossaryItem.carbohydratesOn100Gr,
energyOnPortion: glossaryItem.energyOnPortion,
proteinsOnPortion: glossaryItem.proteinsOnPortion,
fatsOnPortion: glossaryItem.fatsOnPortion,
carbohydratesOnPortion: glossaryItem.carbohydratesOnPortion,
),
);
}
updatedMeals[mealIndex] = meal.copyWith(portions: updatedPortions);
}
final updatedDiaryDay = diaryDay.copyWith(meals: updatedMeals);
return updatedDiaryDay;
}
return diaryDay;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
Future<DiaryDayModel> getDiaryByDate(String date) async {
final ids = <int>{};
final diaryDays = await getDiaryIndexByDate(date);
if (diaryDays.isEmpty) {
throw Exception('No diary entry found for $date');
}
final DiaryDayModel diaryDay = diaryDays.last;
for (var meal in diaryDay.meals) {
for (var portion in meal.portions) {
if (portion.productNutritionalValueId != null) {
ids.add(portion.productNutritionalValueId!);
}
}
}
if (ids.isEmpty) {
return diaryDay;
}
final glossary = await getGlossariesIndex(ids: ids.toList());
final glossaries = glossary.glossariesList;
final updatedMeals = diaryDay.meals.map((meal) {
final updatedPortions = meal.portions.map((p) => _portionWithGlossary(p, glossaries)).toList();
return meal.copyWith(portions: updatedPortions);
}).toList();
return diaryDay.copyWith(meals: updatedMeals);
}
Future<List<DiaryDayModel>> getDiaryIndexByDate(String date) async {
try {
return (await diaryService.getDiaryIndexByDate(date: date));
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return diaryService.getDiaryIndexByDate(date: date);
}
Future<DiaryDayModel> getDiaryDay(int diaryId) async {
try {
// Align with getDiaryByDate: enrich portions with glossary nutritional info
final diaryWrapper = await diaryService.getDiaryDay(diary: diaryId);
final DiaryDayModel diaryDay = diaryWrapper.diary;
Set<int> ids = {};
for (var meal in diaryDay.meals) {
for (var portion in meal.portions) {
if (portion.productNutritionalValueId != null) {
ids.add(portion.productNutritionalValueId!);
}
final diaryWrapper = await diaryService.getDiaryDay(diary: diaryId);
final DiaryDayModel diaryDay = diaryWrapper.diary;
final ids = <int>{};
for (var meal in diaryDay.meals) {
for (var portion in meal.portions) {
if (portion.productNutritionalValueId != null) {
ids.add(portion.productNutritionalValueId!);
}
}
if (ids.isNotEmpty) {
final glossary = await getGlossariesIndex(ids: ids.toList());
final List<MealModel> updatedMeals = List.from(diaryDay.meals);
for (var mealIndex = 0; mealIndex < updatedMeals.length; mealIndex++) {
final meal = updatedMeals[mealIndex];
final updatedPortions = meal.portions.map((portion) {
if (portion.productNutritionalValueId != null) {
try {
final glossaryItem = glossary.glossariesList.firstWhere((g) => g.id == portion.productNutritionalValueId);
return portion.copyWith(
productCaloriesInfo: ProductCaloriesInfoModel(
id: glossaryItem.id,
productName: glossaryItem.name,
calories: glossaryItem.energyOn100Gr,
proteins: glossaryItem.proteinsOn100Gr,
fats: glossaryItem.fatsOn100Gr,
carbohydrates: glossaryItem.carbohydratesOn100Gr,
energyOnPortion: glossaryItem.energyOnPortion,
proteinsOnPortion: glossaryItem.proteinsOnPortion,
fatsOnPortion: glossaryItem.fatsOnPortion,
carbohydratesOnPortion: glossaryItem.carbohydratesOnPortion,
),
);
} catch (e) {
return portion;
}
}
return portion;
}).toList();
updatedMeals[mealIndex] = meal.copyWith(portions: updatedPortions);
}
final updatedDiaryDay = diaryDay.copyWith(meals: updatedMeals);
return updatedDiaryDay;
}
return diaryDay;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
if (ids.isEmpty) {
return diaryDay;
}
final glossary = await getGlossariesIndex(ids: ids.toList());
final glossaries = glossary.glossariesList;
final updatedMeals = diaryDay.meals.map((meal) {
final updatedPortions = meal.portions.map((p) => _portionWithGlossary(p, glossaries)).toList();
return meal.copyWith(portions: updatedPortions);
}).toList();
return diaryDay.copyWith(meals: updatedMeals);
}
Future<NutritionDataModel> getNutritionData(int index) async {
try {
return await diaryService.getNutrition(diary: index);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return diaryService.getNutrition(diary: index);
}
Future<void> addPortionToMeal({
@@ -157,19 +106,14 @@ class DiaryRepository {
required int quantity,
int? productNutritionalValueId,
}) async {
try {
return await diaryService.addProductToMeal(
diary: diaryId,
typeNo: mealId,
productName: productName,
portion: portion,
quantity: quantity,
productNutritionalValueId: productNutritionalValueId,
);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return diaryService.addProductToMeal(
diary: diaryId,
typeNo: mealId,
productName: productName,
portion: portion,
quantity: quantity,
productNutritionalValueId: productNutritionalValueId,
);
}
Future<void> updateProductInMeal({
@@ -182,83 +126,43 @@ class DiaryRepository {
int? productNutritionalValueId,
required PortionState portionState,
}) async {
try {
Map<String, Object?> data = ProductInMeal(productName: productName, portion: portion, quantity: quantity).toJson();
if (productNutritionalValueId != null) data['product_nutritional_value_id'] = productNutritionalValueId;
data['state'] = portionState.name;
return await diaryService.updateProductInMeal(diary: diaryId, typeNo: mealId, portionModel: portionId, productInMeal: data);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
final data = ProductInMeal(productName: productName, portion: portion, quantity: quantity).toJson();
if (productNutritionalValueId != null) data['product_nutritional_value_id'] = productNutritionalValueId;
data['state'] = portionState.name;
return diaryService.updateProductInMeal(diary: diaryId, typeNo: mealId, portionModel: portionId, productInMeal: data);
}
Future<void> markEaten({required int diaryId, required int mealId, required int portionId}) async {
try {
return await diaryService.markEaten(diary: diaryId, typeNo: mealId, portionModel: portionId);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return diaryService.markEaten(diary: diaryId, typeNo: mealId, portionModel: portionId);
}
Future<void> unmarkEaten({required int diaryId, required int mealId, required int portionId}) async {
try {
return await diaryService.unmarkEaten(diary: diaryId, typeNo: mealId, portionModel: portionId);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return diaryService.unmarkEaten(diary: diaryId, typeNo: mealId, portionModel: portionId);
}
Future<void> restore({required int diaryId, required int mealId, required int portionId}) async {
try {
return await diaryService.restore(diary: diaryId, typeNo: mealId, portionModel: portionId);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return diaryService.restore(diary: diaryId, typeNo: mealId, portionModel: portionId);
}
Future<void> deleteProductFromMeal({required int diaryId, required int mealId, required int portionId}) async {
try {
return await diaryService.deleteProductFromMeal(diary: diaryId, typeNo: mealId, portionModel: portionId);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return diaryService.deleteProductFromMeal(diary: diaryId, typeNo: mealId, portionModel: portionId);
}
Future<void> uploadPhotos({required int diaryId, required int mealId, required List<File> files}) async {
try {
for (int i = 0; i < files.length; i++) {
await diaryService.uploadPhotoOfMeal(diary: diaryId, typeNo: mealId, photo: files[i]);
}
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
for (var i = 0; i < files.length; i++) {
await diaryService.uploadPhotoOfMeal(diary: diaryId, typeNo: mealId, photo: files[i]);
}
}
Future<void> deletePhoto({required int diaryId, required int mealId, required DiaryPhotoModel photo}) async {
try {
await diaryService.deletePhotoOfMeal(diaryId: diaryId, typeNo: mealId, userAttachment: photo.id);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await diaryService.deletePhotoOfMeal(diaryId: diaryId, typeNo: mealId, userAttachment: photo.id);
}
Future<void> finishDiaryDay({required int diaryId, required String wakeUp, required String retire, String? comment}) async {
try {
return await diaryService.finishDiaryDay(
diary: diaryId,
finishDiaryDayModel: FinishDiaryDayModel(wakeUp: wakeUp, retire: retire, comment: comment),
);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return diaryService.finishDiaryDay(
diary: diaryId,
finishDiaryDayModel: FinishDiaryDayModel(wakeUp: wakeUp, retire: retire, comment: comment),
);
}
Future<GlossaryModerationModel> sendProductToModeration({
@@ -268,126 +172,66 @@ class DiaryRepository {
required double fats,
required double carbohydrates,
}) async {
try {
return await diaryService.sendProductToModeration(
data: ProductModerationModel(name: name, energy: energy, proteins: proteins, fats: fats, carbohydrates: carbohydrates).toJson(),
);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return diaryService.sendProductToModeration(
data: ProductModerationModel(name: name, energy: energy, proteins: proteins, fats: fats, carbohydrates: carbohydrates).toJson(),
);
}
Future<void> fillSatiety({required int diaryId, required int mealId, required int satiety}) async {
try {
return await diaryService.fillSatiety(diary: diaryId, typeNo: mealId, satiety: satiety);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return diaryService.fillSatiety(diary: diaryId, typeNo: mealId, satiety: satiety);
}
Future<void> fillMood({required int diaryId, required int mealId, required int mood}) async {
try {
return await diaryService.fillMood(diary: diaryId, typeNo: mealId, mood: mood);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return diaryService.fillMood(diary: diaryId, typeNo: mealId, mood: mood);
}
Future<void> fillTime({required int diaryId, required int mealId, required String time}) async {
try {
return await diaryService.fillTime(diary: diaryId, typeNo: mealId, time: time);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return diaryService.fillTime(diary: diaryId, typeNo: mealId, time: time);
}
Future<void> fillWakeup({required int diaryId, required String time}) async {
try {
return await diaryService.fillWakeup(diary: diaryId, wakeUp: time);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return diaryService.fillWakeup(diary: diaryId, wakeUp: time);
}
Future<void> fillRetire({required int diaryId, required String time}) async {
try {
return await diaryService.fillRetire(diary: diaryId, retire: time);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return diaryService.fillRetire(diary: diaryId, retire: time);
}
Future<void> commentUserDay({required int diaryId, required ConsultantCommentModel commentModel}) async {
try {
await diaryService.commentUserDay(id: diaryId, body: commentModel);
_diaryForceFetchController.add('force');
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await diaryService.commentUserDay(id: diaryId, body: commentModel);
_diaryForceFetchController.add('force');
}
Future<ConsultantReviewModel> getDiaryComments({required int planId, required int diaryId}) async {
try {
return await diaryService.getDiaryComments(planId: planId, diaryId: diaryId);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return diaryService.getDiaryComments(planId: planId, diaryId: diaryId);
}
Future<void> viewCommentOfDiaryDayByConsultant({required int diaryId}) async {
try {
await diaryService.viewCommentOfDiaryDayByConsultant(diaryId: diaryId);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await diaryService.viewCommentOfDiaryDayByConsultant(diaryId: diaryId);
}
Future<GlossaryPaginationModel> getGlossariesIndex({required List<int> ids}) async {
Map<String, dynamic> filters = {};
final filters = <String, dynamic>{};
if (ids.isNotEmpty) {
for (int i = 0; i < ids.length; i++) {
for (var i = 0; i < ids.length; i++) {
filters['filters[id][\$in][$i]'] = ids[i];
}
}
try {
return await diaryService.getGlossariesIndex(filters: filters);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
return diaryService.getGlossariesIndex(filters: filters);
}
Future<DiaryDayModel> getDiaryDayV2({required String day}) async {
try {
final diaryDay = await diaryService.getDiaryDayV2(day: day);
return setCPFCToDiary(diaryDay);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
final diaryDay = await diaryService.getDiaryDayV2(day: day);
return setCPFCToDiary(diaryDay);
}
Future<void> createDiaryDay({required String day}) async {
try {
await diaryService.createDay(body: CreateDayModel(day: day));
await getDiaryByDate(day);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
await diaryService.createDay(body: CreateDayModel(day: day));
await getDiaryByDate(day);
}
Future<DiaryDayModel> setCPFCToDiary(DiaryDayModel diaryDay) async {
Set<int> ids = {};
final ids = <int>{};
for (var meal in diaryDay.meals) {
for (var portion in meal.portions) {
if (portion.productNutritionalValueId != null) {
@@ -395,41 +239,15 @@ class DiaryRepository {
}
}
}
if (ids.isNotEmpty) {
final glossary = await getGlossariesIndex(ids: ids.toList());
final List<MealModel> updatedMeals = List.from(diaryDay.meals);
for (var mealIndex = 0; mealIndex < updatedMeals.length; mealIndex++) {
final meal = updatedMeals[mealIndex];
final updatedPortions = meal.portions.map((portion) {
if (portion.productNutritionalValueId != null) {
try {
final glossaryItem = glossary.glossariesList.firstWhere((g) => g.id == portion.productNutritionalValueId);
return portion.copyWith(
productCaloriesInfo: ProductCaloriesInfoModel(
id: glossaryItem.id,
productName: glossaryItem.name,
calories: glossaryItem.energyOn100Gr,
proteins: glossaryItem.proteinsOn100Gr,
fats: glossaryItem.fatsOn100Gr,
carbohydrates: glossaryItem.carbohydratesOn100Gr,
energyOnPortion: glossaryItem.energyOnPortion,
proteinsOnPortion: glossaryItem.proteinsOnPortion,
fatsOnPortion: glossaryItem.fatsOnPortion,
carbohydratesOnPortion: glossaryItem.carbohydratesOnPortion,
),
);
} catch (e) {
return portion;
}
}
return portion;
}).toList();
updatedMeals[mealIndex] = meal.copyWith(portions: updatedPortions);
}
final updatedDiaryDay = diaryDay.copyWith(meals: updatedMeals);
return updatedDiaryDay;
if (ids.isEmpty) {
return diaryDay;
}
return diaryDay;
final glossary = await getGlossariesIndex(ids: ids.toList());
final glossaries = glossary.glossariesList;
final updatedMeals = diaryDay.meals.map((meal) {
final updatedPortions = meal.portions.map((p) => _portionWithGlossary(p, glossaries)).toList();
return meal.copyWith(portions: updatedPortions);
}).toList();
return diaryDay.copyWith(meals: updatedMeals);
}
}

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