Compare commits

...

92 Commits

Author SHA1 Message Date
misha
087436da1c changed logic of view requests sending 2025-09-17 15:13:16 +03:00
misha
6ac93d42df Merge branch 'main' into issue-687 2025-09-15 14:13:28 +03:00
misha
1a6fb39d84 gitignore 2025-09-15 14:12:05 +03:00
mso7nueh
b4776c3be3 Merge pull request 'Issues: 416+675' (#699) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#699
2025-09-12 13:27:29 +00:00
mso7nueh
06e07a92b7 Merge pull request 'Issues-236, 332, 256' (#698) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#698
2025-09-12 10:48:22 +00:00
mso7nueh
0fce3f9d30 fix(diary): Issue-675 2025-09-12 13:38:50 +03:00
mso7nueh
4bc8cba941 fix(profile): Issue-416 2025-09-12 13:34:16 +03:00
mso7nueh
8c348f61b1 fix(settings): Issue-256 2025-09-11 21:35:28 +03:00
mso7nueh
0446e64409 merge upstream 2025-09-11 18:25:01 +00:00
mso7nueh
74b5c6732d fix(settings): Issue-256 2025-09-11 20:11:37 +03:00
mso7nueh
b840c936bf fix(finances): Issue-332 2025-09-11 20:02:48 +03:00
mso7nueh
f3dbbf3537 fix(settings): Issue-236 2025-09-11 19:51:16 +03:00
misha
ff49562e86 feat(views): added view handling for diets, recipes, plans and posts 2025-09-11 18:04:35 +03:00
mso7nueh
8b7180c2da Merge pull request 'fix(comments): Issue-548' (#695) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#695
2025-09-10 14:37:20 +00:00
mso7nueh
5c911707a3 fix(comments): Issue-548 2025-09-10 17:36:46 +03:00
mso7nueh
aa604b667b Merge pull request 'fix(notifications): Issue-236' (#693) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#693
2025-09-10 13:45:40 +00:00
mso7nueh
0a5d78be1f fix: ver upd 2025-09-10 16:45:22 +03:00
mso7nueh
f1f5c79661 fix(notifications): Issue-236 2025-09-10 15:58:26 +03:00
mso7nueh
1c517807c4 Merge pull request 'feat(metrica): Issue-685 + 675 + 180' (#688) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#688
2025-09-10 12:58:21 +00:00
mso7nueh
cad54913d1 Merge branch 'main' of http://gitea.office.local:3000/mso7nueh/eatvio_app 2025-09-09 18:58:51 +03:00
mso7nueh
89f0a46192 fix(diet): Issue-180 2025-09-09 18:58:39 +03:00
mso7nueh
65542020a7 merge upstream 2025-09-08 14:45:58 +00:00
mso7nueh
b6b1c8b3c1 feat(diary): кбжу 2025-09-08 14:14:41 +03:00
mso7nueh
bf6e23e96a feat(metrica): Issue-685 2025-09-03 11:34:41 +03:00
mso7nueh
952cf76164 Merge pull request 'fix: Issue-645' (#684) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#684
2025-09-01 10:04:31 +00:00
mso7nueh
b665692779 fix: Issue-645 2025-09-01 13:03:43 +03:00
mso7nueh
54ac20978f Merge pull request 'feat(notifications): Issue-236' (#682) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#682
2025-09-01 09:01:48 +00:00
mso7nueh
b9dc5c0136 Удалить env/.env 2025-08-29 12:16:55 +00:00
mso7nueh
39fce4b0f9 fix: mv env 2025-08-29 15:16:40 +03:00
mso7nueh
73ff67aa13 merge upstream 2025-08-29 12:10:23 +00:00
mso7nueh
dcf2805df7 fix: analyzer 2025-08-29 14:26:28 +03:00
mso7nueh
d13eb78f0e feat(notifications): Issue-236 2025-08-29 14:25:13 +03:00
mso7nueh
5f21a65028 Merge pull request 'feat(verify_email): add dialog' (#679) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#679
2025-08-27 11:40:22 +00:00
mso7nueh
12bf9f9e5f feat(verify_email): add dialog 2025-08-27 14:39:22 +03:00
mso7nueh
1680bd2556 fix: testflight 2025-08-27 12:04:20 +03:00
mso7nueh
91d5118f60 Merge pull request 'feat(verify_email): Issue-645' (#676) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#676
2025-08-27 08:44:19 +00:00
mso7nueh
d8324c1635 feat(verify_email): Issue-645 2025-08-27 11:33:06 +03:00
mso7nueh
0f1dbe16f1 Merge pull request 'fix: testflight' (#673) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#673
2025-08-25 15:31:56 +00:00
Alexander
020682ae80 fix: testflight 2025-08-25 18:31:37 +03:00
mso7nueh
e54476656b Merge pull request 'fix: test-flight' (#671) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#671
2025-08-25 15:28:43 +00:00
Alexander
dd56924076 fix: test-flight 2025-08-25 18:28:15 +03:00
mso7nueh
b47da44cf2 Merge pull request 'fix: testflight' (#669) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#669
2025-08-25 15:19:56 +00:00
Alexander
964550a325 fix: testflight 2025-08-25 18:19:18 +03:00
mso7nueh
7ffa061f3e Merge pull request 'fix: testflight' (#667) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#667
2025-08-25 15:16:06 +00:00
Alexander
997da4f940 fix: testflight 2025-08-25 18:15:49 +03:00
mso7nueh
ca1777ece0 Merge pull request 'main' (#665) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#665
2025-08-25 15:12:32 +00:00
Alexander
1a427d6f5d Merge branch 'main' of http://gitea.office.local:3000/mso7nueh/eatvio_app 2025-08-25 18:12:05 +03:00
Alexander
f190e70f3a fix: testflight 2025-08-25 18:11:04 +03:00
mso7nueh
bc6469a6ae fix: testflight 2025-08-25 17:55:34 +03:00
mso7nueh
e3ff359e1b Merge pull request 'fix: testflight' (#662) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#662
2025-08-25 14:42:39 +00:00
Alexander
b5edf13bd9 fix: testflight 2025-08-25 17:42:05 +03:00
mso7nueh
c05d027e3d Merge pull request 'fix: add product modal and auth error, add appmetrica' (#659) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#659
2025-08-25 14:29:20 +00:00
Alexander
bae79b60fd fix: testflight 2025-08-25 17:29:07 +03:00
mso7nueh
6061e413b2 fix: upd ver to 71 2025-08-25 14:40:50 +03:00
mso7nueh
5a8fa7089c feat: appmetrica 2025-08-25 14:24:06 +03:00
mso7nueh
886cfa8f1c feat: AppMetrica 2025-08-25 13:05:53 +03:00
mso7nueh
a416912d2a fix: add product modal and auth error 2025-08-25 11:26:14 +03:00
mso7nueh
f46392ec8e Merge pull request 'fix: Issue-592' (#655) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#655
2025-08-22 11:18:00 +00:00
mso7nueh
766d06fc4d fix: Issue-592 2025-08-22 14:17:11 +03:00
mso7nueh
6f4023dd0b Merge pull request #512: Add consultant rating functionality 2025-08-22 10:32:39 +03:00
mso7nueh
f892dfed9f Merge pull request 'fix: Issue-618' (#654) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#654
2025-08-20 12:52:12 +00:00
Alexander
98803e56cf fix: Issue-618 2025-08-20 15:51:46 +03:00
mso7nueh
4391870e72 Merge pull request 'fix: build ios' (#652) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#652
2025-08-20 10:03:32 +00:00
Alexander
7fd24f57e3 fix: build ios 2025-08-20 13:03:16 +03:00
mso7nueh
384a25c4a6 Merge pull request 'fix: ios build' (#650) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#650
2025-08-20 09:59:07 +00:00
Alexander
88558d62ef fix: ios build 2025-08-20 12:58:48 +03:00
mso7nueh
09b04118aa Merge pull request 'fix: ios build' (#648) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#648
2025-08-20 09:55:07 +00:00
Alexander
f76b142117 fix: ios build 2025-08-20 12:54:54 +03:00
mso7nueh
13e66edd27 Merge pull request 'fix: Issue-613' (#647) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#647
2025-08-20 09:43:06 +00:00
Alexander
5b74ae45b3 fix: Issue-613 2025-08-20 12:42:39 +03:00
mso7nueh
3d2634b96a Merge pull request 'Добавление таймера для подтверждения почты' (#643) from misha/eatvio_app:issue-642 into main
Reviewed-on: eatvio/eatvio_app#643
2025-08-20 09:39:16 +00:00
mso7nueh
1281abaf24 Merge pull request 'Issue-191: Notifications' (#644) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#644
2025-08-20 09:39:08 +00:00
Alexander
7dfac3f792 fix: baseUrl -> prod 2025-08-20 11:57:06 +03:00
Alexander
9d4687553a fix: analyzer and token storage 2025-08-20 11:34:28 +03:00
mso7nueh
442da40622 fix: token storage 2025-08-19 14:24:14 +03:00
mso7nueh
ccf586234b fix: user charts in consultations 2025-08-15 14:13:30 +03:00
mso7nueh
4f249499a9 fix(notifications): Issue-191 2025-08-15 13:28:12 +03:00
mso7nueh
2f74387df2 fix: firebase notificatons when qr login 2025-08-14 18:13:34 +03:00
Username1548
45774c3875 fix 2025-08-14 16:58:00 +03:00
Username1548
62c0914fe2 fix 2025-08-14 16:55:15 +03:00
Username1548
48f5b64223 feat(emailVerification): added timer for email code resending 2025-08-14 16:54:13 +03:00
ed4db5b59e Merge pull request 'feat(menu): Added build date' (#640) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#640
2025-08-12 06:02:06 +00:00
Username1548
91bb5cf483 feat(menu): fixed indentation 2025-08-12 09:01:31 +03:00
Username1548
c4dedac3f3 added build date 2025-08-12 12:27:58 +07:00
mso7nueh
8ed243f63c Merge pull request 'fix: upd ver' (#638) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#638
2025-08-11 14:19:46 +00:00
mso7nueh
a10fe6d2f5 fix: upd ver 2025-08-11 17:19:14 +03:00
mso7nueh
35b78bdeee Merge pull request 'fix(blackList+notifications): Issue-523, 191' (#636) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#636
2025-08-11 14:00:25 +00:00
mso7nueh
ef037eb4d5 Merge pull request 'feat(chats): added message resend and deleting, feat(menu): added build number' (#633) from misha/eatvio_app:issue-594 into main
Reviewed-on: eatvio/eatvio_app#633
2025-08-11 14:00:14 +00:00
mso7nueh
5a3385cacf Merge pull request 'removed public offer from ui on ios' (#637) from misha/eatvio_app:issue-635 into main
Reviewed-on: eatvio/eatvio_app#637
2025-08-11 14:00:05 +00:00
mso7nueh
dca4b4c471 fix(blackList+notifications): Issue-523, 191 2025-08-11 14:53:43 +03:00
Username1548
48f433288f feat(chats): added message resend and deleting, feat(menu): added build number 2025-08-11 02:52:41 +07:00
Username1548
96e80ced38 feat(consultantRating): added api requests and state to consultant rating page 2025-06-17 13:33:41 +07:00
115 changed files with 5727 additions and 2400 deletions

View File

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

View File

@@ -3,7 +3,7 @@ name: Eatvio App Android Test
on: [ pull_request ] on: [ pull_request ]
env: env:
flutter_version: "3.32.8" flutter_version: "3.35.1"
java_version: "21.x" java_version: "21.x"
flutter_channel: "stable" flutter_channel: "stable"
my_local_cache_dir: "/opt/hostedtoolcache/" my_local_cache_dir: "/opt/hostedtoolcache/"
@@ -26,20 +26,19 @@ jobs:
- name: Copy .env file - name: Copy .env file
run: | run: |
mkdir -p env cp env_repo/.env .env
cp env_repo/.env env/.env
- name: Load environment variables from .env - name: Load environment variables from .env
run: | run: |
set -a set -a
source env/.env source .env
set +a set +a
# Export all variables to GITHUB_ENV # Export all variables to GITHUB_ENV
while IFS= read -r line; do while IFS= read -r line; do
if [[ $line =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then if [[ $line =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then
echo "$line" >> $GITHUB_ENV echo "$line" >> $GITHUB_ENV
fi fi
done < env/.env done < .env
- name: Create key.properties file - name: Create key.properties file
run: | run: |
@@ -144,7 +143,11 @@ jobs:
run: flutter test run: flutter test
- name: Build apk - name: Build apk
run: flutter build apk --release --dart-define=HIDE_PLANS=false run: |
BUILD_DATE=$(TZ=Europe/Moscow date +'%Y-%m-%d %H:%M:%S')
flutter build apk --release \
--dart-define=HIDE_PLANS=false \
--dart-define=BUILD_DATE="$BUILD_DATE"
- name: Rename and move apk - name: Rename and move apk
run: | run: |

3
.gitignore vendored
View File

@@ -16,6 +16,5 @@
*.mocks.dart *.mocks.dart
android/build/ android/build/
ios/XCBuildData/ ios/XCBuildData/
.env
env.example
env/.env env/.env
.env

View File

@@ -29,7 +29,7 @@ android {
defaultConfig { defaultConfig {
applicationId = "com.eatvioapp.app" applicationId = "com.eatvioapp.app"
minSdk = 23 minSdkVersion flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode versionCode = flutter.versionCode
versionName = flutter.versionName versionName = flutter.versionName

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-all.zip

View File

@@ -18,11 +18,11 @@ pluginManagement {
plugins { plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.7.3" apply false id "com.android.application" version "8.12.0" apply false
// START: FlutterFire Configuration // START: FlutterFire Configuration
id "com.google.gms.google-services" version "4.4.2" apply false id "com.google.gms.google-services" version "4.4.3" apply false
// END: FlutterFire Configuration // END: FlutterFire Configuration
id "org.jetbrains.kotlin.android" version "2.1.0" apply false id "org.jetbrains.kotlin.android" version "2.2.10" apply false
} }
include ":app" include ":app"

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

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

After

Width:  |  Height:  |  Size: 1.7 KiB

0
env/.env vendored
View File

View File

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

View File

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

View File

@@ -213,14 +213,10 @@
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
); );
inputPaths = (
);
name = "[CP] Embed Pods Frameworks"; name = "[CP] Embed Pods Frameworks";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
); );
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
@@ -287,14 +283,10 @@
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
); );
inputPaths = (
);
name = "[CP] Copy Pods Resources"; name = "[CP] Copy Pods Resources";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
); );
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
@@ -378,7 +370,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos; SUPPORTED_PLATFORMS = iphoneos;
@@ -396,7 +388,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 67; CURRENT_PROJECT_VERSION = 72;
DEVELOPMENT_TEAM = VL68C36HYS; DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
@@ -418,6 +410,7 @@
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
@@ -474,7 +467,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MTL_ENABLE_DEBUG_INFO = YES; MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -526,7 +519,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos; SUPPORTED_PLATFORMS = iphoneos;
@@ -546,7 +539,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 67; CURRENT_PROJECT_VERSION = 72;
DEVELOPMENT_TEAM = VL68C36HYS; DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
@@ -568,6 +561,7 @@
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@@ -584,7 +578,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 67; CURRENT_PROJECT_VERSION = 72;
DEVELOPMENT_TEAM = VL68C36HYS; DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
@@ -606,6 +600,7 @@
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";

View File

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

View File

@@ -21,7 +21,7 @@
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>67</string> <string>72</string>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
<false/> <false/>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>

View File

@@ -44,6 +44,10 @@ platform :ios do
UI.message "Using auth_key_path: #{auth_key_path}" UI.message "Using auth_key_path: #{auth_key_path}"
# Configure Flutter for release mode
UI.message "Configuring Flutter for release mode..."
sh("cd .. && flutter build ios --config-only --release")
match(type: "appstore", readonly: true) match(type: "appstore", readonly: true)
increment_build_number(xcodeproj: "Runner.xcodeproj") increment_build_number(xcodeproj: "Runner.xcodeproj")
@@ -52,7 +56,6 @@ platform :ios do
scheme: "Runner", scheme: "Runner",
export_method: "app-store", export_method: "app-store",
codesigning_identity: "Apple Distribution", codesigning_identity: "Apple Distribution",
xcargs: "--dart-define=HIDE_PLANS=true"
) )

View File

@@ -24,6 +24,7 @@ import 'package:eatvio_app/features/consultations/presentation/pages/consultatio
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart';
import 'package:eatvio_app/features/menu/presentation/pages/blocked_users_screen.dart'; import 'package:eatvio_app/features/menu/presentation/pages/blocked_users_screen.dart';
import 'package:eatvio_app/features/menu/presentation/pages/notifications_settings_screen.dart';
import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart'; import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart';
import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart'; import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart';
import 'package:eatvio_app/features/create_diet/presentation/diet_preview_screen.dart'; import 'package:eatvio_app/features/create_diet/presentation/diet_preview_screen.dart';
@@ -111,23 +112,13 @@ class AppRouter {
name: 'authScreen', name: 'authScreen',
builder: (context, state) => const AuthScreen(), builder: (context, state) => const AuthScreen(),
routes: [ routes: [
GoRoute( GoRoute(path: 'registration-screen', name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()),
path: 'registration-screen',
name: 'registrationScreen',
builder: (context, state) => const RegistrationScreen(),
),
GoRoute( GoRoute(
path: 'forget-password-screen', path: 'forget-password-screen',
name: 'forgetPasswordScreen', name: 'forgetPasswordScreen',
builder: (context, state) => ForgetPasswordScreen( builder: (context, state) => ForgetPasswordScreen(isAuth: state.extra as bool),
isAuth: state.extra as bool,
),
),
GoRoute(
path: 'scan-qr-code-screen',
name: 'scanQRCodeScreen',
builder: (context, state) => const ScanQRCodeScreen(),
), ),
GoRoute(path: 'scan-qr-code-screen', name: 'scanQRCodeScreen', builder: (context, state) => const ScanQRCodeScreen()),
], ],
), ),
ShellRoute( ShellRoute(
@@ -165,19 +156,23 @@ class AppRouter {
GoRoute( GoRoute(
path: '/diary-screen', path: '/diary-screen',
name: 'diaryScreen', name: 'diaryScreen',
pageBuilder: (context, state) => const NoTransitionPage( pageBuilder: (context, state) => const NoTransitionPage(child: DiaryScreen()),
child: DiaryScreen(),
),
), ),
GoRoute( GoRoute(
path: ConsultationsScreen.path, path: ConsultationsScreen.path,
name: ConsultationsScreen.name, name: ConsultationsScreen.name,
pageBuilder: (context, state) => const NoTransitionPage(child: ConsultationsScreen())), pageBuilder: (context, state) => const NoTransitionPage(child: ConsultationsScreen()),
GoRoute(path: '/chat-select-screen', name: 'chatSelectScreen', pageBuilder: (context, state) => const NoTransitionPage(child: ChatsScreen())), ),
GoRoute( GoRoute(
path: '/notifications-screen', path: '/chat-select-screen',
name: 'notificationsScreen', name: 'chatSelectScreen',
pageBuilder: (context, state) => const NoTransitionPage(child: NotificationsScreen())), pageBuilder: (context, state) => const NoTransitionPage(child: ChatsScreen()),
),
GoRoute(
path: '/notifications-screen',
name: 'notificationsScreen',
pageBuilder: (context, state) => const NoTransitionPage(child: NotificationsScreen()),
),
GoRoute( GoRoute(
path: '/menu-screen', path: '/menu-screen',
name: 'menuScreen', name: 'menuScreen',
@@ -194,11 +189,7 @@ class AppRouter {
), ),
], ],
), ),
GoRoute( GoRoute(name: 'blockedUsersScreen', path: '/blocked-users-screen', builder: (context, state) => const BlockedUsersScreen()),
name: 'blockedUsersScreen',
path: '/blocked-users-screen',
builder: (context, state) => const BlockedUsersScreen(),
),
GoRoute( GoRoute(
path: '/welcome-screen', path: '/welcome-screen',
name: 'welcomeScreen', name: 'welcomeScreen',
@@ -241,26 +232,10 @@ class AppRouter {
} }
}, },
), ),
GoRoute( GoRoute(path: ConsultationsArchiveScreen.path, name: ConsultationsArchiveScreen.name, builder: (context, state) => const ConsultationsArchiveScreen()),
path: ConsultationsArchiveScreen.path, GoRoute(path: ConsultantFinancesScreen.path, name: ConsultantFinancesScreen.name, builder: (context, state) => const ConsultantFinancesScreen()),
name: ConsultationsArchiveScreen.name, GoRoute(path: UserPurchasesScreen.path, name: UserPurchasesScreen.name, builder: (context, state) => const UserPurchasesScreen()),
builder: (context, state) => const ConsultationsArchiveScreen(), GoRoute(path: '/user-finance-screen', name: 'userFinanceScreen', builder: (context, state) => const UserFinanceScreen()),
),
GoRoute(
path: ConsultantFinancesScreen.path,
name: ConsultantFinancesScreen.name,
builder: (context, state) => const ConsultantFinancesScreen(),
),
GoRoute(
path: UserPurchasesScreen.path,
name: UserPurchasesScreen.name,
builder: (context, state) => const UserPurchasesScreen(),
),
GoRoute(
path: '/user-finance-screen',
name: 'userFinanceScreen',
builder: (context, state) => const UserFinanceScreen(),
),
GoRoute( GoRoute(
path: CardSettingsScreen.path, path: CardSettingsScreen.path,
name: CardSettingsScreen.name, name: CardSettingsScreen.name,
@@ -275,11 +250,7 @@ class AppRouter {
builder: (context, state) { builder: (context, state) {
final slug = state.uri.queryParameters['slug']; final slug = state.uri.queryParameters['slug'];
if (slug != null) { if (slug != null) {
return AboutPlanScreen( return AboutPlanScreen(slug: slug, navigateToComments: false, index: null);
slug: slug,
navigateToComments: false,
index: null,
);
} }
final data = state.extra as Map<String, dynamic>; final data = state.extra as Map<String, dynamic>;
@@ -293,29 +264,23 @@ class AppRouter {
builder: (context, state) { builder: (context, state) {
final slug = state.uri.queryParameters['slug']; final slug = state.uri.queryParameters['slug'];
if (slug != null) { if (slug != null) {
return AboutRecipeScreen( return AboutRecipeScreen(recipeSlug: slug, navigateToComments: false, index: null);
recipeSlug: slug,
navigateToComments: false,
index: null,
);
} }
final data = state.extra as Map<String, dynamic>; final data = state.extra as Map<String, dynamic>;
return AboutRecipeScreen( return AboutRecipeScreen(recipeSlug: data['recipe'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
recipeSlug: data['recipe'] as String,
index: data['index'] as int?,
navigateToComments: data['navigateToComments'] as bool,
);
}, },
), ),
GoRoute( GoRoute(
path: RecipeCreateScreen.path, path: RecipeCreateScreen.path,
name: RecipeCreateScreen.name, name: RecipeCreateScreen.name,
builder: (context, state) => RecipeCreateScreen( builder: (context, state) => RecipeCreateScreen(recipe: state.extra != null ? state.extra as Recipe : null),
recipe: state.extra != null ? state.extra as Recipe : null, ),
), GoRoute(
path: '/profile-screen',
name: 'profileScreen',
builder: (context, state) => ProfileScreen(author: state.extra as Author),
), ),
GoRoute(path: '/profile-screen', name: 'profileScreen', builder: (context, state) => ProfileScreen(author: state.extra as Author)),
GoRoute( GoRoute(
path: '/subs-screen/:slug', path: '/subs-screen/:slug',
name: 'subsScreen', name: 'subsScreen',
@@ -327,11 +292,7 @@ class AppRouter {
builder: (context, state) { builder: (context, state) {
final slug = state.uri.queryParameters['slug']; final slug = state.uri.queryParameters['slug'];
if (slug != null) { if (slug != null) {
return AboutDietScreen( return AboutDietScreen(slug: slug, index: null, navigateToComments: false);
slug: slug,
index: null,
navigateToComments: false,
);
} }
final data = state.extra as Map<String, dynamic>; final data = state.extra as Map<String, dynamic>;
return AboutDietScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); return AboutDietScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
@@ -343,19 +304,18 @@ class AppRouter {
builder: (context, state) => BlogPostCreateScreen(feedBlogPostModel: state.extra != null ? state.extra as FeedBlogPostModel : null), builder: (context, state) => BlogPostCreateScreen(feedBlogPostModel: state.extra != null ? state.extra as FeedBlogPostModel : null),
), ),
GoRoute( GoRoute(
path: '/blog-post-screen', path: '/blog-post-screen',
name: 'blogPostScreen', name: 'blogPostScreen',
builder: (context, state) { builder: (context, state) {
final id = state.uri.queryParameters['id']; final id = state.uri.queryParameters['id'];
final parsedId = int.tryParse(id ?? ''); final parsedId = int.tryParse(id ?? '');
if (parsedId != null) { if (parsedId != null) {
return BlogPostScreen( return BlogPostScreen(id: parsedId);
id: parsedId, }
);
}
return BlogPostScreen(id: state.extra as int); return BlogPostScreen(id: state.extra as int);
}), },
),
GoRoute( GoRoute(
path: '/chat-screen', path: '/chat-screen',
name: 'chatScreen', name: 'chatScreen',
@@ -363,10 +323,7 @@ class AppRouter {
final data = state.extra as Map<String, dynamic>; final data = state.extra as Map<String, dynamic>;
final uuid = data['uuid'] as String; final uuid = data['uuid'] as String;
final sendInitialMessage = data['sendInitialMessage'] as bool?; final sendInitialMessage = data['sendInitialMessage'] as bool?;
return ChatScreen( return ChatScreen(uuid: uuid, sendInitialMessage: sendInitialMessage);
uuid: uuid,
sendInitialMessage: sendInitialMessage,
);
}, },
), ),
GoRoute( GoRoute(
@@ -399,7 +356,11 @@ class AppRouter {
name: 'physicalFormScreen', name: 'physicalFormScreen',
builder: (context, state) => PhysicalFormScreen(settingsEntity: state.extra as SettingsEntity), builder: (context, state) => PhysicalFormScreen(settingsEntity: state.extra as SettingsEntity),
), ),
GoRoute(path: 'pressure-screen', name: 'pressureScreen', builder: (context, state) => PressureScreen(settingsEntity: state.extra as SettingsEntity)), GoRoute(
path: 'pressure-screen',
name: 'pressureScreen',
builder: (context, state) => PressureScreen(settingsEntity: state.extra as SettingsEntity),
),
GoRoute( GoRoute(
path: 'change-password-screen', path: 'change-password-screen',
name: 'changePasswordScreen', name: 'changePasswordScreen',
@@ -416,9 +377,7 @@ class AppRouter {
GoRoute( GoRoute(
path: '/stepper-become-consultant-screen', path: '/stepper-become-consultant-screen',
name: 'stepperBecomeConsultantScreen', name: 'stepperBecomeConsultantScreen',
builder: (context, state) => StepperBecomeConsultantScreen( builder: (context, state) => StepperBecomeConsultantScreen(slug: state.extra == null ? null : state.extra as String),
slug: state.extra == null ? null : state.extra as String,
),
), ),
GoRoute(path: '/calendar-screen', name: 'calendarScreen', builder: (context, state) => const CalendarScreen()), GoRoute(path: '/calendar-screen', name: 'calendarScreen', builder: (context, state) => const CalendarScreen()),
GoRoute(path: '/support-screen', name: 'supportScreen', builder: (context, state) => const SupportScreen()), GoRoute(path: '/support-screen', name: 'supportScreen', builder: (context, state) => const SupportScreen()),
@@ -446,7 +405,11 @@ class AppRouter {
GoRoute(path: CreatePlanScreen.path, name: CreatePlanScreen.name, builder: (context, state) => const CreatePlanScreen()), GoRoute(path: CreatePlanScreen.path, name: CreatePlanScreen.name, builder: (context, state) => const CreatePlanScreen()),
GoRoute(path: MyDietsScreen.path, name: MyDietsScreen.name, builder: (context, state) => const MyDietsScreen()), GoRoute(path: MyDietsScreen.path, name: MyDietsScreen.name, builder: (context, state) => const MyDietsScreen()),
GoRoute(path: MyPlansScreen.path, name: MyPlansScreen.name, builder: (context, state) => const MyPlansScreen()), GoRoute(path: MyPlansScreen.path, name: MyPlansScreen.name, builder: (context, state) => const MyPlansScreen()),
GoRoute(path: UserDetailsScreen.path, name: UserDetailsScreen.name, builder: (context, state) => UserDetailsScreen(slug: state.extra as String)), GoRoute(
path: UserDetailsScreen.path,
name: UserDetailsScreen.name,
builder: (context, state) => UserDetailsScreen(slug: state.extra as String),
),
GoRoute(path: DevToolsScreen.path, name: DevToolsScreen.name, builder: (context, state) => const DevToolsScreen()), GoRoute(path: DevToolsScreen.path, name: DevToolsScreen.name, builder: (context, state) => const DevToolsScreen()),
GoRoute(path: ConsultantRatingScreen.path, name: ConsultantRatingScreen.name, builder: (context, state) => const ConsultantRatingScreen()), GoRoute(path: ConsultantRatingScreen.path, name: ConsultantRatingScreen.name, builder: (context, state) => const ConsultantRatingScreen()),
GoRoute( GoRoute(
@@ -456,10 +419,7 @@ class AppRouter {
final data = state.extra as Map<String, dynamic>; final data = state.extra as Map<String, dynamic>;
final planId = data['planId'] as int; final planId = data['planId'] as int;
final successfulPayment = data['successfulPayment'] as bool?; final successfulPayment = data['successfulPayment'] as bool?;
return InfoByOrderScreen( return InfoByOrderScreen(planId: planId, successfulPayment: successfulPayment);
planId: planId,
successfulPayment: successfulPayment,
);
}, },
), ),
GoRoute( GoRoute(
@@ -469,10 +429,7 @@ class AppRouter {
final data = state.extra as Map<String, dynamic>; final data = state.extra as Map<String, dynamic>;
final planId = data['planId'] as int; final planId = data['planId'] as int;
final url = data['url'] as String; final url = data['url'] as String;
return PaymentScreen( return PaymentScreen(url: url, planId: planId);
url: url,
planId: planId,
);
}, },
), ),
GoRoute( GoRoute(
@@ -482,10 +439,7 @@ class AppRouter {
final data = state.extra as Map<String, int>; final data = state.extra as Map<String, int>;
final diaryId = data['diaryId'] as int; final diaryId = data['diaryId'] as int;
final currentPlanDay = data['currentDiaryDay'] as int; final currentPlanDay = data['currentDiaryDay'] as int;
return UserDiaryScreen( return UserDiaryScreen(diaryId: diaryId, currentPlanDay: currentPlanDay);
diaryId: diaryId,
currentPlanDay: currentPlanDay,
);
}, },
), ),
GoRoute( GoRoute(
@@ -493,16 +447,9 @@ class AppRouter {
name: 'verifyScreen', name: 'verifyScreen',
builder: (context, state) => VerifyScreen(passKey: state.extra as String), builder: (context, state) => VerifyScreen(passKey: state.extra as String),
), ),
GoRoute( GoRoute(path: '/verify-email-screen', name: 'verifyEmailScreen', builder: (context, state) => const VerifyEmailScreen()),
path: '/verify-email-screen', GoRoute(path: MyRecipesScreen.path, name: MyRecipesScreen.name, builder: (context, state) => const MyRecipesScreen()),
name: 'verifyEmailScreen', GoRoute(path: '/notifications-settings-screen', name: 'notificationsSettingsScreen', builder: (context, state) => const NotificationsSettingsScreen()),
builder: (context, state) => const VerifyEmailScreen(),
),
GoRoute(
path: MyRecipesScreen.path,
name: MyRecipesScreen.name,
builder: (context, state) => const MyRecipesScreen(),
),
], ],
); );
} }

View File

@@ -1,11 +1,10 @@
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_action_widget.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart'; import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart';
import 'package:eatvio_app/core/widgets/custom_divider.dart'; import 'package:eatvio_app/core/widgets/custom_divider.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
void showCommentsActionsBottomSheet( void showCommentsActionsBottomSheet(
@@ -19,25 +18,23 @@ void showCommentsActionsBottomSheet(
required bool isMyComment, required bool isMyComment,
}) { }) {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
builder: (context) => Container( builder: (context) => Container(
margin: EdgeInsets.only(left: 8, right: 8, bottom: MediaQuery.of(context).padding.bottom + 20), margin: EdgeInsets.only(left: 8, right: 8, bottom: MediaQuery.of(context).viewInsets.bottom + MediaQuery.of(context).padding.bottom + 20),
decoration: BoxDecoration( decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: appTheme.whiteA700),
borderRadius: BorderRadius.circular(12), child: CommentsActionsBottomSheet(
color: appTheme.whiteA700, isMyComment: isMyComment,
), reply: reply,
child: CommentsActionsBottomSheet( copy: copy,
isMyComment: isMyComment, edit: edit,
reply: reply, delete: delete,
copy: copy, complain: complain,
edit: edit, commentText: commentText,
delete: delete, ),
complain: complain, ),
commentText: commentText, );
),
));
} }
class CommentsActionsBottomSheet extends StatefulWidget { class CommentsActionsBottomSheet extends StatefulWidget {
@@ -83,47 +80,36 @@ class _CommentsActionsBottomSheetState extends State<CommentsActionsBottomSheet>
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Row(children: [ Row(
Expanded( children: [
child: CustomTextFormField( Expanded(
autofocus: true, child: CustomTextFormField(autofocus: true, controller: controller, onChanged: (value) => setState(() {})),
controller: controller,
onChanged: (value) => setState(() {}),
), ),
), const SizedBox(width: 8),
const SizedBox(width: 8), GestureDetector(
GestureDetector( onTap: () {
onTap: () { widget.edit(controller.text);
widget.edit(controller.text); GoRouter.of(context).pop();
GoRouter.of(context).pop(); },
}, child: Container(
child: Container( decoration: BoxDecoration(color: controller.text.length > 2 ? appTheme.blueA400 : appTheme.gray50, shape: BoxShape.circle),
decoration: BoxDecoration( height: 32,
color: controller.text.length > 2 ? appTheme.blueA400 : appTheme.gray50, width: 32,
shape: BoxShape.circle, child: Icon(Icons.send_rounded, size: 18, color: controller.text.length > 2 ? appTheme.whiteA700 : appTheme.black),
),
height: 32,
width: 32,
child: Icon(
Icons.send_rounded,
size: 18,
color: controller.text.length > 2 ? appTheme.whiteA700 : appTheme.black,
), ),
), ),
) ],
]) ),
], ],
), ),
) )
: Column( : Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const SizedBox( const SizedBox(height: 7),
height: 7,
),
const BottomSheetHandlerBar(), const BottomSheetHandlerBar(),
if (widget.isMyComment) if (widget.isMyComment)
_ActionWidget( BottomSheetActionWidget(
text: s.edit, text: s.edit,
iconPath: 'assets/icons/edit_paint.svg', iconPath: 'assets/icons/edit_paint.svg',
needToPop: false, needToPop: false,
@@ -132,73 +118,22 @@ class _CommentsActionsBottomSheetState extends State<CommentsActionsBottomSheet>
}), }),
) )
else if (widget.reply != null) else if (widget.reply != null)
_ActionWidget( BottomSheetActionWidget(text: s.reply, iconPath: 'assets/icons/reply.svg', onTap: widget.reply!),
text: s.reply,
iconPath: 'assets/icons/reply.svg',
onTap: widget.reply!,
),
if (widget.reply != null) const CustomDivider(), if (widget.reply != null) const CustomDivider(),
_ActionWidget( BottomSheetActionWidget(text: s.copy, iconPath: 'assets/icons/copy2.svg', onTap: widget.copy),
text: s.copy,
iconPath: 'assets/icons/copy2.svg',
onTap: widget.copy,
),
if (widget.isMyComment) ...[ if (widget.isMyComment) ...[
if (widget.reply != null) ...[ if (widget.reply != null) ...[
const CustomDivider(), const CustomDivider(),
_ActionWidget( BottomSheetActionWidget(text: s.reply, iconPath: 'assets/icons/reply.svg', onTap: widget.reply!),
text: s.reply,
iconPath: 'assets/icons/reply.svg',
onTap: widget.reply!,
),
], ],
const CustomDivider(), const CustomDivider(),
_ActionWidget( BottomSheetActionWidget(text: s.deleteComment, iconPath: 'assets/icons/red_trash.svg', onTap: widget.delete),
text: s.deleteComment,
iconPath: 'assets/icons/red_trash.svg',
onTap: widget.delete,
),
] else ...[ ] else ...[
const CustomDivider(), const CustomDivider(),
_ActionWidget( BottomSheetActionWidget(text: s.complain, iconPath: 'assets/icons/complain.svg', onTap: widget.complain),
text: s.complain, ],
iconPath: 'assets/icons/complain.svg',
onTap: widget.complain,
),
]
], ],
), ),
); );
} }
} }
class _ActionWidget extends StatelessWidget {
const _ActionWidget({required this.text, required this.iconPath, required this.onTap, this.needToPop = true});
final String text;
final String iconPath;
final void Function() onTap;
final bool needToPop;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
if (needToPop) Navigator.pop(context);
onTap();
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Row(
children: [
Expanded(
child: Text(
text,
style: TextStyles.textLMedium.copyWith(color: appTheme.black),
),
),
SvgPicture.asset(iconPath)
],
),
),
);
}
}

View File

@@ -2,6 +2,7 @@ import 'package:eatvio_app/core/env_config.dart';
String baseUrl = EnvConfig.baseUrl; String baseUrl = EnvConfig.baseUrl;
bool hidePlans = const bool.fromEnvironment('HIDE_PLANS', defaultValue: false); bool hidePlans = const bool.fromEnvironment('HIDE_PLANS', defaultValue: false);
const buildDate = String.fromEnvironment('BUILD_DATE', defaultValue: '');
const List<int> durationValues = [7, 14, 21]; const List<int> durationValues = [7, 14, 21];

View File

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

View File

@@ -7,6 +7,9 @@ class EnvConfig {
// Sentry Configuration // Sentry Configuration
static String get sentryDsn => dotenv.env['SENTRY_DSN'] ?? ''; static String get sentryDsn => dotenv.env['SENTRY_DSN'] ?? '';
// AppMetrica Configuration
static String get appMetricaApiKey => dotenv.env['APPMETRICA_KEY'] ?? '';
// Firebase Configuration // Firebase Configuration
static String get firebaseProjectId => dotenv.env['FIREBASE_PROJECT_ID'] ?? ''; static String get firebaseProjectId => dotenv.env['FIREBASE_PROJECT_ID'] ?? '';
static String get firebaseDatabaseUrl => dotenv.env['FIREBASE_DATABASE_URL'] ?? ''; static String get firebaseDatabaseUrl => dotenv.env['FIREBASE_DATABASE_URL'] ?? '';

View File

@@ -5,6 +5,9 @@ import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/dio/phone_verify_error_interceptor.dart'; import 'package:eatvio_app/core/dio/phone_verify_error_interceptor.dart';
import 'package:eatvio_app/core/firebase_api.dart'; import 'package:eatvio_app/core/firebase_api.dart';
import 'package:eatvio_app/core/interceptors/token_interceptor.dart'; import 'package:eatvio_app/core/interceptors/token_interceptor.dart';
import 'package:eatvio_app/core/views/cubit/views_cubit.dart';
import 'package:eatvio_app/core/views/repository/views_repository.dart';
import 'package:eatvio_app/core/views/service/views_service.dart';
import 'package:eatvio_app/core/websockets/ws_manager.dart'; import 'package:eatvio_app/core/websockets/ws_manager.dart';
import 'package:eatvio_app/core/websockets/ws_service.dart'; import 'package:eatvio_app/core/websockets/ws_service.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart';
@@ -20,6 +23,9 @@ import 'package:eatvio_app/features/cancel_plan_bottom_sheet/service/cancel_plan
import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_cubit.dart'; import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_cubit.dart';
import 'package:eatvio_app/features/complaining/repositories/complaining_repository.dart'; import 'package:eatvio_app/features/complaining/repositories/complaining_repository.dart';
import 'package:eatvio_app/features/complaining/services/complaining_service.dart'; import 'package:eatvio_app/features/complaining/services/complaining_service.dart';
import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart';
import 'package:eatvio_app/features/consultant_rating/repositories/consultant_rating_repository.dart';
import 'package:eatvio_app/features/consultant_rating/services/consultant_rating_service.dart';
import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_cubit.dart'; import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_cubit.dart';
import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart'; import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart';
import 'package:eatvio_app/features/consultations/services/consultation_service.dart'; import 'package:eatvio_app/features/consultations/services/consultation_service.dart';
@@ -40,8 +46,11 @@ import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_cubit.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_cubit.dart';
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart'; import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart';
import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart';
import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart'; import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart';
import 'package:eatvio_app/features/menu/repositories/notification_settings_repository.dart';
import 'package:eatvio_app/features/menu/services/blocked_users_service.dart'; import 'package:eatvio_app/features/menu/services/blocked_users_service.dart';
import 'package:eatvio_app/features/menu/services/notification_settings_service.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
import 'package:eatvio_app/features/calendar/presentation/bloc/calendar_cubit.dart'; import 'package:eatvio_app/features/calendar/presentation/bloc/calendar_cubit.dart';
@@ -107,7 +116,6 @@ import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/se
import 'package:eatvio_app/features/settings/repositories/service_repository.dart'; import 'package:eatvio_app/features/settings/repositories/service_repository.dart';
import 'package:eatvio_app/features/settings/services/settings_service.dart'; import 'package:eatvio_app/features/settings/services/settings_service.dart';
import 'package:eatvio_app/features/user_details/presentation/cubit/user_details_cubit.dart'; import 'package:eatvio_app/features/user_details/presentation/cubit/user_details_cubit.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart' as profile_plans; import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart' as profile_plans;
@@ -132,16 +140,18 @@ Future<void> init() async {
sl.registerFactory<DietsCubit>(() => DietsCubit(feedRepository: sl(), dietsRepository: sl())); sl.registerFactory<DietsCubit>(() => DietsCubit(feedRepository: sl(), dietsRepository: sl()));
sl.registerFactory<CommentsCubit>(() => CommentsCubit(feedRepository: sl(), commentsRepository: sl())); sl.registerFactory<CommentsCubit>(() => CommentsCubit(feedRepository: sl(), commentsRepository: sl()));
sl.registerFactory<PlansCubit>(() => PlansCubit(feedRepository: sl(), planRepository: sl())); sl.registerFactory<PlansCubit>(() => PlansCubit(feedRepository: sl(), planRepository: sl()));
sl.registerFactory<SettingsCubit>(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl(), consultantRepository: sl())); sl.registerFactory<SettingsCubit>(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl(), consultantRepository: sl(), consultationsRepository: sl()));
sl.registerFactory<RegisterConsultantCubit>(() => RegisterConsultantCubit(consultantRepository: sl())); sl.registerFactory<RegisterConsultantCubit>(() => RegisterConsultantCubit(consultantRepository: sl()));
sl.registerFactory<DiaryCubit>(() => DiaryCubit( sl.registerFactory<DiaryCubit>(
chatRepository: sl(), () => DiaryCubit(
diaryRepository: sl(), chatRepository: sl(),
glossaryRepository: sl(), diaryRepository: sl(),
profileRepository: sl(), glossaryRepository: sl(),
dietRepository: sl(), profileRepository: sl(),
settingsRepository: sl(), dietRepository: sl(),
)); settingsRepository: sl(),
),
);
sl.registerFactory<AboutDietCubit>(() => AboutDietCubit(dietsRepository: sl(), profileRepository: sl())); sl.registerFactory<AboutDietCubit>(() => AboutDietCubit(dietsRepository: sl(), profileRepository: sl()));
sl.registerFactory<BlogPostCreateCubit>(() => BlogPostCreateCubit(blogPostRepository: sl())); sl.registerFactory<BlogPostCreateCubit>(() => BlogPostCreateCubit(blogPostRepository: sl()));
sl.registerFactory<SubCubit>(() => SubCubit(profileRepository: sl())); sl.registerFactory<SubCubit>(() => SubCubit(profileRepository: sl()));
@@ -170,6 +180,9 @@ Future<void> init() async {
sl.registerFactory<ResetPasswordCubit>(() => ResetPasswordCubit(resetPasswordRepository: sl())); sl.registerFactory<ResetPasswordCubit>(() => ResetPasswordCubit(resetPasswordRepository: sl()));
sl.registerFactory<BlogPostCubit>(() => BlogPostCubit(sl())); sl.registerFactory<BlogPostCubit>(() => BlogPostCubit(sl()));
sl.registerFactory<BlockedUsersCubit>(() => BlockedUsersCubit(blockedUsersRepository: sl())); sl.registerFactory<BlockedUsersCubit>(() => BlockedUsersCubit(blockedUsersRepository: sl()));
sl.registerFactory<NotificationSettingsCubit>(() => NotificationSettingsCubit(notificationSettingsRepository: sl()));
sl.registerFactory<ConsultantRatingCubit>(() => ConsultantRatingCubit(sl()));
sl.registerFactory<ViewsCubit>(() => ViewsCubit(viewsRepository: sl()));
// Repository // Repository
sl.registerLazySingleton<ChatRepository>(() => ChatRepository(chatService: sl())); sl.registerLazySingleton<ChatRepository>(() => ChatRepository(chatService: sl()));
@@ -201,6 +214,9 @@ Future<void> init() async {
sl.registerFactory<ResetPasswordRepository>(() => ResetPasswordRepository(resetPasswordService: sl())); sl.registerFactory<ResetPasswordRepository>(() => ResetPasswordRepository(resetPasswordService: sl()));
sl.registerLazySingleton<BlockedUsersRepository>(() => BlockedUsersRepository(blockedUserService: sl())); sl.registerLazySingleton<BlockedUsersRepository>(() => BlockedUsersRepository(blockedUserService: sl()));
sl.registerLazySingleton<CommentsRepository>(() => CommentsRepository(service: sl())); sl.registerLazySingleton<CommentsRepository>(() => CommentsRepository(service: sl()));
sl.registerLazySingleton<NotificationSettingsRepository>(() => NotificationSettingsRepository(sl()));
sl.registerFactory<ConsultantRatingRepository>(() => ConsultantRatingRepository(sl()));
sl.registerFactory<ViewsRepository>(() => ViewsRepository(viewsService: sl()));
// Services // Services
sl.registerLazySingleton<ChatService>(() => ChatService(sl())); sl.registerLazySingleton<ChatService>(() => ChatService(sl()));
@@ -229,28 +245,19 @@ Future<void> init() async {
sl.registerLazySingleton<CancelPlanService>(() => CancelPlanService(sl())); sl.registerLazySingleton<CancelPlanService>(() => CancelPlanService(sl()));
sl.registerLazySingleton<CreatePlanService>(() => CreatePlanService(sl())); sl.registerLazySingleton<CreatePlanService>(() => CreatePlanService(sl()));
sl.registerLazySingleton<NotificationsService>(() => NotificationsService(sl())); sl.registerLazySingleton<NotificationsService>(() => NotificationsService(sl()));
sl.registerLazySingleton<MyRecipesService>(() => MyRecipesService(sl()));
sl.registerLazySingleton<FirebaseApi>(() => FirebaseApi()); sl.registerLazySingleton<FirebaseApi>(() => FirebaseApi());
sl.registerLazySingleton<ResetPasswordService>(() => ResetPasswordService(sl())); sl.registerLazySingleton<ResetPasswordService>(() => ResetPasswordService(sl()));
sl.registerLazySingleton<BlockedUserService>(() => BlockedUserService(sl())); sl.registerLazySingleton<BlockedUserService>(() => BlockedUserService(sl()));
sl.registerLazySingleton<CommentsService>(() => CommentsService(sl())); sl.registerLazySingleton<CommentsService>(() => CommentsService(sl()));
sl.registerLazySingleton<MyRecipesService>(() => MyRecipesService(sl()));
sl.registerLazySingleton<ConsultantRatingService>(() => ConsultantRatingService(sl()));
sl.registerLazySingleton<NotificationSettingsService>(() => NotificationSettingsService(sl()));
sl.registerLazySingleton<ViewsService>(() => ViewsService(sl()));
// External // External
sl.registerLazySingleton( sl.registerLazySingleton(
() => Dio( () =>
BaseOptions( Dio(BaseOptions(baseUrl: baseUrl, headers: {'accept': 'application/json', 'content-type': 'application/json'}))
baseUrl: baseUrl, ..interceptors.addAll([TokenInterceptor(), PhoneVerifyErrorInterceptor()]),
headers: {
'accept': 'application/json',
'content-type': 'application/json',
},
),
)..interceptors.addAll(
[
TokenInterceptor(),
PhoneVerifyErrorInterceptor(),
],
),
); );
sl.registerLazySingleton(() async => const FlutterSecureStorage());
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -47,12 +47,20 @@ class _AuthScreenState extends State<AuthScreen> {
super.initState(); super.initState();
var keyboardVisibilityController = KeyboardVisibilityController(); var keyboardVisibilityController = KeyboardVisibilityController();
keyboardSubscription = keyboardVisibilityController.onChange.listen((bool visible) { keyboardSubscription = keyboardVisibilityController.onChange.listen((bool visible) {
setState(() { if (mounted) {
isFocused = visible; setState(() {
}); isFocused = visible;
});
}
}); });
} }
@override
void dispose() {
keyboardSubscription.cancel();
super.dispose();
}
Future<void> _reconnectWebSocket(BuildContext context) async { Future<void> _reconnectWebSocket(BuildContext context) async {
try { try {
await sl<WsManager>().clear(); await sl<WsManager>().clear();

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:audioplayers/audioplayers.dart'; import 'package:audioplayers/audioplayers.dart';
import 'package:collection/collection.dart';
import 'package:dart_pusher_channels/dart_pusher_channels.dart'; import 'package:dart_pusher_channels/dart_pusher_channels.dart';
import 'package:easy_debounce/easy_debounce.dart'; import 'package:easy_debounce/easy_debounce.dart';
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
@@ -19,6 +20,7 @@ import 'package:eatvio_app/features/my_finance/models/card_model.dart';
import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart'; import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart';
import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart'; import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:uuid/uuid.dart';
class ChatRoomCubit extends Cubit<ChatRoomState> { class ChatRoomCubit extends Cubit<ChatRoomState> {
final ChatRepository chatRepository; final ChatRepository chatRepository;
@@ -31,12 +33,9 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
late int lastPage; late int lastPage;
bool isLoading = false; bool isLoading = false;
Timer? _timer; Timer? _timer;
var uuid = const Uuid();
ChatRoomCubit({ ChatRoomCubit({required this.chatRepository, required this.wsManager, required this.cardsRepository}) : super(const ChatRoomState.loading());
required this.chatRepository,
required this.wsManager,
required this.cardsRepository,
}) : super(const ChatRoomState.loading());
void setTimer() { void setTimer() {
_timer?.cancel(); _timer?.cancel();
@@ -89,10 +88,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
} }
} }
Future<void> initChat( Future<void> initChat(String uuid, {bool sendInitialMessage = false}) async {
String uuid, {
bool sendInitialMessage = false,
}) async {
try { try {
final chatRoom = await chatRepository.getChatRoom(uuid); final chatRoom = await chatRepository.getChatRoom(uuid);
ChosenPlanModel? chosenPlan; ChosenPlanModel? chosenPlan;
@@ -171,11 +167,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
List<ChatMessageModel> messagesList = [message, ...(state as ChatRoomStateLoaded).chatMessages]; List<ChatMessageModel> messagesList = [message, ...(state as ChatRoomStateLoaded).chatMessages];
chatRepository.emitNewMessageToChats(message); chatRepository.emitNewMessageToChats(message);
emit((state as ChatRoomStateLoaded).copyWith(chatMessages: messagesList, isLoading: isLoading)); emit((state as ChatRoomStateLoaded).copyWith(chatMessages: messagesList, isLoading: isLoading));
EasyDebounce.debounce( EasyDebounce.debounce('searchChatRoom', const Duration(milliseconds: 300), () => sl<ChatsCubit>().getChatRooms());
'searchChatRoom',
const Duration(milliseconds: 300),
() => sl<ChatsCubit>().getChatRooms(),
);
}); });
typingSubscription = wsManager.clientTypingStream?.listen((event) { typingSubscription = wsManager.clientTypingStream?.listen((event) {
@@ -183,22 +175,14 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
EasyDebounce.debounce( EasyDebounce.debounce(
'client-typing', 'client-typing',
const Duration(seconds: 3), const Duration(seconds: 3),
() => emit( () => emit((state as ChatRoomStateLoaded).copyWith(isTyping: false, isLoading: isLoading)),
(state as ChatRoomStateLoaded).copyWith(
isTyping: false,
isLoading: isLoading,
),
),
); );
}); });
} }
Future<bool> sendMessage({ Future<bool> sendMessage({String? data, List<File>? files, required String userSlug}) async {
String? data,
List<File>? files,
required String userSlug,
}) async {
try { try {
final messageUuid = uuid.v4();
final compressedFiles = files != null ? await ImageCompressUtility.compressFilesList(List<File>.from(files)) : null; final compressedFiles = files != null ? await ImageCompressUtility.compressFilesList(List<File>.from(files)) : null;
AudioPlayer().play((AssetSource('audio/send_message.wav'))); AudioPlayer().play((AssetSource('audio/send_message.wav')));
emit( emit(
@@ -213,16 +197,38 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
isRead: true, isRead: true,
isCurrentUserAuthor: true, isCurrentUserAuthor: true,
roomId: '', roomId: '',
messageState: MessageState.loading,
uuid: messageUuid,
), ),
...(state as ChatRoomStateLoaded).chatMessages, ...(state as ChatRoomStateLoaded).chatMessages,
], ],
), ),
); );
await chatRepository.sendMessage(
data: data, try {
files: compressedFiles, await chatRepository.sendMessage(data: data, files: compressedFiles, slug: userSlug);
slug: userSlug, emit(
); (state as ChatRoomStateLoaded).copyWith(
chatMessages: (state as ChatRoomStateLoaded).chatMessages.map((elem) {
if (elem.uuid == messageUuid) {
return elem.copyWith(messageState: MessageState.sent);
}
return elem;
}).toList(),
),
);
} catch (e) {
emit(
(state as ChatRoomStateLoaded).copyWith(
chatMessages: (state as ChatRoomStateLoaded).chatMessages.map((elem) {
if (elem.uuid == messageUuid) {
return elem.copyWith(messageState: MessageState.error);
}
return elem;
}).toList(),
),
);
}
sl<ChatsCubit>().getChatRooms(); sl<ChatsCubit>().getChatRooms();
return true; return true;
} catch (e, st) { } catch (e, st) {
@@ -268,7 +274,10 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
try { try {
final planId = (state as ChatRoomStateLoaded).chatRoom.chosenPlan?.id; final planId = (state as ChatRoomStateLoaded).chatRoom.chosenPlan?.id;
if (planId == null) return false; if (planId == null) return false;
await chatRepository.complainAboutConsultation(id: planId, body: PlanComplainModel(description: description)); await chatRepository.complainAboutConsultation(
id: planId,
body: PlanComplainModel(description: description),
);
return true; return true;
} catch (e) { } catch (e) {
log(e.toString()); log(e.toString());
@@ -296,4 +305,70 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
typingSubscription?.cancel(); typingSubscription?.cancel();
super.close(); super.close();
} }
void deleteMessage(String uuid) {
final currentState = state;
if (currentState is ChatRoomStateLoaded) {
emit(currentState.copyWith(chatMessages: currentState.chatMessages.where((message) => message.uuid != uuid).toList()));
}
}
Future<void> resendMessage({required String uuid, required String userSlug}) async {
final currentState = state;
if (currentState is ChatRoomStateLoaded) {
final message = currentState.chatMessages.firstWhereOrNull((message) => message.uuid == uuid);
if (message != null) {
try {
AudioPlayer().play((AssetSource('audio/send_message.wav')));
emit(
(state as ChatRoomStateLoaded).copyWith(
chatMessages: [
ChatMessageModel(
id: 1,
data: message.data,
mediaData: message.mediaData,
authorId: 1,
updatedAt: DateTime.now(),
isRead: true,
isCurrentUserAuthor: true,
roomId: '',
messageState: MessageState.loading,
uuid: uuid,
),
...(state as ChatRoomStateLoaded).chatMessages.where((message) => message.uuid != uuid),
],
),
);
try {
await chatRepository.sendMessage(data: message.data, files: message.mediaData as List<File>?, slug: userSlug);
emit(
(state as ChatRoomStateLoaded).copyWith(
chatMessages: (state as ChatRoomStateLoaded).chatMessages.map((elem) {
if (elem.uuid == uuid) {
return elem.copyWith(messageState: MessageState.sent);
}
return elem;
}).toList(),
),
);
} catch (e) {
emit(
(state as ChatRoomStateLoaded).copyWith(
chatMessages: (state as ChatRoomStateLoaded).chatMessages.map((elem) {
if (elem.uuid == uuid) {
return elem.copyWith(messageState: MessageState.error);
}
return elem;
}).toList(),
),
);
}
sl<ChatsCubit>().getChatRooms();
} catch (e, st) {
log(e.toString(), stackTrace: st);
}
}
}
}
} }

View File

@@ -13,6 +13,8 @@ import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.da
import 'package:eatvio_app/features/chat/presentation/widgets/about_order_button.dart'; import 'package:eatvio_app/features/chat/presentation/widgets/about_order_button.dart';
import 'package:eatvio_app/features/chat/presentation/widgets/chat_bubble_widget.dart'; import 'package:eatvio_app/features/chat/presentation/widgets/chat_bubble_widget.dart';
import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart';
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_state.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/chat/presentation/widgets/notification_banner.dart'; import 'package:eatvio_app/features/chat/presentation/widgets/notification_banner.dart';
@@ -93,7 +95,10 @@ class _ChatScreenState extends State<ChatScreen> {
if (state is ChatRoomStateLoaded) { if (state is ChatRoomStateLoaded) {
return _buildChat(context, state); return _buildChat(context, state);
} else if (state is ChatRoomStateError) { } else if (state is ChatRoomStateError) {
return Scaffold(appBar: AppBar(), body: DataErrorWidget(onReload: () => chatRoomCubit.initChat(widget.uuid))); return Scaffold(
appBar: AppBar(),
body: DataErrorWidget(onReload: () => chatRoomCubit.initChat(widget.uuid)),
);
} else { } else {
return const Scaffold(body: Loader()); return const Scaffold(body: Loader());
} }
@@ -168,34 +173,19 @@ class _ChatScreenState extends State<ChatScreen> {
width: 40, width: 40,
height: 40, height: 40,
alignment: Alignment.center, alignment: Alignment.center,
decoration: const BoxDecoration( decoration: const BoxDecoration(color: Color(0xFFEBF4FF), shape: BoxShape.circle),
color: Color(0xFFEBF4FF),
shape: BoxShape.circle,
),
child: Text( child: Text(
state.chatRoom.users.receiver.name.split(' ').first[0] + state.chatRoom.users.receiver.name.split(' ')[1][0], state.chatRoom.users.receiver.name.split(' ').first[0] + state.chatRoom.users.receiver.name.split(' ')[1][0],
style: const TextStyle( style: const TextStyle(color: Color(0xFF7F9CF5), fontWeight: FontWeight.w400, fontSize: 20),
color: Color(0xFF7F9CF5),
fontWeight: FontWeight.w400,
fontSize: 20,
),
), ),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(state.chatRoom.users.receiver.name, style: CustomTextStyles.titleSmallBold.copyWith(color: PrimaryColors().black)),
state.chatRoom.users.receiver.name,
style: CustomTextStyles.titleSmallBold.copyWith(
color: PrimaryColors().black,
),
),
AnimatedCrossFade( AnimatedCrossFade(
firstChild: Text( firstChild: Text('Печатает...', style: CustomTextStyles.bodySmallMulishPrimary),
'Печатает...',
style: CustomTextStyles.bodySmallMulishPrimary,
),
firstCurve: Curves.easeIn, firstCurve: Curves.easeIn,
secondCurve: Curves.easeOut, secondCurve: Curves.easeOut,
secondChild: const SizedBox.shrink(), secondChild: const SizedBox.shrink(),
@@ -251,9 +241,9 @@ class _ChatScreenState extends State<ChatScreen> {
if (!result) return; if (!result) return;
if (!context.mounted) return; if (!context.mounted) return;
context.read<ChatsCubit>().reduceUnreadMessagesCount( context.read<ChatsCubit>().reduceUnreadMessagesCount(
chatId: state.chatRoom.id, chatId: state.chatRoom.id,
count: unreadMessagesIdsCopy.length, count: unreadMessagesIdsCopy.length,
); );
unreadMessagesIdsCopy.clear(); unreadMessagesIdsCopy.clear();
}); });
} }
@@ -264,6 +254,9 @@ class _ChatScreenState extends State<ChatScreen> {
text: state.chatMessages[index].data.trim(), text: state.chatMessages[index].data.trim(),
isCurrentUser: isCurrentUserAuthor, isCurrentUser: isCurrentUserAuthor,
mediaData: state.chatMessages[index].mediaData ?? [], mediaData: state.chatMessages[index].mediaData ?? [],
messageState: state.chatMessages[index].messageState,
messageUuid: state.chatMessages[index].uuid ?? '',
userSlug: state.chatRoom.users.receiver.slug,
), ),
), ),
], ],
@@ -290,6 +283,9 @@ class _ChatScreenState extends State<ChatScreen> {
text: state.chatMessages[index].data.trim(), text: state.chatMessages[index].data.trim(),
isCurrentUser: isCurrentUserAuthor, isCurrentUser: isCurrentUserAuthor,
mediaData: state.chatMessages[index].mediaData ?? [], mediaData: state.chatMessages[index].mediaData ?? [],
messageState: state.chatMessages[index].messageState,
messageUuid: state.chatMessages[index].uuid ?? '',
userSlug: state.chatRoom.users.receiver.slug,
), ),
); );
}, },
@@ -361,11 +357,7 @@ class _ChatScreenState extends State<ChatScreen> {
final imageIndex = _imageIndexAt(pickedFiles, i); final imageIndex = _imageIndexAt(pickedFiles, i);
context.pushTransparentRoute( context.pushTransparentRoute(
PhotoViewScreen( PhotoViewScreen(imageUrls: imageFiles.map((e) => e.path).toList(), isLocal: true, index: imageIndex),
imageUrls: imageFiles.map((e) => e.path).toList(),
isLocal: true,
index: imageIndex,
),
); );
}, },
child: Hero( child: Hero(
@@ -373,14 +365,7 @@ class _ChatScreenState extends State<ChatScreen> {
transitionOnUserGestures: true, transitionOnUserGestures: true,
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: SizedBox( child: SizedBox(height: 104, width: 104, child: Image.file(pickedFiles[i], fit: BoxFit.cover)),
height: 104,
width: 104,
child: Image.file(
pickedFiles[i],
fit: BoxFit.cover,
),
),
), ),
), ),
) )
@@ -397,15 +382,8 @@ class _ChatScreenState extends State<ChatScreen> {
}, },
child: Container( child: Container(
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),
decoration: BoxDecoration( decoration: BoxDecoration(color: appTheme.gray50, shape: BoxShape.circle),
color: appTheme.gray50, child: const Icon(size: 20, Icons.close, color: Colors.black),
shape: BoxShape.circle,
),
child: const Icon(
size: 20,
Icons.close,
color: Colors.black,
),
), ),
), ),
), ),
@@ -427,7 +405,10 @@ class _ChatScreenState extends State<ChatScreen> {
child: Ink( child: Ink(
height: 40, height: 40,
width: 40, width: 40,
child: Transform.rotate(angle: pi / 4, child: const Icon(Icons.attach_file_rounded, size: 30, color: Color(0xFF858E99))), child: Transform.rotate(
angle: pi / 4,
child: const Icon(Icons.attach_file_rounded, size: 30, color: Color(0xFF858E99)),
),
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
@@ -449,7 +430,9 @@ class _ChatScreenState extends State<ChatScreen> {
hintStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w400, height: 1, color: Color(0xFFAEAEB2)), hintStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w400, height: 1, color: Color(0xFFAEAEB2)),
filled: true, filled: true,
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(100), borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1)), borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(100), borderRadius: BorderRadius.circular(100),
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1), borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
@@ -465,7 +448,27 @@ class _ChatScreenState extends State<ChatScreen> {
const SizedBox(width: 8), const SizedBox(width: 8),
InkWell( InkWell(
customBorder: const CircleBorder(), customBorder: const CircleBorder(),
onTap: () { onTap: () async {
// Проверяем, заблокирован ли пользователь
final blockedUsersCubit = sl<BlockedUsersCubit>();
await blockedUsersCubit.getBlockedUsers();
final blockedState = blockedUsersCubit.state;
if (blockedState is BlockedUsersStateLoaded) {
final isBlocked = blockedState.profilesList.any((profile) => profile.slug == state.chatRoom.users.receiver.slug);
if (isBlocked) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Вы не можете отправить сообщение заблокированному пользователю'),
backgroundColor: Colors.red,
),
);
}
return;
}
}
if (textEditingController.text.isNotEmpty || pickedFiles.isNotEmpty) { if (textEditingController.text.isNotEmpty || pickedFiles.isNotEmpty) {
chatRoomCubit.sendMessage( chatRoomCubit.sendMessage(
data: textEditingController.text.isEmpty ? null : textEditingController.text.trim(), data: textEditingController.text.isEmpty ? null : textEditingController.text.trim(),
@@ -525,13 +528,7 @@ class _ChatScreenState extends State<ChatScreen> {
const Icon(Icons.attach_file, size: 32, color: Colors.black54), const Icon(Icons.attach_file, size: 32, color: Colors.black54),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 4), padding: const EdgeInsets.symmetric(horizontal: 4),
child: Text( child: Text(fileName, textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12)),
fileName,
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 12),
),
), ),
], ],
), ),
@@ -579,13 +576,7 @@ class _ChatScreenState extends State<ChatScreen> {
// await chatRoomCubit.sendMessage(files: [File(image.path)], userSlug: slug); // await chatRoomCubit.sendMessage(files: [File(image.path)], userSlug: slug);
// } // }
}, },
child: const Row( child: const Row(children: [Icon(Icons.photo_camera), SizedBox(width: 8), Text('Фото')]),
children: [
Icon(Icons.photo_camera),
SizedBox(width: 8),
Text('Фото'),
],
),
), ),
CupertinoActionSheetAction( CupertinoActionSheetAction(
onPressed: () async { onPressed: () async {
@@ -600,13 +591,7 @@ class _ChatScreenState extends State<ChatScreen> {
// await chatRoomCubit.sendMessage(files: files, userSlug: slug); // await chatRoomCubit.sendMessage(files: files, userSlug: slug);
} }
}, },
child: const Row( child: const Row(children: [Icon(Icons.photo_library_rounded), SizedBox(width: 8), Text('Галерея')]),
children: [
Icon(Icons.photo_library_rounded),
SizedBox(width: 8),
Text('Галерея'),
],
),
), ),
CupertinoActionSheetAction( CupertinoActionSheetAction(
onPressed: () async { onPressed: () async {
@@ -643,20 +628,10 @@ class _ChatScreenState extends State<ChatScreen> {
// await chatRoomCubit.sendMessage(files: files, userSlug: slug); // await chatRoomCubit.sendMessage(files: files, userSlug: slug);
} }
}, },
child: const Row( child: const Row(children: [Icon(Icons.file_present_rounded), SizedBox(width: 8), Text('Файлы')]),
children: [
Icon(Icons.file_present_rounded),
SizedBox(width: 8),
Text('Файлы'),
],
),
), ),
], ],
cancelButton: CupertinoActionSheetAction( cancelButton: CupertinoActionSheetAction(onPressed: () => Navigator.of(context).pop(), isDestructiveAction: true, child: const Text('Отмена')),
onPressed: () => Navigator.of(context).pop(),
isDestructiveAction: true,
child: const Text('Отмена'),
),
), ),
); );
} }
@@ -665,7 +640,10 @@ class _ChatScreenState extends State<ChatScreen> {
return Center( return Center(
child: Padding( child: Padding(
padding: const EdgeInsets.only(top: 32), padding: const EdgeInsets.only(top: 32),
child: Text(DateFormat.yMMMMd('RU').format(date), style: const TextStyle(height: 1, color: Color(0xFF8393A1), fontWeight: FontWeight.w500)), child: Text(
DateFormat.yMMMMd('RU').format(date),
style: const TextStyle(height: 1, color: Color(0xFF8393A1), fontWeight: FontWeight.w500),
),
), ),
); );
} }

View File

@@ -12,6 +12,8 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart';
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_state.dart';
class EmptyChatScreen extends StatefulWidget { class EmptyChatScreen extends StatefulWidget {
final Author author; final Author author;
@@ -133,21 +135,45 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
InkWell( InkWell(
customBorder: const CircleBorder(), customBorder: const CircleBorder(),
onTap: () async { onTap: () async {
// Проверяем, заблокирован ли пользователь
final blockedUsersCubit = sl<BlockedUsersCubit>();
await blockedUsersCubit.getBlockedUsers();
final blockedState = blockedUsersCubit.state;
if (blockedState is BlockedUsersStateLoaded) {
final isBlocked = blockedState.profilesList.any((profile) => profile.slug == widget.author.authorSlug);
if (isBlocked) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Вы не можете отправить сообщение заблокированному пользователю'),
backgroundColor: Colors.red,
),
);
}
return;
}
}
if (textEditingController.text.isNotEmpty) { if (textEditingController.text.isNotEmpty) {
await sl<ChatRepository>().sendMessage( await sl<ChatRepository>().sendMessage(
data: textEditingController.text.trim(), data: textEditingController.text.trim(),
slug: widget.author.authorSlug, slug: widget.author.authorSlug,
); );
setState(() { if (mounted) {
textEditingController.clear(); setState(() {
}); textEditingController.clear();
});
}
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug); final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
AppRouter.navigatorKey.currentContext?.pushReplacementNamed( if (context.mounted) {
'chatScreen', AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
extra: { 'chatScreen',
'uuid': chatIdOrError, extra: {
}, 'uuid': chatIdOrError,
); },
);
}
} }
}, },
child: Ink( child: Ink(
@@ -175,20 +201,44 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
CupertinoActionSheetAction( CupertinoActionSheetAction(
onPressed: () async { onPressed: () async {
Navigator.of(context).pop(); Navigator.of(context).pop();
// Проверяем, заблокирован ли пользователь
final blockedUsersCubit = sl<BlockedUsersCubit>();
await blockedUsersCubit.getBlockedUsers();
final blockedState = blockedUsersCubit.state;
if (blockedState is BlockedUsersStateLoaded) {
final isBlocked = blockedState.profilesList.any((profile) => profile.slug == widget.author.authorSlug);
if (isBlocked) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Вы не можете отправить сообщение заблокированному пользователю'),
backgroundColor: Colors.red,
),
);
}
return;
}
}
final picker = ImagePicker(); final picker = ImagePicker();
XFile? image = await picker.pickImage(source: ImageSource.camera); List<XFile>? images = await picker.pickMultiImage();
if (image != null) { if (images.isNotEmpty) {
List<File> files = images.map((image) => File(image.path)).toList();
await sl<ChatRepository>().sendMessage( await sl<ChatRepository>().sendMessage(
files: [File(image.path)], files: files,
slug: widget.author.authorSlug, slug: widget.author.authorSlug,
); );
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug); final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
AppRouter.navigatorKey.currentContext?.pushReplacementNamed( if (mounted) {
'chatScreen', AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
extra: { 'chatScreen',
'uuid': chatIdOrError, extra: {
}, 'uuid': chatIdOrError,
); },
);
}
} }
}, },
child: const Row( child: const Row(
@@ -211,12 +261,14 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
slug: widget.author.authorSlug, slug: widget.author.authorSlug,
); );
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug); final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
AppRouter.navigatorKey.currentContext?.pushReplacementNamed( if (mounted) {
'chatScreen', AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
extra: { 'chatScreen',
'uuid': chatIdOrError, extra: {
}, 'uuid': chatIdOrError,
); },
);
}
} }
}, },
child: const Row( child: const Row(
@@ -261,12 +313,14 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
slug: widget.author.authorSlug, slug: widget.author.authorSlug,
); );
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug); final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
AppRouter.navigatorKey.currentContext?.pushReplacementNamed( if (mounted) {
'chatScreen', AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
extra: { 'chatScreen',
'uuid': chatIdOrError, extra: {
}, 'uuid': chatIdOrError,
); },
);
}
} }
}, },
child: const Row( child: const Row(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,13 +15,55 @@ class DiaryRepository {
Stream<String> get forceFetchStream => _diaryForceFetchController.stream; Stream<String> get forceFetchStream => _diaryForceFetchController.stream;
DiaryRepository({ DiaryRepository({required this.diaryService});
required this.diaryService,
});
Future<DiaryDayModel> getDiaryByDate(String date) async { Future<DiaryDayModel> getDiaryByDate(String date) async {
try { try {
return (await getDiaryIndexByDate(date)).last; Set<int> ids = {};
final DiaryDayModel diaryDay = (await getDiaryIndexByDate(date)).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();
updatedMeals[mealIndex] = meal.copyWith(portions: updatedPortions);
}
final updatedDiaryDay = diaryDay.copyWith(meals: updatedMeals);
return updatedDiaryDay;
}
return diaryDay;
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
@@ -75,109 +117,47 @@ class DiaryRepository {
int? productNutritionalValueId, int? productNutritionalValueId,
required PortionState portionState, required PortionState portionState,
}) async { }) async {
Map<String, Object?> data = ProductInMeal( Map<String, Object?> data = ProductInMeal(productName: productName, portion: portion, quantity: quantity).toJson();
productName: productName,
portion: portion,
quantity: quantity,
).toJson();
data['product_nutritional_value_id'] = productNutritionalValueId; data['product_nutritional_value_id'] = productNutritionalValueId;
data['state'] = portionState.name; data['state'] = portionState.name;
return await diaryService.updateProductInMeal( return await diaryService.updateProductInMeal(diary: diaryId, typeNo: mealId, portionModel: portionId, productInMeal: data);
diary: diaryId,
typeNo: mealId,
portionModel: portionId,
productInMeal: data,
);
} }
Future<void> markEaten({ Future<void> markEaten({required int diaryId, required int mealId, required int portionId}) async {
required int diaryId, return await diaryService.markEaten(diary: diaryId, typeNo: mealId, portionModel: portionId);
required int mealId,
required int portionId,
}) async {
return await diaryService.markEaten(
diary: diaryId,
typeNo: mealId,
portionModel: portionId,
);
} }
Future<void> unmarkEaten({ Future<void> unmarkEaten({required int diaryId, required int mealId, required int portionId}) async {
required int diaryId, return await diaryService.unmarkEaten(diary: diaryId, typeNo: mealId, portionModel: portionId);
required int mealId,
required int portionId,
}) async {
return await diaryService.unmarkEaten(
diary: diaryId,
typeNo: mealId,
portionModel: portionId,
);
} }
Future<void> restore({ Future<void> restore({required int diaryId, required int mealId, required int portionId}) async {
required int diaryId, return await diaryService.restore(diary: diaryId, typeNo: mealId, portionModel: portionId);
required int mealId,
required int portionId,
}) async {
return await diaryService.restore(
diary: diaryId,
typeNo: mealId,
portionModel: portionId,
);
} }
Future<void> deleteProductFromMeal({ Future<void> deleteProductFromMeal({required int diaryId, required int mealId, required int portionId}) async {
required int diaryId, return await diaryService.deleteProductFromMeal(diary: diaryId, typeNo: mealId, portionModel: portionId);
required int mealId,
required int portionId,
}) async {
return await diaryService.deleteProductFromMeal(
diary: diaryId,
typeNo: mealId,
portionModel: portionId,
);
} }
Future<void> uploadPhotos({ Future<void> uploadPhotos({required int diaryId, required int mealId, required List<File> files}) async {
required int diaryId,
required int mealId,
required List<File> files,
}) async {
try { try {
for (int i = 0; i < files.length; i++) { for (int i = 0; i < files.length; i++) {
await diaryService.uploadPhotoOfMeal( await diaryService.uploadPhotoOfMeal(diary: diaryId, typeNo: mealId, photo: files[i]);
diary: diaryId,
typeNo: mealId,
photo: files[i],
);
} }
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
} }
Future<void> deletePhoto({ Future<void> deletePhoto({required int diaryId, required int mealId, required DiaryPhotoModel photo}) async {
required int diaryId,
required int mealId,
required DiaryPhotoModel photo,
}) async {
try { try {
await diaryService.deletePhotoOfMeal( await diaryService.deletePhotoOfMeal(diaryId: diaryId, typeNo: mealId, userAttachment: photo.id);
diaryId: diaryId,
typeNo: mealId,
userAttachment: photo.id,
);
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
} }
Future<void> finishDiaryDay({ Future<void> finishDiaryDay({required int diaryId, required String wakeUp, required String retire, String? comment}) async {
required int diaryId,
required String wakeUp,
required String retire,
String? comment,
}) async {
return await diaryService.finishDiaryDay( return await diaryService.finishDiaryDay(
diary: diaryId, diary: diaryId,
finishDiaryDayModel: FinishDiaryDayModel(wakeUp: wakeUp, retire: retire, comment: comment), finishDiaryDayModel: FinishDiaryDayModel(wakeUp: wakeUp, retire: retire, comment: comment),
@@ -192,87 +172,45 @@ class DiaryRepository {
required double carbohydrates, required double carbohydrates,
}) async { }) async {
return await diaryService.sendProductToModeration( return await diaryService.sendProductToModeration(
data: ProductModerationModel( data: ProductModerationModel(name: name, energy: energy, proteins: proteins, fats: fats, carbohydrates: carbohydrates).toJson(),
name: name,
energy: energy,
proteins: proteins,
fats: fats,
carbohydrates: carbohydrates,
).toJson(),
); );
} }
Future<void> fillSatiety({ Future<void> fillSatiety({required int diaryId, required int mealId, required int satiety}) async {
required int diaryId,
required int mealId,
required int satiety,
}) async {
try { try {
return await diaryService.fillSatiety( return await diaryService.fillSatiety(diary: diaryId, typeNo: mealId, satiety: satiety);
diary: diaryId,
typeNo: mealId,
satiety: satiety,
);
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
} }
Future<void> fillMood({ Future<void> fillMood({required int diaryId, required int mealId, required int mood}) async {
required int diaryId,
required int mealId,
required int mood,
}) async {
try { try {
return await diaryService.fillMood( return await diaryService.fillMood(diary: diaryId, typeNo: mealId, mood: mood);
diary: diaryId,
typeNo: mealId,
mood: mood,
);
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
} }
Future<void> fillTime({ Future<void> fillTime({required int diaryId, required int mealId, required String time}) async {
required int diaryId,
required int mealId,
required String time,
}) async {
try { try {
return await diaryService.fillTime( return await diaryService.fillTime(diary: diaryId, typeNo: mealId, time: time);
diary: diaryId,
typeNo: mealId,
time: time,
);
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
} }
Future<void> fillWakeup({ Future<void> fillWakeup({required int diaryId, required String time}) async {
required int diaryId,
required String time,
}) async {
try { try {
return await diaryService.fillWakeup( return await diaryService.fillWakeup(diary: diaryId, wakeUp: time);
diary: diaryId,
wakeUp: time,
);
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
} }
Future<void> fillRetire({ Future<void> fillRetire({required int diaryId, required String time}) async {
required int diaryId,
required String time,
}) async {
try { try {
return await diaryService.fillRetire( return await diaryService.fillRetire(diary: diaryId, retire: time);
diary: diaryId,
retire: time,
);
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
@@ -287,10 +225,7 @@ class DiaryRepository {
} }
} }
Future<ConsultantReviewModel> getDiaryComments({ Future<ConsultantReviewModel> getDiaryComments({required int planId, required int diaryId}) async {
required int planId,
required int diaryId,
}) async {
try { try {
return await diaryService.getDiaryComments(planId: planId, diaryId: diaryId); return await diaryService.getDiaryComments(planId: planId, diaryId: diaryId);
} catch (e) { } catch (e) {
@@ -305,4 +240,18 @@ class DiaryRepository {
rethrow; rethrow;
} }
} }
Future<GlossaryPaginationModel> getGlossariesIndex({required List<int> ids}) async {
Map<String, dynamic> filters = {};
if (ids.isNotEmpty) {
for (int i = 0; i < ids.length; i++) {
filters['filters[id][\$in][$i]'] = ids[i];
}
}
try {
return await diaryService.getGlossariesIndex(filters: filters);
} catch (e) {
rethrow;
}
}
} }

View File

@@ -15,32 +15,20 @@ abstract class DiaryService {
factory DiaryService(Dio dio, {String baseUrl}) = _DiaryService; factory DiaryService(Dio dio, {String baseUrl}) = _DiaryService;
@GET('/api/diary/index/{date}') @GET('/api/diary/index/{date}')
Future<List<DiaryDayModel>> getDiaryIndexByDate({ Future<List<DiaryDayModel>> getDiaryIndexByDate({@Path('date') required String date});
@Path('date') required String date,
});
@GET('/api/diary/index/{date}') @GET('/api/diary/index/{date}')
Future<List<DiaryFutureDietDayModel>> getDiaryIndexByDateForDietOnly({ Future<List<DiaryFutureDietDayModel>> getDiaryIndexByDateForDietOnly({@Path('date') required String date});
@Path('date') required String date,
});
@GET('/api/diary/{diary}/show') @GET('/api/diary/{diary}/show')
Future<DiaryDayWrapperModel> getDiaryDay({ Future<DiaryDayWrapperModel> getDiaryDay({@Path('diary') required int diary});
@Path('diary') required int diary,
});
@GET('/api/diary/{diary}/dailyNutritionData') @GET('/api/diary/{diary}/dailyNutritionData')
Future<NutritionDataModel> getNutrition({ Future<NutritionDataModel> getNutrition({@Path('diary') required int diary});
@Path('diary') required int diary,
});
@POST('/api/diary/{diary}/meals/{type_no}/photo/upload') @POST('/api/diary/{diary}/meals/{type_no}/photo/upload')
@MultiPart() @MultiPart()
Future<void> uploadPhotoOfMeal({ Future<void> uploadPhotoOfMeal({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Part(name: 'photo') required File photo});
@Path('diary') required int diary,
@Path('type_no') required int typeNo,
@Part(name: 'photo') required File photo,
});
@DELETE('/api/diary/{diary}/meals/{type_no}/photo/{user_attachment}/delete') @DELETE('/api/diary/{diary}/meals/{type_no}/photo/{user_attachment}/delete')
Future<void> deletePhotoOfMeal({ Future<void> deletePhotoOfMeal({
@@ -68,25 +56,13 @@ abstract class DiaryService {
}); });
@POST('/api/diary/{diary}/meals/{type_no}/{portionModel}/mark_eaten') @POST('/api/diary/{diary}/meals/{type_no}/{portionModel}/mark_eaten')
Future<void> markEaten({ Future<void> markEaten({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Path('portionModel') required int portionModel});
@Path('diary') required int diary,
@Path('type_no') required int typeNo,
@Path('portionModel') required int portionModel,
});
@POST('/api/diary/{diary}/meals/{type_no}/{portionModel}/unmark_eaten') @POST('/api/diary/{diary}/meals/{type_no}/{portionModel}/unmark_eaten')
Future<void> unmarkEaten({ Future<void> unmarkEaten({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Path('portionModel') required int portionModel});
@Path('diary') required int diary,
@Path('type_no') required int typeNo,
@Path('portionModel') required int portionModel,
});
@POST('/api/diary/{diary}/meals/{type_no}/portions/{portionModel}/restore') @POST('/api/diary/{diary}/meals/{type_no}/portions/{portionModel}/restore')
Future<void> restore({ Future<void> restore({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Path('portionModel') required int portionModel});
@Path('diary') required int diary,
@Path('type_no') required int typeNo,
@Path('portionModel') required int portionModel,
});
@DELETE('/api/diary/{diary}/meals/{type_no}/portions/{portionModel}/delete') @DELETE('/api/diary/{diary}/meals/{type_no}/portions/{portionModel}/delete')
Future<void> deleteProductFromMeal({ Future<void> deleteProductFromMeal({
@@ -96,63 +72,35 @@ abstract class DiaryService {
}); });
@POST('/api/diary/{diary}/finishDay') @POST('/api/diary/{diary}/finishDay')
Future<void> finishDiaryDay({ Future<void> finishDiaryDay({@Path('diary') required int diary, @Body(nullToAbsent: true) required FinishDiaryDayModel finishDiaryDayModel});
@Path('diary') required int diary,
@Body(nullToAbsent: true) required FinishDiaryDayModel finishDiaryDayModel,
});
@POST('/api/diary/{diary}/meals/{type_no}/satiety/fill') @POST('/api/diary/{diary}/meals/{type_no}/satiety/fill')
Future<void> fillSatiety({ Future<void> fillSatiety({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Field() required int satiety});
@Path('diary') required int diary,
@Path('type_no') required int typeNo,
@Field() required int satiety,
});
@POST('/api/diary/{diary}/meals/{type_no}/mood/fill') @POST('/api/diary/{diary}/meals/{type_no}/mood/fill')
Future<void> fillMood({ Future<void> fillMood({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Field() required int mood});
@Path('diary') required int diary,
@Path('type_no') required int typeNo,
@Field() required int mood,
});
@POST('/api/diary/{diary}/meals/{type_no}/time/fill') @POST('/api/diary/{diary}/meals/{type_no}/time/fill')
Future<void> fillTime({ Future<void> fillTime({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Field() required String time});
@Path('diary') required int diary,
@Path('type_no') required int typeNo,
@Field() required String time,
});
@POST('/api/diary/{diary}/wakeUp/fill') @POST('/api/diary/{diary}/wakeUp/fill')
Future<void> fillWakeup({ Future<void> fillWakeup({@Path('diary') required int diary, @Field() required String wakeUp});
@Path('diary') required int diary,
@Field() required String wakeUp,
});
@POST('/api/diary/{diary}/retire/fill') @POST('/api/diary/{diary}/retire/fill')
Future<void> fillRetire({ Future<void> fillRetire({@Path('diary') required int diary, @Field() required String retire});
@Path('diary') required int diary,
@Field() required String retire,
});
@POST('/api/diary/product_nutritional_values/sendToModeration') @POST('/api/diary/product_nutritional_values/sendToModeration')
Future<GlossaryModerationModel> sendProductToModeration({ Future<GlossaryModerationModel> sendProductToModeration({@Body() required Map<String, dynamic> data});
@Body() required Map<String, dynamic> data,
});
@POST('/api/diary/{id}/consultant/comment') @POST('/api/diary/{id}/consultant/comment')
Future<void> commentUserDay({ Future<void> commentUserDay({@Path('id') required int id, @Body() required ConsultantCommentModel body});
@Path('id') required int id,
@Body() required ConsultantCommentModel body,
});
@GET('/api/chosen_plans/{planId}/{diaryId}/comments/show') @GET('/api/chosen_plans/{planId}/{diaryId}/comments/show')
Future<ConsultantReviewModel> getDiaryComments({ Future<ConsultantReviewModel> getDiaryComments({@Path() required int planId, @Path() required int diaryId});
@Path() required int planId,
@Path() required int diaryId,
});
@POST('/api/diary/{diaryId}/consultant/comment/view') @POST('/api/diary/{diaryId}/consultant/comment/view')
Future<void> viewCommentOfDiaryDayByConsultant({ Future<void> viewCommentOfDiaryDayByConsultant({@Path() required int diaryId});
@Path() required int diaryId,
}); @GET('/api/glossaries/index')
Future<GlossaryPaginationModel> getGlossariesIndex({@Queries() Map<String, dynamic>? filters});
} }

View File

@@ -0,0 +1,16 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'view_model.freezed.dart';
part 'view_model.g.dart';
@freezed
abstract class ViewModel with _$ViewModel {
factory ViewModel({
required String type,
List<int>? ids,
List<String>? slugs,
}) = _ViewModel;
factory ViewModel.fromJson(Map<String, dynamic> json) => _$ViewModelFromJson(json);
}

View File

@@ -12,70 +12,57 @@ class FeedCubit extends Cubit<FeedState> {
bool isLoading = false; bool isLoading = false;
String? _query; String? _query;
FeedCubit({ FeedCubit({required this.feedRepository}) : super(const FeedState.loading());
required this.feedRepository,
}) : super(const FeedState.loading());
void updateFeedBlogPostList(FeedBlogPostModel? feedBlogPostModel, bool wasLikedByMe) { void updateFeedBlogPostList(FeedBlogPostModel? feedBlogPostModel, bool wasLikedByMe) {
if (feedBlogPostModel == null) return; if (feedBlogPostModel == null || isClosed) return;
List<FeedBlogPostModel> feedBlogPostList = [...(state as FeedStateLoaded).feedBlogPostList]; List<FeedBlogPostModel> feedBlogPostList = [...(state as FeedStateLoaded).feedBlogPostList];
final int index = feedBlogPostList.indexOf(feedBlogPostModel); final int index = feedBlogPostList.indexOf(feedBlogPostModel);
feedBlogPostList[index] = feedBlogPostModel.copyWith( feedBlogPostList[index] = feedBlogPostModel.copyWith(
wasLikedByMe: wasLikedByMe, wasLikedByMe: wasLikedByMe,
totalLikes: wasLikedByMe == false ? feedBlogPostList[index].totalLikes - 1 : feedBlogPostList[index].totalLikes + 1, totalLikes: wasLikedByMe == false ? feedBlogPostList[index].totalLikes - 1 : feedBlogPostList[index].totalLikes + 1,
); );
emit( if (!isClosed) {
FeedState.loaded( emit(FeedState.loaded(feedBlogPostList: feedBlogPostList, isLoading: isLoading));
feedBlogPostList: feedBlogPostList, }
isLoading: isLoading,
),
);
} }
Future<void> loadFeedBlogPostList({ Future<void> loadFeedBlogPostList({String? query}) async {
String? query,
}) async {
try { try {
_query = query; _query = query;
final feedBlogPosts = await feedRepository.getFeedBlogPosts(query: query); final feedBlogPosts = await feedRepository.getFeedBlogPosts(query: query);
lastPage = feedBlogPosts.meta.lastPage; lastPage = feedBlogPosts.meta.lastPage;
emit( if (!isClosed) {
FeedState.loaded( emit(FeedState.loaded(feedBlogPostList: feedBlogPosts.feedBlogPostList, isLoading: false));
feedBlogPostList: feedBlogPosts.feedBlogPostList, }
isLoading: false,
),
);
} catch (e) { } catch (e) {
emit(FeedState.error(errorMessage: e.toString())); if (!isClosed) {
emit(FeedState.error(errorMessage: e.toString()));
}
} }
} }
Future<void> loadNextPage() async { Future<void> loadNextPage() async {
if (currentPage < lastPage && !isLoading) { if (currentPage < lastPage && !isLoading && !isClosed) {
isLoading = true; isLoading = true;
emit((state as FeedStateLoaded).copyWith(isLoading: isLoading)); if (!isClosed) {
emit((state as FeedStateLoaded).copyWith(isLoading: isLoading));
}
try { try {
if (currentPage < lastPage) { if (currentPage < lastPage && !isClosed) {
currentPage++; currentPage++;
final feedBlogPosts = await feedRepository.getFeedBlogPosts( final feedBlogPosts = await feedRepository.getFeedBlogPosts(page: currentPage, query: _query);
page: currentPage,
query: _query,
);
isLoading = false; isLoading = false;
emit( if (!isClosed) {
FeedState.loaded( emit(FeedState.loaded(feedBlogPostList: [...(state as FeedStateLoaded).feedBlogPostList, ...feedBlogPosts.feedBlogPostList], isLoading: isLoading));
feedBlogPostList: [ }
...(state as FeedStateLoaded).feedBlogPostList,
...feedBlogPosts.feedBlogPostList,
],
isLoading: isLoading,
),
);
} }
} catch (e) { } catch (e) {
emit(FeedState.error(errorMessage: e.toString())); if (!isClosed) {
emit(FeedState.error(errorMessage: e.toString()));
}
} }
} }
} }
@@ -94,4 +81,5 @@ class FeedCubit extends Cubit<FeedState> {
Future<void> viewRecipe(String slug) async => await feedRepository.viewRecipe(slug); Future<void> viewRecipe(String slug) async => await feedRepository.viewRecipe(slug);
Future<void> viewDiet(int id) async => await feedRepository.viewDiet(id); Future<void> viewDiet(int id) async => await feedRepository.viewDiet(id);
Future<void> viewPlan(String slug) async => await feedRepository.viewPlan(slug); Future<void> viewPlan(String slug) async => await feedRepository.viewPlan(slug);
} }

View File

@@ -1,5 +1,6 @@
import 'package:easy_debounce/easy_debounce.dart'; import 'package:easy_debounce/easy_debounce.dart';
import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/views/cubit/views_cubit.dart';
import 'package:eatvio_app/core/widgets/add_item_button.dart'; import 'package:eatvio_app/core/widgets/add_item_button.dart';
import 'package:eatvio_app/core/widgets/custom_search_bar.dart'; import 'package:eatvio_app/core/widgets/custom_search_bar.dart';
import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart'; import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart';
@@ -66,12 +67,9 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
final dietsScrollController = ScrollController(); final dietsScrollController = ScrollController();
final plansScrollController = ScrollController(); final plansScrollController = ScrollController();
final consultantsScrollController = ScrollController(); final consultantsScrollController = ScrollController();
@override @override
void initState() { void initState() {
// Initialize chipsLabelList based on hidePlans setting
chipsLabelList = hidePlans ? ['Все', 'Рецепты', 'Диеты', 'Консультанты'] : ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты']; chipsLabelList = hidePlans ? ['Все', 'Рецепты', 'Диеты', 'Консультанты'] : ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты'];
consultantsScrollController.addListener(_onScrollConsultants); consultantsScrollController.addListener(_onScrollConsultants);
super.initState(); super.initState();
} }
@@ -80,6 +78,8 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
if (consultantsScrollController.offset >= consultantsScrollController.position.maxScrollExtent && !consultantsScrollController.position.outOfRange) {} if (consultantsScrollController.offset >= consultantsScrollController.position.maxScrollExtent && !consultantsScrollController.position.outOfRange) {}
} }
@override @override
void dispose() { void dispose() {
blogPostsScrollController.dispose(); blogPostsScrollController.dispose();
@@ -90,6 +90,8 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
super.dispose(); super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
FlutterNativeSplash.remove(); FlutterNativeSplash.remove();
@@ -228,10 +230,10 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
]; ];
}, },
body: switch (_selectedChipIndex) { body: switch (_selectedChipIndex) {
0 => const BlogPosts(), 0 => BlogPosts(handleVisibility: context.read<ViewsCubit>().handleVisibility),
1 => const Recipes(), 1 => Recipes(handleVisibility: context.read<ViewsCubit>().handleVisibility),
2 => const Diets(), 2 => Diets(handleVisibility: context.read<ViewsCubit>().handleVisibility),
3 when !hidePlans => const Plans(), 3 when !hidePlans => Plans(handleVisibility: context.read<ViewsCubit>().handleVisibility),
3 when hidePlans => const Consultants(), 3 when hidePlans => const Consultants(),
4 when !hidePlans => const Consultants(), 4 when !hidePlans => const Consultants(),
_ => Center(child: Text(chipsLabelList[_selectedChipIndex])), _ => Center(child: Text(chipsLabelList[_selectedChipIndex])),
@@ -240,3 +242,20 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
); );
} }
} }
enum ViewableContentType {
plans,
posts,
diets,
recipes;
String getStringifiedVersion() => switch (this) {
ViewableContentType.plans => 'plans',
ViewableContentType.posts => 'blog_posts',
ViewableContentType.diets => 'diets',
ViewableContentType.recipes => 'recipes',
};
}

View File

@@ -245,9 +245,9 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
const SizedBox(width: 8), const SizedBox(width: 8),
FeedIconButton( FeedIconButton(
svgIcon: SvgPicture.asset( svgIcon: SvgPicture.asset(
height: 20, height: 15,
width: 20, width: 12,
'assets/icons/share.svg', 'assets/icons/repost.svg',
colorFilter: ColorFilter.mode(appTheme.gray600, BlendMode.srcIn), colorFilter: ColorFilter.mode(appTheme.gray600, BlendMode.srcIn),
), ),
onTap: () { onTap: () {

View File

@@ -3,13 +3,16 @@ import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_state.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_state.dart';
import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/blog_post_widget.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/blog_post_widget.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:visibility_detector/visibility_detector.dart';
class BlogPosts extends StatelessWidget { class BlogPosts extends StatelessWidget {
const BlogPosts({super.key}); const BlogPosts({super.key, required this.handleVisibility});
final Function({required double visibleFraction,required int id, required ViewableContentType type}) handleVisibility;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return RefreshIndicator( return RefreshIndicator(
@@ -29,25 +32,26 @@ class BlogPosts extends StatelessWidget {
if (index == state.feedBlogPostList.length) { if (index == state.feedBlogPostList.length) {
return const Loader(); return const Loader();
} }
return BlogPostWidget(index: index); return VisibilityDetector(
onVisibilityChanged: (info) {
final id = state.feedBlogPostList[index].id;
handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.posts);
},
key: ValueKey(index),
child: BlogPostWidget(index: index),
);
}, },
separatorBuilder: (context, index) => const SizedBox(height: 8), separatorBuilder: (context, index) => const SizedBox(height: 8),
itemCount: state.isLoading ? state.feedBlogPostList.length + 1 : state.feedBlogPostList.length, itemCount: state.isLoading ? state.feedBlogPostList.length + 1 : state.feedBlogPostList.length,
), ),
); );
} else if (state is FeedStateError) { } else if (state is FeedStateError) {
return DataErrorWidget( return DataErrorWidget(onReload: () => context.read<FeedCubit>().loadFeedBlogPostList());
onReload: () => context.read<FeedCubit>().loadFeedBlogPostList(),
);
} else { } else {
return Scaffold( return Scaffold(backgroundColor: Colors.grey.shade200, body: const Loader());
backgroundColor: Colors.grey.shade200,
body: const Loader(),
);
} }
}, },
), ),
); );
} }
} }

View File

@@ -25,13 +25,7 @@ class CommentsWidget extends StatefulWidget {
final CommentModel comment; final CommentModel comment;
final int? replyId; final int? replyId;
const CommentsWidget({ const CommentsWidget({super.key, required this.id, required this.commentSourceType, required this.comment, this.replyId});
super.key,
required this.id,
required this.commentSourceType,
required this.comment,
this.replyId,
});
@override @override
State<CommentsWidget> createState() => _CommentsWidgetState(); State<CommentsWidget> createState() => _CommentsWidgetState();
@@ -47,10 +41,7 @@ class _CommentsWidgetState extends State<CommentsWidget> {
_comment(s, widget.comment.data[i]), _comment(s, widget.comment.data[i]),
if (widget.comment.data[i].childrenComments.isNotEmpty) const SizedBox(height: 8), if (widget.comment.data[i].childrenComments.isNotEmpty) const SizedBox(height: 8),
for (int j = 0; j < widget.comment.data[i].childrenComments.length; j++) for (int j = 0; j < widget.comment.data[i].childrenComments.length; j++)
Padding( Padding(padding: const EdgeInsets.only(left: 40), child: _comment(s, widget.comment.data[i].childrenComments[j], 16, true)),
padding: const EdgeInsets.only(left: 40),
child: _comment(s, widget.comment.data[i].childrenComments[j], 16, true),
),
const SizedBox(height: 8), const SizedBox(height: 8),
], ],
const SizedBox(height: 48), const SizedBox(height: 48),
@@ -72,7 +63,7 @@ class _CommentsWidgetState extends State<CommentsWidget> {
: null, : null,
copy: () { copy: () {
Clipboard.setData(ClipboardData(text: comment.body)); Clipboard.setData(ClipboardData(text: comment.body));
showInfoToast(s.link_was_copied); showInfoToast(s.text_was_copied);
}, },
edit: (String text) { edit: (String text) {
context.read<CommentsCubit>().updateComment(id: comment.id, body: text); context.read<CommentsCubit>().updateComment(id: comment.id, body: text);
@@ -84,10 +75,7 @@ class _CommentsWidgetState extends State<CommentsWidget> {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: (context) => ComplainingBottomSheet( builder: (context) => ComplainingBottomSheet(complainedContentType: ComplainedContent.comment, id: comment.id),
complainedContentType: ComplainedContent.comment,
id: comment.id,
),
); );
}, },
isMyComment: isMe, isMyComment: isMe,
@@ -117,9 +105,7 @@ class _CommentsWidgetState extends State<CommentsWidget> {
); );
}, },
child: CircleAvatar( child: CircleAvatar(
foregroundImage: NetworkImage( foregroundImage: NetworkImage(comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : baseUrl + comment.authorProfileIcon),
comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : baseUrl + comment.authorProfileIcon,
),
backgroundImage: const AssetImage('assets/images/no_avatar.png'), backgroundImage: const AssetImage('assets/images/no_avatar.png'),
radius: radius, radius: radius,
), ),
@@ -131,24 +117,14 @@ class _CommentsWidgetState extends State<CommentsWidget> {
children: [ children: [
Text( Text(
comment.authorName, comment.authorName,
style: TextStyle( style: TextStyle(height: 1, color: appTheme.gray, fontSize: 14, fontWeight: FontWeight.w500),
height: 1,
color: appTheme.gray,
fontSize: 14,
fontWeight: FontWeight.w500,
),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
SizedBox( SizedBox(
width: isChild ? MediaQuery.of(context).size.width * 0.72 : MediaQuery.of(context).size.width * 0.8, width: isChild ? MediaQuery.of(context).size.width * 0.72 : MediaQuery.of(context).size.width * 0.8,
child: Text( child: Text(
comment.body, comment.body,
style: TextStyle( style: TextStyle(height: 1, color: appTheme.black, fontSize: 16, fontWeight: FontWeight.w500),
height: 1,
color: appTheme.black,
fontSize: 16,
fontWeight: FontWeight.w500,
),
), ),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
@@ -156,12 +132,7 @@ class _CommentsWidgetState extends State<CommentsWidget> {
children: [ children: [
Text( Text(
comment.createdAt.format('dd MMMM', 'ru'), comment.createdAt.format('dd MMMM', 'ru'),
style: TextStyle( style: TextStyle(height: 1, color: appTheme.gray, fontSize: 14, fontWeight: FontWeight.w500),
height: 1,
color: appTheme.gray,
fontSize: 14,
fontWeight: FontWeight.w500,
),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
if (!isChild) if (!isChild)
@@ -171,12 +142,7 @@ class _CommentsWidgetState extends State<CommentsWidget> {
}, },
child: Text( child: Text(
s.answer, s.answer,
style: TextStyle( style: TextStyle(height: 1, color: appTheme.gray, fontSize: 14, fontWeight: FontWeight.w600),
height: 1,
color: appTheme.gray,
fontSize: 14,
fontWeight: FontWeight.w600,
),
), ),
), ),
], ],

View File

@@ -3,15 +3,17 @@ import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_state.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_state.dart';
import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/diet_filter_bottom_sheet.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/diet_filter_bottom_sheet.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/diets_widget.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/diets_widget.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/filter_badge.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/filter_badge.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:visibility_detector/visibility_detector.dart';
class Diets extends StatelessWidget { class Diets extends StatelessWidget {
const Diets({super.key}); const Diets({super.key, required this.handleVisibility});
final Function({required double visibleFraction, required int id, required ViewableContentType type}) handleVisibility;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return RefreshIndicator( return RefreshIndicator(
@@ -19,10 +21,7 @@ class Diets extends StatelessWidget {
child: BlocBuilder<DietsCubit, DietsState>( child: BlocBuilder<DietsCubit, DietsState>(
builder: (BuildContext context, DietsState state) { builder: (BuildContext context, DietsState state) {
if (state.isLoading) { if (state.isLoading) {
return Scaffold( return Scaffold(backgroundColor: Colors.grey.shade200, body: const Loader());
backgroundColor: Colors.grey.shade200,
body: const Loader(),
);
} else if (state.errorMessage != null) { } else if (state.errorMessage != null) {
return DataErrorWidget(onReload: () => context.read<DietsCubit>().loadDiets()); return DataErrorWidget(onReload: () => context.read<DietsCubit>().loadDiets());
} else { } else {
@@ -39,7 +38,14 @@ class Diets extends StatelessWidget {
shrinkWrap: true, shrinkWrap: true,
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
itemBuilder: (context, index) { itemBuilder: (context, index) {
return DietsWidget(diet: state.dietsList[index], index: index); return VisibilityDetector(
key: ValueKey(index),
onVisibilityChanged: (info) {
final id = state.dietsList[index].id;
handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.diets);
},
child: DietsWidget(diet: state.dietsList[index], index: index),
);
}, },
separatorBuilder: (context, index) { separatorBuilder: (context, index) {
return const SizedBox(height: 8); return const SizedBox(height: 8);
@@ -50,11 +56,7 @@ class Diets extends StatelessWidget {
Positioned( Positioned(
right: 0, right: 0,
bottom: 74, bottom: 74,
child: FilterBadge( child: FilterBadge(filterBottomSheet: DietFilterBottomSheet(dietsCubit: context.read<DietsCubit>())),
filterBottomSheet: DietFilterBottomSheet(
dietsCubit: context.read<DietsCubit>(),
),
),
), ),
], ],
); );

View File

@@ -17,7 +17,7 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
class DietsWidget extends StatefulWidget { class DietsWidget extends StatefulWidget {
final Diet diet; final Diet diet;
final int index; final int index;
const DietsWidget({ const DietsWidget({

View File

@@ -3,15 +3,17 @@ import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_state.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_state.dart';
import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/filter_badge.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/filter_badge.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/plan_widget.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/plan_widget.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/plans_filter_bottom_sheet.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/plans_filter_bottom_sheet.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:visibility_detector/visibility_detector.dart';
class Plans extends StatelessWidget { class Plans extends StatelessWidget {
const Plans({super.key}); const Plans({super.key, required this.handleVisibility});
final Function({required double visibleFraction, required int id, required ViewableContentType type}) handleVisibility;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return RefreshIndicator( return RefreshIndicator(
@@ -19,10 +21,7 @@ class Plans extends StatelessWidget {
child: BlocBuilder<PlansCubit, PlansState>( child: BlocBuilder<PlansCubit, PlansState>(
builder: (BuildContext context, PlansState state) { builder: (BuildContext context, PlansState state) {
if (state.isLoading) { if (state.isLoading) {
return Scaffold( return Scaffold(backgroundColor: Colors.grey.shade200, body: const Loader());
backgroundColor: Colors.grey.shade200,
body: const Loader(),
);
} else if (state.errorMessage != null) { } else if (state.errorMessage != null) {
return DataErrorWidget(onReload: () => context.read<PlansCubit>().loadPlans()); return DataErrorWidget(onReload: () => context.read<PlansCubit>().loadPlans());
} }
@@ -39,9 +38,13 @@ class Plans extends StatelessWidget {
shrinkWrap: true, shrinkWrap: true,
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
itemBuilder: (context, index) { itemBuilder: (context, index) {
return PlanWidget( return VisibilityDetector(
plan: state.plans[index], key: ValueKey(index),
index: index, onVisibilityChanged: (info) {
final id = state.plans[index].id;
handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.plans);
},
child: PlanWidget(plan: state.plans[index], index: index),
); );
}, },
separatorBuilder: (context, index) { separatorBuilder: (context, index) {
@@ -53,11 +56,7 @@ class Plans extends StatelessWidget {
Positioned( Positioned(
right: 0, right: 0,
bottom: 74, bottom: 74,
child: FilterBadge( child: FilterBadge(filterBottomSheet: PlansFilterBottomSheet(plansCubit: context.read<PlansCubit>())),
filterBottomSheet: PlansFilterBottomSheet(
plansCubit: context.read<PlansCubit>(),
),
),
), ),
], ],
); );

View File

@@ -3,13 +3,15 @@ import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_state.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_state.dart';
import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/recipes_widget.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/recipes_widget.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:visibility_detector/visibility_detector.dart';
class Recipes extends StatelessWidget { class Recipes extends StatelessWidget {
const Recipes({super.key}); const Recipes({super.key, required this.handleVisibility});
final Function({required double visibleFraction,required int id, required ViewableContentType type}) handleVisibility;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return RefreshIndicator( return RefreshIndicator(
@@ -40,9 +42,16 @@ class Recipes extends StatelessWidget {
shrinkWrap: true, shrinkWrap: true,
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
itemBuilder: (context, index) { itemBuilder: (context, index) {
return RecipesWidget( return VisibilityDetector(
recipe: state.recipesList[index], key: ValueKey(index),
index: index, onVisibilityChanged: (info) {
final id = state.recipesList[index].id;
handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.recipes);
},
child: RecipesWidget(
recipe: state.recipesList[index],
index: index,
),
); );
}, },
separatorBuilder: (context, index) { separatorBuilder: (context, index) {

View File

@@ -231,7 +231,7 @@ class _RecipesWidgetState extends State<RecipesWidget> {
final box = context.findRenderObject() as RenderBox?; final box = context.findRenderObject() as RenderBox?;
SharePlus.instance.share( SharePlus.instance.share(
ShareParams( ShareParams(
uri: Uri.parse('$baseUrl/ru/blog_post/${widget.recipe.id}'), uri: Uri.parse('$baseUrl/ru/recipe/${widget.recipe.id}'),
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
), ),
); );

View File

@@ -5,103 +5,55 @@ import 'package:eatvio_app/features/feed/models/feed_blog_post_settings_model.da
import 'package:eatvio_app/features/feed/models/feed_consultant_model.dart'; import 'package:eatvio_app/features/feed/models/feed_consultant_model.dart';
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
import 'package:eatvio_app/features/feed/models/recipes_model.dart'; import 'package:eatvio_app/features/feed/models/recipes_model.dart';
import 'package:eatvio_app/features/feed/models/view_model.dart';
import 'package:eatvio_app/features/feed/services/feed_service.dart'; import 'package:eatvio_app/features/feed/services/feed_service.dart';
class FeedRepository { class FeedRepository {
final FeedService feedService; final FeedService feedService;
FeedRepository({ FeedRepository({required this.feedService});
required this.feedService,
});
Future<FeedBlogPostSettingsModel> getFeedBlogPosts({ Future<FeedBlogPostSettingsModel> getFeedBlogPosts({String? query, int page = 1}) async {
String? query,
int page = 1,
}) async {
try { try {
return (await feedService.getBlogPostList( return (await feedService.getBlogPostList(page: page, query: query));
page: page,
query: query,
));
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
} }
Future<FeedBlogPostModel> getBlogPost({ Future<FeedBlogPostModel> getBlogPost({required int id}) async {
required int id
}) async {
try { try {
return await feedService.getBlogPost( return await feedService.getBlogPost(postId: id);
postId: id
);
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
} }
Future<List<Recipe>> getRecipes({ Future<List<Recipe>> getRecipes({String? query, List<int>? tags, int page = 1}) async {
String? query,
List<int>? tags,
int page = 1,
}) async {
try { try {
return (await feedService.getRecipes( return (await feedService.getRecipes(query: query, tags: tags, page: page)).recipesList;
query: query,
tags: tags,
page: page,
))
.recipesList;
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
} }
Future<DietsPagination> getDiets({ Future<DietsPagination> getDiets({int page = 1, String? query, String? types, int? duration, String? sorting}) async {
int page = 1,
String? query,
String? types,
int? duration,
String? sorting,
}) async {
try { try {
return (await feedService.getDiets( return (await feedService.getDiets(page: page, query: query, types: types, duration: duration, sorting: sorting));
page: page,
query: query,
types: types,
duration: duration,
sorting: sorting,
));
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
} }
Future<PlanPaginationModel> getPlans({ Future<PlanPaginationModel> getPlans({int page = 1, String? query, String? types, int? duration, int? minPrice, int? maxPrice}) async {
int page = 1,
String? query,
String? types,
int? duration,
int? minPrice,
int? maxPrice,
}) async {
try { try {
return (await feedService.getPlans( return (await feedService.getPlans(page: page, query: query, types: types, duration: duration, minPrice: minPrice, maxPrice: maxPrice));
page: page,
query: query,
types: types,
duration: duration,
minPrice: minPrice,
maxPrice: maxPrice,
));
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
} }
Future<CommentModel> getBlogPostComments({ Future<CommentModel> getBlogPostComments({required String id}) async {
required String id,
}) async {
try { try {
return (await feedService.getBlogPostComments(id: id)); return (await feedService.getBlogPostComments(id: id));
} catch (e) { } catch (e) {
@@ -109,25 +61,15 @@ class FeedRepository {
} }
} }
Future<void> sendBlogPostComment({ Future<void> sendBlogPostComment({required String id, required String commentText, int? parentCommentId}) async {
required String id,
required String commentText,
int? parentCommentId,
}) async {
try { try {
await feedService.sendBlogPostComment( await feedService.sendBlogPostComment(id: id, body: commentText, parentCommentId: parentCommentId);
id: id,
body: commentText,
parentCommentId: parentCommentId,
);
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
} }
Future<CommentModel> getDietsComments({ Future<CommentModel> getDietsComments({required String id}) async {
required String id,
}) async {
try { try {
return (await feedService.getDietsComments(id: id)); return (await feedService.getDietsComments(id: id));
} catch (e) { } catch (e) {
@@ -135,25 +77,15 @@ class FeedRepository {
} }
} }
Future<void> sendDietsComment({ Future<void> sendDietsComment({required String id, required String commentText, int? parentCommentId}) async {
required String id,
required String commentText,
int? parentCommentId,
}) async {
try { try {
await feedService.sendDietsComment( await feedService.sendDietsComment(id: id, body: commentText, parentCommentId: parentCommentId);
id: id,
body: commentText,
parentCommentId: parentCommentId,
);
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
} }
Future<CommentModel> getRecipesComments({ Future<CommentModel> getRecipesComments({required String id}) async {
required String id,
}) async {
try { try {
return (await feedService.getRecipesComments(id: id)); return (await feedService.getRecipesComments(id: id));
} catch (e) { } catch (e) {
@@ -161,25 +93,15 @@ class FeedRepository {
} }
} }
Future<void> sendRecipesComment({ Future<void> sendRecipesComment({required String id, required String commentText, int? parentCommentId}) async {
required String id,
required String commentText,
int? parentCommentId,
}) async {
try { try {
await feedService.sendRecipesComment( await feedService.sendRecipesComment(id: id, body: commentText, parentCommentId: parentCommentId);
id: id,
body: commentText,
parentCommentId: parentCommentId,
);
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
} }
Future<CommentModel> getPlansComments({ Future<CommentModel> getPlansComments({required String slug}) async {
required String slug,
}) async {
try { try {
return (await feedService.getPlansComments(slug: slug)); return (await feedService.getPlansComments(slug: slug));
} catch (e) { } catch (e) {
@@ -187,25 +109,15 @@ class FeedRepository {
} }
} }
Future<void> sendPlansComment({ Future<void> sendPlansComment({required String slug, required String commentText, int? parentCommentId}) async {
required String slug,
required String commentText,
int? parentCommentId,
}) async {
try { try {
await feedService.sendPlansComment( await feedService.sendPlansComment(slug: slug, body: commentText, parentCommentId: parentCommentId);
slug: slug,
body: commentText,
parentCommentId: parentCommentId,
);
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
} }
Future<void> likeBlogPost({ Future<void> likeBlogPost({required int id}) async {
required int id,
}) async {
try { try {
await feedService.likeBlogPost(id: id); await feedService.likeBlogPost(id: id);
} catch (e) { } catch (e) {
@@ -213,9 +125,7 @@ class FeedRepository {
} }
} }
Future<void> unlikedBlogPost({ Future<void> unlikedBlogPost({required int id}) async {
required int id,
}) async {
try { try {
await feedService.unlikedBlogPost(id: id); await feedService.unlikedBlogPost(id: id);
} catch (e) { } catch (e) {
@@ -262,4 +172,12 @@ class FeedRepository {
rethrow; rethrow;
} }
} }
Future<void> viewContent({required ViewModel viewModel}) async {
try {
await feedService.viewContent(viewModel: viewModel);
} catch (e) {
rethrow;
}
}
} }

View File

@@ -6,6 +6,7 @@ import 'package:eatvio_app/features/feed/models/feed_blog_post_settings_model.da
import 'package:eatvio_app/features/feed/models/feed_consultant_model.dart'; import 'package:eatvio_app/features/feed/models/feed_consultant_model.dart';
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
import 'package:eatvio_app/features/feed/models/recipes_pagination_model.dart'; import 'package:eatvio_app/features/feed/models/recipes_pagination_model.dart';
import 'package:eatvio_app/features/feed/models/view_model.dart';
import 'package:retrofit/retrofit.dart'; import 'package:retrofit/retrofit.dart';
part 'feed_service.g.dart'; part 'feed_service.g.dart';
@@ -131,4 +132,9 @@ abstract class FeedService {
Future<void> viewPlan({ Future<void> viewPlan({
@Path() required String slug, @Path() required String slug,
}); });
@POST('/api/views')
Future<void> viewContent({
@Body() required ViewModel viewModel,
});
} }

View File

@@ -33,21 +33,12 @@ class _UserPurchasesScreen extends State<UserPurchasesScreen> {
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF0F2F5), backgroundColor: const Color(0xFFF0F2F5),
appBar: AppBar( appBar: AppBar(
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(borderRadius: BorderRadiusDirectional.vertical(bottom: Radius.circular(12))),
borderRadius: BorderRadiusDirectional.vertical( title: Text('Финансы', style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
bottom: Radius.circular(12),
),
),
title: Text(
'Финансы',
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
centerTitle: true, centerTitle: true,
), ),
body: state.isLoading body: state.isLoading
? const Center( ? const Center(child: Loader())
child: Loader(),
)
: SingleChildScrollView( : SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
child: Column( child: Column(
@@ -67,10 +58,7 @@ class _UserPurchasesScreen extends State<UserPurchasesScreen> {
.map( .map(
(value) => DropdownMenuItem( (value) => DropdownMenuItem(
value: value, value: value,
child: Text( child: Text(value.name, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
value.name,
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
),
), ),
) )
.toList(), .toList(),
@@ -105,60 +93,41 @@ class _UserPurchasesScreen extends State<UserPurchasesScreen> {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Builder(builder: (context) { Builder(
final idLen = state.purchasesModels[index].bankId.length; builder: (context) {
return Text( final idLen = state.purchasesModels[index].bankId.length;
'${state.purchasesModels[index].bankId.substring(0, 4)}...${state.purchasesModels[index].bankId.substring(idLen - 4, idLen)} ', return Text(
style: TextStyles.textSMedium.copyWith(color: appTheme.black), '${state.purchasesModels[index].bankId.substring(0, 4)}...${state.purchasesModels[index].bankId.substring(idLen - 4, idLen)} ',
); style: TextStyles.textSMedium.copyWith(color: appTheme.black),
}), );
},
),
Text( Text(
state.purchasesModels[index].startDate.format('yyyy.MM.dd'), state.purchasesModels[index].startDate.format('dd.MM.yyyy'),
style: const TextStyle( style: const TextStyle(color: Color(0xFF6D6F71), height: 1, fontWeight: FontWeight.w500),
color: Color(0xFF6D6F71),
height: 1,
fontWeight: FontWeight.w500,
),
), ),
], ],
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
state.purchasesModels[index].title, state.purchasesModels[index].title,
style: TextStyle( style: TextStyle(color: appTheme.blueA400, height: 1, fontSize: 18, fontWeight: FontWeight.w600),
color: appTheme.blueA400,
height: 1,
fontSize: 18,
fontWeight: FontWeight.w600,
),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'Продавец: ${state.purchasesModels[index].consultantName}', 'Продавец: ${state.purchasesModels[index].consultantName}',
style: const TextStyle( style: const TextStyle(color: Color(0xFF323F4B), height: 1, fontSize: 14, fontWeight: FontWeight.w500),
color: Color(0xFF323F4B),
height: 1,
fontSize: 14,
fontWeight: FontWeight.w500,
),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'${state.purchasesModels[index].duration} ${getDayForm(state.purchasesModels[index].duration, s)}', '${state.purchasesModels[index].duration} ${getDayForm(state.purchasesModels[index].duration, s)}',
style: const TextStyle( style: const TextStyle(color: Color(0xFF323F4B), height: 1, fontSize: 14, fontWeight: FontWeight.w500),
color: Color(0xFF323F4B),
height: 1,
fontSize: 14,
fontWeight: FontWeight.w500,
),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Container( Container(
height: 1, height: 1,
width: double.infinity, width: double.infinity,
decoration: const BoxDecoration( decoration: const BoxDecoration(color: Color(0xFFDEDEDF)),
color: Color(0xFFDEDEDF),
),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Row( Row(
@@ -166,16 +135,9 @@ class _UserPurchasesScreen extends State<UserPurchasesScreen> {
children: [ children: [
Text( Text(
'${state.purchasesModels[index].paymentPrice} ₽', '${state.purchasesModels[index].paymentPrice} ₽',
style: const TextStyle( style: const TextStyle(color: Color(0xFF323F4B), height: 1, fontSize: 18, fontWeight: FontWeight.w600),
color: Color(0xFF323F4B),
height: 1,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
StatusWidget(
status: purchaseStatus,
), ),
StatusWidget(status: purchaseStatus),
], ],
), ),
], ],
@@ -189,10 +151,7 @@ class _UserPurchasesScreen extends State<UserPurchasesScreen> {
else else
const Padding( const Padding(
padding: EdgeInsets.symmetric(vertical: 40.0), padding: EdgeInsets.symmetric(vertical: 40.0),
child: Text( child: Text('У вас нет таких консультаций', style: TextStyles.textMMedium),
'У вас нет таких консультаций',
style: TextStyles.textMMedium,
),
), ),
], ],
), ),
@@ -200,7 +159,8 @@ class _UserPurchasesScreen extends State<UserPurchasesScreen> {
), ),
), ),
], ],
)), ),
),
); );
}, },
), ),

View File

@@ -0,0 +1,39 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'notification_settings_model.g.dart';
part 'notification_settings_model.freezed.dart';
@freezed
abstract class NotificationSettingsModel with _$NotificationSettingsModel {
const factory NotificationSettingsModel({
@JsonKey(name: 'new_updates_notification') required bool newUpdatesNotification,
@JsonKey(name: 'weight_measurements_notification') required bool weightMeasurementsNotification,
@JsonKey(name: 'is_email_notifications_enabled') required bool isEmailNotificationsEnabled,
@JsonKey(name: 'is_push_notifications_enabled') required bool isPushNotificationsEnabled,
@JsonKey(name: 'diary_reminder_notifications') required DiaryReminderNotifications diaryReminderNotifications,
}) = _NotificationSettingsModel;
factory NotificationSettingsModel.fromJson(Map<String, Object?> json) => _$NotificationSettingsModelFromJson(json);
}
@freezed
abstract class DiaryReminderNotifications with _$DiaryReminderNotifications {
const factory DiaryReminderNotifications({
@JsonKey(name: 'first_reminder') required ReminderSettings firstReminder,
@JsonKey(name: 'second_reminder') required ReminderSettings secondReminder,
@JsonKey(name: 'third_reminder') required ReminderSettings thirdReminder,
}) = _DiaryReminderNotifications;
factory DiaryReminderNotifications.fromJson(Map<String, Object?> json) => _$DiaryReminderNotificationsFromJson(json);
}
@freezed
abstract class ReminderSettings with _$ReminderSettings {
const factory ReminderSettings({
required bool enabled,
@JsonKey(name: 'local_time_hours') required int localTimeHours,
@JsonKey(name: 'local_time_mins') required int localTimeMins,
}) = _ReminderSettings;
factory ReminderSettings.fromJson(Map<String, Object?> json) => _$ReminderSettingsFromJson(json);
}

View File

@@ -0,0 +1,33 @@
import 'dart:developer';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_state.dart';
import 'package:eatvio_app/features/menu/repositories/notification_settings_repository.dart';
class NotificationSettingsCubit extends Cubit<NotificationSettingsState> {
final NotificationSettingsRepository _notificationSettingsRepository;
NotificationSettingsCubit({required NotificationSettingsRepository notificationSettingsRepository})
: _notificationSettingsRepository = notificationSettingsRepository,
super(const NotificationSettingsStateLoading());
Future<void> getNotificationSettings() async {
emit(const NotificationSettingsStateLoading());
try {
final notificationSettings = await _notificationSettingsRepository.getNotificationSettings();
emit(NotificationSettingsStateLoaded(notificationSettings: notificationSettings));
} catch (e, stackTrace) {
log('Error getting notification settings: $e', stackTrace: stackTrace);
emit(NotificationSettingsStateError(errorMessage: e.toString()));
}
}
Future<void> updateNotificationSettings(Map<String, dynamic> data) async {
try {
final notificationSettings = await _notificationSettingsRepository.updateNotificationSettings(data);
emit(NotificationSettingsStateLoaded(notificationSettings: notificationSettings));
} catch (e, stackTrace) {
log('Error updating notification settings: $e', stackTrace: stackTrace);
emit(NotificationSettingsStateError(errorMessage: e.toString()));
}
}
}

View File

@@ -0,0 +1,31 @@
import 'package:equatable/equatable.dart';
import 'package:eatvio_app/features/menu/models/notification_settings_model.dart';
abstract class NotificationSettingsState extends Equatable {
const NotificationSettingsState();
@override
List<Object?> get props => [];
}
class NotificationSettingsStateLoading extends NotificationSettingsState {
const NotificationSettingsStateLoading();
}
class NotificationSettingsStateLoaded extends NotificationSettingsState {
final NotificationSettingsModel notificationSettings;
const NotificationSettingsStateLoaded({required this.notificationSettings});
@override
List<Object?> get props => [notificationSettings];
}
class NotificationSettingsStateError extends NotificationSettingsState {
final String errorMessage;
const NotificationSettingsStateError({required this.errorMessage});
@override
List<Object?> get props => [errorMessage];
}

View File

@@ -1,9 +1,12 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/core/utils/image_constant.dart'; import 'package:eatvio_app/core/utils/image_constant.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/consultant_rating/presentation/consultant_rating_screen.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart';
import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart'; import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart';
@@ -14,6 +17,7 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:package_info_plus/package_info_plus.dart';
class MenuScreen extends StatelessWidget { class MenuScreen extends StatelessWidget {
final ProfileModel profileModel; final ProfileModel profileModel;
@@ -51,9 +55,20 @@ class MenuScreen extends StatelessWidget {
}, },
), ),
], ],
if (profileModel.roles.contains('consultant')) _divider(context),
if (profileModel.roles.contains('consultant'))
_tile(
leading: 'assets/icons/finance.svg',
title: 'Мой рейтинг',
onTap: () {
GoRouter.of(context).push(ConsultantRatingScreen.path);
},
),
_divider(context), _divider(context),
_tile(leading: ImageConstant.icnGlossary, title: 'Глоссарий', onTap: () => GoRouter.of(context).push('/glossary-screen')), _tile(leading: ImageConstant.icnGlossary, title: 'Глоссарий', onTap: () => GoRouter.of(context).push('/glossary-screen')),
_divider(context), _divider(context),
_tile(leading: 'assets/icons/notifications.svg', title: 'Уведомления', onTap: () => GoRouter.of(context).pushNamed('notificationsSettingsScreen')),
_divider(context),
_tile(leading: ImageConstant.icnSupport, title: 'Поддержка', onTap: () => GoRouter.of(context).push('/support-screen')), _tile(leading: ImageConstant.icnSupport, title: 'Поддержка', onTap: () => GoRouter.of(context).push('/support-screen')),
_divider(context), _divider(context),
_tile(leading: 'assets/icons/blocked_users.svg', title: 'Чёрный список', onTap: () => GoRouter.of(context).pushNamed('blockedUsersScreen')), _tile(leading: 'assets/icons/blocked_users.svg', title: 'Чёрный список', onTap: () => GoRouter.of(context).pushNamed('blockedUsersScreen')),
@@ -67,6 +82,8 @@ class MenuScreen extends StatelessWidget {
// ), // ),
_divider(context), _divider(context),
_tile(leading: ImageConstant.icnExit, title: 'Выход', onTap: _onTapLogout(context)), _tile(leading: ImageConstant.icnExit, title: 'Выход', onTap: _onTapLogout(context)),
const SizedBox(height: 10),
const AppInfo(),
], ],
), ),
), ),
@@ -124,3 +141,42 @@ class MenuScreen extends StatelessWidget {
}; };
} }
} }
class AppInfo extends StatefulWidget {
const AppInfo({super.key});
@override
State<AppInfo> createState() => _AppInfoState();
}
class _AppInfoState extends State<AppInfo> {
String version = '';
String buildNumber = '';
@override
void didChangeDependencies() {
PackageInfo.fromPlatform().then((PackageInfo packageInfo) {
setState(() {
version = packageInfo.version;
buildNumber = packageInfo.buildNumber;
});
});
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsetsGeometry.only(left: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Версия приложения: $version ($buildNumber)', style: TextStyles.textXSSemibold.copyWith(color: appTheme.gray)),
if (buildDate != '') Text('Дата сборки: ${formatBuildDate(buildDate)}', style: TextStyles.textXSSemibold.copyWith(color: appTheme.gray)),
],
),
),
);
}
}

View File

@@ -0,0 +1,285 @@
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/menu/models/notification_settings_model.dart';
import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart';
import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_state.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
class NotificationsSettingsScreen extends StatelessWidget {
const NotificationsSettingsScreen({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(create: (context) => sl<NotificationSettingsCubit>()..getNotificationSettings(), child: const NotificationsSettingsContent());
}
}
class NotificationsSettingsContent extends StatelessWidget {
const NotificationsSettingsContent({super.key});
@override
Widget build(BuildContext context) {
final s = S.of(context);
return Scaffold(
appBar: AppBar(title: Text(s.notifications)),
body: BlocBuilder<NotificationSettingsCubit, NotificationSettingsState>(
builder: (context, state) {
if (state is NotificationSettingsStateLoading) {
return const Center(child: CircularProgressIndicator());
} else if (state is NotificationSettingsStateError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Ошибка: ${state.errorMessage}'),
ElevatedButton(
onPressed: () {
context.read<NotificationSettingsCubit>().getNotificationSettings();
},
child: const Text('Повторить'),
),
],
),
);
} else if (state is NotificationSettingsStateLoaded) {
return _buildSettingsContent(context, state.notificationSettings);
}
return const Center(child: Text('Нет данных'));
},
),
);
}
Widget _buildSettingsContent(BuildContext context, NotificationSettingsModel notificationSettings) {
final cubit = context.read<NotificationSettingsCubit>();
return Column(
children: [
_tile(
padding: const EdgeInsets.symmetric(horizontal: 16),
onTap: () {},
title: 'Новые обновления',
subtitle: null,
trailing: CupertinoSwitch(
value: notificationSettings.newUpdatesNotification,
activeTrackColor: appTheme.blueA400,
onChanged: (newValue) {
_updateNotificationSetting(cubit, {'new_updates_notification': newValue});
},
),
),
_divider(context),
_tile(
padding: const EdgeInsets.symmetric(horizontal: 16),
onTap: () {},
title: 'Напоминать о взвешивании',
subtitle: null,
trailing: CupertinoSwitch(
value: notificationSettings.weightMeasurementsNotification,
activeTrackColor: appTheme.blueA400,
onChanged: (newValue) {
_updateNotificationSetting(cubit, {'weight_measurements_notification': newValue});
},
),
),
_divider(context),
_tile(
padding: const EdgeInsets.symmetric(horizontal: 16),
onTap: () {},
title: 'Email уведомления',
subtitle: null,
trailing: CupertinoSwitch(
value: notificationSettings.isEmailNotificationsEnabled,
activeTrackColor: appTheme.blueA400,
onChanged: (newValue) {
_updateNotificationSetting(cubit, {'is_email_notifications_enabled': newValue});
},
),
),
_divider(context),
_tile(
padding: const EdgeInsets.symmetric(horizontal: 16),
onTap: () {},
title: 'Push уведомления',
subtitle: null,
trailing: CupertinoSwitch(
value: notificationSettings.isPushNotificationsEnabled,
activeTrackColor: appTheme.blueA400,
onChanged: (newValue) {
_updateNotificationSetting(cubit, {'is_push_notifications_enabled': newValue});
},
),
),
_divider(context),
_buildDiaryRemindersSection(context, notificationSettings.diaryReminderNotifications, cubit),
],
);
}
Widget _buildDiaryRemindersSection(BuildContext context, DiaryReminderNotifications diaryReminderNotifications, NotificationSettingsCubit cubit) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Напоминание о заполнении дневника',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: appTheme.cardNotificationsBG),
),
const SizedBox(height: 8),
_buildReminderTile(context, '№1 Напоминание о приеме пищи', diaryReminderNotifications.firstReminder, 'first_reminder', cubit),
const SizedBox(height: 8),
_buildReminderTile(context, '№2 Напоминание о приеме пищи', diaryReminderNotifications.secondReminder, 'second_reminder', cubit),
const SizedBox(height: 8),
_buildReminderTile(context, '№3 Напоминание о приеме пищи', diaryReminderNotifications.thirdReminder, 'third_reminder', cubit),
],
),
);
}
void _updateNotificationSetting(NotificationSettingsCubit cubit, Map<String, dynamic> data) {
cubit.updateNotificationSettings(data);
}
void _updateReminderSetting(NotificationSettingsCubit cubit, String reminderType, Map<String, dynamic> data) {
final updateData = {
'diary_reminder_notifications': {reminderType: data},
};
cubit.updateNotificationSettings(updateData);
}
Widget _buildReminderTile(BuildContext context, String title, ReminderSettings reminder, String reminderType, NotificationSettingsCubit cubit) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: appTheme.dividerColor),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
reminderType == 'third_reminder' ? SvgPicture.asset('assets/icons/retire.svg') : SvgPicture.asset('assets/icons/wakeup.svg'),
const SizedBox(width: 8),
Text(
title,
style: const TextStyle(color: Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w500),
),
],
),
const SizedBox(height: 8),
Row(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: GestureDetector(
onTap: reminder.enabled
? () {
_showTimePicker(context, reminder, reminderType, cubit);
}
: null,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(color: reminder.enabled ? Colors.white : Colors.white.withAlpha((255 * 0.7).toInt()), borderRadius: BorderRadius.circular(48)),
child: Center(
child: Text(
'${reminder.localTimeHours.toString().padLeft(2, '0')}:${reminder.localTimeMins.toString().padLeft(2, '0')}',
style: TextStyle(fontSize: 17, color: appTheme.gray, fontWeight: FontWeight.w500),
),
),
),
),
),
const SizedBox(width: 8),
CupertinoSwitch(
value: reminder.enabled,
activeTrackColor: appTheme.blueA400,
onChanged: (newValue) {
_updateReminderSetting(cubit, reminderType, {
'enabled': newValue,
'local_time_hours': reminder.localTimeHours,
'local_time_mins': reminder.localTimeMins,
});
},
),
],
),
],
),
);
}
void _showTimePicker(BuildContext context, ReminderSettings reminder, String reminderType, NotificationSettingsCubit cubit) {
int selectedHour = reminder.localTimeHours;
int selectedMinute = reminder.localTimeMins;
showCupertinoModalPopup<void>(
context: context,
builder: (BuildContext context) {
return Container(
height: 300,
padding: const EdgeInsets.only(top: 6.0),
margin: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
color: CupertinoColors.systemBackground.resolveFrom(context),
child: SafeArea(
top: false,
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CupertinoButton(child: const Text('Отмена'), onPressed: () => Navigator.of(context).pop()),
CupertinoButton(
child: const Text('Готово'),
onPressed: () {
_updateReminderSetting(cubit, reminderType, {
'enabled': reminder.enabled,
'local_time_hours': selectedHour,
'local_time_mins': selectedMinute,
});
Navigator.of(context).pop();
},
),
],
),
Expanded(
child: CupertinoDatePicker(
mode: CupertinoDatePickerMode.time,
use24hFormat: true,
initialDateTime: DateTime(2024, 1, 1, selectedHour, selectedMinute),
onDateTimeChanged: (DateTime newDateTime) {
selectedHour = newDateTime.hour;
selectedMinute = newDateTime.minute;
},
),
),
],
),
),
);
},
);
}
ListTile _tile({required String title, required Function()? onTap, Widget? trailing, EdgeInsets? padding, Widget? subtitle}) {
return ListTile(
contentPadding: padding,
onTap: onTap,
title: Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500, height: 1)),
subtitle: subtitle,
trailing: trailing ?? const Icon(Icons.arrow_forward_ios_rounded, size: 12, color: Colors.black38),
);
}
Center _divider(BuildContext context) {
return Center(
child: Container(
height: 0.4,
width: MediaQuery.of(context).size.width - 32,
decoration: BoxDecoration(color: Colors.black26, borderRadius: BorderRadius.circular(16)),
),
);
}
}

View File

@@ -0,0 +1,16 @@
import 'package:eatvio_app/features/menu/models/notification_settings_model.dart';
import 'package:eatvio_app/features/menu/services/notification_settings_service.dart';
class NotificationSettingsRepository {
final NotificationSettingsService _notificationSettingsService;
NotificationSettingsRepository(this._notificationSettingsService);
Future<NotificationSettingsModel> getNotificationSettings() async {
return await _notificationSettingsService.getNotificationSettings();
}
Future<NotificationSettingsModel> updateNotificationSettings(Map<String, dynamic> data) async {
return await _notificationSettingsService.updateNotificationSettings(data: data);
}
}

View File

@@ -0,0 +1,16 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/menu/models/notification_settings_model.dart';
import 'package:retrofit/retrofit.dart';
part 'notification_settings_service.g.dart';
@RestApi()
abstract class NotificationSettingsService {
factory NotificationSettingsService(Dio dio, {String baseUrl}) = _NotificationSettingsService;
@GET('/api/profile/notification_settings')
Future<NotificationSettingsModel> getNotificationSettings();
@POST('/api/profile/update_notification_settings')
Future<NotificationSettingsModel> updateNotificationSettings({@Body() required Map<String, dynamic> data});
}

File diff suppressed because it is too large Load Diff

View File

@@ -66,12 +66,6 @@ class NotificationsCubit extends Cubit<NotificationsState> {
isRead: isRead, isRead: isRead,
createdAt: createdAt, createdAt: createdAt,
), ),
NotificationType.chosenPlanFinishedToClient => ChosenPlanFinishedToClientNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.planSubscriptionCanceledToCustomer => PlanSubscriptionCanceledToAuthorNotification.fromJson( NotificationType.planSubscriptionCanceledToCustomer => PlanSubscriptionCanceledToAuthorNotification.fromJson(
notificationJson, notificationJson,
type, type,
@@ -96,12 +90,6 @@ class NotificationsCubit extends Cubit<NotificationsState> {
isRead: isRead, isRead: isRead,
createdAt: createdAt, createdAt: createdAt,
), ),
NotificationType.chosenPlanFinishedToConsultant => ChosenPlanFinishedToConsultantNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.chosenPlanLastDayFinishedForUser => ChosenPlanLastDayFinishedForUser.fromJson( NotificationType.chosenPlanLastDayFinishedForUser => ChosenPlanLastDayFinishedForUser.fromJson(
notificationJson, notificationJson,
type, type,
@@ -113,6 +101,166 @@ class NotificationsCubit extends Cubit<NotificationsState> {
NotificationType.commentReplied => CommentReplied.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), NotificationType.commentReplied => CommentReplied.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.scoreAdded => ScoreAdded.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), NotificationType.scoreAdded => ScoreAdded.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.entityCommented => EntityCommented.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt), NotificationType.entityCommented => EntityCommented.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
// Новые типы уведомлений
NotificationType.planSubscriptionExpired => PlanSubscriptionExpiredNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.planSubscriptionRenewed => PlanSubscriptionRenewedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.consultantAssigned => ConsultantAssignedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.consultantUnassigned => ConsultantUnassignedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.paymentFailed => PaymentFailedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.paymentSuccessful => PaymentSuccessfulNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.goalAchieved => GoalAchievedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.workoutReminder => WorkoutReminderNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.mealReminder => MealReminderNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
// New notification types
NotificationType.dietCancelled => DietCancelledNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.dietStarted => DietStartedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.dietExpired => DietExpiredNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.dietWillStartedIn => DietWillStartedInNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.dietCanNotBeStarted => DietCanNotBeStartedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.personalDietCreationNeeded => PersonalDietCreationNeededNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.chosenPlanClientLastDayFinished => ChosenPlanClientLastDayFinishedNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.chosenPlanClientLastDayStarted => ChosenPlanClientLastDayStartedNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.stayOnTrackToAuthor => StayOnTrackToAuthorNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.chosenPlanStartedToClient => ChosenPlanStartedToClientNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.clientStartPlanSubscriptionToSubscriber => ClientStartPlanSubscriptionToSubscriberNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.consultantConfirmPlanSubscriptionToSubscriber => ConsultantConfirmPlanSubscriptionToSubscriberNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.clientPaidPlanSubscriptionToAuthor => ClientPaidPlanSubscriptionToAuthorNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.chosenPlanTomorrowReminderToClient => ChosenPlanTomorrowReminderToClientNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.chosenPlanTomorrowReminderToConsultant => ChosenPlanTomorrowReminderToConsultantNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.chosenPlanStartedToConsultant => ChosenPlanStartedToConsultantNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.consultantLeftDiaryComment => ConsultantLeftDiaryCommentNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.chosenPlanFinishedToClient => ChosenPlanFinishedToClientNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.consultantConfirmPlanSubscriptionToAuthor => ConsultantConfirmPlanSubscriptionToAuthorNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.clientPaidPlanSubscriptionToSubscriber => ClientPaidPlanSubscriptionToSubscriberNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.clientStartPlanSubscriptionToAuthor => ClientStartPlanSubscriptionToAuthorNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.clientCancelPlanSubscriptionToAuthor => ClientCancelPlanSubscriptionToAuthorNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.clientCancelPlanSubscriptionToSubscriber => ClientCancelPlanSubscriptionToSubscriberNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.userDailyReportCompleted => UserDailyReportCompletedNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.chosenPlanFinishedToConsultant => ChosenPlanFinishedToConsultantNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.stayOnTrackToCustomer => StayOnTrackToCustomerNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.phoneChanged => PhoneChangedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.subscription => SubscriptionNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.badgeAwarded => BadgeAwardedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.badgeRevoked => BadgeRevokedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.consultantRequestStarted => ConsultantRequestStartedNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.clientCloseComplaintSuccess => ClientCloseComplaintSuccessNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.clientRefundSuccess => ClientRefundSuccessNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.consultantRefundSuccess => ConsultantRefundSuccessNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.clientRejectSuccess => ClientRejectSuccessNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.consultantRejectSuccess => ConsultantRejectSuccessNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.scoreSubstracted => ScoreSubstractedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.productStatusUpdatedForAuthor => ProductStatusUpdatedForAuthorNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.diaryReminderForUsers => DiaryReminderForUsersNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.unknown => UnknownNotification(type: type), NotificationType.unknown => UnknownNotification(type: type),
}; };
} catch (e) { } catch (e) {
@@ -122,8 +270,36 @@ class NotificationsCubit extends Cubit<NotificationsState> {
void _handleNotification(String notificationData) { void _handleNotification(String notificationData) {
final json = jsonDecode(notificationData) as Map<String, dynamic>; final json = jsonDecode(notificationData) as Map<String, dynamic>;
final stringifiedType = json['notification_class_name'];
final type = NotificationType.getTypeFromString(stringifiedType); // Улучшенная логика определения типа уведомления
String? stringifiedType;
// Сначала проверяем поле 'type' в корне JSON
if (json.containsKey('type') && json['type'] != null) {
stringifiedType = json['type'].toString();
log('Found type in root: $stringifiedType');
}
// Затем проверяем поле 'notification_class_name' в корне JSON
else if (json.containsKey('notification_class_name') && json['notification_class_name'] != null) {
stringifiedType = json['notification_class_name'].toString();
log('Found notification_class_name in root: $stringifiedType');
}
// Если есть поле 'data', проверяем внутри него
else if (json.containsKey('data') && json['data'] is Map<String, dynamic>) {
final data = json['data'] as Map<String, dynamic>;
if (data.containsKey('type') && data['type'] != null) {
stringifiedType = data['type'].toString();
log('Found type in data: $stringifiedType');
} else if (data.containsKey('notification_class_name') && data['notification_class_name'] != null) {
stringifiedType = data['notification_class_name'].toString();
log('Found notification_class_name in data: $stringifiedType');
}
}
// Если тип не найден, используем unknown
final type = stringifiedType != null ? NotificationType.getTypeFromString(stringifiedType) : NotificationType.unknown;
log('Final type: $type, stringifiedType: $stringifiedType');
final notification = _parseNotification(json, type); final notification = _parseNotification(json, type);
_notifications.insert(0, notification); _notifications.insert(0, notification);

View File

@@ -47,6 +47,72 @@ class NotificationWidget extends StatelessWidget {
CommentReplied() => _CommentRepliedWidget(notification: notification as CommentReplied), CommentReplied() => _CommentRepliedWidget(notification: notification as CommentReplied),
ScoreAdded() => _ScoreAddedWidget(notification: notification as ScoreAdded), ScoreAdded() => _ScoreAddedWidget(notification: notification as ScoreAdded),
EntityCommented() => _EntityCommentedWidget(notification: notification as EntityCommented), EntityCommented() => _EntityCommentedWidget(notification: notification as EntityCommented),
// Новые типы уведомлений
PlanSubscriptionExpiredNotification() => _PlanSubscriptionExpiredWidget(notification: notification as PlanSubscriptionExpiredNotification),
PlanSubscriptionRenewedNotification() => _PlanSubscriptionRenewedWidget(notification: notification as PlanSubscriptionRenewedNotification),
ConsultantAssignedNotification() => _ConsultantAssignedWidget(notification: notification as ConsultantAssignedNotification),
ConsultantUnassignedNotification() => _ConsultantUnassignedWidget(notification: notification as ConsultantUnassignedNotification),
PaymentFailedNotification() => _PaymentFailedWidget(notification: notification as PaymentFailedNotification),
PaymentSuccessfulNotification() => _PaymentSuccessfulWidget(notification: notification as PaymentSuccessfulNotification),
GoalAchievedNotification() => _GoalAchievedWidget(notification: notification as GoalAchievedNotification),
WorkoutReminderNotification() => _WorkoutReminderWidget(notification: notification as WorkoutReminderNotification),
MealReminderNotification() => _MealReminderWidget(notification: notification as MealReminderNotification),
// New notification types
DietCancelledNotification() => _DietCancelledWidget(notification: notification as DietCancelledNotification),
DietStartedNotification() => _DietStartedWidget(notification: notification as DietStartedNotification),
DietExpiredNotification() => _DietExpiredWidget(notification: notification as DietExpiredNotification),
DietWillStartedInNotification() => _DietWillStartedInWidget(notification: notification as DietWillStartedInNotification),
DietCanNotBeStartedNotification() => _DietCanNotBeStartedWidget(notification: notification as DietCanNotBeStartedNotification),
PersonalDietCreationNeededNotification() => _PersonalDietCreationNeededWidget(notification: notification as PersonalDietCreationNeededNotification),
ChosenPlanClientLastDayFinishedNotification() => _ChosenPlanClientLastDayFinishedWidget(
notification: notification as ChosenPlanClientLastDayFinishedNotification,
),
ChosenPlanClientLastDayStartedNotification() => _ChosenPlanClientLastDayStartedWidget(
notification: notification as ChosenPlanClientLastDayStartedNotification,
),
StayOnTrackToAuthorNotification() => _StayOnTrackToAuthorWidget(notification: notification as StayOnTrackToAuthorNotification),
ClientStartPlanSubscriptionToSubscriberNotification() => _ClientStartPlanSubscriptionToSubscriberWidget(
notification: notification as ClientStartPlanSubscriptionToSubscriberNotification,
),
ConsultantConfirmPlanSubscriptionToSubscriberNotification() => _ConsultantConfirmPlanSubscriptionToSubscriberWidget(
notification: notification as ConsultantConfirmPlanSubscriptionToSubscriberNotification,
),
ClientPaidPlanSubscriptionToAuthorNotification() => _ClientPaidPlanSubscriptionToAuthorWidget(
notification: notification as ClientPaidPlanSubscriptionToAuthorNotification,
),
ConsultantLeftDiaryCommentNotification() => _ConsultantLeftDiaryCommentWidget(notification: notification as ConsultantLeftDiaryCommentNotification),
ConsultantConfirmPlanSubscriptionToAuthorNotification() => _ConsultantConfirmPlanSubscriptionToAuthorWidget(
notification: notification as ConsultantConfirmPlanSubscriptionToAuthorNotification,
),
ClientPaidPlanSubscriptionToSubscriberNotification() => _ClientPaidPlanSubscriptionToSubscriberWidget(
notification: notification as ClientPaidPlanSubscriptionToSubscriberNotification,
),
ClientStartPlanSubscriptionToAuthorNotification() => _ClientStartPlanSubscriptionToAuthorWidget(
notification: notification as ClientStartPlanSubscriptionToAuthorNotification,
),
ClientCancelPlanSubscriptionToAuthorNotification() => _ClientCancelPlanSubscriptionToAuthorWidget(
notification: notification as ClientCancelPlanSubscriptionToAuthorNotification,
),
ClientCancelPlanSubscriptionToSubscriberNotification() => _ClientCancelPlanSubscriptionToSubscriberWidget(
notification: notification as ClientCancelPlanSubscriptionToSubscriberNotification,
),
UserDailyReportCompletedNotification() => _UserDailyReportCompletedWidget(notification: notification as UserDailyReportCompletedNotification),
StayOnTrackToCustomerNotification() => _StayOnTrackToCustomerWidget(notification: notification as StayOnTrackToCustomerNotification),
PhoneChangedNotification() => _PhoneChangedWidget(notification: notification as PhoneChangedNotification),
SubscriptionNotification() => _SubscriptionWidget(notification: notification as SubscriptionNotification),
BadgeAwardedNotification() => _BadgeAwardedWidget(notification: notification as BadgeAwardedNotification),
BadgeRevokedNotification() => _BadgeRevokedWidget(notification: notification as BadgeRevokedNotification),
ConsultantRequestStartedNotification() => _ConsultantRequestStartedWidget(notification: notification as ConsultantRequestStartedNotification),
ClientCloseComplaintSuccessNotification() => _ClientCloseComplaintSuccessWidget(notification: notification as ClientCloseComplaintSuccessNotification),
ClientRefundSuccessNotification() => _ClientRefundSuccessWidget(notification: notification as ClientRefundSuccessNotification),
ConsultantRefundSuccessNotification() => _ConsultantRefundSuccessWidget(notification: notification as ConsultantRefundSuccessNotification),
ClientRejectSuccessNotification() => _ClientRejectSuccessWidget(notification: notification as ClientRejectSuccessNotification),
ConsultantRejectSuccessNotification() => _ConsultantRejectSuccessWidget(notification: notification as ConsultantRejectSuccessNotification),
ScoreSubstractedNotification() => _ScoreSubstractedWidget(notification: notification as ScoreSubstractedNotification),
ProductStatusUpdatedForAuthorNotification() => _ProductStatusUpdatedForAuthorWidget(
notification: notification as ProductStatusUpdatedForAuthorNotification,
),
DiaryReminderForUsersNotification() => _DiaryReminderForUsersWidget(notification: notification as DiaryReminderForUsersNotification),
UnknownNotification() => _UnknownNotificationWidget(notification: notification as UnknownNotification), UnknownNotification() => _UnknownNotificationWidget(notification: notification as UnknownNotification),
}; };
} }
@@ -443,3 +509,547 @@ class _UnknownNotificationWidget extends StatelessWidget {
); );
} }
} }
// Новые виджеты для дополнительных типов уведомлений
class _PlanSubscriptionExpiredWidget extends StatelessWidget {
const _PlanSubscriptionExpiredWidget({required this.notification});
final PlanSubscriptionExpiredNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Подписка на план "${notification.planName}" истекла',
createdAt: notification.createdAt,
);
}
}
class _PlanSubscriptionRenewedWidget extends StatelessWidget {
const _PlanSubscriptionRenewedWidget({required this.notification});
final PlanSubscriptionRenewedNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Подписка на план "${notification.planName}" продлена до ${notification.renewalDate}',
createdAt: notification.createdAt,
);
}
}
class _ConsultantAssignedWidget extends StatelessWidget {
const _ConsultantAssignedWidget({required this.notification});
final ConsultantAssignedNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'К плану "${notification.planName}" назначен консультант ${notification.consultantName}',
createdAt: notification.createdAt,
);
}
}
class _ConsultantUnassignedWidget extends StatelessWidget {
const _ConsultantUnassignedWidget({required this.notification});
final ConsultantUnassignedNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Консультант ${notification.consultantName} отстранен от плана "${notification.planName}"',
createdAt: notification.createdAt,
);
}
}
class _PaymentFailedWidget extends StatelessWidget {
const _PaymentFailedWidget({required this.notification});
final PaymentFailedNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Ошибка оплаты плана "${notification.planName}": ${notification.errorMessage}',
createdAt: notification.createdAt,
);
}
}
class _PaymentSuccessfulWidget extends StatelessWidget {
const _PaymentSuccessfulWidget({required this.notification});
final PaymentSuccessfulNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Успешная оплата плана "${notification.planName}" на сумму ${notification.amount} ${notification.currency}',
createdAt: notification.createdAt,
);
}
}
class _GoalAchievedWidget extends StatelessWidget {
const _GoalAchievedWidget({required this.notification});
final GoalAchievedNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Достигнута цель "${notification.goalName}": ${notification.achievedValue}/${notification.targetValue}',
createdAt: notification.createdAt,
);
}
}
class _WorkoutReminderWidget extends StatelessWidget {
const _WorkoutReminderWidget({required this.notification});
final WorkoutReminderNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Напоминание о тренировке "${notification.workoutName}" (${notification.duration} мин)',
createdAt: notification.createdAt,
);
}
}
class _MealReminderWidget extends StatelessWidget {
const _MealReminderWidget({required this.notification});
final MealReminderNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Напоминание о приеме пищи "${notification.mealName}" (${notification.calories} ккал)',
createdAt: notification.createdAt,
);
}
}
// New notification widget classes
class _DietCancelledWidget extends StatelessWidget {
const _DietCancelledWidget({required this.notification});
final DietCancelledNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Диета "${notification.dietName}" отменена',
createdAt: notification.createdAt,
);
}
}
class _DietStartedWidget extends StatelessWidget {
const _DietStartedWidget({required this.notification});
final DietStartedNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Диета "${notification.dietName}" началась',
createdAt: notification.createdAt,
);
}
}
class _DietExpiredWidget extends StatelessWidget {
const _DietExpiredWidget({required this.notification});
final DietExpiredNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Диета "${notification.dietName}" истекла',
createdAt: notification.createdAt,
);
}
}
class _DietWillStartedInWidget extends StatelessWidget {
const _DietWillStartedInWidget({required this.notification});
final DietWillStartedInNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Диета "${notification.dietName}" начнется ${notification.startDate}',
createdAt: notification.createdAt,
);
}
}
class _DietCanNotBeStartedWidget extends StatelessWidget {
const _DietCanNotBeStartedWidget({required this.notification});
final DietCanNotBeStartedNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Диета "${notification.dietName}" не может быть начата: ${notification.reason}',
createdAt: notification.createdAt,
);
}
}
class _PersonalDietCreationNeededWidget extends StatelessWidget {
const _PersonalDietCreationNeededWidget({required this.notification});
final PersonalDietCreationNeededNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Требуется создание персональной диеты для клиента ${notification.clientName} по плану "${notification.planName}"',
createdAt: notification.createdAt,
);
}
}
class _ChosenPlanClientLastDayFinishedWidget extends StatelessWidget {
const _ChosenPlanClientLastDayFinishedWidget({required this.notification});
final ChosenPlanClientLastDayFinishedNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Последний день плана "${notification.planName}" завершен',
createdAt: notification.createdAt,
);
}
}
class _ChosenPlanClientLastDayStartedWidget extends StatelessWidget {
const _ChosenPlanClientLastDayStartedWidget({required this.notification});
final ChosenPlanClientLastDayStartedNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Последний день плана "${notification.planName}" начался',
createdAt: notification.createdAt,
);
}
}
class _StayOnTrackToAuthorWidget extends StatelessWidget {
const _StayOnTrackToAuthorWidget({required this.notification});
final StayOnTrackToAuthorNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Клиент ${notification.clientName} остается на плане "${notification.planName}"',
createdAt: notification.createdAt,
);
}
}
class _ClientStartPlanSubscriptionToSubscriberWidget extends StatelessWidget {
const _ClientStartPlanSubscriptionToSubscriberWidget({required this.notification});
final ClientStartPlanSubscriptionToSubscriberNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Подписка на план "${notification.planName}" началась',
createdAt: notification.createdAt,
);
}
}
class _ConsultantConfirmPlanSubscriptionToSubscriberWidget extends StatelessWidget {
const _ConsultantConfirmPlanSubscriptionToSubscriberWidget({required this.notification});
final ConsultantConfirmPlanSubscriptionToSubscriberNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Консультант ${notification.consultantName} подтвердил подписку на план "${notification.planName}"',
createdAt: notification.createdAt,
);
}
}
class _ClientPaidPlanSubscriptionToAuthorWidget extends StatelessWidget {
const _ClientPaidPlanSubscriptionToAuthorWidget({required this.notification});
final ClientPaidPlanSubscriptionToAuthorNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Клиент ${notification.clientName} оплатил подписку на план "${notification.planName}" на сумму ${notification.amount}',
createdAt: notification.createdAt,
);
}
}
class _ConsultantLeftDiaryCommentWidget extends StatelessWidget {
const _ConsultantLeftDiaryCommentWidget({required this.notification});
final ConsultantLeftDiaryCommentNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Консультант ${notification.consultantName} оставил комментарий к дневнику по плану "${notification.planName}"',
createdAt: notification.createdAt,
);
}
}
class _ConsultantConfirmPlanSubscriptionToAuthorWidget extends StatelessWidget {
const _ConsultantConfirmPlanSubscriptionToAuthorWidget({required this.notification});
final ConsultantConfirmPlanSubscriptionToAuthorNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Консультант ${notification.consultantName} подтвердил подписку на план "${notification.planName}"',
createdAt: notification.createdAt,
);
}
}
class _ClientPaidPlanSubscriptionToSubscriberWidget extends StatelessWidget {
const _ClientPaidPlanSubscriptionToSubscriberWidget({required this.notification});
final ClientPaidPlanSubscriptionToSubscriberNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Оплачена подписка на план "${notification.planName}" на сумму ${notification.amount}',
createdAt: notification.createdAt,
);
}
}
class _ClientStartPlanSubscriptionToAuthorWidget extends StatelessWidget {
const _ClientStartPlanSubscriptionToAuthorWidget({required this.notification});
final ClientStartPlanSubscriptionToAuthorNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Клиент ${notification.clientName} начал подписку на план "${notification.planName}"',
createdAt: notification.createdAt,
);
}
}
class _ClientCancelPlanSubscriptionToAuthorWidget extends StatelessWidget {
const _ClientCancelPlanSubscriptionToAuthorWidget({required this.notification});
final ClientCancelPlanSubscriptionToAuthorNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Клиент ${notification.clientName} отменил подписку на план "${notification.planName}"',
createdAt: notification.createdAt,
);
}
}
class _ClientCancelPlanSubscriptionToSubscriberWidget extends StatelessWidget {
const _ClientCancelPlanSubscriptionToSubscriberWidget({required this.notification});
final ClientCancelPlanSubscriptionToSubscriberNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Подписка на план "${notification.planName}" отменена',
createdAt: notification.createdAt,
);
}
}
class _UserDailyReportCompletedWidget extends StatelessWidget {
const _UserDailyReportCompletedWidget({required this.notification});
final UserDailyReportCompletedNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Клиент ${notification.clientName} завершил ежедневный отчет по плану "${notification.planName}"',
createdAt: notification.createdAt,
);
}
}
class _StayOnTrackToCustomerWidget extends StatelessWidget {
const _StayOnTrackToCustomerWidget({required this.notification});
final StayOnTrackToCustomerNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Консультант ${notification.consultantName} остается на плане "${notification.planName}"',
createdAt: notification.createdAt,
);
}
}
class _PhoneChangedWidget extends StatelessWidget {
const _PhoneChangedWidget({required this.notification});
final PhoneChangedNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Номер телефона изменен с ${notification.oldPhone} на ${notification.newPhone}',
createdAt: notification.createdAt,
);
}
}
class _SubscriptionWidget extends StatelessWidget {
const _SubscriptionWidget({required this.notification});
final SubscriptionNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Подписка ${notification.subscriptionType} на план "${notification.planName}"',
createdAt: notification.createdAt,
);
}
}
class _BadgeAwardedWidget extends StatelessWidget {
const _BadgeAwardedWidget({required this.notification});
final BadgeAwardedNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Получен значок "${notification.badgeName}": ${notification.badgeDescription}',
createdAt: notification.createdAt,
);
}
}
class _BadgeRevokedWidget extends StatelessWidget {
const _BadgeRevokedWidget({required this.notification});
final BadgeRevokedNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Отозван значок "${notification.badgeName}": ${notification.reason}',
createdAt: notification.createdAt,
);
}
}
class _ConsultantRequestStartedWidget extends StatelessWidget {
const _ConsultantRequestStartedWidget({required this.notification});
final ConsultantRequestStartedNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Запрос к консультанту ${notification.consultantName} начат',
createdAt: notification.createdAt,
);
}
}
class _ClientCloseComplaintSuccessWidget extends StatelessWidget {
const _ClientCloseComplaintSuccessWidget({required this.notification});
final ClientCloseComplaintSuccessNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Жалоба на ${notification.complaintType} "${notification.entityName}" успешно закрыта',
createdAt: notification.createdAt,
);
}
}
class _ClientRefundSuccessWidget extends StatelessWidget {
const _ClientRefundSuccessWidget({required this.notification});
final ClientRefundSuccessNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Возврат средств по плану "${notification.planName}" на сумму ${notification.amount}: ${notification.reason}',
createdAt: notification.createdAt,
);
}
}
class _ConsultantRefundSuccessWidget extends StatelessWidget {
const _ConsultantRefundSuccessWidget({required this.notification});
final ConsultantRefundSuccessNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Возврат средств клиенту ${notification.clientName} по плану "${notification.planName}" на сумму ${notification.amount}',
createdAt: notification.createdAt,
);
}
}
class _ClientRejectSuccessWidget extends StatelessWidget {
const _ClientRejectSuccessWidget({required this.notification});
final ClientRejectSuccessNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Отклонение по плану "${notification.planName}": ${notification.reason}',
createdAt: notification.createdAt,
);
}
}
class _ConsultantRejectSuccessWidget extends StatelessWidget {
const _ConsultantRejectSuccessWidget({required this.notification});
final ConsultantRejectSuccessNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Отклонение клиентом ${notification.clientName} по плану "${notification.planName}": ${notification.reason}',
createdAt: notification.createdAt,
);
}
}
class _ScoreSubstractedWidget extends StatelessWidget {
const _ScoreSubstractedWidget({required this.notification});
final ScoreSubstractedNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Вычтено ${notification.scoreValue} баллов за ${notification.scorableName}',
createdAt: notification.createdAt,
);
}
}
class _ProductStatusUpdatedForAuthorWidget extends StatelessWidget {
const _ProductStatusUpdatedForAuthorWidget({required this.notification});
final ProductStatusUpdatedForAuthorNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: 'Статус продукта "${notification.productName}" изменен с ${notification.oldStatus} на ${notification.newStatus}',
createdAt: notification.createdAt,
);
}
}
class _DiaryReminderForUsersWidget extends StatelessWidget {
const _DiaryReminderForUsersWidget({required this.notification});
final DiaryReminderForUsersNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: 'Напоминание о заполнении дневника', createdAt: notification.createdAt);
}
}

View File

@@ -13,10 +13,30 @@ class NotificationsService {
final notifications = data.map((notificationJson) { final notifications = data.map((notificationJson) {
try { try {
final stringifiedType = notificationJson['data']['notification_class_name']; // Улучшенная логика определения типа уведомления
String? stringifiedType;
// Сначала проверяем поле 'type' в корне JSON
if (notificationJson.containsKey('type') && notificationJson['type'] != null) {
stringifiedType = notificationJson['type'].toString();
}
// Затем проверяем поле 'notification_class_name' в корне JSON
else if (notificationJson.containsKey('notification_class_name') && notificationJson['notification_class_name'] != null) {
stringifiedType = notificationJson['notification_class_name'].toString();
}
// Если есть поле 'data', проверяем внутри него
else if (notificationJson.containsKey('data') && notificationJson['data'] is Map<String, dynamic>) {
final data = notificationJson['data'] as Map<String, dynamic>;
if (data.containsKey('type') && data['type'] != null) {
stringifiedType = data['type'].toString();
} else if (data.containsKey('notification_class_name') && data['notification_class_name'] != null) {
stringifiedType = data['notification_class_name'].toString();
}
}
final readDate = notificationJson['read_at']; final readDate = notificationJson['read_at'];
final createdAt = notificationJson['created_at']; final createdAt = notificationJson['created_at'];
final type = NotificationType.getTypeFromString(stringifiedType); final type = stringifiedType != null ? NotificationType.getTypeFromString(stringifiedType) : NotificationType.unknown;
return switch (type) { return switch (type) {
NotificationType.planSubscriptionStartedToAuthor => PlanSubscriptionStartedToAuthorNotification.fromJson( NotificationType.planSubscriptionStartedToAuthor => PlanSubscriptionStartedToAuthorNotification.fromJson(
@@ -119,6 +139,274 @@ class NotificationsService {
NotificationType.commentReplied => CommentReplied.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt), NotificationType.commentReplied => CommentReplied.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt),
NotificationType.scoreAdded => ScoreAdded.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt), NotificationType.scoreAdded => ScoreAdded.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt),
NotificationType.entityCommented => EntityCommented.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt), NotificationType.entityCommented => EntityCommented.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt),
// Новые типы уведомлений
NotificationType.planSubscriptionExpired => PlanSubscriptionExpiredNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.planSubscriptionRenewed => PlanSubscriptionRenewedNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.consultantAssigned => ConsultantAssignedNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.consultantUnassigned => ConsultantUnassignedNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.paymentFailed => PaymentFailedNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.paymentSuccessful => PaymentSuccessfulNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.goalAchieved => GoalAchievedNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.workoutReminder => WorkoutReminderNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.mealReminder => MealReminderNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
// New notification types
NotificationType.dietCancelled => DietCancelledNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.dietStarted => DietStartedNotification.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt),
NotificationType.dietExpired => DietExpiredNotification.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt),
NotificationType.dietWillStartedIn => DietWillStartedInNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.dietCanNotBeStarted => DietCanNotBeStartedNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.personalDietCreationNeeded => PersonalDietCreationNeededNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.chosenPlanClientLastDayFinished => ChosenPlanClientLastDayFinishedNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.chosenPlanClientLastDayStarted => ChosenPlanClientLastDayStartedNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.stayOnTrackToAuthor => StayOnTrackToAuthorNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.chosenPlanStartedToClient => ChosenPlanStartedToClientNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.clientStartPlanSubscriptionToSubscriber => ClientStartPlanSubscriptionToSubscriberNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.consultantConfirmPlanSubscriptionToSubscriber => ConsultantConfirmPlanSubscriptionToSubscriberNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.clientPaidPlanSubscriptionToAuthor => ClientPaidPlanSubscriptionToAuthorNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.chosenPlanTomorrowReminderToClient => ChosenPlanTomorrowReminderToClientNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.chosenPlanTomorrowReminderToConsultant => ChosenPlanTomorrowReminderToConsultantNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.chosenPlanStartedToConsultant => ChosenPlanStartedToConsultantNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.consultantLeftDiaryComment => ConsultantLeftDiaryCommentNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.consultantConfirmPlanSubscriptionToAuthor => ConsultantConfirmPlanSubscriptionToAuthorNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.clientPaidPlanSubscriptionToSubscriber => ClientPaidPlanSubscriptionToSubscriberNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.clientStartPlanSubscriptionToAuthor => ClientStartPlanSubscriptionToAuthorNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.clientCancelPlanSubscriptionToAuthor => ClientCancelPlanSubscriptionToAuthorNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.clientCancelPlanSubscriptionToSubscriber => ClientCancelPlanSubscriptionToSubscriberNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.userDailyReportCompleted => UserDailyReportCompletedNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.stayOnTrackToCustomer => StayOnTrackToCustomerNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.phoneChanged => PhoneChangedNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.subscription => SubscriptionNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.badgeAwarded => BadgeAwardedNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.badgeRevoked => BadgeRevokedNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.consultantRequestStarted => ConsultantRequestStartedNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.clientCloseComplaintSuccess => ClientCloseComplaintSuccessNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.clientRefundSuccess => ClientRefundSuccessNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.consultantRefundSuccess => ConsultantRefundSuccessNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.clientRejectSuccess => ClientRejectSuccessNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.consultantRejectSuccess => ConsultantRejectSuccessNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.scoreSubstracted => ScoreSubstractedNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.productStatusUpdatedForAuthor => ProductStatusUpdatedForAuthorNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.diaryReminderForUsers => DiaryReminderForUsersNotification.fromJson(
notificationJson,
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.unknown => UnknownNotification(type: type, createdAt: parseDate(createdAt)), NotificationType.unknown => UnknownNotification(type: type, createdAt: parseDate(createdAt)),
}; };
} catch (e) { } catch (e) {

View File

@@ -19,6 +19,7 @@ abstract class ProfileModel with _$ProfileModel {
@JsonKey(name: "updated_at") DateTime? updatedAt, @JsonKey(name: "updated_at") DateTime? updatedAt,
@JsonKey(name: "is_public_profile", fromJson: parseBool) required bool isPublicProfile, @JsonKey(name: "is_public_profile", fromJson: parseBool) required bool isPublicProfile,
@JsonKey(name: "is_blocked_by_me", fromJson: parseBool) required bool isBlockedByMe, @JsonKey(name: "is_blocked_by_me", fromJson: parseBool) required bool isBlockedByMe,
@JsonKey(name: "is_blocked_by_user", fromJson: parseBool, defaultValue: false) required bool isBlockedByUser,
@JsonKey(name: 'is_entity_subscribed_by_me') required bool isEntitySubscribedByMe, @JsonKey(name: 'is_entity_subscribed_by_me') required bool isEntitySubscribedByMe,
@JsonKey(name: "slug") required String slug, @JsonKey(name: "slug") required String slug,
@JsonKey(name: "profile_photo_url") required String profilePhotoUrl, @JsonKey(name: "profile_photo_url") required String profilePhotoUrl,

View File

@@ -93,6 +93,31 @@ class _ProfileScreenState extends State<ProfileScreen> {
} }
isFirstFetch = false; isFirstFetch = false;
} }
// Проверяем, заблокировал ли пользователь меня
if (state.profile.isBlockedByUser) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.block, size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
'Пользователь ограничил доступ к своей странице',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500, color: Colors.grey[600]),
),
],
),
),
),
);
}
return widget.author == null || widget.author!.authorIsMe ? _myProfileScreen(state) : _profileScreen(state); return widget.author == null || widget.author!.authorIsMe ? _myProfileScreen(state) : _profileScreen(state);
} else if (state is ProfileStateError) { } else if (state is ProfileStateError) {
return Scaffold( return Scaffold(
@@ -174,10 +199,18 @@ class _ProfileScreenState extends State<ProfileScreen> {
), ),
_subscriptionsAndSubscribers(state), _subscriptionsAndSubscribers(state),
if (state.profile.badges.isNotEmpty) Padding(padding: const EdgeInsets.only(bottom: 16), child: _badges(state)), if (state.profile.badges.isNotEmpty) Padding(padding: const EdgeInsets.only(bottom: 16), child: _badges(state)),
if (state.profile.roles.contains('consultant') && state.consultantInfo != null) _socialLinks(state), if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe) _socialLinks(state),
if (state.profile.roles.contains('consultant') && state.consultantInfo != null) _education(state), if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe) _education(state),
if (state.profile.roles.contains('consultant') && state.consultantInfo != null) _diplomasAndCertificates(state), if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe) _diplomasAndCertificates(state),
if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !hidePlans) _activePlans(state), if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !hidePlans && !state.profile.isBlockedByMe) _activePlans(state),
if (state.blogPosts.isNotEmpty)
const Padding(
padding: EdgeInsets.only(left: 8, top: 16, bottom: 8),
child: Text(
'Блог',
style: TextStyle(color: Color(0xFF323F4B), fontSize: 24, fontWeight: FontWeight.w600),
),
),
_blogPosts(state), _blogPosts(state),
], ],
), ),
@@ -244,9 +277,9 @@ class _ProfileScreenState extends State<ProfileScreen> {
_subscriptionsAndSubscribers(state), _subscriptionsAndSubscribers(state),
if (state.profile.badges.isNotEmpty) Padding(padding: const EdgeInsets.only(bottom: 16), child: _badges(state)), if (state.profile.badges.isNotEmpty) Padding(padding: const EdgeInsets.only(bottom: 16), child: _badges(state)),
if (state.profile.roles.contains('consultant')) _socialLinks(state), if (state.profile.roles.contains('consultant')) _socialLinks(state),
Padding(padding: const EdgeInsets.only(bottom: 16), child: _charts()),
if (state.profile.roles.contains('consultant')) _education(state), if (state.profile.roles.contains('consultant')) _education(state),
if (state.profile.roles.contains('consultant')) _diplomasAndCertificates(state), if (state.profile.roles.contains('consultant')) _diplomasAndCertificates(state),
Padding(padding: const EdgeInsets.only(bottom: 16), child: _charts()),
if (state.profile.roles.contains('consultant') && !hidePlans) _activePlans(state), if (state.profile.roles.contains('consultant') && !hidePlans) _activePlans(state),
_newBlogPostButton(), _newBlogPostButton(),
_blogPosts(state), _blogPosts(state),
@@ -479,6 +512,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
children: [ children: [
ListView.separated( ListView.separated(
shrinkWrap: true, shrinkWrap: true,
padding: const EdgeInsets.all(0),
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _educationBlock( itemBuilder: (context, index) => _educationBlock(
state.consultantInfo!.educationModel.completedCourses![index].values.first.title, state.consultantInfo!.educationModel.completedCourses![index].values.first.title,
@@ -733,7 +767,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
ListView _blogPosts(ProfileStateLoaded state) { ListView _blogPosts(ProfileStateLoaded state) {
return ListView.separated( return ListView.separated(
padding: const EdgeInsets.symmetric(vertical: 16), padding: const EdgeInsets.only(bottom: 16),
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => ProfileBlogPostWidget(index: index, profileCubit: profileCubit), itemBuilder: (context, index) => ProfileBlogPostWidget(index: index, profileCubit: profileCubit),

View File

@@ -12,9 +12,7 @@ import 'package:eatvio_app/features/profile/services/profile_service.dart';
class ProfileRepository { class ProfileRepository {
final ProfileService profileService; final ProfileService profileService;
ProfileRepository({ ProfileRepository({required this.profileService});
required this.profileService,
});
Future<ProfileModel> getProfileBySlug(String slug) async { Future<ProfileModel> getProfileBySlug(String slug) async {
try { try {
@@ -48,11 +46,7 @@ class ProfileRepository {
} }
} }
Future<void> addCircumference({ Future<void> addCircumference({required int chestCircumference, required int waistCircumference, required int hipCircumference}) async {
required int chestCircumference,
required int waistCircumference,
required int hipCircumference,
}) async {
try { try {
return (await profileService.addCircumference( return (await profileService.addCircumference(
chestCircumference: chestCircumference, chestCircumference: chestCircumference,
@@ -64,9 +58,7 @@ class ProfileRepository {
} }
} }
Future<void> addWeight({ Future<void> addWeight({required double weight}) async {
required double weight,
}) async {
try { try {
return (await profileService.addWeight(weight: weight)); return (await profileService.addWeight(weight: weight));
} catch (e) { } catch (e) {
@@ -74,52 +66,32 @@ class ProfileRepository {
} }
} }
Future<List<FeedBlogPostModel>> getBlogPostsByAuthorSlug({ Future<List<FeedBlogPostModel>> getBlogPostsByAuthorSlug({required String slug}) async {
required String slug,
}) async {
try { try {
return (await profileService.getBlogPostByAuthorSlug( return (await profileService.getBlogPostByAuthorSlug(slug: slug));
slug: slug,
));
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
} }
Future<List<WeightModel>> getWeight({ Future<List<WeightModel>> getWeight({required String period, String? slug}) async {
required String period,
String? slug,
}) async {
try { try {
if (slug != null) { if (slug != null) {
return (await profileService.getWeightBySlug( return (await profileService.getWeightBySlug(period: period, slug: slug));
period: period,
slug: slug,
));
} else { } else {
return (await profileService.getWeight( return (await profileService.getWeight(period: period));
period: period,
));
} }
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
} }
Future<List<CircumferenceModel>> getCircumferences({ Future<List<CircumferenceModel>> getCircumferences({required String period, String? slug}) async {
required String period,
String? slug,
}) async {
try { try {
if (slug != null) { if (slug != null) {
return (await profileService.getCircumferencesBySlug( return (await profileService.getCircumferencesBySlug(period: period, slug: slug));
period: period,
slug: slug,
));
} else { } else {
return (await profileService.getCircumferences( return (await profileService.getCircumferences(period: period));
period: period,
));
} }
} catch (e) { } catch (e) {
rethrow; rethrow;
@@ -184,15 +156,20 @@ class ProfileRepository {
Future<String> getPassKey(String phone) async { Future<String> getPassKey(String phone) async {
return (jsonDecode( return (jsonDecode(
(await profileService.getPassKey(json: '{"route_name": "user_detailz.change-phone-verify", "route_params": {"phone": "$phone"}}'))))['key_is']!; (await profileService.getPassKey(json: '{"route_name": "user_detailz.change-phone-verify", "route_params": {"phone": "$phone"}}')),
))['key_is']!;
} }
Future<String> getMyPhone() async { Future<String> getMyPhone() async {
return (jsonDecode(await profileService.getMyPhone())['phone']); return (jsonDecode(await profileService.getMyPhone())['phone']);
} }
Future<void> sendEmailCode() async { Future<void> sendEmailCode({String? newEmail}) async {
return await profileService.sendEmailCode(); if (newEmail != null) {
return await profileService.sendEmailCode(data: {'new_email': newEmail});
} else {
return await profileService.sendEmailCode();
}
} }
Future<bool> tryVerifyEmailByCode(String code) async { Future<bool> tryVerifyEmailByCode(String code) async {
@@ -206,7 +183,7 @@ class ProfileRepository {
Future<void> deleteAccount() async { Future<void> deleteAccount() async {
try { try {
await profileService.deleteAccount(); await profileService.deleteAccount();
} catch (e) { } catch (e) {
rethrow; rethrow;
} }

View File

@@ -16,9 +16,7 @@ abstract class ProfileService {
factory ProfileService(Dio dio, {String baseUrl}) = _ProfileService; factory ProfileService(Dio dio, {String baseUrl}) = _ProfileService;
@GET('/api/profile/show/{slug}') @GET('/api/profile/show/{slug}')
Future<ProfileModel> getProfileBySlug({ Future<ProfileModel> getProfileBySlug({@Path() required String slug});
@Path() required String slug,
});
@GET('/api/profile/index') @GET('/api/profile/index')
Future<ProfileModel> getMyProfile(); Future<ProfileModel> getMyProfile();
@@ -27,9 +25,7 @@ abstract class ProfileService {
Future<ProfileDetailsModel> getMyProfileDetails(); Future<ProfileDetailsModel> getMyProfileDetails();
@GET('/api/profile/detailz/{slug}') @GET('/api/profile/detailz/{slug}')
Future<ProfileDetailsModel> getProfileDetailsBySlug({ Future<ProfileDetailsModel> getProfileDetailsBySlug({@Path() required String slug});
@Path() required String slug,
});
@POST('/api/profile/addCircumference') @POST('/api/profile/addCircumference')
Future<void> addCircumference({ Future<void> addCircumference({
@@ -39,64 +35,38 @@ abstract class ProfileService {
}); });
@POST('/api/profile/addWeight') @POST('/api/profile/addWeight')
Future<void> addWeight({ Future<void> addWeight({@Part() required double weight});
@Part() required double weight,
});
@GET('/api/blogPosts/search_by_author/{slug}') @GET('/api/blogPosts/search_by_author/{slug}')
Future<List<FeedBlogPostModel>> getBlogPostByAuthorSlug({ Future<List<FeedBlogPostModel>> getBlogPostByAuthorSlug({@Path() required String slug});
@Path() required String slug,
});
@GET('/api/profile/measure/weight/{period}') @GET('/api/profile/measure/weight/{period}')
Future<List<WeightModel>> getWeight({ Future<List<WeightModel>> getWeight({@Path() required String period});
@Path() required String period,
});
@GET('/api/profile/measure/weight/{period}/{slug}') @GET('/api/profile/measure/weight/{period}/{slug}')
Future<List<WeightModel>> getWeightBySlug({ Future<List<WeightModel>> getWeightBySlug({@Path() required String period, @Path() required String slug});
@Path() required String period,
@Path() required String slug,
});
@GET('/api/profile/measure/circumferences/{period}') @GET('/api/profile/measure/circumferences/{period}')
Future<List<CircumferenceModel>> getCircumferences({ Future<List<CircumferenceModel>> getCircumferences({@Path() required String period});
@Path() required String period,
});
@GET('/api/profile/measure/circumferences/{period}/{user}') @GET('/api/profile/measure/circumferences/{period}/{slug}')
Future<List<CircumferenceModel>> getCircumferencesBySlug({ Future<List<CircumferenceModel>> getCircumferencesBySlug({@Path() required String period, @Path() required String slug});
@Path() required String period,
@Path() required String slug,
});
@GET('/api/profile/{slug}/subscribers') @GET('/api/profile/{slug}/subscribers')
Future<SubPaginationModel> getSubscribers({ Future<SubPaginationModel> getSubscribers({@Path() required String slug, @Query('filters[name][\$startsWith]') String? query});
@Path() required String slug,
@Query('filters[name][\$startsWith]') String? query,
});
@GET('/api/profile/{slug}/subscriptions') @GET('/api/profile/{slug}/subscriptions')
Future<SubPaginationModel> getSubscriptions({ Future<SubPaginationModel> getSubscriptions({@Path() required String slug, @Query('filters[name][\$startsWith]') String? query});
@Path() required String slug,
@Query('filters[name][\$startsWith]') String? query,
});
@POST('/api/profile/subscribe/{slug}') @POST('/api/profile/subscribe/{slug}')
Future<void> subscribe({ Future<void> subscribe({@Path() required String slug});
@Path() required String slug,
});
@POST('/api/profile/unsubscribe/{slug}') @POST('/api/profile/unsubscribe/{slug}')
Future<void> unsubscribe({ Future<void> unsubscribe({@Path() required String slug});
@Path() required String slug,
});
@POST('/api/profile/photo/upload') @POST('/api/profile/photo/upload')
@MultiPart() @MultiPart()
Future<void> uploadPhoto({ Future<void> uploadPhoto({@Part(name: 'photo') required File photo});
@Part(name: 'photo') required File photo,
});
@DELETE('/api/profile/photo/delete') @DELETE('/api/profile/photo/delete')
Future<void> deletePhoto(); Future<void> deletePhoto();
@@ -105,20 +75,16 @@ abstract class ProfileService {
Future<ChosenDietsPaginationModel> getChosenDiets(); Future<ChosenDietsPaginationModel> getChosenDiets();
@POST('/api/profile/make_pass_key') @POST('/api/profile/make_pass_key')
Future<String> getPassKey({ Future<String> getPassKey({@Body() required String json});
@Body() required String json,
});
@GET('/api/profile/my_phone') @GET('/api/profile/my_phone')
Future<String> getMyPhone(); Future<String> getMyPhone();
@POST('/api/profile/send_email_code') @POST('/api/profile/send_email_code')
Future<void> sendEmailCode(); Future<void> sendEmailCode({@Body() Map<String, dynamic> data = const {}});
@POST('/api/profile/verificate_email_code') @POST('/api/profile/verificate_email_code')
Future<void> tryVerifyEmailByCode({ Future<void> tryVerifyEmailByCode({@Body() required String code});
@Body() required String code,
});
@DELETE('/api/profile/delete_my_account') @DELETE('/api/profile/delete_my_account')
Future<void> deleteAccount(); Future<void> deleteAccount();

View File

@@ -8,62 +8,61 @@ import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/features/registration/models/user_registration_model.dart'; import 'package:eatvio_app/features/registration/models/user_registration_model.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_state.dart'; import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_state.dart';
import 'package:eatvio_app/features/registration/repositories/registration_repository.dart'; import 'package:eatvio_app/features/registration/repositories/registration_repository.dart';
import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
class RegistrationCubit extends Cubit<RegistrationState> { class RegistrationCubit extends Cubit<RegistrationState> {
final RegistrationRepository registrationRepository; final RegistrationRepository registrationRepository;
RegistrationCubit({ RegistrationCubit({required this.registrationRepository})
required this.registrationRepository, : super(
}) : super( const RegistrationState(
const RegistrationState( sheet: UserRegistrationSheet(
sheet: UserRegistrationSheet( firstName: '',
firstName: '', lastName: '',
lastName: '', email: '',
email: '', password: '',
password: '', passwordConfirmation: '',
passwordConfirmation: '', birthdate: '',
birthdate: '', gender: '',
gender: '', location: '',
location: '', responseBody: '{}',
responseBody: '{}', lang: 'Русский',
lang: 'Русский', weight: 0,
weight: 0, height: 0,
height: 0, chestCircumference: 0,
chestCircumference: 0, waistCircumference: 0,
waistCircumference: 0, hipCircumference: 0,
hipCircumference: 0, bloodPressureUp: 0,
bloodPressureUp: 0, bloodPressureDown: 0,
bloodPressureDown: 0, hasHypo: 0,
hasHypo: 0, hasHyper: 0,
hasHyper: 0, aboutAllergy: '',
aboutAllergy: '', aboutTakingPils: '',
aboutTakingPils: '', aboutSportHobbies: '',
aboutSportHobbies: '', favoriteFoods: '',
favoriteFoods: '', unfavoriteFoods: '',
unfavoriteFoods: '', additionalInfo: '',
additionalInfo: '', phone: '',
phone: '',
),
), ),
); ),
);
void updateSheet(UserRegistrationSheet sheet) => emit(RegistrationState(sheet: sheet)); void updateSheet(UserRegistrationSheet sheet) => emit(RegistrationState(sheet: sheet));
Future<void> tryRegistration() async { Future<void> tryRegistration() async {
try { try {
final token = await registrationRepository.tryRegister( final token = await registrationRepository.tryRegister(
state.sheet.copyWith( state.sheet.copyWith(phone: state.sheet.phone.replaceAll(' ', '').replaceAll('(', '').replaceAll(')', ''), responseBody: '{}'),
phone: state.sheet.phone.replaceAll(' ', '').replaceAll('(', '').replaceAll(')', ''),
responseBody: '{}',
),
); );
await TokenStorage.saveTokenInStorage(token); await TokenStorage.saveTokenInStorage(token);
await sl<FirebaseApi>().registerPushToken(); await sl<FirebaseApi>().registerPushToken();
RegistrationAnalyticsService.reportRegistrationComplete();
AppRouter.navigatorKey.currentContext?.replaceNamed('verifyEmailScreen'); AppRouter.navigatorKey.currentContext?.replaceNamed('verifyEmailScreen');
} catch (e, st) { } catch (e, st) {
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
RegistrationAnalyticsService.reportRegistrationError(e.toString());
emit(RegistrationState(sheet: state.sheet, error: e.toString())); emit(RegistrationState(sheet: state.sheet, error: e.toString()));
} }
} }

View File

@@ -1,13 +1,12 @@
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart'; import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart';
import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class VerifyEmailCubit extends Cubit<VerifyEmailState> { class VerifyEmailCubit extends Cubit<VerifyEmailState> {
final ProfileRepository profileRepository; final ProfileRepository profileRepository;
VerifyEmailCubit({ VerifyEmailCubit({required this.profileRepository}) : super(const VerifyEmailState.loading());
required this.profileRepository,
}) : super(const VerifyEmailState.loading());
Future<void> sendEmailCode() async { Future<void> sendEmailCode() async {
try { try {
@@ -24,6 +23,7 @@ class VerifyEmailCubit extends Cubit<VerifyEmailState> {
emit(const VerifyEmailState.verifyLoading()); emit(const VerifyEmailState.verifyLoading());
final result = await profileRepository.tryVerifyEmailByCode(code); final result = await profileRepository.tryVerifyEmailByCode(code);
if (result == true) { if (result == true) {
RegistrationAnalyticsService.reportEmailVerification();
emit(const VerifyEmailState.verifySuccess()); emit(const VerifyEmailState.verifySuccess());
} else { } else {
emit(const VerifyEmailState.verifyError()); emit(const VerifyEmailState.verifyError());
@@ -32,4 +32,16 @@ class VerifyEmailCubit extends Cubit<VerifyEmailState> {
emit(const VerifyEmailState.verifyError()); emit(const VerifyEmailState.verifyError());
} }
} }
Future<void> changeEmail(String newEmail) async {
try {
emit(const VerifyEmailState.changeEmailLoading());
await profileRepository.sendEmailCode(newEmail: newEmail);
emit(const VerifyEmailState.changeEmailSuccess());
// Обновляем email в состоянии после успешной смены
emit(VerifyEmailState.loaded(email: newEmail));
} catch (e) {
emit(const VerifyEmailState.changeEmailError());
}
}
} }

View File

@@ -6,13 +6,9 @@ part 'verify_email_state.freezed.dart';
abstract class VerifyEmailState with _$VerifyEmailState { abstract class VerifyEmailState with _$VerifyEmailState {
const factory VerifyEmailState.loading() = VerifyEmailStateLoading; const factory VerifyEmailState.loading() = VerifyEmailStateLoading;
const factory VerifyEmailState.loaded({ const factory VerifyEmailState.loaded({required String email}) = VerifyEmailStateLoaded;
required String email,
}) = VerifyEmailStateLoaded;
const factory VerifyEmailState.error({ const factory VerifyEmailState.error({required String errorMessage}) = VerifyEmailStateError;
required String errorMessage,
}) = VerifyEmailStateError;
const factory VerifyEmailState() = _VerifyEmailState; const factory VerifyEmailState() = _VerifyEmailState;
@@ -21,4 +17,10 @@ abstract class VerifyEmailState with _$VerifyEmailState {
const factory VerifyEmailState.verifySuccess() = VerifyEmailStateVerifySuccess; const factory VerifyEmailState.verifySuccess() = VerifyEmailStateVerifySuccess;
const factory VerifyEmailState.verifyError() = VerifyEmailStateVerifyError; const factory VerifyEmailState.verifyError() = VerifyEmailStateVerifyError;
const factory VerifyEmailState.changeEmailLoading() = VerifyEmailStateChangeEmailLoading;
const factory VerifyEmailState.changeEmailSuccess() = VerifyEmailStateChangeEmailSuccess;
const factory VerifyEmailState.changeEmailError() = VerifyEmailStateChangeEmailError;
} }

View File

@@ -27,48 +27,46 @@ class _GenderScreenState extends State<GenderScreen> {
padding: const EdgeInsets.only(bottom: 80, top: 16, left: 16, right: 16), padding: const EdgeInsets.only(bottom: 80, top: 16, left: 16, right: 16),
child: Column( child: Column(
children: [ children: [
RadioListTile( Row(
contentPadding: EdgeInsets.zero, children: [
shape: RoundedRectangleBorder( Radio<String>(
borderRadius: BorderRadius.circular(16), value: 'male',
), // ignore: deprecated_member_use
title: const Text('Мужской'), groupValue: currentValue,
value: 'male', // ignore: deprecated_member_use
groupValue: currentValue, onChanged: (newValue) {
onChanged: (newValue) { final registrationCubit = context.read<RegistrationCubit>();
final registrationCubit = context.read<RegistrationCubit>(); registrationCubit.updateSheet(registrationCubit.state.sheet.copyWith(gender: 'male'));
registrationCubit.updateSheet(registrationCubit.state.sheet.copyWith(gender: 'male')); setState(() {
setState(() { currentValue = 'male';
currentValue = 'male'; });
}); },
}, ),
secondary: Image.asset( Expanded(
'assets/images/img_male.png', child: Row(children: [Image.asset('assets/images/img_male.png', width: 40, height: 40), const SizedBox(width: 8), const Text('Мужской')]),
width: 40, ),
height: 40, ],
),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
RadioListTile( Row(
contentPadding: EdgeInsets.zero, children: [
shape: RoundedRectangleBorder( Radio<String>(
borderRadius: BorderRadius.circular(16), value: 'female',
), // ignore: deprecated_member_use
title: const Text('Женский'), groupValue: currentValue,
value: 'female', // ignore: deprecated_member_use
groupValue: currentValue, onChanged: (newValue) {
onChanged: (newValue) { final registrationCubit = context.read<RegistrationCubit>();
final registrationCubit = context.read<RegistrationCubit>(); registrationCubit.updateSheet(registrationCubit.state.sheet.copyWith(gender: 'female'));
registrationCubit.updateSheet(registrationCubit.state.sheet.copyWith(gender: 'female')); setState(() {
setState(() { currentValue = 'female';
currentValue = 'female'; });
}); },
}, ),
secondary: Image.asset( Expanded(
'assets/images/img_female.png', child: Row(children: [Image.asset('assets/images/img_female.png', width: 40, height: 40), const SizedBox(width: 8), const Text('Женский')]),
width: 40, ),
height: 40, ],
),
), ),
], ],
), ),

View File

@@ -17,6 +17,7 @@ import 'package:eatvio_app/features/registration/presentation/pages/medicine_int
import 'package:eatvio_app/features/registration/presentation/pages/name_screen.dart'; import 'package:eatvio_app/features/registration/presentation/pages/name_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/phone_screen.dart'; import 'package:eatvio_app/features/registration/presentation/pages/phone_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/weight_screen.dart'; import 'package:eatvio_app/features/registration/presentation/pages/weight_screen.dart';
import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart';
import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/generated/l10n.dart';
import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart';
@@ -39,6 +40,12 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
final pageController = PageController(); final pageController = PageController();
final RegistrationCubit registrationCubit = sl<RegistrationCubit>(); final RegistrationCubit registrationCubit = sl<RegistrationCubit>();
@override
void initState() {
super.initState();
RegistrationAnalyticsService.reportRegistrationStep(step);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider<RegistrationCubit>( return BlocProvider<RegistrationCubit>(
@@ -51,12 +58,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('Ошибка при регистрации'), title: const Text('Ошибка при регистрации'),
content: Text(state.error!), content: Text(state.error!),
actions: [ actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
), ),
); );
} }
@@ -76,22 +78,14 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
highlightElevation: 0, highlightElevation: 0,
focusElevation: 0, focusElevation: 0,
elevation: 0, elevation: 0,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
borderRadius: BorderRadius.circular(20),
),
backgroundColor: const Color(0xFFEAF2FD), backgroundColor: const Color(0xFFEAF2FD),
onPressed: () async { onPressed: () async {
setState(() { setState(() {
pageController.previousPage( pageController.previousPage(duration: const Duration(milliseconds: 350), curve: Curves.easeInOut);
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
);
}); });
}, },
label: Icon( label: Icon(Icons.arrow_back_ios_new_rounded, color: appTheme.blueA400),
Icons.arrow_back_ios_new_rounded,
color: appTheme.blueA400,
),
), ),
), ),
Container( Container(
@@ -103,24 +97,16 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
highlightElevation: 0, highlightElevation: 0,
elevation: 0, elevation: 0,
focusElevation: 0, focusElevation: 0,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
borderRadius: BorderRadius.circular(20),
),
backgroundColor: appTheme.blueA400, backgroundColor: appTheme.blueA400,
onPressed: () async { onPressed: () async {
if (await canGoNext(state, context)) { if (await canGoNext(state, context)) {
setState(() { setState(() {
pageController.nextPage( pageController.nextPage(duration: const Duration(milliseconds: 350), curve: Curves.easeInOut);
duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut,
);
}); });
} }
}, },
label: Text( label: Text('Продолжить', style: theme.textTheme.titleSmall!.copyWith(color: Colors.white)),
'Продолжить',
style: theme.textTheme.titleSmall!.copyWith(color: Colors.white),
),
), ),
), ),
], ],
@@ -128,23 +114,11 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
appBar: AppBar( appBar: AppBar(
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
leading: Builder( leading: Builder(
builder: (context) => IconButton( builder: (context) => IconButton(icon: const Icon(Icons.close_rounded), onPressed: () => GoRouter.of(context).pop()),
icon: const Icon(Icons.close_rounded),
onPressed: () => GoRouter.of(context).pop(),
),
), ),
centerTitle: true, centerTitle: true,
title: SvgPicture.asset( title: SvgPicture.asset(ImageConstant.imgLogoEatvio, height: 48, fit: BoxFit.contain),
ImageConstant.imgLogoEatvio, actions: [Padding(padding: const EdgeInsets.only(right: 16), child: Text('шаг $step из 15'))],
height: 48,
fit: BoxFit.contain,
),
actions: [
Padding(
padding: const EdgeInsets.only(right: 16),
child: Text('шаг $step из 15'),
),
],
bottom: PreferredSize( bottom: PreferredSize(
preferredSize: Size(MediaQuery.of(context).size.width, 32), preferredSize: Size(MediaQuery.of(context).size.width, 32),
child: Padding( child: Padding(
@@ -152,10 +126,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
child: TweenAnimationBuilder<double>( child: TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 350), duration: const Duration(milliseconds: 350),
curve: Curves.easeInOut, curve: Curves.easeInOut,
tween: Tween<double>( tween: Tween<double>(begin: 1 / 15, end: progressValue),
begin: 1 / 15,
end: progressValue,
),
builder: (context, value, _) => LinearProgressIndicator( builder: (context, value, _) => LinearProgressIndicator(
value: value, value: value,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
@@ -175,6 +146,8 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
step = index + 1; step = index + 1;
progressValue = step / 15; progressValue = step / 15;
}); });
// Report analytics for step completion
RegistrationAnalyticsService.reportRegistrationStep(step);
}, },
children: [ children: [
// LanguageSelectScreen(sheet: state.sheet), // LanguageSelectScreen(sheet: state.sheet),
@@ -211,12 +184,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('Ошибка'), title: const Text('Ошибка'),
content: const Text('Введите корректный номер телефона'), content: const Text('Введите корректный номер телефона'),
actions: [ actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
), ),
); );
return false; return false;
@@ -224,12 +192,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
return true; return true;
} }
case 2: case 2:
List<String> errors = validateCredentialsFields( List<String> errors = validateCredentialsFields(state.sheet.email, state.sheet.password, state.sheet.passwordConfirmation, context);
state.sheet.email,
state.sheet.password,
state.sheet.passwordConfirmation,
context,
);
if (errors.isEmpty) { if (errors.isEmpty) {
return true; return true;
} else { } else {
@@ -244,22 +207,13 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('Ошибка'), title: const Text('Ошибка'),
content: Text(errorMessage), content: Text(errorMessage),
actions: [ actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
), ),
); );
return false; return false;
} }
case 3: case 3:
List<String> errors = validateNameFields( List<String> errors = validateNameFields(state.sheet.firstName.toString(), state.sheet.lastName.toString(), context);
state.sheet.firstName.toString(),
state.sheet.lastName.toString(),
context,
);
if (errors.isEmpty) { if (errors.isEmpty) {
return true; return true;
} else { } else {
@@ -274,12 +228,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('Ошибка'), title: const Text('Ошибка'),
content: Text(errorMessage), content: Text(errorMessage),
actions: [ actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
), ),
); );
return false; return false;
@@ -293,12 +242,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('Ошибка'), title: const Text('Ошибка'),
content: const Text('Выберете пол'), content: const Text('Выберете пол'),
actions: [ actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
), ),
); );
return false; return false;
@@ -312,12 +256,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('Ошибка'), title: const Text('Ошибка'),
content: const Text('Заполните все поля'), content: const Text('Заполните все поля'),
actions: [ actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
), ),
); );
return false; return false;
@@ -343,12 +282,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('Ошибка'), title: const Text('Ошибка'),
content: Text(errorMessage), content: Text(errorMessage),
actions: [ actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
), ),
); );
return false; return false;
@@ -359,10 +293,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
barrierDismissible: false, barrierDismissible: false,
useSafeArea: false, useSafeArea: false,
context: context, context: context,
builder: (context) => const PopScope( builder: (context) => const PopScope(canPop: false, child: Loader()),
canPop: false,
child: Loader(),
),
); );
await registrationCubit.tryRegistration(); await registrationCubit.tryRegistration();
if (context.mounted) { if (context.mounted) {
@@ -373,9 +304,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
Navigator.pop(context); Navigator.pop(context);
showDialog( showDialog(
context: context, context: context,
builder: (context) => const AlertDialog( builder: (context) => const AlertDialog(title: Text('Ошибка при попытке регистрации')),
title: Text('Ошибка при попытке регистрации'),
),
); );
} }
} }
@@ -385,12 +314,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
} }
} }
List<String> validateCircumferencesFields( List<String> validateCircumferencesFields(String chestCircumference, String waistCircumference, String hipCircumference, BuildContext context) {
String chestCircumference,
String waistCircumference,
String hipCircumference,
BuildContext context,
) {
final s = S.of(context); final s = S.of(context);
List<String> errors = []; List<String> errors = [];
@@ -406,12 +330,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
return errors; return errors;
} }
List<String> validateCredentialsFields( List<String> validateCredentialsFields(String email, String password, String passwordConfirmation, BuildContext context) {
String email,
String password,
String passwordConfirmation,
BuildContext context,
) {
final s = S.of(context); final s = S.of(context);
List<String> errors = []; List<String> errors = [];
@@ -435,11 +354,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
return errors; return errors;
} }
List<String> validateNameFields( List<String> validateNameFields(String firstName, String lastName, BuildContext context) {
String firstName,
String lastName,
BuildContext context,
) {
List<String> errors = []; List<String> errors = [];
if (firstName.isEmpty) { if (firstName.isEmpty) {

View File

@@ -1,7 +1,15 @@
import 'dart:async';
import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart'; import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart'; import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart';
import 'package:eatvio_app/features/registration/presentation/widgets/change_email_dialog.dart';
import 'package:eatvio_app/features/registration/presentation/widgets/code_not_received_dialog.dart';
import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@@ -18,11 +26,17 @@ class _VerifyEmailScreenState extends State<VerifyEmailScreen> {
final List<TextEditingController> _controllers = List.generate(4, (_) => TextEditingController()); final List<TextEditingController> _controllers = List.generate(4, (_) => TextEditingController());
String email = ''; String email = '';
late final GoRouter goRouter; late final GoRouter goRouter;
late Timer _timer;
int _secondsRemaining = 60;
bool _canSendAgain = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
goRouter = GoRouter.of(context); goRouter = GoRouter.of(context);
_startTimer();
// Report email verification screen opened
RegistrationAnalyticsService.reportRegistrationStep(16); // Email verification step
} }
@override @override
@@ -36,6 +50,23 @@ class _VerifyEmailScreenState extends State<VerifyEmailScreen> {
super.dispose(); super.dispose();
} }
void _startTimer() {
setState(() {
_canSendAgain = false;
_secondsRemaining = 60;
});
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
setState(() {
if (_secondsRemaining > 0) {
_secondsRemaining--;
} else {
_canSendAgain = true;
_timer.cancel();
}
});
});
}
void _checkAndSubmitCode(BuildContext context) { void _checkAndSubmitCode(BuildContext context) {
final code = _controllers.map((c) => c.text).join(); final code = _controllers.map((c) => c.text).join();
if (code.length == 4) { if (code.length == 4) {
@@ -47,48 +78,96 @@ class _VerifyEmailScreenState extends State<VerifyEmailScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider<VerifyEmailCubit>( return BlocProvider<VerifyEmailCubit>(
create: (_) => sl<VerifyEmailCubit>()..sendEmailCode(), create: (_) => sl<VerifyEmailCubit>()..sendEmailCode(),
child: BlocConsumer<VerifyEmailCubit, VerifyEmailState>( child: Builder(
listener: (context, state) { builder: (context) {
if (state is VerifyEmailStateLoaded) { return BlocConsumer<VerifyEmailCubit, VerifyEmailState>(
setState(() { listener: (context, state) {
email = state.email; if (state is VerifyEmailStateLoaded) {
}); setState(() {
} else if (state is VerifyEmailStateVerifySuccess) { email = state.email;
showSuccessToast('Почта успешно подтверждена'); });
goRouter.pushNamed('feedScreen'); _startTimer();
} else if (state is VerifyEmailStateVerifyError) { } else if (state is VerifyEmailStateVerifySuccess) {
showErrorToast("Неверный код. Попробуйте снова."); showSuccessToast('Почта успешно подтверждена');
for (var controller in _controllers) { goRouter.pushNamed('feedScreen');
controller.clear(); } else if (state is VerifyEmailStateVerifyError) {
} showErrorToast("Неверный код. Попробуйте снова.");
FocusScope.of(context).requestFocus(_focusNodes[0]); for (var controller in _controllers) {
} controller.clear();
}, }
builder: (context, state) { FocusScope.of(context).requestFocus(_focusNodes[0]);
return Scaffold( } else if (state is VerifyEmailStateChangeEmailSuccess) {
appBar: AppBar(title: const Text("Подтвердите почту")), showSuccessToast('Код подтверждения отправлен на новый email');
body: Padding( // Диалог закроется автоматически в ChangeEmailDialog при успешной смене
padding: const EdgeInsets.all(24.0), } else if (state is VerifyEmailStateChangeEmailError) {
child: Column( showErrorToast('Ошибка при смене email. Попробуйте снова.');
mainAxisAlignment: MainAxisAlignment.center, }
children: [ },
Text("Код отправлен на почту\n$email", textAlign: TextAlign.center), builder: (context, state) {
const SizedBox(height: 20), return Scaffold(
Row( appBar: AppBar(title: const Text("Подтвердите почту")),
mainAxisAlignment: MainAxisAlignment.spaceEvenly, body: Padding(
children: List.generate(4, (index) => _buildCodeField(context, index)), padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("Код отправлен на почту\n$email", textAlign: TextAlign.center),
const SizedBox(height: 20),
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: List.generate(4, (index) => _buildCodeField(context, index))),
const SizedBox(height: 16),
GestureDetector(
onTap: () {
final verifyEmailCubit = context.read<VerifyEmailCubit>();
showDialog(context: context, builder: (context) => CodeNotReceivedDialog(
currentEmail: email,
verifyEmailCubit: verifyEmailCubit,
));
},
child: Text(
'Не приходит код?',
style: TextStyles.textSMedium.copyWith(color: appTheme.blueA400, decoration: TextDecoration.underline),
),
),
const SizedBox(height: 24),
CustomOutlinedButton(
text: 'Запросить код повторно',
height: 52,
isDisabled: !_canSendAgain,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(_canSendAgain ? appTheme.blueA400 : appTheme.gray),
),
onPressed: () async {
await context.read<VerifyEmailCubit>().sendEmailCode();
_startTimer();
},
),
const SizedBox(height: 8),
if (!_canSendAgain)
Text("Зпросить код повторно можно через $_secondsRemaining с.", style: TextStyles.textSMedium.copyWith(color: appTheme.gray)),
const SizedBox(height: 16),
CustomOutlinedButton(
text: 'Сменить email',
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.blueA400),
buttonStyle: ButtonStyle(
side: WidgetStatePropertyAll(BorderSide(color: appTheme.blueA400)),
backgroundColor: const WidgetStatePropertyAll(Colors.transparent),
),
onPressed: () {
final verifyEmailCubit = context.read<VerifyEmailCubit>();
showDialog(
context: context,
builder: (context) => ChangeEmailDialog(currentEmail: email, verifyEmailCubit: verifyEmailCubit),
);
},
),
],
), ),
const SizedBox(height: 20), ),
const Text("Запросить код повторно", style: TextStyle(color: Colors.grey)), );
TextButton( },
onPressed: () {
context.read<VerifyEmailCubit>().sendEmailCode();
},
child: const Text("Не приходит код?"),
),
],
),
),
); );
}, },
), ),

View File

@@ -1,6 +1,7 @@
import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:lottie/lottie.dart'; import 'package:lottie/lottie.dart';
@@ -9,10 +10,7 @@ import 'package:webview_flutter/webview_flutter.dart';
class VerifyScreen extends StatefulWidget { class VerifyScreen extends StatefulWidget {
final String passKey; final String passKey;
const VerifyScreen({ const VerifyScreen({super.key, required this.passKey});
super.key,
required this.passKey,
});
@override @override
State<VerifyScreen> createState() => _VerifyScreenState(); State<VerifyScreen> createState() => _VerifyScreenState();
@@ -25,6 +23,8 @@ class _VerifyScreenState extends State<VerifyScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Report phone verification screen opened
RegistrationAnalyticsService.reportRegistrationStep(17); // Phone verification step
_controller = WebViewController() _controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted) ..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate( ..setNavigationDelegate(
@@ -36,6 +36,8 @@ class _VerifyScreenState extends State<VerifyScreen> {
}, },
onNavigationRequest: (NavigationRequest request) async { onNavigationRequest: (NavigationRequest request) async {
if (request.url.contains('user_detailz') && !request.url.contains('change-phone-verify') || request.url.contains('email/verify')) { if (request.url.contains('user_detailz') && !request.url.contains('change-phone-verify') || request.url.contains('email/verify')) {
// Report phone verification success
RegistrationAnalyticsService.reportPhoneVerification();
GoRouter.of(context).pop(); GoRouter.of(context).pop();
await showModalBottomSheet( await showModalBottomSheet(
useRootNavigator: true, useRootNavigator: true,
@@ -50,27 +52,15 @@ class _VerifyScreenState extends State<VerifyScreen> {
const Text( const Text(
'Номер телефона\nуспешно привязан', 'Номер телефона\nуспешно привязан',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(fontSize: 20, height: 1, fontWeight: FontWeight.w600),
fontSize: 20,
height: 1,
fontWeight: FontWeight.w600,
),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Lottie.asset( Lottie.asset('assets/lottie_animations/success.json', width: 160, height: 160),
'assets/lottie_animations/success.json',
width: 160,
height: 160,
),
const SizedBox(height: 16), const SizedBox(height: 16),
const Text( const Text(
'Теперь Вам доступен весь функционал!', 'Теперь Вам доступен весь функционал!',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(fontSize: 16, height: 1, fontWeight: FontWeight.w500),
fontSize: 16,
height: 1,
fontWeight: FontWeight.w500,
),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
InkWell( InkWell(
@@ -80,18 +70,11 @@ class _VerifyScreenState extends State<VerifyScreen> {
child: Ink( child: Ink(
width: MediaQuery.of(context).size.width - 32, width: MediaQuery.of(context).size.width - 32,
height: 52, height: 52,
decoration: BoxDecoration( decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
borderRadius: BorderRadius.circular(20),
color: appTheme.blueA400,
),
child: const Center( child: const Center(
child: Text( child: Text(
'Закрыть', 'Закрыть',
style: TextStyle( style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600),
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w600,
),
), ),
), ),
), ),
@@ -117,10 +100,7 @@ class _VerifyScreenState extends State<VerifyScreen> {
child: Stack( child: Stack(
children: [ children: [
WebViewWidget(controller: _controller), WebViewWidget(controller: _controller),
if (_isLoading) if (_isLoading) const Center(child: Loader()),
const Center(
child: Loader(),
),
], ],
), ),
), ),

View File

@@ -4,6 +4,7 @@ import 'dart:developer';
import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart';
import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart'; import 'package:webview_flutter/webview_flutter.dart';
@@ -24,6 +25,8 @@ class _WebViewRegistrationScreenState extends State<WebViewRegistrationScreen> {
void initState() { void initState() {
super.initState(); super.initState();
authCubit = sl<AuthCubit>(); authCubit = sl<AuthCubit>();
// Report webview registration started
RegistrationAnalyticsService.reportRegistrationStep(18); // Webview registration step
_controller = WebViewController() _controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted) ..setJavaScriptMode(JavaScriptMode.unrestricted)
..addJavaScriptChannel( ..addJavaScriptChannel(
@@ -43,10 +46,9 @@ class _WebViewRegistrationScreenState extends State<WebViewRegistrationScreen> {
NavigationDelegate( NavigationDelegate(
onNavigationRequest: (NavigationRequest request) { onNavigationRequest: (NavigationRequest request) {
if (request.url.contains('profile')) { if (request.url.contains('profile')) {
authCubit.getTokenByCredentials( // Report webview registration completion
email!, RegistrationAnalyticsService.reportRegistrationComplete();
password!, authCubit.getTokenByCredentials(email!, password!);
);
} }
return NavigationDecision.navigate; return NavigationDecision.navigate;
}, },
@@ -86,12 +88,8 @@ class _WebViewRegistrationScreenState extends State<WebViewRegistrationScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(toolbarHeight: 0),
toolbarHeight: 0, body: SafeArea(child: WebViewWidget(controller: _controller)),
),
body: SafeArea(
child: WebViewWidget(controller: _controller),
),
); );
} }
} }

View File

@@ -0,0 +1,174 @@
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class ChangeEmailDialog extends StatefulWidget {
final String currentEmail;
final VerifyEmailCubit verifyEmailCubit;
const ChangeEmailDialog({super.key, required this.currentEmail, required this.verifyEmailCubit});
@override
State<ChangeEmailDialog> createState() => _ChangeEmailDialogState();
}
class _ChangeEmailDialogState extends State<ChangeEmailDialog> {
final TextEditingController _emailController = TextEditingController();
final FocusNode _emailFocusNode = FocusNode();
bool _isLoading = false;
String? _errorText;
@override
void initState() {
super.initState();
_emailController.text = widget.currentEmail;
}
@override
void dispose() {
_emailController.dispose();
_emailFocusNode.dispose();
super.dispose();
}
bool _isValidEmail(String email) {
return RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(email);
}
void _validateEmail() {
final email = _emailController.text.trim();
if (email.isEmpty) {
setState(() {
_errorText = 'Введите email';
});
} else if (!_isValidEmail(email)) {
setState(() {
_errorText = 'Введите корректный email';
});
} else if (email == widget.currentEmail) {
setState(() {
_errorText = 'Новый email должен отличаться от текущего';
});
} else {
setState(() {
_errorText = null;
});
}
}
Future<void> _changeEmail() async {
_validateEmail();
if (_errorText != null) return;
setState(() {
_isLoading = true;
});
try {
await widget.verifyEmailCubit.changeEmail(_emailController.text.trim());
// Диалог закроется автоматически при успешной смене email через основной экран
} catch (e) {
if (mounted) {
showErrorToast('Ошибка при смене email. Попробуйте снова.');
setState(() {
_isLoading = false;
});
}
}
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: widget.verifyEmailCubit,
child: BlocListener<VerifyEmailCubit, VerifyEmailState>(
listener: (context, state) {
if (state is VerifyEmailStateChangeEmailSuccess) {
Navigator.of(context).pop(); // Закрываем диалог при успешной смене
} else if (state is VerifyEmailStateChangeEmailError) {
setState(() {
_isLoading = false;
});
}
},
child: Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Сменить email', style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
const SizedBox(height: 16),
Text('Введите новый email для получения кода подтверждения', style: TextStyles.textSMedium.copyWith(color: appTheme.gray)),
const SizedBox(height: 20),
TextField(
controller: _emailController,
focusNode: _emailFocusNode,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
hintText: 'Введите новый email',
hintStyle: TextStyles.textSMedium.copyWith(color: appTheme.gray),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: appTheme.gray),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: appTheme.blueA400),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: appTheme.red),
),
errorText: _errorText,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
),
onChanged: (_) => _validateEmail(),
onSubmitted: (_) => _changeEmail(),
),
const SizedBox(height: 24),
Row(
children: [
Expanded(
child: CustomOutlinedButton(
text: 'Отмена',
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.black),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0),
),
onPressed: () => Navigator.of(context).pop(),
),
),
const SizedBox(width: 12),
Expanded(
child: CustomOutlinedButton(
text: _isLoading ? 'Отправка...' : 'Сменить',
height: 52,
isDisabled: _isLoading,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(_isLoading ? appTheme.gray : appTheme.blueA400),
),
onPressed: _isLoading ? null : _changeEmail,
),
),
],
),
],
),
),
),
),
);
}
}

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