Compare commits

..

2 Commits

Author SHA1 Message Date
Username1548
26ff548752 changed base url 2025-07-07 19:55:16 +07:00
Username1548
43e9098d8d added date parsing for reminder 2025-07-07 14:16:23 +07:00
182 changed files with 5852 additions and 8712 deletions

View File

@@ -11,29 +11,6 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Checkout env repository
uses: actions/checkout@v2
with:
repository: mso7nueh/eatvio_app_env
path: env_repo
token: ${{ vars.MSO7NUEH_TOKEN }}
ref: main
- name: Copy .env file
run: |
cp env_repo/.env .env
- name: Load environment variables from .env
run: |
set -a
source .env
set +a
# Export all variables to GITHUB_ENV
while IFS= read -r line; do
if [[ $line =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then
echo "$line" >> $GITHUB_ENV
fi
done < .env
- name: Unlock login keychain - name: Unlock login keychain
run: | run: |
@@ -41,25 +18,12 @@ jobs:
env: env:
KEYCHAIN_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }} KEYCHAIN_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }}
- name: Setup API key
run: |
echo "${API_APPSTORE_KEY}" | tr -d '\n' | base64 --decode > ios/AuthKey.p8
chmod 600 ios/AuthKey.p8
ls -la ios/AuthKey.p8
echo "File size: $(wc -c < ios/AuthKey.p8) bytes"
echo "Contents of ios directory:"
ls -la ios/
env:
API_APPSTORE_KEY: ${{ vars.API_APPSTORE_KEY }}
- name: Flutter build - name: Flutter build
run: | run: |
flutter clean flutter clean
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
@@ -67,8 +31,6 @@ jobs:
LANG: en_US.UTF-8 LANG: en_US.UTF-8
LC_ALL: en_US.UTF-8 LC_ALL: en_US.UTF-8
MATCH_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }} MATCH_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }}
FASTLANE_PASSWORD: ${{ vars.FASTLANE_PASSWORD }}
MATCH_GIT_URL: http://gitea.office.local:3000/mso7nueh/fastlane MATCH_GIT_URL: http://gitea.office.local:3000/mso7nueh/fastlane
FASTLANE_USER: eatvio2023@icloud.com FASTLANE_USER: eatvio2023@icloud.com
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ vars.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }} FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ vars.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }}
API_APPSTORE_KEY: ${{ vars.API_APPSTORE_KEY }}

View File

@@ -3,7 +3,7 @@ name: Eatvio App Android Test
on: [ pull_request ] on: [ pull_request ]
env: env:
flutter_version: "3.35.1" flutter_version: "3.32.5"
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/"
@@ -16,30 +16,6 @@ jobs:
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Checkout env repository
uses: actions/checkout@v3
with:
repository: mso7nueh/eatvio_app_env
path: env_repo
token: ${{ vars.MSO7NUEH_TOKEN }}
ref: main
- name: Copy .env file
run: |
cp env_repo/.env .env
- name: Load environment variables from .env
run: |
set -a
source .env
set +a
# Export all variables to GITHUB_ENV
while IFS= read -r line; do
if [[ $line =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then
echo "$line" >> $GITHUB_ENV
fi
done < .env
- name: Create key.properties file - name: Create key.properties file
run: | run: |
cat > android/key.properties <<EOF cat > android/key.properties <<EOF
@@ -143,11 +119,7 @@ jobs:
run: flutter test run: flutter test
- name: Build apk - name: Build apk
run: | run: flutter build apk --release
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: |

4
.gitignore vendored
View File

@@ -12,9 +12,5 @@
/android/app/.cxx /android/app/.cxx
*.g.dart *.g.dart
*.freezed.dart *.freezed.dart
*.gr.dart
*.mocks.dart
android/build/ android/build/
ios/XCBuildData/ ios/XCBuildData/
env/.env
.env

View File

@@ -1,84 +0,0 @@
# Настройка переменных окружения
Этот проект использует переменные окружения для хранения секретных данных и конфигурации.
## Установка
1. Скопируйте файл `env.example` в `.env`:
```bash
cp env.example .env
```
2. Отредактируйте файл `.env` и замените значения на ваши собственные секреты.
## Переменные окружения
### API URLs
- `BASE_URL` - основной URL API (по умолчанию: https://eatvio.ru)
- `DEV_BASE_URL` - URL API для разработки (по умолчанию: https://dev2.eatvio.com)
### Sentry Configuration
- `SENTRY_DSN` - DSN для Sentry мониторинга
### Firebase Configuration
- `FIREBASE_PROJECT_ID` - ID проекта Firebase
- `FIREBASE_DATABASE_URL` - URL базы данных Firebase
- `FIREBASE_STORAGE_BUCKET` - bucket для хранения файлов
- `FIREBASE_MESSAGING_SENDER_ID` - ID отправителя сообщений
### Firebase Android Configuration
- `FIREBASE_ANDROID_API_KEY` - API ключ для Android
- `FIREBASE_ANDROID_APP_ID` - ID приложения для Android
### Firebase iOS Configuration
- `FIREBASE_IOS_API_KEY` - API ключ для iOS
- `FIREBASE_IOS_APP_ID` - ID приложения для iOS
- `FIREBASE_IOS_BUNDLE_ID` - Bundle ID для iOS
### App Store URLs
- `APP_STORE_URL` - URL App Store
- `RUSTORE_URL` - URL RuStore
### Support URLs
- `TELEGRAM_SUPPORT_URL` - URL поддержки в Telegram
- `SUPPORT_EMAIL` - email для поддержки
### Test Credentials (только для разработки)
- `TEST_EMAIL` - тестовый email
- `TEST_PASSWORD` - тестовый пароль
### Email Validation API
- `EMAIL_VALIDATION_API_URL` - URL API для валидации email
### Connection Check URLs
- `GOOGLE_CHECK_URL` - URL для проверки соединения (Google)
- `YANDEX_CHECK_URL` - URL для проверки соединения (Yandex)
## Безопасность
⚠️ **ВАЖНО**: Файл `.env` добавлен в `.gitignore` и не должен попадать в репозиторий. Все секретные данные должны храниться только в этом файле.
## Использование в коде
Для доступа к переменным окружения используйте класс `EnvConfig`:
```dart
import 'package:eatvio_app/core/env_config.dart';
String baseUrl = EnvConfig.baseUrl;
String sentryDsn = EnvConfig.sentryDsn;
```
## Миграция
Все секреты были перенесены из кода в переменные окружения:
1. **Sentry DSN** - из `main.dart`
2. **Firebase конфигурация** - из `firebase_options.dart`
3. **API URLs** - из `const.dart`
4. **URL для проверки соединения** - из `internet_connection_checker_screen.dart`
5. **URL для обновления приложения** - из `update_screen.dart`
6. **URL поддержки Telegram** - из `support_screen.dart`
7. **API для валидации email** - из `registration_repository.dart`
8. **Тестовые учетные данные** - из `test_cases.dart`
9. **Email поддержки** - из `support_screen.dart`

View File

@@ -19,17 +19,17 @@ android {
ndkVersion = "27.0.12077973" ndkVersion = "27.0.12077973"
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_21
} }
kotlinOptions { kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString() jvmTarget = 21
} }
defaultConfig { defaultConfig {
applicationId = "com.eatvioapp.app" applicationId = "com.eatvioapp.app"
minSdkVersion flutter.minSdkVersion minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode versionCode = flutter.versionCode
versionName = flutter.versionName versionName = flutter.versionName

View File

@@ -2,12 +2,9 @@
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
<application <application
android:label="Итвио" android:label="Eatvio"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +1,3 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true

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-9.0.0-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-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.12.0" apply false id "com.android.application" version "8.7.3" apply false
// START: FlutterFire Configuration // START: FlutterFire Configuration
id "com.google.gms.google-services" version "4.4.3" apply false id "com.google.gms.google-services" version "4.4.2" apply false
// END: FlutterFire Configuration // END: FlutterFire Configuration
id "org.jetbrains.kotlin.android" version "2.2.10" apply false id "org.jetbrains.kotlin.android" version "2.0.21" apply false
} }
include ":app" include ":app"

View File

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

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,3 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.7 KiB

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>16.0</string> <string>12.0</string>
</dict> </dict>
</plist> </plist>

View File

@@ -1,3 +1,3 @@
source "https://rubygems.org" source "https://rubygems.org"
gem "fastlane", "~> 2.217" gem "fastlane"

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, '16.0' platform :ios, '14.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'] = '16.0' config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0'
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)', '$(inherited)',
## dart: PermissionGroup.camera ## dart: PermissionGroup.camera

View File

@@ -370,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 = 16.0; IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos; SUPPORTED_PLATFORMS = iphoneos;
@@ -388,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 = 72; CURRENT_PROJECT_VERSION = 39;
DEVELOPMENT_TEAM = VL68C36HYS; DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
@@ -406,11 +406,10 @@
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)/Flutter", "$(PROJECT_DIR)/Flutter",
); );
MARKETING_VERSION = 1.2.3; MARKETING_VERSION = 1.0.50;
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";
@@ -467,7 +466,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 = 16.0; IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES; MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -519,7 +518,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 = 16.0; IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos; SUPPORTED_PLATFORMS = iphoneos;
@@ -539,7 +538,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 = 72; CURRENT_PROJECT_VERSION = 39;
DEVELOPMENT_TEAM = VL68C36HYS; DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
@@ -557,11 +556,10 @@
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)/Flutter", "$(PROJECT_DIR)/Flutter",
); );
MARKETING_VERSION = 1.2.3; MARKETING_VERSION = 1.0.50;
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;
@@ -578,7 +576,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 = 72; CURRENT_PROJECT_VERSION = 39;
DEVELOPMENT_TEAM = VL68C36HYS; DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
@@ -596,11 +594,10 @@
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)/Flutter", "$(PROJECT_DIR)/Flutter",
); );
MARKETING_VERSION = 1.2.3; MARKETING_VERSION = 1.0.50;
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 = "Release" buildConfiguration = "Debug"
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

@@ -13,7 +13,7 @@
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>Итвио</string> <string>eatvio_app</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
@@ -21,7 +21,7 @@
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>72</string> <string>39</string>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
<false/> <false/>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>

View File

@@ -18,69 +18,9 @@ default_platform(:ios)
platform :ios do platform :ios do
desc "Push a new beta build to TestFlight" desc "Push a new beta build to TestFlight"
lane :beta do lane :beta do
UI.message "Current working directory: #{Dir.pwd}" match(type: "appstore")
UI.message "Checking API key file..."
possible_paths = [
"../AuthKey.p8",
"./AuthKey.p8",
"../../AuthKey.p8",
File.expand_path("../AuthKey.p8"),
File.expand_path("./AuthKey.p8")
]
auth_key_path = nil
possible_paths.each do |path|
if File.exist?(path)
UI.message "Found AuthKey.p8 at: #{path}"
auth_key_path = File.expand_path(path)
break
end
end
if auth_key_path.nil?
UI.user_error!("API key file not found. Tried paths: #{possible_paths.join(', ')}")
end
UI.message "Using auth_key_path: #{auth_key_path}"
# Configure Flutter for release mode
UI.message "Configuring Flutter for release mode..."
sh("cd .. && flutter build ios --config-only --release")
match(type: "appstore", readonly: true)
increment_build_number(xcodeproj: "Runner.xcodeproj") increment_build_number(xcodeproj: "Runner.xcodeproj")
build_app(workspace: "Runner.xcworkspace", scheme: "Runner", export_method: "app-store", codesigning_identity: "Apple Distribution")
build_app( upload_to_testflight
workspace: "Runner.xcworkspace",
scheme: "Runner",
export_method: "app-store",
codesigning_identity: "Apple Distribution",
)
app_store_connect_api_key(
key_id: "325JS9JCZH",
issuer_id: "dfa27d1d-2bb8-4d4d-847e-0b32f9b880b3",
key_filepath: auth_key_path,
duration: 1200,
in_house: false
)
begin
upload_to_testflight(
skip_waiting_for_build_processing: true,
skip_submission: true,
distribute_external: false,
notify_external_testers: false,
changelog: "TestFlight build #{get_build_number(xcodeproj: 'Runner.xcodeproj')}",
verbose: true
)
rescue => ex
UI.error "Upload to TestFlight failed: #{ex.message}"
UI.error "Full error: #{ex.backtrace.join("\n")}"
raise ex
end
end end
end end

View File

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

View File

@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
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/firebase_api.dart'; import 'package:eatvio_app/core/firebase_api.dart';
@@ -24,7 +26,6 @@ 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';
@@ -112,13 +113,21 @@ class AppRouter {
name: 'authScreen', name: 'authScreen',
builder: (context, state) => const AuthScreen(), builder: (context, state) => const AuthScreen(),
routes: [ routes: [
GoRoute(path: 'registration-screen', name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()), GoRoute(
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(isAuth: state.extra as bool), builder: (context, state) => const ForgetPasswordScreen(),
),
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(
@@ -156,23 +165,19 @@ class AppRouter {
GoRoute( GoRoute(
path: '/diary-screen', path: '/diary-screen',
name: 'diaryScreen', name: 'diaryScreen',
pageBuilder: (context, state) => const NoTransitionPage(child: DiaryScreen()), pageBuilder: (context, state) => const NoTransitionPage(
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: '/chat-select-screen', path: '/notifications-screen',
name: 'chatSelectScreen', name: 'notificationsScreen',
pageBuilder: (context, state) => const NoTransitionPage(child: ChatsScreen()), pageBuilder: (context, state) => const NoTransitionPage(child: NotificationsScreen())),
),
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',
@@ -189,16 +194,32 @@ class AppRouter {
), ),
], ],
), ),
GoRoute(name: 'blockedUsersScreen', path: '/blocked-users-screen', builder: (context, state) => const BlockedUsersScreen()), GoRoute(
name: 'blockedUsersScreen',
path: '/blocked-users-screen',
builder: (context, state) => const BlockedUsersScreen(),
),
GoRoute( GoRoute(
path: '/welcome-screen', path: '/welcome-screen',
name: 'welcomeScreen', name: 'welcomeScreen',
builder: (context, state) => const WelcomeScreen(), builder: (context, state) => const WelcomeScreen(),
redirect: (context, state) async { redirect: (context, state) async {
try { try {
if (await TokenStorage.haveTokenInStorage()) {
final firebaseApi = sl<FirebaseApi>();
final fbToken = await firebaseApi.getToken();
final deviceInfo = DeviceInfoPlugin();
await sl<Dio>().post(
'/api/profile/notification/token/register',
data: {
'platform': Platform.isIOS ? 'ios' : 'android',
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
'token': fbToken,
},
);
}
final sharedPreferences = await SharedPreferences.getInstance(); final sharedPreferences = await SharedPreferences.getInstance();
final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed') ?? false; final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed') ?? false;
await sl<FirebaseApi>().registerPushToken();
if (welcomeScreenWasShowed == false) { if (welcomeScreenWasShowed == false) {
return null; return null;
} else if (await TokenStorage.haveTokenInStorage()) { } else if (await TokenStorage.haveTokenInStorage()) {
@@ -207,7 +228,7 @@ class AppRouter {
return '/auth-screen'; return '/auth-screen';
} }
} catch (e) { } catch (e) {
return '/feed-screen'; rethrow;
} }
}, },
), ),
@@ -232,10 +253,26 @@ class AppRouter {
} }
}, },
), ),
GoRoute(path: ConsultationsArchiveScreen.path, name: ConsultationsArchiveScreen.name, builder: (context, state) => const ConsultationsArchiveScreen()), GoRoute(
GoRoute(path: ConsultantFinancesScreen.path, name: ConsultantFinancesScreen.name, builder: (context, state) => const ConsultantFinancesScreen()), path: ConsultationsArchiveScreen.path,
GoRoute(path: UserPurchasesScreen.path, name: UserPurchasesScreen.name, builder: (context, state) => const UserPurchasesScreen()), name: ConsultationsArchiveScreen.name,
GoRoute(path: '/user-finance-screen', name: 'userFinanceScreen', builder: (context, state) => const UserFinanceScreen()), builder: (context, state) => const ConsultationsArchiveScreen(),
),
GoRoute(
path: ConsultantFinancesScreen.path,
name: ConsultantFinancesScreen.name,
builder: (context, state) => const ConsultantFinancesScreen(),
),
GoRoute(
path: UserPurchasesScreen.path,
name: UserPurchasesScreen.name,
builder: (context, state) => const UserPurchasesScreen(),
),
GoRoute(
path: '/user-finance-screen',
name: 'userFinanceScreen',
builder: (context, state) => const UserFinanceScreen(),
),
GoRoute( GoRoute(
path: CardSettingsScreen.path, path: CardSettingsScreen.path,
name: CardSettingsScreen.name, name: CardSettingsScreen.name,
@@ -250,7 +287,11 @@ 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(slug: slug, navigateToComments: false, index: null); return AboutPlanScreen(
slug: slug,
navigateToComments: false,
index: null,
);
} }
final data = state.extra as Map<String, dynamic>; final data = state.extra as Map<String, dynamic>;
@@ -264,23 +305,29 @@ 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(recipeSlug: slug, navigateToComments: false, index: null); return AboutRecipeScreen(
recipeSlug: slug,
navigateToComments: false,
index: null,
);
} }
final data = state.extra as Map<String, dynamic>; final data = state.extra as Map<String, dynamic>;
return AboutRecipeScreen(recipeSlug: data['recipe'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); return AboutRecipeScreen(
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(recipe: state.extra != null ? state.extra as Recipe : null), builder: (context, state) => RecipeCreateScreen(
), recipe: state.extra != null ? state.extra as Recipe : null,
GoRoute( ),
path: '/profile-screen',
name: 'profileScreen',
builder: (context, state) => ProfileScreen(author: state.extra as Author),
), ),
GoRoute(path: '/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',
@@ -292,7 +339,11 @@ 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(slug: slug, index: null, navigateToComments: false); return AboutDietScreen(
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);
@@ -304,26 +355,30 @@ 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(id: parsedId); return BlogPostScreen(
} 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',
builder: (context, state) { builder: (context, state) {
final data = state.extra as Map<String, dynamic>; final data = state.extra as Map<String, dynamic>;
final uuid = data['uuid'] as String; final chatId = data['chatId'] as String;
final sendInitialMessage = data['sendInitialMessage'] as bool?; final sendInitialMessage = data['sendInitialMessage'] as bool?;
return ChatScreen(uuid: uuid, sendInitialMessage: sendInitialMessage); return ChatScreen(
chatId: chatId,
sendInitialMessage: sendInitialMessage,
);
}, },
), ),
GoRoute( GoRoute(
@@ -356,11 +411,7 @@ 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( GoRoute(path: 'pressure-screen', name: 'pressureScreen', builder: (context, state) => PressureScreen(settingsEntity: state.extra as SettingsEntity)),
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',
@@ -377,7 +428,9 @@ class AppRouter {
GoRoute( GoRoute(
path: '/stepper-become-consultant-screen', path: '/stepper-become-consultant-screen',
name: 'stepperBecomeConsultantScreen', name: 'stepperBecomeConsultantScreen',
builder: (context, state) => StepperBecomeConsultantScreen(slug: state.extra == null ? null : state.extra as String), builder: (context, state) => StepperBecomeConsultantScreen(
slug: state.extra == null ? null : state.extra as String,
),
), ),
GoRoute(path: '/calendar-screen', name: 'calendarScreen', builder: (context, state) => const CalendarScreen()), GoRoute(path: '/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()),
@@ -405,11 +458,7 @@ 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( GoRoute(path: UserDetailsScreen.path, name: UserDetailsScreen.name, builder: (context, state) => UserDetailsScreen(slug: state.extra as String)),
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(
@@ -419,7 +468,10 @@ 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(planId: planId, successfulPayment: successfulPayment); return InfoByOrderScreen(
planId: planId,
successfulPayment: successfulPayment,
);
}, },
), ),
GoRoute( GoRoute(
@@ -429,7 +481,10 @@ 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(url: url, planId: planId); return PaymentScreen(
url: url,
planId: planId,
);
}, },
), ),
GoRoute( GoRoute(
@@ -439,7 +494,10 @@ 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(diaryId: diaryId, currentPlanDay: currentPlanDay); return UserDiaryScreen(
diaryId: diaryId,
currentPlanDay: currentPlanDay,
);
}, },
), ),
GoRoute( GoRoute(
@@ -447,9 +505,16 @@ 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(path: '/verify-email-screen', name: 'verifyEmailScreen', builder: (context, state) => const VerifyEmailScreen()), GoRoute(
GoRoute(path: MyRecipesScreen.path, name: MyRecipesScreen.name, builder: (context, state) => const MyRecipesScreen()), path: '/verify-email-screen',
GoRoute(path: '/notifications-settings-screen', name: 'notificationsSettingsScreen', builder: (context, state) => const NotificationsSettingsScreen()), name: 'verifyEmailScreen',
builder: (context, state) => const VerifyEmailScreen(),
),
GoRoute(
path: MyRecipesScreen.path,
name: MyRecipesScreen.name,
builder: (context, state) => const MyRecipesScreen(),
),
], ],
); );
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,32 @@
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

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

View File

@@ -1,46 +0,0 @@
import 'package:flutter_dotenv/flutter_dotenv.dart';
class EnvConfig {
static String get baseUrl => dotenv.env['BASE_URL'] ?? '';
static String get devBaseUrl => dotenv.env['DEV_BASE_URL'] ?? '';
// Sentry Configuration
static String get sentryDsn => dotenv.env['SENTRY_DSN'] ?? '';
// AppMetrica Configuration
static String get appMetricaApiKey => dotenv.env['APPMETRICA_KEY'] ?? '';
// Firebase Configuration
static String get firebaseProjectId => dotenv.env['FIREBASE_PROJECT_ID'] ?? '';
static String get firebaseDatabaseUrl => dotenv.env['FIREBASE_DATABASE_URL'] ?? '';
static String get firebaseStorageBucket => dotenv.env['FIREBASE_STORAGE_BUCKET'] ?? '';
static String get firebaseMessagingSenderId => dotenv.env['FIREBASE_MESSAGING_SENDER_ID'] ?? '';
// Firebase Android Configuration
static String get firebaseAndroidApiKey => dotenv.env['FIREBASE_ANDROID_API_KEY'] ?? '';
static String get firebaseAndroidAppId => dotenv.env['FIREBASE_ANDROID_APP_ID'] ?? '';
// Firebase iOS Configuration
static String get firebaseIosApiKey => dotenv.env['FIREBASE_IOS_API_KEY'] ?? '';
static String get firebaseIosAppId => dotenv.env['FIREBASE_IOS_APP_ID'] ?? '';
static String get firebaseIosBundleId => dotenv.env['FIREBASE_IOS_BUNDLE_ID'] ?? '';
// App Store URLs
static String get appStoreUrl => dotenv.env['APP_STORE_URL'] ?? '';
static String get rustoreUrl => dotenv.env['RUSTORE_URL'] ?? '';
// Telegram Support
static String get telegramSupportUrl => dotenv.env['TELEGRAM_SUPPORT_URL'] ?? '';
static String get supportEmail => dotenv.env['SUPPORT_EMAIL'] ?? '';
// Test Credentials (for development only)
static String get testEmail => dotenv.env['TEST_EMAIL'] ?? '';
static String get testPassword => dotenv.env['TEST_PASSWORD'] ?? '';
// Email Validation API
static String get emailValidationApiUrl => dotenv.env['EMAIL_VALIDATION_API_URL'] ?? '';
// Connection Check URLs
static String get googleCheckUrl => dotenv.env['GOOGLE_CHECK_URL'] ?? '';
static String get yandexCheckUrl => dotenv.env['YANDEX_CHECK_URL'] ?? '';
}

View File

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

View File

@@ -1,13 +1,8 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:eatvio_app/core/comments/repositories/comments_repository.dart';
import 'package:eatvio_app/core/comments/services/comments_service.dart';
import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/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';
@@ -23,9 +18,6 @@ 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';
@@ -46,11 +38,8 @@ 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';
@@ -67,7 +56,7 @@ import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:eatvio_app/features/diary/repositories/diary_repository.dart'; import 'package:eatvio_app/features/diary/repositories/diary_repository.dart';
import 'package:eatvio_app/features/diary/services/diary_service.dart'; import 'package:eatvio_app/features/diary/services/diary_service.dart';
import 'package:eatvio_app/features/diets/presentation/cubit/about_diet_cubit.dart'; import 'package:eatvio_app/features/diets/presentation/cubit/about_diet_cubit.dart';
import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/consultants_cubit/consultants_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/consultants_cubit/consultants_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
@@ -116,6 +105,7 @@ 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;
@@ -138,20 +128,18 @@ Future<void> init() async {
sl.registerFactory<ProfileCubit>(() => ProfileCubit(profileRepository: sl(), chatRepository: sl(), consultantRepository: sl())); sl.registerFactory<ProfileCubit>(() => ProfileCubit(profileRepository: sl(), chatRepository: sl(), consultantRepository: sl()));
sl.registerFactory<profile_plans.PlansCubit>(() => profile_plans.PlansCubit(plansRepository: sl())); sl.registerFactory<profile_plans.PlansCubit>(() => profile_plans.PlansCubit(plansRepository: sl()));
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()));
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(), consultationsRepository: sl())); sl.registerFactory<SettingsCubit>(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl(), consultantRepository: sl()));
sl.registerFactory<RegisterConsultantCubit>(() => RegisterConsultantCubit(consultantRepository: sl())); sl.registerFactory<RegisterConsultantCubit>(() => RegisterConsultantCubit(consultantRepository: sl()));
sl.registerFactory<DiaryCubit>( sl.registerFactory<DiaryCubit>(() => DiaryCubit(
() => DiaryCubit( chatRepository: sl(),
chatRepository: sl(), diaryRepository: sl(),
diaryRepository: sl(), glossaryRepository: sl(),
glossaryRepository: sl(), profileRepository: sl(),
profileRepository: sl(), dietRepository: sl(),
dietRepository: sl(), settingsRepository: 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()));
@@ -180,9 +168,6 @@ 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()));
@@ -213,10 +198,6 @@ Future<void> init() async {
sl.registerFactory<MyRecipesRepository>(() => MyRecipesRepository(myRecipesService: sl())); sl.registerFactory<MyRecipesRepository>(() => MyRecipesRepository(myRecipesService: sl()));
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<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()));
@@ -245,19 +226,28 @@ 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<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(
Dio(BaseOptions(baseUrl: baseUrl, headers: {'accept': 'application/json', 'content-type': 'application/json'})) BaseOptions(
..interceptors.addAll([TokenInterceptor(), PhoneVerifyErrorInterceptor()]), baseUrl: baseUrl,
headers: {
'accept': 'application/json',
'content-type': 'application/json',
},
validateStatus: (status) => status != null && status < 500,
),
)..interceptors.addAll(
[
TokenInterceptor(),
PhoneVerifyErrorInterceptor(),
],
),
); );
sl.registerLazySingleton(() async => const FlutterSecureStorage());
} }

View File

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

View File

@@ -202,8 +202,6 @@ 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,10 +1,9 @@
import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.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 (sharedPreferences.getString('token') == null) { if (await const FlutterSecureStorage().read(key: 'token') == null) {
return false; return false;
} else { } else {
return true; return true;
@@ -14,48 +13,11 @@ abstract class TokenStorage {
} }
} }
static Future<String?> getTokenFromStorage() async { static Future<String?> getTokenFromStorage() async => await const FlutterSecureStorage().read(key: 'token');
final sharedPreferences = await SharedPreferences.getInstance();
return sharedPreferences.getString('token');
}
static Future<void> saveTokenInStorage(String token) async { static Future<void> saveTokenInStorage(String token) async {
final sharedPreferences = await SharedPreferences.getInstance(); await const FlutterSecureStorage().write(key: 'token', value: token);
await sharedPreferences.setString('token', token);
} }
static Future<void> removeTokenInStorage() async { static Future<void> removeTokenInStorage() async => (await const FlutterSecureStorage().delete(key: 'token'));
final sharedPreferences = await SharedPreferences.getInstance();
await sharedPreferences.remove('token');
}
}
abstract class SocketIdStorage {
static Future<bool> haveSocketIdInStorage() async {
final sharedPreferences = await SharedPreferences.getInstance();
try {
if (sharedPreferences.getString('socketId') == null) {
return false;
} else {
return true;
}
} catch (e) {
return false;
}
}
static Future<String?> getSocketIdFromStorage() async {
final sharedPreferences = await SharedPreferences.getInstance();
return sharedPreferences.getString('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,30 +3,11 @@ 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 pattern = dateTimeFormatPattern, String? locale]) { String format([
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);
@@ -38,13 +19,17 @@ 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 && localDate.month == yesterday.month && localDate.day == yesterday.day; return localDate.year == yesterday.year &&
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 && now.month == localDate.month && now.day == localDate.day; return now.year == localDate.year &&
now.month == localDate.month &&
now.day == localDate.day;
} }
} }

View File

@@ -1,128 +0,0 @@
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

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

View File

@@ -1,16 +0,0 @@
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

@@ -1,15 +0,0 @@
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

@@ -1,4 +1,3 @@
import 'dart:convert';
import 'dart:developer'; import 'dart:developer';
import 'package:dart_pusher_channels/dart_pusher_channels.dart'; import 'package:dart_pusher_channels/dart_pusher_channels.dart';
@@ -60,24 +59,6 @@ class WsManager {
), ),
); );
String? extractSocketId(String? data) {
if (data == null) return null;
try {
final decoded = jsonDecode(data);
return decoded['socket_id'] as String?;
} catch (_) {
return null;
}
}
client?.eventStream.listen((event) async {
if (event.name == 'pusher:connection_established') {
final data = event.data;
final socketId = extractSocketId(data);
await SocketIdStorage.saveSocketIdInStorage(socketId!);
}
});
await client?.connect(); await client?.connect();
client = client; client = client;
@@ -96,8 +77,9 @@ class WsManager {
EasyDebounce.debounce( EasyDebounce.debounce(
'searchChatRoom', 'searchChatRoom',
const Duration(milliseconds: 300), const Duration(milliseconds: 300),
() => sl<ChatsCubit>().getChatRooms(), () => sl<ChatsCubit>().getChatRooms(),
); );
log(event.rootObject.toString());
}); });
_subscribe(); _subscribe();
} catch (e, st) { } catch (e, st) {

View File

@@ -1,33 +0,0 @@
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,8 +2,7 @@ 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({ const CustomDropDown({super.key,
super.key,
this.alignment, this.alignment,
this.width, this.width,
this.focusNode, this.focusNode,
@@ -49,7 +48,12 @@ class CustomDropDown extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return alignment != null ? Align(alignment: alignment ?? Alignment.center, child: _buildDropDownWidget(context)) : _buildDropDownWidget(context); return alignment != null
? Align(
alignment: alignment ?? Alignment.center,
child: _buildDropDownWidget(context),
)
: _buildDropDownWidget(context);
} }
DropdownButtonFormField<SelectionPopupModel> _buildDropDownWidget(BuildContext context) { DropdownButtonFormField<SelectionPopupModel> _buildDropDownWidget(BuildContext context) {
@@ -57,7 +61,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!,
initialValue: value, value: value,
isDense: true, isDense: true,
isExpanded: true, isExpanded: true,
items: items?.map((SelectionPopupModel item) { items: items?.map((SelectionPopupModel item) {
@@ -65,7 +69,12 @@ class CustomDropDown extends StatelessWidget {
value: item, value: item,
child: Text( child: Text(
item.title, item.title,
style: hintStyle ?? const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black), style: hintStyle ??
const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Colors.black,
),
), ),
); );
}).toList(), }).toList(),
@@ -81,9 +90,19 @@ class CustomDropDown extends StatelessWidget {
InputDecoration _buildInputDecoration(BuildContext context) { InputDecoration _buildInputDecoration(BuildContext context) {
return InputDecoration( return InputDecoration(
labelStyle: const TextStyle(fontSize: 16, color: Colors.black, fontWeight: FontWeight.w400), labelStyle: const TextStyle(
fontSize: 16,
color: Colors.black,
fontWeight: FontWeight.w400,
),
hintText: hintText ?? "", hintText: hintText ?? "",
hintStyle: hintStyle ?? TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.grey.shade400, height: 1), hintStyle: hintStyle ??
TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Colors.grey.shade400,
height: 1,
),
prefixIcon: prefix, prefixIcon: prefix,
prefixIconConstraints: prefixConstraints, prefixIconConstraints: prefixConstraints,
suffixIcon: suffix, suffixIcon: suffix,
@@ -92,9 +111,18 @@ 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(borderRadius: BorderRadius.all(Radius.circular(20)), borderSide: BorderSide.none), border: const OutlineInputBorder(
enabledBorder: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(20)), borderSide: BorderSide.none), borderRadius: BorderRadius.all(Radius.circular(20)),
focusedBorder: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(20)), borderSide: BorderSide.none), 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,7 +3,14 @@ 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({super.key, required this.items, required this.onChanged, this.value, this.hint, this.hasError = false}); const CustomDropDownButton({
super.key,
required this.items,
required this.onChanged,
this.value,
this.hint,
this.hasError = false,
});
final T? value; final T? value;
final List<DropdownMenuItem<T>> items; final List<DropdownMenuItem<T>> items;
final void Function(T?)? onChanged; final void Function(T?)? onChanged;
@@ -13,7 +20,11 @@ class CustomDropDownButton<T> extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DropdownButtonFormField( return DropdownButtonFormField(
hint: hint, hint: hint,
icon: SvgPicture.asset('assets/icons/arrow_down2.svg', width: 12, height: 8), icon: SvgPicture.asset(
'assets/icons/arrow_down2.svg',
width: 12,
height: 8,
),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
dropdownColor: appTheme.lightGray0, dropdownColor: appTheme.lightGray0,
decoration: InputDecoration( decoration: InputDecoration(
@@ -32,7 +43,7 @@ class CustomDropDownButton<T> extends StatelessWidget {
fillColor: appTheme.lightGray0, fillColor: appTheme.lightGray0,
filled: true, filled: true,
), ),
initialValue: value, value: value,
items: items, items: items,
onChanged: onChanged, onChanged: onChanged,
); );

View File

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

View File

@@ -1,7 +1,6 @@
import 'dart:async'; import 'dart:async';
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/theme/theme_helper.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/image_constant.dart'; import 'package:eatvio_app/core/utils/image_constant.dart';
@@ -47,20 +46,12 @@ 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) {
if (mounted) { setState(() {
setState(() { isFocused = visible;
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();
@@ -75,20 +66,19 @@ class _AuthScreenState extends State<AuthScreen> {
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();
showSuccessToast('Успешный вход'); showSuccessToast('Успешный вход');
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
} }
AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false}); AppRouter.navigatorKey.currentContext?.go(
'/feed-screen',
extra: {'needToInitializeWebSocket': false},
);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var shortestSide = MediaQuery.sizeOf(context).shortestSide;
final bool useMobileLayout = shortestSide < 600;
if (!useMobileLayout) isFocused = true;
final s = S.of(context); final s = S.of(context);
FlutterNativeSplash.remove(); FlutterNativeSplash.remove();
return BlocProvider<AuthCubit>( return BlocProvider<AuthCubit>(
@@ -107,7 +97,11 @@ class _AuthScreenState extends State<AuthScreen> {
GoRouter.of(context).pushNamed(DevToolsScreen.name); GoRouter.of(context).pushNamed(DevToolsScreen.name);
} }
}, },
child: SvgPicture.asset(ImageConstant.imgLogoEatvio, height: isFocused ? 64 : MediaQuery.of(context).size.width - 64, fit: BoxFit.contain), child: SvgPicture.asset(
ImageConstant.imgLogoEatvio,
height: isFocused ? 64 : MediaQuery.of(context).size.width - 64,
fit: BoxFit.contain,
),
), ),
), ),
body: SafeArea( body: SafeArea(
@@ -138,16 +132,23 @@ class _AuthScreenState extends State<AuthScreen> {
hidePassword = !hidePassword; hidePassword = !hidePassword;
}); });
}, },
icon: Icon(hidePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined, color: Colors.grey), icon: Icon(
hidePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined,
color: Colors.grey,
),
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Center( Center(
child: GestureDetector( child: GestureDetector(
onTap: () => GoRouter.of(context).goNamed('forgetPasswordScreen', extra: false), onTap: () => GoRouter.of(context).goNamed('forgetPasswordScreen'),
child: Text( child: Text(
'Забыли пароль?', 'Забыли пароль?',
style: TextStyle(color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline), style: TextStyle(
color: appTheme.blueA400,
decorationColor: appTheme.blueA400,
decoration: TextDecoration.underline,
),
), ),
), ),
), ),
@@ -160,7 +161,11 @@ class _AuthScreenState extends State<AuthScreen> {
onTap: () => GoRouter.of(context).goNamed('registrationScreen'), onTap: () => GoRouter.of(context).goNamed('registrationScreen'),
child: Text( child: Text(
'Зарегистрироваться', 'Зарегистрироваться',
style: TextStyle(color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline), style: TextStyle(
color: appTheme.blueA400,
decorationColor: appTheme.blueA400,
decoration: TextDecoration.underline,
),
), ),
), ),
], ],
@@ -171,22 +176,23 @@ class _AuthScreenState extends State<AuthScreen> {
textAlign: TextAlign.center, textAlign: TextAlign.center,
text: TextSpan( text: TextSpan(
text: 'Нажимая "Войти" или "Зарегистрироваться", Вы соглашаетесь \n', text: 'Нажимая "Войти" или "Зарегистрироваться", Вы соглашаетесь \n',
style: const TextStyle(fontSize: 12, color: Colors.black), style: const TextStyle(
fontSize: 12,
color: Colors.black,
),
children: [ children: [
const TextSpan(text: 'c '), const TextSpan(text: 'c '),
if (!hidePlans) ...[ TextSpan(
TextSpan( text: ' Публичной офертой',
text: ' Публичной офертой', style: TextStyle(
style: TextStyle( fontSize: 12,
fontSize: 12, color: appTheme.blueA400,
color: appTheme.blueA400, decorationColor: appTheme.blueA400,
decorationColor: appTheme.blueA400, decoration: TextDecoration.underline,
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')),
), ),
const TextSpan(text: ' и '), recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')),
], ),
const TextSpan(text: ' и '),
TextSpan( TextSpan(
text: 'Политикой конфиденциальности\nи защитой персональных данных', text: 'Политикой конфиденциальности\nи защитой персональных данных',
style: TextStyle( style: TextStyle(
@@ -212,12 +218,17 @@ class _AuthScreenState extends State<AuthScreen> {
highlightElevation: 0, highlightElevation: 0,
elevation: 0, elevation: 0,
focusElevation: 0, focusElevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400, backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400,
onPressed: state is AuthStateLoading onPressed: state is AuthStateLoading
? null ? null
: () async { : () async {
final isAuthenticated = await authCubit.getTokenByCredentials(emailController.text, passwordController.text); final isAuthenticated = await authCubit.getTokenByCredentials(
emailController.text,
passwordController.text,
);
if (isAuthenticated) { if (isAuthenticated) {
if (context.mounted) { if (context.mounted) {
_reconnectWebSocket(context); _reconnectWebSocket(context);
@@ -226,7 +237,10 @@ class _AuthScreenState extends State<AuthScreen> {
}, },
label: state is AuthStateLoading label: state is AuthStateLoading
? const CupertinoActivityIndicator() ? const CupertinoActivityIndicator()
: Text('Войти', style: theme.textTheme.titleSmall!.copyWith(color: Colors.white)), : Text(
'Войти',
style: theme.textTheme.titleSmall!.copyWith(color: Colors.white),
),
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
@@ -238,7 +252,9 @@ class _AuthScreenState extends State<AuthScreen> {
highlightElevation: 0, highlightElevation: 0,
elevation: 0, elevation: 0,
focusElevation: 0, focusElevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400, backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400,
onPressed: state is AuthStateLoading onPressed: state is AuthStateLoading
? null ? null
@@ -253,10 +269,14 @@ class _AuthScreenState extends State<AuthScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const Text('Где взять QR-код?\n', style: TextStyle(fontSize: 18)), const Text(
Text( 'Где взять QR-код?\n',
'1. Войдите на сайте $baseUrl в свой аккаунт\n\n2. Перейдите в настройки профиля\n\n3. В разделе "Доп информация" перейдите на "QR Login"', style: TextStyle(
fontSize: 18,
),
), ),
Text(
'1. Войдите на сайте $baseUrl в свой аккаунт\n\n2. Перейдите в настройки профиля\n\n3. В разделе "Доп информация" перейдите на "QR Login"'),
const SizedBox(height: 12), const SizedBox(height: 12),
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
@@ -268,10 +288,15 @@ class _AuthScreenState extends State<AuthScreen> {
}, },
child: Ink( child: Ink(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)), decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
),
child: const Text( child: const Text(
'Отмена', 'Отмена',
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w400), style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.w400,
),
), ),
), ),
), ),
@@ -284,10 +309,15 @@ class _AuthScreenState extends State<AuthScreen> {
}, },
child: Ink( child: Ink(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)), decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
),
child: Text( child: Text(
'Перейти', 'Перейти',
style: TextStyle(color: appTheme.blueA400, fontWeight: FontWeight.w600), style: TextStyle(
color: appTheme.blueA400,
fontWeight: FontWeight.w600,
),
), ),
), ),
), ),
@@ -302,7 +332,10 @@ class _AuthScreenState extends State<AuthScreen> {
}, },
label: state is AuthStateLoading label: state is AuthStateLoading
? const CupertinoActivityIndicator() ? const CupertinoActivityIndicator()
: const Icon(Icons.qr_code_scanner_rounded, color: Colors.white), : const Icon(
Icons.qr_code_scanner_rounded,
color: Colors.white,
),
), ),
), ),
], ],

View File

@@ -9,8 +9,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
class ForgetPasswordScreen extends StatefulWidget { class ForgetPasswordScreen extends StatefulWidget {
final bool isAuth; const ForgetPasswordScreen({super.key});
const ForgetPasswordScreen({super.key, required this.isAuth});
@override @override
State<ForgetPasswordScreen> createState() => _ForgetPasswordScreenState(); State<ForgetPasswordScreen> createState() => _ForgetPasswordScreenState();
@@ -30,7 +29,7 @@ class _ForgetPasswordScreenState extends State<ForgetPasswordScreen> {
builder: (BuildContext context, ResetPasswordState state) { builder: (BuildContext context, ResetPasswordState state) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: widget.isAuth ? const Text('Смена пароля') : const Text('Восстановление пароля'), title: const Text('Смена пароля'),
centerTitle: false, centerTitle: false,
), ),
body: state is ResetPasswordStateLoadingResetPassword || state is ResetPasswordStateLoadingSendEmailCode body: state is ResetPasswordStateLoadingResetPassword || state is ResetPasswordStateLoadingSendEmailCode
@@ -43,9 +42,9 @@ class _ForgetPasswordScreenState extends State<ForgetPasswordScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (state is ResetPasswordStateCodeSendError || state is ResetPasswordStateEmpty) ...[ if (state is ResetPasswordStateCodeSendError || state is ResetPasswordStateEmpty) ...[
Text( const Text(
'${widget.isAuth ? '' : 'Забыли пароль? '}Укажите свой email,\nи мы пришлём код для сброса.', 'Забыли пароль? Укажите свой email,\nи мы пришлём код для сброса.',
style: const TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
@@ -157,7 +156,7 @@ class _ForgetPasswordScreenState extends State<ForgetPasswordScreen> {
} }
if (state is ResetPasswordStateResetPassowrdSuccess) { if (state is ResetPasswordStateResetPassowrdSuccess) {
GoRouter.of(context).pop(); GoRouter.of(context).pop();
showSuccessToast('Пароль успешно сменён. ${widget.isAuth ? '' : 'Войдите в аккаунт'}'); showSuccessToast('Пароль успешно сменён. Войдите в аккаунт');
} }
}, },
), ),

View File

@@ -3,7 +3,6 @@ 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';
@@ -93,7 +92,6 @@ 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

@@ -7,8 +7,8 @@ import 'package:eatvio_app/core/widgets/custom_popup.dart';
import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_cubit.dart'; import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_cubit.dart';
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_state.dart'; import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_state.dart';
import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_cubit.dart';
import 'package:eatvio_app/core/comments/comments_cubit/comments_state.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_state.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart';
import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/locator_service.dart';

View File

@@ -1,55 +1,24 @@
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({required this.profileRepository}) : super(const ProfileAvatarState.loading()); ProfileAvatarCubit({
required this.profileRepository,
int _calculateAge(String birthdate) { }) : super(const ProfileAvatarState.loading());
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(ProfileAvatarState.loaded(profile: profile)); emit(
final notificationStatus = await Permission.notification.status; ProfileAvatarState.loaded(
final isNotificationEnabled = notificationStatus.isGranted; profile: await profileRepository.getMyProfile(),
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,11 +15,7 @@ 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

@@ -22,9 +22,13 @@ abstract class ChatRoomModel with _$ChatRoomModel {
@freezed @freezed
abstract class Users with _$Users { abstract class Users with _$Users {
const factory Users({ const factory Users({
@JsonKey(name: 'from_chat_user') required ChatUserModel sender, @JsonKey(name: 'from_chat_user') required ChatUserModel sender,
@JsonKey(name: 'to_chat_user') required ChatUserModel receiver, @JsonKey(name: 'to_chat_user') required ChatUserModel receiver,
}) = _Users; }) = _Users;
factory Users.fromJson(Map<String, Object?> json) => _$UsersFromJson(json); factory Users.fromJson(Map<String, Object?> json) => _$UsersFromJson(json);
} }

View File

@@ -1,4 +1,3 @@
import 'package:eatvio_app/core/models/pagination_model.dart';
import 'package:eatvio_app/features/chat/models/chat_message_model.dart'; import 'package:eatvio_app/features/chat/models/chat_message_model.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:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
@@ -9,18 +8,29 @@ part 'chat_room_pagination_model.g.dart';
@freezed @freezed
abstract class ChatRoomPaginationModel with _$ChatRoomPaginationModel { abstract class ChatRoomPaginationModel with _$ChatRoomPaginationModel {
const factory ChatRoomPaginationModel({ const factory ChatRoomPaginationModel({
@JsonKey(name: "data") required List<ChatRoom> data, @JsonKey(name: "chatRooms") required ChatRooms chatRooms,
@JsonKey(name: "number_of_unread_chat_rooms") required int unreadChatsCount, @JsonKey(name: "number_of_unread_chat_rooms") required int unreadChatsCount,
required Meta meta,
}) = _ChatRoomPreviewModel; }) = _ChatRoomPreviewModel;
factory ChatRoomPaginationModel.fromJson(Map<String, dynamic> json) => _$ChatRoomPaginationModelFromJson(json); factory ChatRoomPaginationModel.fromJson(Map<String, dynamic> json) => _$ChatRoomPaginationModelFromJson(json);
} }
@freezed
abstract class ChatRooms with _$ChatRooms {
const factory ChatRooms({
@JsonKey(name: "data") required List<ChatRoom> data,
@JsonKey(name: "last_page") required int lastPage,
@JsonKey(name: "current_page") required int currentPage,
}) = _ChatRooms;
factory ChatRooms.fromJson(Map<String, dynamic> json) => _$ChatRoomsFromJson(json);
}
@freezed @freezed
abstract class ChatRoom with _$ChatRoom { abstract class ChatRoom with _$ChatRoom {
const factory ChatRoom({ const factory ChatRoom({
@JsonKey(name: "id") required String id, @JsonKey(name: "id") required String id,
@JsonKey(name: "room_name") required String roomName,
@JsonKey(name: "last_message", fromJson: parseLastMessage) required ChatMessageModel? lastMessage, @JsonKey(name: "last_message", fromJson: parseLastMessage) required ChatMessageModel? lastMessage,
@JsonKey(name: "users") required Users users, @JsonKey(name: "users") required Users users,
@JsonKey(name: "unread_messages_count") required int unreadMessagesCount, @JsonKey(name: "unread_messages_count") required int unreadMessagesCount,

View File

@@ -6,8 +6,11 @@ part 'chat_room_settings_model.g.dart';
@freezed @freezed
abstract class ChatRoomSettingsModel with _$ChatRoomSettingsModel { abstract class ChatRoomSettingsModel with _$ChatRoomSettingsModel {
const factory ChatRoomSettingsModel({ const factory ChatRoomSettingsModel({
@JsonKey(name: 'room_name') required String name, required int id,
@JsonKey(name: 'room_name') required String roomName,
required String state, required String state,
@JsonKey(name: 'access_level') required String accessLevel,
@JsonKey(name: 'updated_at') required String updatedAt,
}) = _ChatRoomSettingsModel; }) = _ChatRoomSettingsModel;
factory ChatRoomSettingsModel.fromJson(Map<String, Object?> json) => _$ChatRoomSettingsModelFromJson(json); factory ChatRoomSettingsModel.fromJson(Map<String, Object?> json) => _$ChatRoomSettingsModelFromJson(json);

View File

@@ -4,7 +4,6 @@ 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';
@@ -20,7 +19,6 @@ 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;
@@ -33,9 +31,12 @@ 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({required this.chatRepository, required this.wsManager, required this.cardsRepository}) : super(const ChatRoomState.loading()); ChatRoomCubit({
required this.chatRepository,
required this.wsManager,
required this.cardsRepository,
}) : super(const ChatRoomState.loading());
void setTimer() { void setTimer() {
_timer?.cancel(); _timer?.cancel();
@@ -46,14 +47,14 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
_timer?.cancel(); _timer?.cancel();
} }
Future<void> loadNextPage(String uuid) async { Future<void> loadNextPage(String chatId) async {
if (currentPage < lastPage && !isLoading) { if (currentPage < lastPage && !isLoading) {
isLoading = true; isLoading = true;
emit((state as ChatRoomStateLoaded).copyWith(isLoading: isLoading)); emit((state as ChatRoomStateLoaded).copyWith(isLoading: isLoading));
try { try {
final chatMessages = await chatRepository.getChatMessages(uuid, currentPage + 1); final chatMessages = await chatRepository.getChatMessages(chatId, currentPage + 1);
isLoading = false; isLoading = false;
emit( emit(
@@ -88,16 +89,17 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
} }
} }
Future<void> initChat(String uuid, {bool sendInitialMessage = false}) async { Future<void> initChat(String chatId, {bool sendInitialMessage = false}) async {
try { try {
final chatRoom = await chatRepository.getChatRoom(uuid); final chatRoom = await chatRepository.getChatRoom(chatId);
ChosenPlanModel? chosenPlan; ChosenPlanModel? chosenPlan;
if (chatRoom.chosenPlanId != null) { if (chatRoom.chosenPlanId != null) {
chosenPlan = await chatRepository.getChosenPlan(id: chatRoom.chosenPlanId!); chosenPlan = await chatRepository.getChosenPlan(id: chatRoom.chosenPlanId!);
} }
final chatMessages = await chatRepository.getChatMessages(uuid, currentPage); final chatMessages = await chatRepository.getChatMessages(chatId, currentPage);
lastPage = chatMessages.meta.lastPage; lastPage = chatMessages.meta.lastPage;
await _subscribeToChatChannel(uuid: uuid, chatId: chatRoom.id);
await _subscribeToChatChannel(chatId: chatId);
emit( emit(
ChatRoomState.loaded( ChatRoomState.loaded(
chatRoom: chatRoom.copyWith(chosenPlan: chosenPlan), chatRoom: chatRoom.copyWith(chosenPlan: chosenPlan),
@@ -156,80 +158,43 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
} }
} }
Future<void> _subscribeToChatChannel({required String uuid, required String chatId}) async { Future<void> _subscribeToChatChannel({required String chatId}) async {
final client = wsManager.client; final client = wsManager.client;
if (client == null) return; if (client == null) return;
await wsManager.initPrivateDirectChatChannel(chatId); await wsManager.initPrivateDirectChatChannel(chatId);
wsManager.subscribeDirectChatChannel(); wsManager.subscribeDirectChatChannel();
messagesSubscription = wsManager.directChatStream?.listen((event) { messagesSubscription = wsManager.directChatStream?.listen((event) {
AudioPlayer().play((AssetSource('audio/recive_message.wav'))); AudioPlayer().play((AssetSource('audio/recive_message.wav')));
final message = ChatMessageModel.fromJson(jsonDecode(event.data)); final message = ChatMessageModel.fromJson(jsonDecode(event.data)['message']);
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('searchChatRoom', const Duration(milliseconds: 300), () => sl<ChatsCubit>().getChatRooms()); EasyDebounce.debounce(
'searchChatRoom',
const Duration(milliseconds: 300),
() => sl<ChatsCubit>().getChatRooms(),
);
}); });
typingSubscription = wsManager.clientTypingStream?.listen((event) { typingSubscription = wsManager.clientTypingStream?.listen((event) {
emit((state as ChatRoomStateLoaded).copyWith(isTyping: true, isLoading: isLoading)); emit((state as ChatRoomStateLoaded).copyWith(isTyping: true, isLoading: isLoading));
EasyDebounce.debounce( EasyDebounce.debounce(
'client-typing', 'client-typing', const Duration(seconds: 3), () => emit((state as ChatRoomStateLoaded).copyWith(isTyping: false, isLoading: isLoading)));
const Duration(seconds: 3),
() => emit((state as ChatRoomStateLoaded).copyWith(isTyping: false, isLoading: isLoading)),
);
}); });
} }
Future<bool> sendMessage({String? data, List<File>? files, required String userSlug}) async { Future<bool> sendMessage({
String? data,
List<File>? files,
required String userSlug,
}) async {
try { try {
final messageUuid = uuid.v4(); await chatRepository.sendMessage(
final compressedFiles = files != null ? await ImageCompressUtility.compressFilesList(List<File>.from(files)) : null; data: data,
AudioPlayer().play((AssetSource('audio/send_message.wav'))); files: files != null ? await ImageCompressUtility.compressFilesList(List<File>.from(files)) : null,
emit( slug: userSlug,
(state as ChatRoomStateLoaded).copyWith(
chatMessages: [
ChatMessageModel(
id: 1,
data: data ?? '',
mediaData: compressedFiles,
authorId: 1,
updatedAt: DateTime.now(),
isRead: true,
isCurrentUserAuthor: true,
roomId: '',
messageState: MessageState.loading,
uuid: messageUuid,
),
...(state as ChatRoomStateLoaded).chatMessages,
],
),
); );
AudioPlayer().play((AssetSource('audio/send_message.wav')));
try {
await chatRepository.sendMessage(data: data, files: compressedFiles, slug: userSlug);
emit(
(state as ChatRoomStateLoaded).copyWith(
chatMessages: (state as ChatRoomStateLoaded).chatMessages.map((elem) {
if (elem.uuid == messageUuid) {
return elem.copyWith(messageState: MessageState.sent);
}
return elem;
}).toList(),
),
);
} catch (e) {
emit(
(state as ChatRoomStateLoaded).copyWith(
chatMessages: (state as ChatRoomStateLoaded).chatMessages.map((elem) {
if (elem.uuid == messageUuid) {
return elem.copyWith(messageState: MessageState.error);
}
return elem;
}).toList(),
),
);
}
sl<ChatsCubit>().getChatRooms();
return true; return true;
} catch (e, st) { } catch (e, st) {
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
@@ -274,10 +239,7 @@ 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( await chatRepository.complainAboutConsultation(id: planId, body: PlanComplainModel(description: description));
id: planId,
body: PlanComplainModel(description: description),
);
return true; return true;
} catch (e) { } catch (e) {
log(e.toString()); log(e.toString());
@@ -305,70 +267,4 @@ 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

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

View File

@@ -13,8 +13,6 @@ 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';
@@ -34,10 +32,10 @@ import 'package:intl/intl.dart';
import 'package:visibility_detector/visibility_detector.dart'; import 'package:visibility_detector/visibility_detector.dart';
class ChatScreen extends StatefulWidget { class ChatScreen extends StatefulWidget {
final String uuid; final String chatId;
final bool? sendInitialMessage; final bool? sendInitialMessage;
const ChatScreen({required this.uuid, super.key, required this.sendInitialMessage}); const ChatScreen({required this.chatId, super.key, required this.sendInitialMessage});
@override @override
State<ChatScreen> createState() => _ChatScreenState(); State<ChatScreen> createState() => _ChatScreenState();
@@ -66,14 +64,14 @@ class _ChatScreenState extends State<ChatScreen> {
void initState() { void initState() {
super.initState(); super.initState();
scrollController.addListener(_onScroll); scrollController.addListener(_onScroll);
chatRoomCubit.initChat(widget.uuid, sendInitialMessage: widget.sendInitialMessage ?? false); chatRoomCubit.initChat(widget.chatId, sendInitialMessage: widget.sendInitialMessage ?? false);
chatRoomCubit.setTimer(); chatRoomCubit.setTimer();
_listener = AppLifecycleListener(onHide: _onHide, onResume: _onResume); _listener = AppLifecycleListener(onHide: _onHide, onResume: _onResume);
} }
void _onScroll() { void _onScroll() {
if (scrollController.offset >= scrollController.position.maxScrollExtent && !scrollController.position.outOfRange) { if (scrollController.offset >= scrollController.position.maxScrollExtent && !scrollController.position.outOfRange) {
chatRoomCubit.loadNextPage(widget.uuid); chatRoomCubit.loadNextPage(widget.chatId);
} }
} }
@@ -95,10 +93,7 @@ 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( return Scaffold(appBar: AppBar(), body: DataErrorWidget(onReload: () => chatRoomCubit.initChat(widget.chatId)));
appBar: AppBar(),
body: DataErrorWidget(onReload: () => chatRoomCubit.initChat(widget.uuid)),
);
} else { } else {
return const Scaffold(body: Loader()); return const Scaffold(body: Loader());
} }
@@ -173,19 +168,34 @@ class _ChatScreenState extends State<ChatScreen> {
width: 40, width: 40,
height: 40, height: 40,
alignment: Alignment.center, alignment: Alignment.center,
decoration: const BoxDecoration(color: Color(0xFFEBF4FF), shape: BoxShape.circle), decoration: const BoxDecoration(
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(color: Color(0xFF7F9CF5), fontWeight: FontWeight.w400, fontSize: 20), style: const TextStyle(
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(state.chatRoom.users.receiver.name, style: CustomTextStyles.titleSmallBold.copyWith(color: PrimaryColors().black)), Text(
state.chatRoom.users.receiver.name,
style: CustomTextStyles.titleSmallBold.copyWith(
color: PrimaryColors().black,
),
),
AnimatedCrossFade( AnimatedCrossFade(
firstChild: Text('Печатает...', style: CustomTextStyles.bodySmallMulishPrimary), firstChild: Text(
'Печатает...',
style: CustomTextStyles.bodySmallMulishPrimary,
),
firstCurve: Curves.easeIn, firstCurve: Curves.easeIn,
secondCurve: Curves.easeOut, secondCurve: Curves.easeOut,
secondChild: const SizedBox.shrink(), secondChild: const SizedBox.shrink(),
@@ -241,9 +251,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();
}); });
} }
@@ -254,9 +264,6 @@ 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,
), ),
), ),
], ],
@@ -283,9 +290,6 @@ 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,
), ),
); );
}, },
@@ -357,7 +361,11 @@ class _ChatScreenState extends State<ChatScreen> {
final imageIndex = _imageIndexAt(pickedFiles, i); final imageIndex = _imageIndexAt(pickedFiles, i);
context.pushTransparentRoute( context.pushTransparentRoute(
PhotoViewScreen(imageUrls: imageFiles.map((e) => e.path).toList(), isLocal: true, index: imageIndex), PhotoViewScreen(
imageUrls: imageFiles.map((e) => e.path).toList(),
isLocal: true,
index: imageIndex,
),
); );
}, },
child: Hero( child: Hero(
@@ -365,7 +373,14 @@ class _ChatScreenState extends State<ChatScreen> {
transitionOnUserGestures: true, transitionOnUserGestures: true,
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: SizedBox(height: 104, width: 104, child: Image.file(pickedFiles[i], fit: BoxFit.cover)), child: SizedBox(
height: 104,
width: 104,
child: Image.file(
pickedFiles[i],
fit: BoxFit.cover,
),
),
), ),
), ),
) )
@@ -382,8 +397,15 @@ class _ChatScreenState extends State<ChatScreen> {
}, },
child: Container( child: Container(
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),
decoration: BoxDecoration(color: appTheme.gray50, shape: BoxShape.circle), decoration: BoxDecoration(
child: const Icon(size: 20, Icons.close, color: Colors.black), color: appTheme.gray50,
shape: BoxShape.circle,
),
child: const Icon(
size: 20,
Icons.close,
color: Colors.black,
),
), ),
), ),
), ),
@@ -405,10 +427,7 @@ class _ChatScreenState extends State<ChatScreen> {
child: Ink( child: Ink(
height: 40, height: 40,
width: 40, width: 40,
child: Transform.rotate( child: Transform.rotate(angle: pi / 4, child: const Icon(Icons.attach_file_rounded, size: 30, color: Color(0xFF858E99))),
angle: pi / 4,
child: const Icon(Icons.attach_file_rounded, size: 30, color: Color(0xFF858E99)),
),
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
@@ -430,9 +449,7 @@ 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), borderRadius: BorderRadius.circular(100), borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1)),
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),
@@ -448,27 +465,7 @@ class _ChatScreenState extends State<ChatScreen> {
const SizedBox(width: 8), const SizedBox(width: 8),
InkWell( InkWell(
customBorder: const CircleBorder(), customBorder: const CircleBorder(),
onTap: () async { onTap: () {
// Проверяем, заблокирован ли пользователь
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(),
@@ -528,7 +525,13 @@ 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(fileName, textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12)), child: Text(
fileName,
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 12),
),
), ),
], ],
), ),
@@ -576,7 +579,13 @@ 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(children: [Icon(Icons.photo_camera), SizedBox(width: 8), Text('Фото')]), child: const Row(
children: [
Icon(Icons.photo_camera),
SizedBox(width: 8),
Text('Фото'),
],
),
), ),
CupertinoActionSheetAction( CupertinoActionSheetAction(
onPressed: () async { onPressed: () async {
@@ -591,7 +600,13 @@ class _ChatScreenState extends State<ChatScreen> {
// await chatRoomCubit.sendMessage(files: files, userSlug: slug); // await chatRoomCubit.sendMessage(files: files, userSlug: slug);
} }
}, },
child: const Row(children: [Icon(Icons.photo_library_rounded), SizedBox(width: 8), Text('Галерея')]), child: const Row(
children: [
Icon(Icons.photo_library_rounded),
SizedBox(width: 8),
Text('Галерея'),
],
),
), ),
CupertinoActionSheetAction( CupertinoActionSheetAction(
onPressed: () async { onPressed: () async {
@@ -628,10 +643,20 @@ class _ChatScreenState extends State<ChatScreen> {
// await chatRoomCubit.sendMessage(files: files, userSlug: slug); // await chatRoomCubit.sendMessage(files: files, userSlug: slug);
} }
}, },
child: const Row(children: [Icon(Icons.file_present_rounded), SizedBox(width: 8), Text('Файлы')]), child: const Row(
children: [
Icon(Icons.file_present_rounded),
SizedBox(width: 8),
Text('Файлы'),
],
),
), ),
], ],
cancelButton: CupertinoActionSheetAction(onPressed: () => Navigator.of(context).pop(), isDestructiveAction: true, child: const Text('Отмена')), cancelButton: CupertinoActionSheetAction(
onPressed: () => Navigator.of(context).pop(),
isDestructiveAction: true,
child: const Text('Отмена'),
),
), ),
); );
} }
@@ -640,10 +665,7 @@ 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( child: Text(DateFormat.yMMMMd('RU').format(date), style: const TextStyle(height: 1, color: Color(0xFF8393A1), fontWeight: FontWeight.w500)),
DateFormat.yMMMMd('RU').format(date),
style: const TextStyle(height: 1, color: Color(0xFF8393A1), fontWeight: FontWeight.w500),
),
), ),
); );
} }

View File

@@ -12,8 +12,6 @@ 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;
@@ -135,45 +133,21 @@ 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,
); );
if (mounted) { setState(() {
setState(() { textEditingController.clear();
textEditingController.clear(); });
});
}
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug); final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
if (context.mounted) { AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
AppRouter.navigatorKey.currentContext?.pushReplacementNamed( 'chatScreen',
'chatScreen', extra: {
extra: { 'chatId': chatIdOrError,
'uuid': chatIdOrError, },
}, );
);
}
} }
}, },
child: Ink( child: Ink(
@@ -201,44 +175,20 @@ 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();
List<XFile>? images = await picker.pickMultiImage(); XFile? image = await picker.pickImage(source: ImageSource.camera);
if (images.isNotEmpty) { if (image != null) {
List<File> files = images.map((image) => File(image.path)).toList();
await sl<ChatRepository>().sendMessage( await sl<ChatRepository>().sendMessage(
files: files, files: [File(image.path)],
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);
if (mounted) { AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
AppRouter.navigatorKey.currentContext?.pushReplacementNamed( 'chatScreen',
'chatScreen', extra: {
extra: { 'chatId': chatIdOrError,
'uuid': chatIdOrError, },
}, );
);
}
} }
}, },
child: const Row( child: const Row(
@@ -261,14 +211,12 @@ 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);
if (mounted) { AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
AppRouter.navigatorKey.currentContext?.pushReplacementNamed( 'chatScreen',
'chatScreen', extra: {
extra: { 'chatId': chatIdOrError,
'uuid': chatIdOrError, },
}, );
);
}
} }
}, },
child: const Row( child: const Row(
@@ -313,14 +261,12 @@ 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);
if (mounted) { AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
AppRouter.navigatorKey.currentContext?.pushReplacementNamed( 'chatScreen',
'chatScreen', extra: {
extra: { 'chatId': chatIdOrError,
'uuid': chatIdOrError, },
}, );
);
}
} }
}, },
child: const Row( child: const Row(

View File

@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:dismissible_page/dismissible_page.dart'; import 'package:dismissible_page/dismissible_page.dart';
import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/const.dart';
@@ -9,7 +11,7 @@ import 'package:photo_view/photo_view_gallery.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
class PhotoViewScreen extends StatefulWidget { class PhotoViewScreen extends StatefulWidget {
final List<dynamic> imageUrls; final List<String> imageUrls;
final int index; final int index;
final bool isLocal; final bool isLocal;
@@ -48,12 +50,14 @@ class _PhotoViewScreenState extends State<PhotoViewScreen> {
), ),
scrollPhysics: const BouncingScrollPhysics(), scrollPhysics: const BouncingScrollPhysics(),
builder: (BuildContext context, int index) { builder: (BuildContext context, int index) {
final imageProvider = widget.isLocal
? FileImage(widget.imageUrls[index]) as ImageProvider<Object>
: CachedNetworkImageProvider(widget.imageUrls[index]) as ImageProvider<Object>;
return PhotoViewGalleryPageOptions( return PhotoViewGalleryPageOptions(
imageProvider: imageProvider, imageProvider: widget.isLocal
? FileImage(
File(widget.imageUrls[index]),
) as ImageProvider
: CachedNetworkImageProvider(
widget.imageUrls[index],
),
initialScale: PhotoViewComputedScale.contained, initialScale: PhotoViewComputedScale.contained,
minScale: PhotoViewComputedScale.contained, minScale: PhotoViewComputedScale.contained,
heroAttributes: PhotoViewHeroAttributes( heroAttributes: PhotoViewHeroAttributes(

View File

@@ -1,17 +1,10 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:dismissible_page/dismissible_page.dart'; 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 {
@@ -22,9 +15,6 @@ 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;
@@ -32,9 +22,7 @@ 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);
@@ -48,33 +36,17 @@ class ChatBubbleWidget extends StatelessWidget {
mainAxisAlignment: isCurrentUser ? MainAxisAlignment.end : MainAxisAlignment.start, mainAxisAlignment: isCurrentUser ? MainAxisAlignment.end : MainAxisAlignment.start,
children: [ children: [
if (isCurrentUser) if (isCurrentUser)
switch (messageState) { Padding(
MessageState.loading => Padding( padding: const EdgeInsets.only(right: 4, bottom: 4),
padding: const EdgeInsets.only(right: 4, bottom: 4), child: Text(
child: SizedBox(width: 10, height: 10, child: CircularProgressIndicator(strokeWidth: 2, color: appTheme.blueA400)), sendTime.format('HH:mm', 'ru'),
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,9 +59,8 @@ class ChatBubbleWidget extends StatelessWidget {
), ),
), ),
child: Padding( child: Padding(
padding: mediaData.isNotEmpty padding:
? const EdgeInsets.all(8) mediaData.isNotEmpty ? const EdgeInsets.all(8) : EdgeInsets.only(top: 8, bottom: 8, left: isCurrentUser ? 16 : 8, right: isCurrentUser ? 8 : 16),
: EdgeInsets.only(top: 8, bottom: 8, left: isCurrentUser ? 16 : 8, right: isCurrentUser ? 8 : 16),
child: Column( child: Column(
crossAxisAlignment: isCurrentUser ? CrossAxisAlignment.end : CrossAxisAlignment.start, crossAxisAlignment: isCurrentUser ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [ children: [
@@ -113,19 +84,32 @@ class ChatBubbleWidget extends StatelessWidget {
throw Exception('Could not launch ${mediaData[index].toString()}'); throw Exception('Could not launch ${mediaData[index].toString()}');
} }
}, },
leading: const Icon(Icons.file_copy_outlined, color: Colors.black), leading: const Icon(
trailing: const Icon(Icons.download_outlined, color: Colors.black), Icons.file_copy_outlined,
color: Colors.white,
),
trailing: const Icon(
Icons.download_outlined,
color: Colors.white,
),
title: Text( title: Text(
mediaData[index].toString().replaceAll('chat_files/', ''), mediaData[index].toString().replaceAll('chat_files/', ''),
maxLines: 2, maxLines: 2,
style: const TextStyle(color: Colors.black, fontSize: 12, overflow: TextOverflow.ellipsis), style: const TextStyle(
color: Colors.white,
fontSize: 12,
overflow: TextOverflow.ellipsis,
),
), ),
); );
} }
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
context.pushTransparentRoute( context.pushTransparentRoute(
PhotoViewScreen(imageUrls: mediaData, index: index, isLocal: mediaData[index] is File ? true : false), PhotoViewScreen(
imageUrls: mediaData.map((e) => '$e').toList(),
index: index,
),
); );
}, },
child: Hero( child: Hero(
@@ -133,7 +117,9 @@ 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 ? Image.file(mediaData[index]) : CachedNetworkImage(imageUrl: '${mediaData[index]}'), child: CachedNetworkImage(
imageUrl: '${mediaData[index]}',
),
), ),
), ),
); );
@@ -145,7 +131,11 @@ 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) Text(text, style: TextStyles.textFS13FW400.copyWith(color: appTheme.black, fontSize: 16)), if (text.isNotEmpty)
Text(
text,
style: TextStyles.textFS13FW400.copyWith(color: appTheme.black, fontSize: 16),
),
], ],
), ),
), ),
@@ -155,7 +145,12 @@ 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(color: Color(0xFF7B8794), height: 1, fontSize: 10, fontWeight: FontWeight.w500), style: const TextStyle(
color: Color(0xFF7B8794),
height: 1,
fontSize: 10,
fontWeight: FontWeight.w500,
),
), ),
), ),
], ],

View File

@@ -24,7 +24,7 @@ class _ChatEntryWidgetState extends State<ChatEntryWidget> {
onTap: () => GoRouter.of(context).pushNamed( onTap: () => GoRouter.of(context).pushNamed(
'chatScreen', 'chatScreen',
extra: { extra: {
'uuid': widget.chatRoom.id.toString(), 'chatId': widget.chatRoom.id.toString(),
}, },
), ),
child: Container( child: Container(

View File

@@ -1,48 +0,0 @@
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

@@ -2,7 +2,6 @@ import 'dart:async';
import 'dart:developer'; import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/features/chat/models/chat_message_model.dart'; import 'package:eatvio_app/features/chat/models/chat_message_model.dart';
import 'package:eatvio_app/features/chat/models/chat_messages_model.dart'; import 'package:eatvio_app/features/chat/models/chat_messages_model.dart';
import 'package:eatvio_app/features/chat/models/chat_room_model.dart'; import 'package:eatvio_app/features/chat/models/chat_room_model.dart';
@@ -48,11 +47,10 @@ class ChatRepository {
} }
} }
await chatService.sendMessage( await chatService.sendMessageByUserSlug(
slug: slug, userSlug: slug,
data: data, data: data,
mediaData: multipartFiles.isEmpty ? null : multipartFiles, mediaData: multipartFiles.isEmpty ? null : multipartFiles,
socketId: (await SocketIdStorage.getSocketIdFromStorage())!,
); );
} catch (e, st) { } catch (e, st) {
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
@@ -60,11 +58,10 @@ class ChatRepository {
} }
} }
Future<ChatRoomModel> getChatRoom(String uuid) async { Future<ChatRoomModel> getChatRoom(String chatId) async {
try { try {
return (await chatService.getChatRoom(uuid: uuid)); return (await chatService.getChatRoom(id: chatId))['chatRoom']!;
} catch (e, st) { } catch (e) {
log(e.toString(), stackTrace: st);
rethrow; rethrow;
} }
} }
@@ -77,9 +74,9 @@ class ChatRepository {
} }
} }
Future<ChatMessagesModel> getChatMessages(String id, int page) async { Future<ChatMessagesModel> getChatMessages(String chatId, int page) async {
try { try {
return (await chatService.getChatMessages(id: id, page: page)); return (await chatService.getChatMessages(id: chatId, page: page));
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
@@ -93,9 +90,9 @@ class ChatRepository {
} }
} }
Future<void> markMessagesAsRead(String id, List<int> messageIds) async { Future<void> markMessagesAsRead(String chatId, List<int> messageIds) async {
try { try {
await chatService.markMessagesAsRead(id: id, body: {'messages_ids': messageIds}); await chatService.markMessagesAsRead(id: chatId, body: {'messages_ids': messageIds});
} catch (e) { } catch (e) {
rethrow; rethrow;
} }

View File

@@ -13,33 +13,32 @@ part 'chat_service.g.dart';
abstract class ChatService { abstract class ChatService {
factory ChatService(Dio dio, {String baseUrl}) = _ChatService; factory ChatService(Dio dio, {String baseUrl}) = _ChatService;
@GET('/api/v2/chat/index') @GET('/api/chat/index')
Future<ChatRoomPaginationModel> getChatRooms({ Future<ChatRoomPaginationModel> getChatRooms({
@Query('page') required int page, @Query('page') required int page,
@Query('filters[data][\$conversation_name]') String? query, @Query('find') String? query,
}); });
@GET('/api/v2/chat/show/{uuid}') @GET('/api/chat/show/{id}')
Future<ChatRoomModel> getChatRoom({ Future<Map<String, ChatRoomModel>> getChatRoom({
@Path() required String uuid, @Path() required String id,
}); });
@POST('/api/v2/chat/message/send/{slug}') @POST('/api/v2/chat/message/send/{id}')
@MultiPart() @MultiPart()
Future<void> sendMessage({ Future<void> sendMessage({
@Header('X-Socket-ID') required String socketId, @Path() required String id,
@Path() required String slug,
@Part() String? data, @Part() String? data,
@Part(name: 'media_data[]', contentType: 'multipart/form-data') List<MultipartFile>? mediaData, @Part(name: 'media_data[]', contentType: 'multipart/form-data') List<MultipartFile>? mediaData,
}); });
@GET('/api/v2/chat/show/{id}/messages?page={page}') @GET('/api/chat/show/{id}/messages?page={page}')
Future<ChatMessagesModel> getChatMessages({ Future<ChatMessagesModel> getChatMessages({
@Path() required String id, @Path() required String id,
@Path() required int page, @Path() required int page,
}); });
@GET('/api/v2/chat/get_by_user/{user}') @GET('/api/chat/get_by_user/{user}')
Future<ChatRoomModel> getChatRoomIdByUserSlug({ Future<ChatRoomModel> getChatRoomIdByUserSlug({
@Path() required String user, @Path() required String user,
}); });
@@ -82,8 +81,16 @@ abstract class ChatService {
@Body() required PlanComplainModel body, @Body() required PlanComplainModel body,
}); });
@GET('/api/chat/get_by_user/{slug}') @GET('/api/v2/chat/get_by_user/{slug}')
Future<HttpResponse> getChatByUserSlug({ Future<HttpResponse> getChatByUserSlug({
@Path() required String slug, @Path() required String slug,
}); });
@POST('/api/v2/chat/message/send/{userSlug}')
@MultiPart()
Future<void> sendMessageByUserSlug({
@Path() required String userSlug,
@Part() String? data,
@Part(name: 'media_data[]', contentType: 'multipart/form-data') List<MultipartFile>? mediaData,
});
} }

View File

@@ -9,7 +9,6 @@ import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_cubit.dart'; import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_cubit.dart';
import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_state.dart'; import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_state.dart';
import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart';
import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/generated/l10n.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';
@@ -195,74 +194,63 @@ class _LawViolationWidgetState extends State<_LawViolationWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final s = S.of(context); final s = S.of(context);
return BlocBuilder<ComplainingCubit, ComplainingState>( return Column(
builder: (context, state) { mainAxisSize: MainAxisSize.min,
return Column( children: [
mainAxisSize: MainAxisSize.min, const BottomSheetHandlerBar(),
children: [ const SizedBox(height: 20),
const BottomSheetHandlerBar(), Text(
const SizedBox(height: 20), s.lawViolation,
Text( style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
s.lawViolation, ),
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), const SizedBox(height: 8),
), SizedBox(
const SizedBox(height: 8), width: MediaQuery.sizeOf(context).width * 0.7,
SizedBox( child: Text(
width: MediaQuery.sizeOf(context).width * 0.7, s.what_do_you_want_to_complaint_about,
child: Text( textAlign: TextAlign.center,
s.what_do_you_want_to_complaint_about, style: TextStyles.textMMedium.copyWith(color: appTheme.black),
textAlign: TextAlign.center, ),
style: TextStyles.textMMedium.copyWith(color: appTheme.black), ),
), const SizedBox(height: 24),
), ...tags.mapIndexed((index, element) {
const SizedBox(height: 24), return Row(
...tags.mapIndexed((index, element) { children: [
return Row( Checkbox(
children: [ activeColor: appTheme.blueA400,
Checkbox( side: BorderSide(width: 2, color: tags[index].$1 ? appTheme.blueA400 : appTheme.blueGray),
activeColor: appTheme.blueA400, value: element.$1,
side: BorderSide(width: 2, color: tags[index].$1 ? appTheme.blueA400 : appTheme.blueGray), onChanged: (isChecked) {
value: element.$1, setState(() {
onChanged: (isChecked) { if (isChecked == null) return;
setState(() { if (index == tags.length - 1 && !isChecked) {
if (isChecked == null) return; context.read<ComplainingCubit>().toggleDescription();
if (index == tags.length - 1 && !isChecked) { }
context.read<ComplainingCubit>().toggleDescription(); tags[index] = (isChecked, element.$2);
} final activeTags = tags.where((elem) => elem.$1).map((e) => e.$2).toList();
tags[index] = (isChecked, element.$2); context.read<ComplainingCubit>().setTags(activeTags);
final activeTags = tags.where((elem) => elem.$1).map((e) => e.$2).toList(); });
context.read<ComplainingCubit>().setTags(activeTags);
});
},
),
Text(
element.$2,
style: TextStyles.textLSemibold.copyWith(color: appTheme.black),
),
],
);
}),
if (tags.last.$1) ...[
const SizedBox(height: 12),
CustomTextFormField(
hintText: s.type_here_your_complaint,
hasError: (state.complainInfo.description?.length ?? 10) < 10,
onChanged: (value) {
context.read<ComplainingCubit>().setDescription(value);
}, },
), ),
if ((state.complainInfo.description?.length ?? 10) < 10) ...[ Text(
const SizedBox(height: 4), element.$2,
const ValidationErrorWidget(error: 'Минимум 10 символов'), style: TextStyles.textLSemibold.copyWith(color: appTheme.black),
] ),
], ],
const SizedBox(height: 20), );
_ComplainButton( }),
isDisabled: (state.complainInfo.description?.length ?? 0) < 10, if (tags.last.$1) ...[
), const SizedBox(height: 12),
], CustomTextFormField(
); hintText: s.type_here_your_complaint,
}, onChanged: (value) {
context.read<ComplainingCubit>().setDescription(value);
},
),
],
const SizedBox(height: 20),
const _ComplainButton(),
],
); );
} }
} }
@@ -527,8 +515,8 @@ class _ComplaintFailedWidget extends StatelessWidget {
} }
class _ComplainButton extends StatelessWidget { class _ComplainButton extends StatelessWidget {
const _ComplainButton({this.isDisabled = false}); const _ComplainButton();
final bool isDisabled;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final s = S.of(context); final s = S.of(context);
@@ -546,10 +534,6 @@ class _ComplainButton extends StatelessWidget {
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
), ),
onPressed: () async { onPressed: () async {
if (isDisabled) {
context.read<ComplainingCubit>().setDescription('');
return;
}
await context.read<ComplainingCubit>().complain(); await context.read<ComplainingCubit>().complain();
}, },
); );

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,42 +1,16 @@
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
abstract class RatingPaginationModel with _$RatingPaginationModel {
factory RatingPaginationModel({
required List<RatingModel> data,
}) = _RatingPaginationModel;
factory RatingPaginationModel.fromJson(Map<String, dynamic> json) => _$RatingPaginationModelFromJson(json);
}
@freezed @freezed
abstract class RatingModel with _$RatingModel { abstract class RatingModel with _$RatingModel {
factory RatingModel({ factory RatingModel({
@JsonKey(name: 'created_at', fromJson: parseDate) required DateTime date, required DateTime date,
@JsonKey(name: 'log_data') required RatingEnum type, required String title,
@JsonKey(name: 'log_scores') required int amount, 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

@@ -1,19 +0,0 @@
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,16 +1,12 @@
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/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/features/consultant_rating/models/rating_model.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';
@@ -19,10 +15,7 @@ class ConsultantRatingScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider<ConsultantRatingCubit>( return const ConsultantRatingPageContent();
create: (context) => sl<ConsultantRatingCubit>()..getConsultantStatistics()..getScoreLogs(),
child: const ConsultantRatingPageContent(),
);
} }
} }
@@ -32,56 +25,52 @@ 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 BlocBuilder<ConsultantRatingCubit, ConsultantRatingState>( return Scaffold(
builder: (context, state) { backgroundColor: appTheme.gray100,
return Scaffold( appBar: AppBar(
backgroundColor: appTheme.gray100, centerTitle: true,
appBar: AppBar( title: Text(
centerTitle: true, s.rating_and_statistics,
title: Text( style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
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),
body: state.error != null Padding(
? DataErrorWidget(onReload: () async { padding: const EdgeInsets.symmetric(horizontal: 16),
await context.read<ConsultantRatingCubit>().getConsultantStatistics(); child: CustomOutlinedButton(
if (!context.mounted) return; text: s.copy_referral_link,
await context.read<ConsultantRatingCubit>().getScoreLogs(); height: 52,
}) buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
: SingleChildScrollView( buttonStyle: ButtonStyle(
child: Column( side: const WidgetStatePropertyAll(BorderSide.none),
children: [ backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
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

@@ -1,32 +0,0 @@
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

@@ -1,16 +0,0 @@
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,29 +1,42 @@
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, required this.isStatistic}); const PeriodFilterWidget({super.key});
final bool isStatistic;
@override @override
State<PeriodFilterWidget> createState() => _PeriodFilterWidgetState(); State<PeriodFilterWidget> createState() => _PeriodFilterWidgetState();
} }
class _PeriodFilterWidgetState extends State<PeriodFilterWidget> { class _PeriodFilterWidgetState extends State<PeriodFilterWidget> {
PeriodFilterStatus _currentState = PeriodFilterStatus.all; final List<String> periodDropDownTitles = [];
int periodDropDownIndex = 0;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final s = S.of(context);
if (periodDropDownTitles.isEmpty) {
periodDropDownTitles.addAll([
s.for_whole_period_of_time,
s.for_a_year,
s.for_a_month,
s.for_a_week,
]);
}
}
@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<PeriodFilterStatus>( CustomDropDownButton(
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),
@@ -31,59 +44,24 @@ class _PeriodFilterWidgetState extends State<PeriodFilterWidget> {
onChanged: (value) { onChanged: (value) {
if (value != null) { if (value != null) {
setState(() { setState(() {
_currentState = value; periodDropDownIndex = periodDropDownTitles.indexOf(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: PeriodFilterStatus.values items: periodDropDownTitles
.mapIndexed( .mapIndexed(
(index, value) => DropdownMenuItem( (index, value) => DropdownMenuItem(
value: PeriodFilterStatus.values[index], value: periodDropDownTitles[index],
child: Text( child: Text(
PeriodFilterStatus.values[index].name, periodDropDownTitles[index],
style: TextStyles.textMMedium.copyWith(color: appTheme.black), style: TextStyles.textMMedium.copyWith(color: appTheme.black),
), ),
), ),
) )
.toList(), .toList(),
value: _currentState, value: periodDropDownTitles[periodDropDownIndex],
), ),
], ],
); );
} }
} }
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,6 +11,7 @@ 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,
@@ -28,7 +29,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 SizedBox(height: 12), const Spacer(),
Text( Text(
title, title,
style: TextStyles.textMMedium.copyWith(color: appTheme.black), style: TextStyles.textMMedium.copyWith(color: appTheme.black),

View File

@@ -1,6 +1,5 @@
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';
@@ -10,8 +9,7 @@ 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, required this.isLoading}); const RatingHistoryWidget({super.key, required this.ratingHistory});
final bool isLoading;
final List<RatingModel> ratingHistory; final List<RatingModel> ratingHistory;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -22,60 +20,50 @@ class RatingHistoryWidget extends StatelessWidget {
right: 16, right: 16,
top: 16, top: 16,
), ),
child: isLoading child: Column(
? const Center(child: Loader()) children: [
: Column( Row(
children: [ children: [
Row( Text(
children: [ s.rating_history,
Text( style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
s.rating_history, ),
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), const Spacer(),
), SvgPicture.asset(
const Spacer(), 'assets/icons/question_mark.svg',
SvgPicture.asset( height: 22,
'assets/icons/question_mark.svg', width: 22,
height: 22, colorFilter: ColorFilter.mode(
width: 22, appTheme.blueA400,
colorFilter: ColorFilter.mode( BlendMode.srcIn,
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( },
height: 12, itemCount: ratingHistory.length,
), 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,8 +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/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';
@@ -10,15 +8,14 @@ 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, required this.isLoading, required this.statistics}); const StatisticWidget({super.key});
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: isLoading ? const Center(child: Loader()) : Column( child: Column(
children: [ children: [
Row( Row(
children: [ children: [
@@ -41,17 +38,15 @@ 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: statistics?.countReferal ?? 0, title: s.invited_people_count)), Expanded(child: StatisticBouble(number: 12, title: s.invited_people_count)),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded(child: StatisticBouble(number: statistics?.blogPostsCount ?? 0, title: s.published_posts_count)), Expanded(child: StatisticBouble(number: 12, title: s.published_posts_count)),
], ],
), ),
const SizedBox( const SizedBox(
@@ -59,9 +54,9 @@ class StatisticWidget extends StatelessWidget {
), ),
Row( Row(
children: [ children: [
Expanded(child: StatisticBouble(number: statistics?.recipesCount ?? 0, title: s.published_recipes_count)), Expanded(child: StatisticBouble(number: 12, title: s.published_recipes_count)),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded(child: StatisticBouble(number: statistics?.dietsCount ?? 0, title: s.published_diets_count)), Expanded(child: StatisticBouble(number: 12, title: s.published_diets_count)),
], ],
), ),
const SizedBox( const SizedBox(
@@ -69,9 +64,9 @@ class StatisticWidget extends StatelessWidget {
), ),
Row( Row(
children: [ children: [
Expanded(child: StatisticBouble(number: statistics?.consultationsCount ?? 0, title: s.consultations_count)), Expanded(child: StatisticBouble(number: 12, title: s.consultations_count)),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded(child: StatisticBouble(number: statistics?.qwe ?? 0, title: s.active_consultations_count)), Expanded(child: StatisticBouble(number: 12, title: s.active_consultations_count)),
], ],
), ),
const SizedBox( const SizedBox(

View File

@@ -1,26 +0,0 @@
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

@@ -1,18 +0,0 @@
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

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

View File

@@ -50,7 +50,6 @@ class ConsultationService {
else if (state != null && stateComparison != null) else if (state != null && stateComparison != null)
'filters[last_state][${stateComparison.value}]': state, 'filters[last_state][${stateComparison.value}]': state,
if (dateComparison != null && date != null) 'filters[start_date][${dateComparison.value}]': date, if (dateComparison != null && date != null) 'filters[start_date][${dateComparison.value}]': date,
'sort[]': 'created_at:desc',
}, },
); );
return ConsultationPaginationModel.fromJson(response.data); return ConsultationPaginationModel.fromJson(response.data);
@@ -78,8 +77,7 @@ class ConsultationService {
final response = await dio.get( final response = await dio.get(
'/api/finances/my_finances', '/api/finances/my_finances',
queryParameters: { queryParameters: {
if (date != null && dateComparison != null) 'filters[start_date][${dateComparison.value}]': date, if (dateComparison != null && date != null) 'filters[end_date]': date,
'sort[]': 'start_date:desc',
}, },
); );
final data = response.data as Map<String, dynamic>; final data = response.data as Map<String, dynamic>;
@@ -98,8 +96,7 @@ class ConsultationService {
final response = await dio.get( final response = await dio.get(
'/api/finances/my_purchases', '/api/finances/my_purchases',
queryParameters: { queryParameters: {
if (date != null && dateComparison != null) 'filters[start_date][${dateComparison.value}]': date, if (dateComparison != null && date != null) 'filters[start_date]': date,
'sort[]': 'start_date:desc',
}, },
); );
final data = response.data as Map<String, dynamic>; final data = response.data as Map<String, dynamic>;

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,6 @@ import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; import 'package:eatvio_app/core/widgets/custom_outlined_button.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/features/dev_tools/test_cases.dart'; import 'package:eatvio_app/features/dev_tools/test_cases.dart';
import 'package:eatvio_app/core/env_config.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@@ -98,15 +97,10 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
padding: const EdgeInsets.symmetric(horizontal: 10), padding: const EdgeInsets.symmetric(horizontal: 10),
child: Column( child: Column(
children: [ children: [
Padding( Padding(padding: const EdgeInsets.all(8.0), child: Row(children: [Text('BaseUrl: $baseUrlForTest')])),
padding: const EdgeInsets.all(8.0),
child: Row(children: [Text('BaseUrl: $baseUrlForTest')]),
),
Row( Row(
children: [ children: [
Expanded( Expanded(child: CustomTextFormField(controller: controller, hintText: 'type new base url')),
child: CustomTextFormField(controller: controller, hintText: 'type new base url'),
),
IconButton( IconButton(
onPressed: () { onPressed: () {
setState(() { setState(() {
@@ -122,13 +116,13 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
CustomOutlinedButton( CustomOutlinedButton(
text: baseUrl == EnvConfig.devBaseUrl ? 'Set production base url' : 'Set dev2 base url', text: baseUrl == 'https://dev2.eatvio.com' ? 'Set production base url' : 'Set dev2 base url',
onPressed: () { onPressed: () {
setState(() { setState(() {
if (baseUrl == EnvConfig.baseUrl) { if (baseUrl == 'https://eatvio.ru') {
baseUrlForTest = EnvConfig.devBaseUrl; baseUrlForTest = 'https://dev2.eatvio.com';
} else { } else {
baseUrlForTest = EnvConfig.baseUrl; baseUrlForTest = 'https://eatvio.ru';
} }
baseUrl = baseUrlForTest.toLowerCase(); baseUrl = baseUrlForTest.toLowerCase();
sl<Dio>().options.baseUrl = baseUrl; sl<Dio>().options.baseUrl = baseUrl;
@@ -138,7 +132,7 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
buttonStyle: const ButtonStyle(), buttonStyle: const ButtonStyle(),
), ),
SizedBox(height: baseUrlForTest != baseUrl ? 40 : 10), SizedBox(height: baseUrlForTest != baseUrl ? 40 : 10),
if (baseUrl.toLowerCase() != EnvConfig.baseUrl.toLowerCase()) if (baseUrl.toLowerCase() != 'https://eatvio.ru')
ListView.builder( ListView.builder(
shrinkWrap: true, shrinkWrap: true,
itemCount: testCases.length, itemCount: testCases.length,
@@ -181,7 +175,12 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('Test Failed'), title: const Text('Test Failed'),
content: SingleChildScrollView(child: Text(result.error ?? 'No error details.')), content: SingleChildScrollView(child: Text(result.error ?? 'No error details.')),
actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('OK'))], actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('OK'),
),
],
), ),
); );
} }
@@ -198,7 +197,12 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('Test Failed'), title: const Text('Test Failed'),
content: SingleChildScrollView(child: Text('$e\n$stack')), content: SingleChildScrollView(child: Text('$e\n$stack')),
actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('OK'))], actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('OK'),
),
],
), ),
); );
} }
@@ -212,10 +216,10 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
TestState.fail => Container(width: 20, height: 20, color: Colors.red), TestState.fail => Container(width: 20, height: 20, color: Colors.red),
TestState.notRun => Container(width: 20, height: 20, color: Colors.grey), TestState.notRun => Container(width: 20, height: 20, color: Colors.grey),
TestState.loading => const SizedBox( TestState.loading => const SizedBox(
width: 20, width: 20,
height: 20, height: 20,
child: SizedBox(height: 20, width: 20, child: Center(child: CircularProgressIndicator())), child: SizedBox(height: 20, width: 20, child: Center(child: CircularProgressIndicator())),
), ),
}, },
], ],
), ),

View File

@@ -35,7 +35,6 @@ import 'package:eatvio_app/features/recipe/services/about_recipe_service.dart';
import 'package:eatvio_app/features/recipe/services/my_recipes_service.dart'; import 'package:eatvio_app/features/recipe/services/my_recipes_service.dart';
import 'package:eatvio_app/features/registration/services/registration_service.dart'; import 'package:eatvio_app/features/registration/services/registration_service.dart';
import 'package:eatvio_app/features/settings/services/settings_service.dart'; import 'package:eatvio_app/features/settings/services/settings_service.dart';
import 'package:eatvio_app/core/env_config.dart';
class TestResult { class TestResult {
final bool success; final bool success;
@@ -478,9 +477,7 @@ class AuthServiceTest implements TestCases {
final dio = sl<Dio>(); final dio = sl<Dio>();
final authService = AuthService(dio, baseUrl: baseUrl); final authService = AuthService(dio, baseUrl: baseUrl);
// Use dummy credentials, expecting failure but no crash // Use dummy credentials, expecting failure but no crash
await authService.getTokenByCredentials( await authService.getTokenByCredentials(credentials: const CredentialsModel(email: 'reshetnick.mike+user1@gmail.com', password: '1234567890'));
credentials: CredentialsModel(email: EnvConfig.testEmail, password: EnvConfig.testPassword),
);
return TestResult(false, 'Should not succeed'); return TestResult(false, 'Should not succeed');
} catch (e) { } catch (e) {
return TestResult(true, 'Exception expected for dummy credentials'); return TestResult(true, 'Exception expected for dummy credentials');

View File

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

View File

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

View File

@@ -22,7 +22,12 @@ enum PortionState {
@freezed @freezed
abstract class DiaryDayWrapperModel with _$DiaryDayWrapperModel { abstract class DiaryDayWrapperModel with _$DiaryDayWrapperModel {
const factory DiaryDayWrapperModel({@JsonKey(name: "diary") required DiaryDayModel diary}) = _DiaryDayWrapperModel; const factory DiaryDayWrapperModel({
@JsonKey(
name: "diary",
)
required DiaryDayModel diary,
}) = _DiaryDayWrapperModel;
factory DiaryDayWrapperModel.fromJson(Map<String, dynamic> json) => _$DiaryDayWrapperModelFromJson(json); factory DiaryDayWrapperModel.fromJson(Map<String, dynamic> json) => _$DiaryDayWrapperModelFromJson(json);
} }
@@ -66,13 +71,21 @@ DiaryDietModel? _parseDiet(dynamic json) {
@freezed @freezed
abstract class DiaryDietModel with _$DiaryDietModel { abstract class DiaryDietModel with _$DiaryDietModel {
const factory DiaryDietModel({required int id, required int duration, required String name}) = _DiaryDietModel; const factory DiaryDietModel({
required int id,
required int duration,
required String name,
}) = _DiaryDietModel;
factory DiaryDietModel.fromJson(Map<String, dynamic> json) => _$DiaryDietModelFromJson(json); factory DiaryDietModel.fromJson(Map<String, dynamic> json) => _$DiaryDietModelFromJson(json);
} }
TypeOfDiaryModel _parseDayTypeForFutureDay(dynamic json) { TypeOfDiaryModel _parseDayTypeForFutureDay(dynamic json) {
return TypeOfDiaryModel(hasChosenDiet: json['has_chosen_diet'] as bool, hasChosenPlan: json['has_chosen_plan'] as bool, diaryState: DiaryStateEnum.init); return TypeOfDiaryModel(
hasChosenDiet: json['has_chosen_diet'] as bool,
hasChosenPlan: json['has_chosen_plan'] as bool,
diaryState: DiaryStateEnum.init,
);
} }
@freezed @freezed
@@ -102,30 +115,11 @@ 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

@@ -136,7 +136,15 @@ class DiaryCubit extends Cubit<DiaryState> {
final fifthMeal = MealModel(id: 0, time: '0', typeNumber: 5, satiety: 0, mood: 0, portions: fifthMealPortions ?? [], photos: []); final fifthMeal = MealModel(id: 0, time: '0', typeNumber: 5, satiety: 0, mood: 0, portions: fifthMealPortions ?? [], photos: []);
emit( emit(
DiaryState.loadedTomorrowWithDiet( DiaryState.loadedTomorrowWithDiet(
diary: diaryDay.copyWith(meals: [firstMeal, secondMeal, thirdMeal, fourthMeal, fifthMeal]), diary: diaryDay.copyWith(
meals: [
firstMeal,
secondMeal,
thirdMeal,
fourthMeal,
fifthMeal,
],
),
selectedDay: selectedDay, selectedDay: selectedDay,
selectedMeal: selectedMeal, selectedMeal: selectedMeal,
), ),
@@ -148,7 +156,7 @@ class DiaryCubit extends Cubit<DiaryState> {
Future<void> fetchDiary([bool needLoading = false, int? diaryId, int? currentDay]) async { Future<void> fetchDiary([bool needLoading = false, int? diaryId, int? currentDay]) async {
this.diaryId ??= diaryId; this.diaryId ??= diaryId;
final isConsultantView = this.diaryId != null;
if (needLoading) { if (needLoading) {
emit(const DiaryState.loading()); emit(const DiaryState.loading());
selectedMeal = 1; selectedMeal = 1;
@@ -171,22 +179,17 @@ class DiaryCubit extends Cubit<DiaryState> {
int? currentPlanDay; int? currentPlanDay;
String? planName; String? planName;
if (diaryDay.chosenPlanId != null) { if (diaryDay.chosenPlanId != null) {
final chosenPlanModel = await chatRepository.getChosenPlan(id: diaryDay.chosenPlanId!);
if (!isConsultantView &&
(diaryDay.typeOfDiary.diaryState == DiaryStateEnum.lastDayCommented || diaryDay.typeOfDiary.diaryState == DiaryStateEnum.commented)) {
await diaryRepository.viewCommentOfDiaryDayByConsultant(diaryId: diaryDay.id);
}
consultantReview = await diaryRepository.getDiaryComments(planId: diaryDay.chosenPlanId!, diaryId: diaryDay.id); consultantReview = await diaryRepository.getDiaryComments(planId: diaryDay.chosenPlanId!, diaryId: diaryDay.id);
final chosenPlanModel = await chatRepository.getChosenPlan(id: diaryDay.chosenPlanId!);
planDuration = chosenPlanModel.plan.duration; planDuration = chosenPlanModel.plan.duration;
planName = chosenPlanModel.plan.name; planName = chosenPlanModel.plan.name;
currentPlanDay = consultantReview.currentPlanDay; currentPlanDay = currentDay ?? DateTime.now().difference(chosenPlanModel.startDate).inDays + 1;
} }
final nutrition = await getNutrition(diaryDay.id); final nutrition = await getNutrition(diaryDay.id);
final isDiaryFinished = diaryDay.typeOfDiary.diaryState == DiaryStateEnum.finished || diaryDay.typeOfDiary.diaryState == DiaryStateEnum.lastDayFinished; final isDiaryFinished = diaryDay.typeOfDiary.diaryState == DiaryStateEnum.finished || diaryDay.typeOfDiary.diaryState == DiaryStateEnum.lastDayFinished;
final isDiaryCommented = final isDiaryCommented = diaryDay.typeOfDiary.diaryState == DiaryStateEnum.commented ||
diaryDay.typeOfDiary.diaryState == DiaryStateEnum.commented ||
diaryDay.typeOfDiary.diaryState == DiaryStateEnum.lastDayCommented || diaryDay.typeOfDiary.diaryState == DiaryStateEnum.lastDayCommented ||
diaryDay.typeOfDiary.diaryState == DiaryStateEnum.reviewedByUser; diaryDay.typeOfDiary.diaryState == DiaryStateEnum.reviewedByUser;
emit( emit(
@@ -195,7 +198,7 @@ class DiaryCubit extends Cubit<DiaryState> {
nutrition: nutrition, nutrition: nutrition,
selectedDay: selectedDay, selectedDay: selectedDay,
selectedMeal: selectedMeal, selectedMeal: selectedMeal,
isConsultantView: isConsultantView, isConsultantView: this.diaryId != null,
consultantReview: consultantReview, consultantReview: consultantReview,
isDiaryFinished: isDiaryFinished, isDiaryFinished: isDiaryFinished,
isDiaryCommented: isDiaryCommented, isDiaryCommented: isDiaryCommented,
@@ -203,7 +206,7 @@ class DiaryCubit extends Cubit<DiaryState> {
chosenPlanDuration: planDuration, chosenPlanDuration: planDuration,
chosenPlanName: planName, chosenPlanName: planName,
isMoodEnabled: isMoodEnabled, isMoodEnabled: isMoodEnabled,
isWakeUpAndRetireEnabled: isWakeUpAndRetireTimeEnabled, isWakeUpAndRetireEnabled: isWakeUpAndRetireTimeEnabled
), ),
); );
} catch (e, st) { } catch (e, st) {
@@ -212,8 +215,17 @@ class DiaryCubit extends Cubit<DiaryState> {
} }
} }
Future<void> finishDiaryDay({required int diaryId, required String wakeUp, required String retire}) async { Future<void> finishDiaryDay({
await diaryRepository.finishDiaryDay(diaryId: diaryId, wakeUp: wakeUp, retire: retire, comment: comment.isEmpty ? null : comment); required int diaryId,
required String wakeUp,
required String retire,
}) async {
await diaryRepository.finishDiaryDay(
diaryId: diaryId,
wakeUp: wakeUp,
retire: retire,
comment: comment.isEmpty ? null : comment,
);
await fetchDiary(); await fetchDiary();
} }
@@ -229,7 +241,6 @@ class DiaryCubit extends Cubit<DiaryState> {
required int quantity, required int quantity,
int? productNutritionalValueId, int? productNutritionalValueId,
}) async { }) async {
emit((state as DiaryStateLoaded).copyWith(isLoadingAddingProductInMeal: true));
await diaryRepository.addPortionToMeal( await diaryRepository.addPortionToMeal(
diaryId: diaryId, diaryId: diaryId,
mealId: mealId, mealId: mealId,

View File

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

View File

@@ -24,8 +24,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class DiaryScreen extends StatelessWidget { class DiaryScreen extends StatelessWidget {
const DiaryScreen({super.key}); const DiaryScreen({super.key, this.date});
final DateTime? date;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const DiaryScreenContent(); return const DiaryScreenContent();
@@ -52,9 +52,10 @@ class UserDiaryScreen extends StatelessWidget {
} }
class DiaryScreenContent extends StatelessWidget { class DiaryScreenContent extends StatelessWidget {
const DiaryScreenContent({super.key, this.diaryId, this.currentPlanDay}); const DiaryScreenContent({super.key, this.diaryId, this.currentPlanDay, this.date});
final int? diaryId; final int? diaryId;
final int? currentPlanDay; final int? currentPlanDay;
final DateTime? date;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final s = S.of(context); final s = S.of(context);

View File

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

View File

@@ -8,8 +8,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:table_calendar/table_calendar.dart'; import 'package:table_calendar/table_calendar.dart';
class CalendarWidget extends StatefulWidget { class CalendarWidget extends StatefulWidget {
const CalendarWidget({super.key}); const CalendarWidget({super.key,});
@override @override
State<CalendarWidget> createState() => _CalendarWidgetState(); State<CalendarWidget> createState() => _CalendarWidgetState();
} }

View File

@@ -1,15 +1,16 @@
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({super.key, required this.mealNumber}); const MealButtonWidget({
super.key,
required this.mealNumber,
});
@override @override
State<MealButtonWidget> createState() => _MealButtonWidgetState(); State<MealButtonWidget> createState() => _MealButtonWidgetState();
@@ -21,7 +22,9 @@ 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((element) => element.typeNo == widget.mealNumber); final consultantComment = state.consultantReview?.consultantComments.mealsComments.firstWhereOrNull(
(element) => element.typeNo == widget.mealNumber,
);
final Color bgColor; final Color bgColor;
if (state.selectedMeal == widget.mealNumber) { if (state.selectedMeal == widget.mealNumber) {
@@ -31,84 +34,55 @@ class _MealButtonWidgetState extends State<MealButtonWidget> {
} else { } else {
bgColor = const Color(0xFFF1F2F6); bgColor = const Color(0xFFF1F2F6);
} }
return InkWell(
final mealIndex = widget.mealNumber - 1; onTap: () {
final portions = mealIndex < state.diary.meals.length ? state.diary.meals[mealIndex].portions : <PortionModel>[]; context.read<DiaryCubit>().setSelectedMeal(widget.mealNumber);
final totals = MealSummaryWidget.calculateMealTotals(portions); },
final calories = totals['calories']!.round(); borderRadius: BorderRadius.circular(100),
child: Ink(
return Column( width: MediaQuery.of(context).size.width / 5 - (96 / 5),
mainAxisSize: MainAxisSize.min, height: 32,
children: [ decoration: BoxDecoration(
InkWell(
onTap: () {
context.read<DiaryCubit>().setSelectedMeal(widget.mealNumber);
},
borderRadius: BorderRadius.circular(100), borderRadius: BorderRadius.circular(100),
child: Ink( color: bgColor,
width: MediaQuery.of(context).size.width / 5 - (96 / 5), ),
height: 32, child: Center(
decoration: BoxDecoration(borderRadius: BorderRadius.circular(100), color: bgColor), child: Text(
child: Center( '${widget.mealNumber}',
child: Text( style: TextStyle(
'${widget.mealNumber}', color: state.selectedMeal == widget.mealNumber ? Colors.white : const Color(0xFF6D6F71),
style: TextStyle( fontSize: 16,
color: state.selectedMeal == widget.mealNumber ? Colors.white : const Color(0xFF6D6F71), fontWeight: FontWeight.w600,
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(
final mealIndex = widget.mealNumber - 1; onTap: () {
final portions = mealIndex < state.diary.meals.length ? state.diary.meals[mealIndex].portions : <PortionModel>[]; context.read<DiaryCubit>().setSelectedMeal(widget.mealNumber);
final totals = MealSummaryWidget.calculateMealTotals(portions); },
final calories = totals['calories']!.round(); borderRadius: BorderRadius.circular(100),
child: Ink(
return Column( width: MediaQuery.of(context).size.width / 5 - (96 / 5),
mainAxisSize: MainAxisSize.min, height: 32,
children: [ decoration: BoxDecoration(
InkWell(
onTap: () {
context.read<DiaryCubit>().setSelectedMeal(widget.mealNumber);
},
borderRadius: BorderRadius.circular(100), borderRadius: BorderRadius.circular(100),
child: Ink( color: state.selectedMeal == widget.mealNumber ? appTheme.blueA400 : const Color(0xFFF1F2F6),
width: MediaQuery.of(context).size.width / 5 - (96 / 5), ),
height: 32, child: Center(
decoration: BoxDecoration( child: Text(
borderRadius: BorderRadius.circular(100), '${widget.mealNumber}',
color: state.selectedMeal == widget.mealNumber ? appTheme.blueA400 : const Color(0xFFF1F2F6), style: TextStyle(
), color: state.selectedMeal == widget.mealNumber ? Colors.white : const Color(0xFF6D6F71),
child: Center( fontSize: 16,
child: Text( fontWeight: FontWeight.w600,
'${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

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

View File

@@ -1,26 +0,0 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class ProductLoaderWidget extends StatefulWidget {
const ProductLoaderWidget({
super.key,
});
@override
State<ProductLoaderWidget> createState() => _ProductLoaderWidgetState();
}
class _ProductLoaderWidgetState extends State<ProductLoaderWidget> {
@override
Widget build(BuildContext context) {
return ListTile(
horizontalTitleGap: 0,
contentPadding: const EdgeInsets.only(right: 24, left: 12),
title: const CupertinoActivityIndicator(),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
tileColor: const Color(0xFFF6FAFF),
);
}
}

View File

@@ -1,4 +1,3 @@
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';
@@ -16,7 +15,10 @@ import 'package:flutter_svg/flutter_svg.dart';
class ProductWidget extends StatefulWidget { class ProductWidget extends StatefulWidget {
final PortionModel portion; final PortionModel portion;
const ProductWidget({super.key, required this.portion}); const ProductWidget({
super.key,
required this.portion,
});
@override @override
State<ProductWidget> createState() => _ProductWidgetState(); State<ProductWidget> createState() => _ProductWidgetState();
@@ -35,60 +37,6 @@ 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);
@@ -100,9 +48,15 @@ class _ProductWidgetState extends State<ProductWidget> {
return showModalBottomSheet( return showModalBottomSheet(
useRootNavigator: true, useRootNavigator: true,
context: context, context: context,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(12))), shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
),
isScrollControlled: true, isScrollControlled: true,
builder: (context) => AddProductModal(state: state, portion: portion, diaryCubit: diaryCubit), builder: (context) => AddProductModal(
state: state,
portion: portion,
diaryCubit: diaryCubit,
),
); );
} }
@@ -115,21 +69,15 @@ 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: Column( subtitle: Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'),
crossAxisAlignment: CrossAxisAlignment.start, shape: RoundedRectangleBorder(
children: [ borderRadius: BorderRadius.circular(20),
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 = final isDiaryCommented = state.diary.typeOfDiary.diaryState == DiaryStateEnum.commented ||
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 =
@@ -151,20 +99,21 @@ class _ProductWidgetState extends State<ProductWidget> {
color: Color(0xFF6D6F71), color: Color(0xFF6D6F71),
), ),
child: ListTile( child: ListTile(
onTap: () => diaryCubit.restore(diaryId: state.diary.id, mealId: state.selectedMeal, portionId: widget.portion.id), onTap: () => diaryCubit.restore(
trailing: SvgPicture.asset('assets/icons/revert_product.svg'), diaryId: state.diary.id,
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: Column( subtitle: Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'),
crossAxisAlignment: CrossAxisAlignment.start, shape: RoundedRectangleBorder(
children: [ borderRadius: BorderRadius.circular(20),
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),
), ),
@@ -173,13 +122,12 @@ 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);
} }
@@ -199,36 +147,47 @@ class _ProductWidgetState extends State<ProductWidget> {
isEaten = value!; isEaten = value!;
}); });
if (isEaten) { if (isEaten) {
diaryCubit.markEaten(diaryId: state.diary.id, mealId: state.selectedMeal, portionId: widget.portion.id); diaryCubit.markEaten(
diaryId: state.diary.id,
mealId: state.selectedMeal,
portionId: widget.portion.id,
);
} else { } else {
diaryCubit.unmarkEaten(diaryId: state.diary.id, mealId: state.selectedMeal, portionId: widget.portion.id); diaryCubit.unmarkEaten(
diaryId: state.diary.id,
mealId: state.selectedMeal,
portionId: widget.portion.id,
);
} }
} }
: null, : null,
activeColor: appTheme.blueA400, activeColor: appTheme.blueA400,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), shape: RoundedRectangleBorder(
side: const BorderSide(color: Color(0xFF809CB9)), borderRadius: BorderRadius.circular(4),
),
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: Column( subtitle: Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'),
crossAxisAlignment: CrossAxisAlignment.start, shape: RoundedRectangleBorder(
children: [ borderRadius: BorderRadius.circular(20),
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(enabled: showCommentWidget, portionId: widget.portion.id, comment: portionComment?.comment), child: PortionCommentWidget(
), enabled: showCommentWidget,
portionId: widget.portion.id,
comment: portionComment?.comment,
),
)
], ],
); );
} }

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