forked from eatvio/eatvio_app
Merge remote-tracking branch 'upstream/main' into deepLinks
This commit is contained in:
@@ -11,6 +11,29 @@ jobs:
|
||||
- name: Checkout
|
||||
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
|
||||
run: |
|
||||
@@ -18,12 +41,25 @@ jobs:
|
||||
env:
|
||||
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
|
||||
run: |
|
||||
flutter clean
|
||||
flutter pub get
|
||||
flutter pub upgrade
|
||||
flutter pub run build_runner build --delete-conflicting-outputs
|
||||
# Configure Flutter for release mode
|
||||
flutter build ios --config-only --release
|
||||
cd ios
|
||||
pod update
|
||||
fastlane beta
|
||||
@@ -31,6 +67,8 @@ jobs:
|
||||
LANG: en_US.UTF-8
|
||||
LC_ALL: en_US.UTF-8
|
||||
MATCH_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }}
|
||||
FASTLANE_PASSWORD: ${{ vars.FASTLANE_PASSWORD }}
|
||||
MATCH_GIT_URL: http://gitea.office.local:3000/mso7nueh/fastlane
|
||||
FASTLANE_USER: eatvio2023@icloud.com
|
||||
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ vars.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }}
|
||||
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ vars.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }}
|
||||
API_APPSTORE_KEY: ${{ vars.API_APPSTORE_KEY }}
|
||||
|
||||
@@ -3,7 +3,7 @@ name: Eatvio App Android Test
|
||||
on: [ pull_request ]
|
||||
|
||||
env:
|
||||
flutter_version: "3.32.5"
|
||||
flutter_version: "3.35.1"
|
||||
java_version: "21.x"
|
||||
flutter_channel: "stable"
|
||||
my_local_cache_dir: "/opt/hostedtoolcache/"
|
||||
@@ -16,6 +16,30 @@ jobs:
|
||||
steps:
|
||||
- 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
|
||||
run: |
|
||||
cat > android/key.properties <<EOF
|
||||
@@ -119,7 +143,11 @@ jobs:
|
||||
run: flutter test
|
||||
|
||||
- name: Build apk
|
||||
run: flutter build apk --release
|
||||
run: |
|
||||
BUILD_DATE=$(TZ=Europe/Moscow date +'%Y-%m-%d %H:%M:%S')
|
||||
flutter build apk --release \
|
||||
--dart-define=HIDE_PLANS=false \
|
||||
--dart-define=BUILD_DATE="$BUILD_DATE"
|
||||
|
||||
- name: Rename and move apk
|
||||
run: |
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -12,6 +12,9 @@
|
||||
/android/app/.cxx
|
||||
*.g.dart
|
||||
*.freezed.dart
|
||||
*.gr.dart
|
||||
*.mocks.dart
|
||||
android/build/
|
||||
ios/XCBuildData/
|
||||
env/.env
|
||||
.env
|
||||
84
ENV_SETUP.md
Normal file
84
ENV_SETUP.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# Настройка переменных окружения
|
||||
|
||||
Этот проект использует переменные окружения для хранения секретных данных и конфигурации.
|
||||
|
||||
## Установка
|
||||
|
||||
1. Скопируйте файл `env.example` в `.env`:
|
||||
```bash
|
||||
cp env.example .env
|
||||
```
|
||||
|
||||
2. Отредактируйте файл `.env` и замените значения на ваши собственные секреты.
|
||||
|
||||
## Переменные окружения
|
||||
|
||||
### API URLs
|
||||
- `BASE_URL` - основной URL API (по умолчанию: https://eatvio.ru)
|
||||
- `DEV_BASE_URL` - URL API для разработки (по умолчанию: https://dev2.eatvio.com)
|
||||
|
||||
### Sentry Configuration
|
||||
- `SENTRY_DSN` - DSN для Sentry мониторинга
|
||||
|
||||
### Firebase Configuration
|
||||
- `FIREBASE_PROJECT_ID` - ID проекта Firebase
|
||||
- `FIREBASE_DATABASE_URL` - URL базы данных Firebase
|
||||
- `FIREBASE_STORAGE_BUCKET` - bucket для хранения файлов
|
||||
- `FIREBASE_MESSAGING_SENDER_ID` - ID отправителя сообщений
|
||||
|
||||
### Firebase Android Configuration
|
||||
- `FIREBASE_ANDROID_API_KEY` - API ключ для Android
|
||||
- `FIREBASE_ANDROID_APP_ID` - ID приложения для Android
|
||||
|
||||
### Firebase iOS Configuration
|
||||
- `FIREBASE_IOS_API_KEY` - API ключ для iOS
|
||||
- `FIREBASE_IOS_APP_ID` - ID приложения для iOS
|
||||
- `FIREBASE_IOS_BUNDLE_ID` - Bundle ID для iOS
|
||||
|
||||
### App Store URLs
|
||||
- `APP_STORE_URL` - URL App Store
|
||||
- `RUSTORE_URL` - URL RuStore
|
||||
|
||||
### Support URLs
|
||||
- `TELEGRAM_SUPPORT_URL` - URL поддержки в Telegram
|
||||
- `SUPPORT_EMAIL` - email для поддержки
|
||||
|
||||
### Test Credentials (только для разработки)
|
||||
- `TEST_EMAIL` - тестовый email
|
||||
- `TEST_PASSWORD` - тестовый пароль
|
||||
|
||||
### Email Validation API
|
||||
- `EMAIL_VALIDATION_API_URL` - URL API для валидации email
|
||||
|
||||
### Connection Check URLs
|
||||
- `GOOGLE_CHECK_URL` - URL для проверки соединения (Google)
|
||||
- `YANDEX_CHECK_URL` - URL для проверки соединения (Yandex)
|
||||
|
||||
## Безопасность
|
||||
|
||||
⚠️ **ВАЖНО**: Файл `.env` добавлен в `.gitignore` и не должен попадать в репозиторий. Все секретные данные должны храниться только в этом файле.
|
||||
|
||||
## Использование в коде
|
||||
|
||||
Для доступа к переменным окружения используйте класс `EnvConfig`:
|
||||
|
||||
```dart
|
||||
import 'package:eatvio_app/core/env_config.dart';
|
||||
|
||||
String baseUrl = EnvConfig.baseUrl;
|
||||
String sentryDsn = EnvConfig.sentryDsn;
|
||||
```
|
||||
|
||||
## Миграция
|
||||
|
||||
Все секреты были перенесены из кода в переменные окружения:
|
||||
|
||||
1. **Sentry DSN** - из `main.dart`
|
||||
2. **Firebase конфигурация** - из `firebase_options.dart`
|
||||
3. **API URLs** - из `const.dart`
|
||||
4. **URL для проверки соединения** - из `internet_connection_checker_screen.dart`
|
||||
5. **URL для обновления приложения** - из `update_screen.dart`
|
||||
6. **URL поддержки Telegram** - из `support_screen.dart`
|
||||
7. **API для валидации email** - из `registration_repository.dart`
|
||||
8. **Тестовые учетные данные** - из `test_cases.dart`
|
||||
9. **Email поддержки** - из `support_screen.dart`
|
||||
@@ -19,17 +19,17 @@ android {
|
||||
ndkVersion = "27.0.12077973"
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_21
|
||||
targetCompatibility = JavaVersion.VERSION_21
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = 21
|
||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.eatvioapp.app"
|
||||
minSdk = flutter.minSdkVersion
|
||||
minSdkVersion flutter.minSdkVersion
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
|
||||
|
||||
<application
|
||||
android:label="Eatvio"
|
||||
android:label="Итвио"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,3 +1,3 @@
|
||||
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
||||
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-all.zip
|
||||
|
||||
@@ -18,11 +18,11 @@ pluginManagement {
|
||||
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "8.7.3" apply false
|
||||
id "com.android.application" version "8.12.0" apply false
|
||||
// START: FlutterFire Configuration
|
||||
id "com.google.gms.google-services" version "4.4.2" apply false
|
||||
id "com.google.gms.google-services" version "4.4.3" apply false
|
||||
// END: FlutterFire Configuration
|
||||
id "org.jetbrains.kotlin.android" version "2.0.21" apply false
|
||||
id "org.jetbrains.kotlin.android" version "2.2.10" apply false
|
||||
}
|
||||
|
||||
include ":app"
|
||||
|
||||
3
assets/icons/reply.svg
Normal file
3
assets/icons/reply.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="19" height="14" viewBox="0 0 19 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.56039 6.73215L7.06827 10.2925C7.22519 10.452 7.3026 10.6385 7.30049 10.8521C7.29855 11.0656 7.21569 11.2555 7.0519 11.4218C6.88812 11.577 6.70269 11.6573 6.49563 11.6629C6.28874 11.6682 6.1034 11.5879 5.93961 11.4218L1.19568 6.60653C1.0048 6.41278 0.909363 6.1868 0.909363 5.92858C0.909363 5.67037 1.0048 5.44439 1.19568 5.25064L5.93961 0.435368C6.08581 0.287154 6.26675 0.211261 6.48243 0.207689C6.69829 0.204297 6.88812 0.280189 7.0519 0.435368C7.21569 0.601618 7.29759 0.792601 7.29759 1.00831C7.29759 1.22385 7.21569 1.41474 7.0519 1.58099L3.56039 5.12501H13.4583C14.8441 5.12501 16.0261 5.62162 17.0045 6.61483C17.983 7.60787 18.4722 8.80769 18.4722 10.2143V13.1607C18.4722 13.3888 18.3964 13.5797 18.2448 13.7334C18.0933 13.8873 17.9052 13.9643 17.6806 13.9643C17.4559 13.9643 17.2678 13.8873 17.1164 13.7334C16.9647 13.5797 16.8889 13.3888 16.8889 13.1607V10.2143C16.8889 9.25269 16.5539 8.43197 15.884 7.75215C15.2143 7.07215 14.4057 6.73215 13.4583 6.73215H3.56039Z" fill="#2F80ED"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
3
assets/icons/resend.svg
Normal file
3
assets/icons/resend.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.377 11L15.3193 6.94225C15.1641 6.78709 15.0865 6.60892 15.0865 6.40775C15.0865 6.20642 15.1641 6.02817 15.3193 5.873C15.4744 5.72817 15.6542 5.65734 15.8587 5.6605C16.0631 5.66384 16.2345 5.73467 16.373 5.873L20.8673 10.3673C20.9609 10.4609 21.0269 10.5597 21.0653 10.6635C21.1038 10.7673 21.123 10.8795 21.123 11C21.123 11.1205 21.1038 11.2327 21.0653 11.3365C21.0269 11.4403 20.9609 11.5391 20.8673 11.6328L16.373 16.127C16.2178 16.282 16.0422 16.357 15.8462 16.352C15.6501 16.3468 15.4744 16.2718 15.3193 16.127C15.1641 15.9718 15.0856 15.7946 15.0837 15.5953C15.0817 15.3959 15.1551 15.2218 15.3038 15.073L19.377 11ZM14.0693 11.75H7.25C6.3525 11.75 5.5865 12.0673 4.952 12.702C4.31733 13.3365 4 14.1025 4 15V17.75C4 17.9628 3.92817 18.141 3.7845 18.2845C3.641 18.4282 3.46283 18.5 3.25 18.5C3.03717 18.5 2.859 18.4282 2.7155 18.2845C2.57183 18.141 2.5 17.9628 2.5 17.75V15C2.5 13.6872 2.9635 12.5673 3.8905 11.6405C4.81733 10.7135 5.93717 10.25 7.25 10.25H14.0693L10.7615 6.94225C10.6065 6.78709 10.529 6.60892 10.529 6.40775C10.529 6.20642 10.6065 6.02817 10.7615 5.873C10.9167 5.72817 11.0965 5.65734 11.301 5.6605C11.5055 5.66384 11.677 5.73467 11.8155 5.873L16.3095 10.3673C16.4032 10.4609 16.4692 10.5597 16.5078 10.6635C16.5463 10.7673 16.5655 10.8795 16.5655 11C16.5655 11.1205 16.5463 11.2327 16.5078 11.3365C16.4692 11.4403 16.4032 11.5391 16.3095 11.6328L11.8155 16.127C11.6603 16.282 11.4847 16.357 11.2885 16.352C11.0923 16.3468 10.9167 16.2718 10.7615 16.127C10.6065 15.9718 10.528 15.7946 10.526 15.5953C10.524 15.3959 10.5974 15.2218 10.7463 15.073L14.0693 11.75Z" fill="#2F80ED"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -21,6 +21,6 @@
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>12.0</string>
|
||||
<string>16.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
source "https://rubygems.org"
|
||||
|
||||
gem "fastlane"
|
||||
gem "fastlane", "~> 2.217"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
platform :ios, '14.0'
|
||||
platform :ios, '16.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
@@ -39,7 +39,7 @@ post_install do |installer|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64"
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0'
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '16.0'
|
||||
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
|
||||
'$(inherited)',
|
||||
## dart: PermissionGroup.camera
|
||||
|
||||
@@ -370,7 +370,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
@@ -388,7 +388,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 38;
|
||||
CURRENT_PROJECT_VERSION = 71;
|
||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
@@ -406,7 +406,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.50;
|
||||
MARKETING_VERSION = 1.2.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
|
||||
@@ -466,7 +466,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -518,7 +518,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
@@ -538,7 +538,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 38;
|
||||
CURRENT_PROJECT_VERSION = 71;
|
||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
@@ -556,7 +556,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.50;
|
||||
MARKETING_VERSION = 1.2.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
|
||||
@@ -576,7 +576,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 38;
|
||||
CURRENT_PROJECT_VERSION = 71;
|
||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
@@ -594,7 +594,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.50;
|
||||
MARKETING_VERSION = 1.2.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
buildConfiguration = "Release"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>eatvio_app</string>
|
||||
<string>Итвио</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
@@ -21,7 +21,7 @@
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>38</string>
|
||||
<string>71</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
|
||||
@@ -18,9 +18,69 @@ default_platform(:ios)
|
||||
platform :ios do
|
||||
desc "Push a new beta build to TestFlight"
|
||||
lane :beta do
|
||||
match(type: "appstore")
|
||||
UI.message "Current working directory: #{Dir.pwd}"
|
||||
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")
|
||||
build_app(workspace: "Runner.xcworkspace", scheme: "Runner", export_method: "app-store", codesigning_identity: "Apple Distribution")
|
||||
upload_to_testflight
|
||||
|
||||
build_app(
|
||||
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
|
||||
|
||||
@@ -5,22 +5,24 @@
|
||||
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000206">
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000227">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.195115">
|
||||
<testcase classname="fastlane.lanes" name="1: match" time="8.292217">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: build_app" time="116.686863">
|
||||
<testcase classname="fastlane.lanes" name="2: increment_build_number" time="0.201631">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="3: upload_to_testflight" time="338.642203">
|
||||
<testcase classname="fastlane.lanes" name="3: build_app" time="9.870647">
|
||||
|
||||
<failure message="/opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/actions/actions_helper.rb:67:in 'Fastlane::Actions.execute_action' /opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/runner.rb:255:in 'block in Fastlane::Runner#execute_action' /opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/runner.rb:229:in 'Dir.chdir' /opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/runner.rb:229:in 'Fastlane::Runner#execute_action' /opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/runner.rb:157:in 'Fastlane::Runner#trigger_action_by_name' /opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/fast_file.rb:159:in 'Fastlane::FastFile#method_missing' Fastfile:23:in 'block (2 levels) in Fastlane::FastFile#parsing_binding' /opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/lane.rb:41:in 'Fastlane::Lane#call' /opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/runner.rb:49:in 'block in Fastlane::Runner#execute' /opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/runner.rb:45:in 'Dir.chdir' /opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/runner.rb:45:in 'Fastlane::Runner#execute' /opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/lane_manager.rb:46:in 'Fastlane::LaneManager.cruise_lane' /opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/command_line_handler.rb:34:in 'Fastlane::CommandLineHandler.handle' /opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/commands_generator.rb:110:in 'block (2 levels) in Fastlane::CommandsGenerator#run' /opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/commander-4.6.0/lib/commander/command.rb:187:in 'Commander::Command#call' /opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/commander-4.6.0/lib/commander/command.rb:157:in 'Commander::Command#run' /opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/commander-4.6.0/lib/commander/runner.rb:444:in 'Commander::Runner#run_active_command' /opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb:124:in 'Commander::Runner#run!' /opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/commander-4.6.0/lib/commander/delegates.rb:18:in 'Commander::Delegates#run!' /opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/commands_generator.rb:363:in 'Fastlane::CommandsGenerator#run' /opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/commands_generator.rb:43:in 'Fastlane::CommandsGenerator.start' /opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/fastlane/lib/fastlane/cli_tools_distributor.rb:123:in 'Fastlane::CLIToolsDistributor.take_off' /opt/homebrew/Cellar/fastlane/2.228.0/libexec/gems/fastlane-2.228.0/bin/fastlane:23:in '<top (required)>' /opt/homebrew/Cellar/fastlane/2.228.0/libexec/bin/fastlane:25:in 'Kernel#load' /opt/homebrew/Cellar/fastlane/2.228.0/libexec/bin/fastlane:25:in '<main>' Error building the application - see the log above" />
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:io';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/firebase_api.dart';
|
||||
@@ -26,6 +24,7 @@ import 'package:eatvio_app/features/consultations/presentation/pages/consultatio
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/user_purchases_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/create_diet/presentation/create_diet_screen.dart';
|
||||
import 'package:eatvio_app/features/create_diet/presentation/diet_preview_screen.dart';
|
||||
@@ -113,21 +112,13 @@ class AppRouter {
|
||||
name: 'authScreen',
|
||||
builder: (context, state) => const AuthScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'registration-screen',
|
||||
name: 'registrationScreen',
|
||||
builder: (context, state) => const RegistrationScreen(),
|
||||
),
|
||||
GoRoute(path: 'registration-screen', name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()),
|
||||
GoRoute(
|
||||
path: 'forget-password-screen',
|
||||
name: 'forgetPasswordScreen',
|
||||
builder: (context, state) => const ForgetPasswordScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'scan-qr-code-screen',
|
||||
name: 'scanQRCodeScreen',
|
||||
builder: (context, state) => const ScanQRCodeScreen(),
|
||||
builder: (context, state) => ForgetPasswordScreen(isAuth: state.extra as bool),
|
||||
),
|
||||
GoRoute(path: 'scan-qr-code-screen', name: 'scanQRCodeScreen', builder: (context, state) => const ScanQRCodeScreen()),
|
||||
],
|
||||
),
|
||||
ShellRoute(
|
||||
@@ -165,19 +156,23 @@ class AppRouter {
|
||||
GoRoute(
|
||||
path: '/diary-screen',
|
||||
name: 'diaryScreen',
|
||||
pageBuilder: (context, state) => const NoTransitionPage(
|
||||
child: DiaryScreen(),
|
||||
),
|
||||
pageBuilder: (context, state) => const NoTransitionPage(child: DiaryScreen()),
|
||||
),
|
||||
GoRoute(
|
||||
path: ConsultationsScreen.path,
|
||||
name: ConsultationsScreen.name,
|
||||
pageBuilder: (context, state) => const NoTransitionPage(child: ConsultationsScreen())),
|
||||
GoRoute(path: '/chat-select-screen', name: 'chatSelectScreen', pageBuilder: (context, state) => const NoTransitionPage(child: ChatsScreen())),
|
||||
path: ConsultationsScreen.path,
|
||||
name: ConsultationsScreen.name,
|
||||
pageBuilder: (context, state) => const NoTransitionPage(child: ConsultationsScreen()),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/notifications-screen',
|
||||
name: 'notificationsScreen',
|
||||
pageBuilder: (context, state) => const NoTransitionPage(child: NotificationsScreen())),
|
||||
path: '/chat-select-screen',
|
||||
name: 'chatSelectScreen',
|
||||
pageBuilder: (context, state) => const NoTransitionPage(child: ChatsScreen()),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/notifications-screen',
|
||||
name: 'notificationsScreen',
|
||||
pageBuilder: (context, state) => const NoTransitionPage(child: NotificationsScreen()),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/menu-screen',
|
||||
name: 'menuScreen',
|
||||
@@ -194,32 +189,16 @@ 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(
|
||||
path: '/welcome-screen',
|
||||
name: 'welcomeScreen',
|
||||
builder: (context, state) => const WelcomeScreen(),
|
||||
redirect: (context, state) async {
|
||||
try {
|
||||
if (await TokenStorage.haveTokenInStorage()) {
|
||||
final firebaseApi = sl<FirebaseApi>();
|
||||
final fbToken = await firebaseApi.getToken();
|
||||
final deviceInfo = DeviceInfoPlugin();
|
||||
await sl<Dio>().post(
|
||||
'/api/profile/notification/token/register',
|
||||
data: {
|
||||
'platform': Platform.isIOS ? 'ios' : 'android',
|
||||
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
|
||||
'token': fbToken,
|
||||
},
|
||||
);
|
||||
}
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed') ?? false;
|
||||
await sl<FirebaseApi>().registerPushToken();
|
||||
if (welcomeScreenWasShowed == false) {
|
||||
return null;
|
||||
} else if (await TokenStorage.haveTokenInStorage()) {
|
||||
@@ -228,7 +207,7 @@ class AppRouter {
|
||||
return '/auth-screen';
|
||||
}
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
return '/feed-screen';
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -253,26 +232,10 @@ class AppRouter {
|
||||
}
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: ConsultationsArchiveScreen.path,
|
||||
name: ConsultationsArchiveScreen.name,
|
||||
builder: (context, state) => const ConsultationsArchiveScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: ConsultantFinancesScreen.path,
|
||||
name: ConsultantFinancesScreen.name,
|
||||
builder: (context, state) => const ConsultantFinancesScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: UserPurchasesScreen.path,
|
||||
name: UserPurchasesScreen.name,
|
||||
builder: (context, state) => const UserPurchasesScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/user-finance-screen',
|
||||
name: 'userFinanceScreen',
|
||||
builder: (context, state) => const UserFinanceScreen(),
|
||||
),
|
||||
GoRoute(path: ConsultationsArchiveScreen.path, name: ConsultationsArchiveScreen.name, builder: (context, state) => const ConsultationsArchiveScreen()),
|
||||
GoRoute(path: ConsultantFinancesScreen.path, name: ConsultantFinancesScreen.name, builder: (context, state) => const ConsultantFinancesScreen()),
|
||||
GoRoute(path: UserPurchasesScreen.path, name: UserPurchasesScreen.name, builder: (context, state) => const UserPurchasesScreen()),
|
||||
GoRoute(path: '/user-finance-screen', name: 'userFinanceScreen', builder: (context, state) => const UserFinanceScreen()),
|
||||
GoRoute(
|
||||
path: CardSettingsScreen.path,
|
||||
name: CardSettingsScreen.name,
|
||||
@@ -287,11 +250,7 @@ class AppRouter {
|
||||
builder: (context, state) {
|
||||
final slug = state.uri.queryParameters['slug'];
|
||||
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>;
|
||||
@@ -305,29 +264,23 @@ class AppRouter {
|
||||
builder: (context, state) {
|
||||
final slug = state.uri.queryParameters['slug'];
|
||||
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>;
|
||||
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(
|
||||
path: RecipeCreateScreen.path,
|
||||
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(
|
||||
path: '/subs-screen/:slug',
|
||||
name: 'subsScreen',
|
||||
@@ -339,11 +292,7 @@ class AppRouter {
|
||||
builder: (context, state) {
|
||||
final slug = state.uri.queryParameters['slug'];
|
||||
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>;
|
||||
return AboutDietScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
|
||||
@@ -355,30 +304,26 @@ class AppRouter {
|
||||
builder: (context, state) => BlogPostCreateScreen(feedBlogPostModel: state.extra != null ? state.extra as FeedBlogPostModel : null),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/blog-post-screen',
|
||||
name: 'blogPostScreen',
|
||||
builder: (context, state) {
|
||||
final id = state.uri.queryParameters['id'];
|
||||
final parsedId = int.tryParse(id ?? '');
|
||||
if (parsedId != null) {
|
||||
return BlogPostScreen(
|
||||
id: parsedId,
|
||||
);
|
||||
}
|
||||
path: '/blog-post-screen',
|
||||
name: 'blogPostScreen',
|
||||
builder: (context, state) {
|
||||
final id = state.uri.queryParameters['id'];
|
||||
final parsedId = int.tryParse(id ?? '');
|
||||
if (parsedId != null) {
|
||||
return BlogPostScreen(id: parsedId);
|
||||
}
|
||||
|
||||
return BlogPostScreen(id: state.extra as int);
|
||||
}),
|
||||
return BlogPostScreen(id: state.extra as int);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/chat-screen',
|
||||
name: 'chatScreen',
|
||||
builder: (context, state) {
|
||||
final data = state.extra as Map<String, dynamic>;
|
||||
final chatId = data['chatId'] as String;
|
||||
final uuid = data['uuid'] as String;
|
||||
final sendInitialMessage = data['sendInitialMessage'] as bool?;
|
||||
return ChatScreen(
|
||||
chatId: chatId,
|
||||
sendInitialMessage: sendInitialMessage,
|
||||
);
|
||||
return ChatScreen(uuid: uuid, sendInitialMessage: sendInitialMessage);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
@@ -411,7 +356,11 @@ class AppRouter {
|
||||
name: 'physicalFormScreen',
|
||||
builder: (context, state) => PhysicalFormScreen(settingsEntity: state.extra as SettingsEntity),
|
||||
),
|
||||
GoRoute(path: 'pressure-screen', name: 'pressureScreen', builder: (context, state) => PressureScreen(settingsEntity: state.extra as SettingsEntity)),
|
||||
GoRoute(
|
||||
path: 'pressure-screen',
|
||||
name: 'pressureScreen',
|
||||
builder: (context, state) => PressureScreen(settingsEntity: state.extra as SettingsEntity),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'change-password-screen',
|
||||
name: 'changePasswordScreen',
|
||||
@@ -428,9 +377,7 @@ class AppRouter {
|
||||
GoRoute(
|
||||
path: '/stepper-become-consultant-screen',
|
||||
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: '/support-screen', name: 'supportScreen', builder: (context, state) => const SupportScreen()),
|
||||
@@ -458,7 +405,11 @@ class AppRouter {
|
||||
GoRoute(path: CreatePlanScreen.path, name: CreatePlanScreen.name, builder: (context, state) => const CreatePlanScreen()),
|
||||
GoRoute(path: MyDietsScreen.path, name: MyDietsScreen.name, builder: (context, state) => const MyDietsScreen()),
|
||||
GoRoute(path: MyPlansScreen.path, name: MyPlansScreen.name, builder: (context, state) => const MyPlansScreen()),
|
||||
GoRoute(path: UserDetailsScreen.path, name: UserDetailsScreen.name, builder: (context, state) => UserDetailsScreen(slug: state.extra as String)),
|
||||
GoRoute(
|
||||
path: UserDetailsScreen.path,
|
||||
name: UserDetailsScreen.name,
|
||||
builder: (context, state) => UserDetailsScreen(slug: state.extra as String),
|
||||
),
|
||||
GoRoute(path: DevToolsScreen.path, name: DevToolsScreen.name, builder: (context, state) => const DevToolsScreen()),
|
||||
GoRoute(path: ConsultantRatingScreen.path, name: ConsultantRatingScreen.name, builder: (context, state) => const ConsultantRatingScreen()),
|
||||
GoRoute(
|
||||
@@ -468,10 +419,7 @@ class AppRouter {
|
||||
final data = state.extra as Map<String, dynamic>;
|
||||
final planId = data['planId'] as int;
|
||||
final successfulPayment = data['successfulPayment'] as bool?;
|
||||
return InfoByOrderScreen(
|
||||
planId: planId,
|
||||
successfulPayment: successfulPayment,
|
||||
);
|
||||
return InfoByOrderScreen(planId: planId, successfulPayment: successfulPayment);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
@@ -481,10 +429,7 @@ class AppRouter {
|
||||
final data = state.extra as Map<String, dynamic>;
|
||||
final planId = data['planId'] as int;
|
||||
final url = data['url'] as String;
|
||||
return PaymentScreen(
|
||||
url: url,
|
||||
planId: planId,
|
||||
);
|
||||
return PaymentScreen(url: url, planId: planId);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
@@ -494,10 +439,7 @@ class AppRouter {
|
||||
final data = state.extra as Map<String, int>;
|
||||
final diaryId = data['diaryId'] as int;
|
||||
final currentPlanDay = data['currentDiaryDay'] as int;
|
||||
return UserDiaryScreen(
|
||||
diaryId: diaryId,
|
||||
currentPlanDay: currentPlanDay,
|
||||
);
|
||||
return UserDiaryScreen(diaryId: diaryId, currentPlanDay: currentPlanDay);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
@@ -505,16 +447,9 @@ class AppRouter {
|
||||
name: 'verifyScreen',
|
||||
builder: (context, state) => VerifyScreen(passKey: state.extra as String),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/verify-email-screen',
|
||||
name: 'verifyEmailScreen',
|
||||
builder: (context, state) => const VerifyEmailScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: MyRecipesScreen.path,
|
||||
name: MyRecipesScreen.name,
|
||||
builder: (context, state) => const MyRecipesScreen(),
|
||||
),
|
||||
GoRoute(path: '/verify-email-screen', name: 'verifyEmailScreen', builder: (context, state) => const VerifyEmailScreen()),
|
||||
GoRoute(path: MyRecipesScreen.path, name: MyRecipesScreen.name, builder: (context, state) => const MyRecipesScreen()),
|
||||
GoRoute(path: '/notifications-settings-screen', name: 'notificationsSettingsScreen', builder: (context, state) => const NotificationsSettingsScreen()),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_state.dart';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:eatvio_app/core/comments/comments_cubit/comments_state.dart';
|
||||
import 'package:eatvio_app/core/comments/repositories/comments_repository.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart';
|
||||
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class CommentsCubit extends Cubit<CommentsState> {
|
||||
final FeedRepository feedRepository;
|
||||
final CommentsRepository commentsRepository;
|
||||
|
||||
CommentsCubit({
|
||||
required this.feedRepository,
|
||||
required this.commentsRepository,
|
||||
}) : super(const CommentsState());
|
||||
|
||||
void setReplyId(int? replyId) {
|
||||
emit((state as CommentsStateLoaded).copyWith(replyId: replyId));
|
||||
}
|
||||
|
||||
Future<void> getComments(String id, CommentSourceType commentSourceType, [bool isFirstFetch = false]) async {
|
||||
Future<void> getComments(String id, CommentSourceType commentSourceType, [bool isFirstFetch = false]) async {
|
||||
if (isFirstFetch) emit(const CommentsState.loading());
|
||||
try {
|
||||
switch (commentSourceType) {
|
||||
@@ -27,7 +32,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
case CommentSourceType.recipe:
|
||||
emit(CommentsState.loaded(comment: await feedRepository.getRecipesComments(id: id)));
|
||||
break;
|
||||
case CommentSourceType.plan:
|
||||
case CommentSourceType.plan:
|
||||
emit(CommentsState.loaded(comment: await feedRepository.getPlansComments(slug: id)));
|
||||
break;
|
||||
}
|
||||
@@ -78,4 +83,34 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
emit(CommentsState.error(errorMessage: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> deleteComment({required int id}) async {
|
||||
if (state is! CommentsStateLoaded) return false;
|
||||
final commentModel = (state as CommentsStateLoaded).comment;
|
||||
final updatedCommentModel = commentModel.copyWith(data: commentModel.data.where((comment) => comment.id != id).toList());
|
||||
try {
|
||||
await commentsRepository.deleteComment(id: id);
|
||||
emit(CommentsState.loaded(comment: updatedCommentModel));
|
||||
return true;
|
||||
} catch (e, st) {
|
||||
emit(CommentsState.loaded(comment: commentModel));
|
||||
log(e.toString(), stackTrace: st);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> updateComment({required int id, required String body}) async {
|
||||
final commentModel = (state as CommentsStateLoaded).comment;
|
||||
final updatedCommentModel =
|
||||
commentModel.copyWith(data: commentModel.data.map((comment) => comment.id == id ? comment.copyWith(body: body) : comment).toList());
|
||||
try {
|
||||
await commentsRepository.updateComment(id: id, body: body);
|
||||
emit(CommentsState.loaded(comment: updatedCommentModel));
|
||||
return true;
|
||||
} catch (e, st) {
|
||||
emit(CommentsState.loaded(comment: commentModel));
|
||||
log(e.toString(), stackTrace: st);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ abstract class CommentsState with _$CommentsState {
|
||||
const factory CommentsState.loaded({
|
||||
required CommentModel comment,
|
||||
int? replyId,
|
||||
int? editedCommentId,
|
||||
}) = CommentsStateLoaded;
|
||||
|
||||
const factory CommentsState.error({
|
||||
12
lib/core/comments/models/comment_update_model.dart
Normal file
12
lib/core/comments/models/comment_update_model.dart
Normal file
@@ -0,0 +1,12 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'comment_update_model.freezed.dart';
|
||||
part 'comment_update_model.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class CommentUpdateModel with _$CommentUpdateModel {
|
||||
|
||||
factory CommentUpdateModel({required String body}) = _CommentUpdateModel;
|
||||
|
||||
factory CommentUpdateModel.fromJson(Map<String, dynamic> json) => _$CommentUpdateModelFromJson(json);
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
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).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,
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
24
lib/core/comments/repositories/comments_repository.dart
Normal file
24
lib/core/comments/repositories/comments_repository.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'package:eatvio_app/core/comments/models/comment_update_model.dart';
|
||||
import 'package:eatvio_app/core/comments/services/comments_service.dart';
|
||||
|
||||
class CommentsRepository {
|
||||
final CommentsService service;
|
||||
|
||||
CommentsRepository({required this.service});
|
||||
|
||||
Future<void> deleteComment({required int id}) async {
|
||||
try {
|
||||
return service.deleteComment(id);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateComment({required int id, required String body}) async {
|
||||
try {
|
||||
return service.updateComment(id, CommentUpdateModel(body: body));
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
16
lib/core/comments/services/comments_service.dart
Normal file
16
lib/core/comments/services/comments_service.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/comments/models/comment_update_model.dart';
|
||||
import 'package:retrofit/retrofit.dart';
|
||||
|
||||
part 'comments_service.g.dart';
|
||||
|
||||
@RestApi()
|
||||
abstract class CommentsService {
|
||||
factory CommentsService(Dio dio, {String baseUrl}) = _CommentsService;
|
||||
|
||||
@DELETE('/api/comments/{id}')
|
||||
Future<void> deleteComment(@Path('id') int id);
|
||||
|
||||
@PATCH('/api/comments/{id}')
|
||||
Future<void> updateComment(@Path('id') int id, @Body() CommentUpdateModel body);
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
String baseUrl = 'https://eatvio.ru';
|
||||
import 'package:eatvio_app/core/env_config.dart';
|
||||
|
||||
String baseUrl = EnvConfig.baseUrl;
|
||||
bool hidePlans = const bool.fromEnvironment('HIDE_PLANS', defaultValue: false);
|
||||
const buildDate = String.fromEnvironment('BUILD_DATE', defaultValue: '');
|
||||
|
||||
const List<int> durationValues = [7, 14, 21];
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
abstract class CredentialsStorage {
|
||||
static Future<bool> haveCredentialsInStorage() async {
|
||||
try {
|
||||
if (await const FlutterSecureStorage().read(key: 'email') == null || await const FlutterSecureStorage().read(key: 'password') == null) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Map<String, String>> getCredentialsFromStorage() async {
|
||||
return {
|
||||
'email': (await const FlutterSecureStorage().read(key: 'email'))!,
|
||||
'password': (await const FlutterSecureStorage().read(key: 'password'))!,
|
||||
};
|
||||
}
|
||||
|
||||
static Future<void> saveCredentials(String email, String password) async {
|
||||
await const FlutterSecureStorage().write(key: 'email', value: email);
|
||||
await const FlutterSecureStorage().write(key: 'password', value: password);
|
||||
}
|
||||
|
||||
static Future<void> removeCredentials() async {
|
||||
await const FlutterSecureStorage().delete(key: 'email');
|
||||
await const FlutterSecureStorage().delete(key: 'password');
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1 @@
|
||||
enum ComplainedContent {
|
||||
diet,
|
||||
plan,
|
||||
recipe,
|
||||
profile,
|
||||
posts
|
||||
}
|
||||
enum ComplainedContent { diet, plan, recipe, profile, posts, comment }
|
||||
|
||||
46
lib/core/env_config.dart
Normal file
46
lib/core/env_config.dart
Normal file
@@ -0,0 +1,46 @@
|
||||
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'] ?? '';
|
||||
}
|
||||
@@ -1,3 +1,10 @@
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/token_storage.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
|
||||
class FirebaseApi {
|
||||
@@ -7,4 +14,22 @@ class FirebaseApi {
|
||||
await _firebaseMessaging.requestPermission();
|
||||
return await _firebaseMessaging.getToken();
|
||||
}
|
||||
|
||||
Future<void> registerPushToken() async {
|
||||
if (await TokenStorage.haveTokenInStorage()) {
|
||||
try {
|
||||
final deviceInfo = DeviceInfoPlugin();
|
||||
await sl<Dio>().post(
|
||||
'/api/profile/notification/token/register',
|
||||
data: {
|
||||
'platform': Platform.isIOS ? 'ios' : 'android',
|
||||
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
|
||||
'token': await getToken(),
|
||||
},
|
||||
);
|
||||
} catch (e, st) {
|
||||
log(e.toString(), stackTrace: st);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/comments/repositories/comments_repository.dart';
|
||||
import 'package:eatvio_app/core/comments/services/comments_service.dart';
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/dio/phone_verify_error_interceptor.dart';
|
||||
import 'package:eatvio_app/core/firebase_api.dart';
|
||||
@@ -18,6 +20,9 @@ import 'package:eatvio_app/features/cancel_plan_bottom_sheet/service/cancel_plan
|
||||
import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_cubit.dart';
|
||||
import 'package:eatvio_app/features/complaining/repositories/complaining_repository.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/repositories/consultations_repository.dart';
|
||||
import 'package:eatvio_app/features/consultations/services/consultation_service.dart';
|
||||
@@ -38,8 +43,11 @@ import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_cubit.dart';
|
||||
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart';
|
||||
import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart';
|
||||
import 'package:eatvio_app/features/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/notification_settings_service.dart';
|
||||
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
||||
import 'package:eatvio_app/features/calendar/presentation/bloc/calendar_cubit.dart';
|
||||
@@ -56,7 +64,7 @@ import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
|
||||
import 'package:eatvio_app/features/diary/repositories/diary_repository.dart';
|
||||
import 'package:eatvio_app/features/diary/services/diary_service.dart';
|
||||
import 'package:eatvio_app/features/diets/presentation/cubit/about_diet_cubit.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_cubit.dart';
|
||||
import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/consultants_cubit/consultants_cubit.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
|
||||
@@ -105,7 +113,6 @@ import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/se
|
||||
import 'package:eatvio_app/features/settings/repositories/service_repository.dart';
|
||||
import 'package:eatvio_app/features/settings/services/settings_service.dart';
|
||||
import 'package:eatvio_app/features/user_details/presentation/cubit/user_details_cubit.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart' as profile_plans;
|
||||
|
||||
@@ -128,18 +135,20 @@ Future<void> init() async {
|
||||
sl.registerFactory<ProfileCubit>(() => ProfileCubit(profileRepository: sl(), chatRepository: sl(), consultantRepository: sl()));
|
||||
sl.registerFactory<profile_plans.PlansCubit>(() => profile_plans.PlansCubit(plansRepository: sl()));
|
||||
sl.registerFactory<DietsCubit>(() => DietsCubit(feedRepository: sl(), dietsRepository: sl()));
|
||||
sl.registerFactory<CommentsCubit>(() => CommentsCubit(feedRepository: sl()));
|
||||
sl.registerFactory<CommentsCubit>(() => CommentsCubit(feedRepository: sl(), commentsRepository: sl()));
|
||||
sl.registerFactory<PlansCubit>(() => PlansCubit(feedRepository: sl(), planRepository: sl()));
|
||||
sl.registerFactory<SettingsCubit>(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl(), consultantRepository: sl()));
|
||||
sl.registerFactory<RegisterConsultantCubit>(() => RegisterConsultantCubit(consultantRepository: sl()));
|
||||
sl.registerFactory<DiaryCubit>(() => DiaryCubit(
|
||||
chatRepository: sl(),
|
||||
diaryRepository: sl(),
|
||||
glossaryRepository: sl(),
|
||||
profileRepository: sl(),
|
||||
dietRepository: sl(),
|
||||
settingsRepository: sl(),
|
||||
));
|
||||
sl.registerFactory<DiaryCubit>(
|
||||
() => DiaryCubit(
|
||||
chatRepository: sl(),
|
||||
diaryRepository: sl(),
|
||||
glossaryRepository: sl(),
|
||||
profileRepository: sl(),
|
||||
dietRepository: sl(),
|
||||
settingsRepository: sl(),
|
||||
),
|
||||
);
|
||||
sl.registerFactory<AboutDietCubit>(() => AboutDietCubit(dietsRepository: sl(), profileRepository: sl()));
|
||||
sl.registerFactory<BlogPostCreateCubit>(() => BlogPostCreateCubit(blogPostRepository: sl()));
|
||||
sl.registerFactory<SubCubit>(() => SubCubit(profileRepository: sl()));
|
||||
@@ -168,6 +177,8 @@ Future<void> init() async {
|
||||
sl.registerFactory<ResetPasswordCubit>(() => ResetPasswordCubit(resetPasswordRepository: sl()));
|
||||
sl.registerFactory<BlogPostCubit>(() => BlogPostCubit(sl()));
|
||||
sl.registerFactory<BlockedUsersCubit>(() => BlockedUsersCubit(blockedUsersRepository: sl()));
|
||||
sl.registerFactory<NotificationSettingsCubit>(() => NotificationSettingsCubit(notificationSettingsRepository: sl()));
|
||||
sl.registerFactory<ConsultantRatingCubit>(() => ConsultantRatingCubit(sl()));
|
||||
|
||||
// Repository
|
||||
sl.registerLazySingleton<ChatRepository>(() => ChatRepository(chatService: sl()));
|
||||
@@ -198,6 +209,9 @@ Future<void> init() async {
|
||||
sl.registerFactory<MyRecipesRepository>(() => MyRecipesRepository(myRecipesService: sl()));
|
||||
sl.registerFactory<ResetPasswordRepository>(() => ResetPasswordRepository(resetPasswordService: sl()));
|
||||
sl.registerLazySingleton<BlockedUsersRepository>(() => BlockedUsersRepository(blockedUserService: sl()));
|
||||
sl.registerLazySingleton<CommentsRepository>(() => CommentsRepository(service: sl()));
|
||||
sl.registerLazySingleton<NotificationSettingsRepository>(() => NotificationSettingsRepository(sl()));
|
||||
sl.registerFactory<ConsultantRatingRepository>(() => ConsultantRatingRepository(sl()));
|
||||
|
||||
// Services
|
||||
sl.registerLazySingleton<ChatService>(() => ChatService(sl()));
|
||||
@@ -226,28 +240,18 @@ Future<void> init() async {
|
||||
sl.registerLazySingleton<CancelPlanService>(() => CancelPlanService(sl()));
|
||||
sl.registerLazySingleton<CreatePlanService>(() => CreatePlanService(sl()));
|
||||
sl.registerLazySingleton<NotificationsService>(() => NotificationsService(sl()));
|
||||
sl.registerLazySingleton<MyRecipesService>(() => MyRecipesService(sl()));
|
||||
sl.registerLazySingleton<FirebaseApi>(() => FirebaseApi());
|
||||
sl.registerLazySingleton<ResetPasswordService>(() => ResetPasswordService(sl()));
|
||||
sl.registerLazySingleton<BlockedUserService>(() => BlockedUserService(sl()));
|
||||
sl.registerLazySingleton<CommentsService>(() => CommentsService(sl()));
|
||||
sl.registerLazySingleton<MyRecipesService>(() => MyRecipesService(sl()));
|
||||
sl.registerLazySingleton<ConsultantRatingService>(() => ConsultantRatingService(sl()));
|
||||
sl.registerLazySingleton<NotificationSettingsService>(() => NotificationSettingsService(sl()));
|
||||
|
||||
// External
|
||||
sl.registerLazySingleton(
|
||||
() => Dio(
|
||||
BaseOptions(
|
||||
baseUrl: baseUrl,
|
||||
headers: {
|
||||
'accept': 'application/json',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
validateStatus: (status) => status != null && status < 500,
|
||||
),
|
||||
)..interceptors.addAll(
|
||||
[
|
||||
TokenInterceptor(),
|
||||
PhoneVerifyErrorInterceptor(),
|
||||
],
|
||||
),
|
||||
() =>
|
||||
Dio(BaseOptions(baseUrl: baseUrl, headers: {'accept': 'application/json', 'content-type': 'application/json'}))
|
||||
..interceptors.addAll([TokenInterceptor(), PhoneVerifyErrorInterceptor()]),
|
||||
);
|
||||
sl.registerLazySingleton(() async => const FlutterSecureStorage());
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
@@ -11,6 +13,10 @@ class PermissionHandler {
|
||||
required Future<void> Function() onGranted,
|
||||
required BuildContext context,
|
||||
}) async {
|
||||
if (Platform.isAndroid) {
|
||||
await onGranted();
|
||||
return;
|
||||
}
|
||||
final permissionState = await permission.request();
|
||||
if (permissionState.isGranted) {
|
||||
await onGranted();
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
abstract class TokenStorage {
|
||||
static Future<bool> haveTokenInStorage() async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
try {
|
||||
if (await const FlutterSecureStorage().read(key: 'token') == null) {
|
||||
if (sharedPreferences.getString('token') == null) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
@@ -13,11 +14,48 @@ abstract class TokenStorage {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<String?> getTokenFromStorage() async => await const FlutterSecureStorage().read(key: 'token');
|
||||
|
||||
static Future<void> saveTokenInStorage(String token) async {
|
||||
await const FlutterSecureStorage().write(key: 'token', value: token);
|
||||
static Future<String?> getTokenFromStorage() async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
return sharedPreferences.getString('token');
|
||||
}
|
||||
|
||||
static Future<void> removeTokenInStorage() async => (await const FlutterSecureStorage().delete(key: 'token'));
|
||||
static Future<void> saveTokenInStorage(String token) async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
await sharedPreferences.setString('token', token);
|
||||
}
|
||||
|
||||
static Future<void> removeTokenInStorage() async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
await sharedPreferences.remove('token');
|
||||
}
|
||||
}
|
||||
|
||||
abstract class SocketIdStorage {
|
||||
static Future<bool> haveSocketIdInStorage() async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
try {
|
||||
if (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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,30 @@ import 'package:intl/intl.dart';
|
||||
|
||||
const String dateTimeFormatPattern = 'dd/MM/yyyy';
|
||||
|
||||
/// Преобразует дату из формата гггг-мм-дд чч:мм:сс в дд.мм.гггг
|
||||
String formatBuildDate(String dateString) {
|
||||
if (dateString.isEmpty) return '';
|
||||
|
||||
try {
|
||||
final dateTimeParts = dateString.split(' ');
|
||||
if (dateTimeParts.isNotEmpty) {
|
||||
final datePart = dateTimeParts[0];
|
||||
final dateParts = datePart.split('-');
|
||||
if (dateParts.length == 3) {
|
||||
final year = dateParts[0];
|
||||
final month = dateParts[1];
|
||||
final day = dateParts[2];
|
||||
return '$day.$month.$year';
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
return dateString;
|
||||
}
|
||||
return dateString;
|
||||
}
|
||||
|
||||
extension DateTimeExtension on DateTime {
|
||||
String format([
|
||||
String pattern = dateTimeFormatPattern,
|
||||
String? locale,
|
||||
]) {
|
||||
String format([String pattern = dateTimeFormatPattern, String? locale]) {
|
||||
final localDate = isUtc ? toLocal() : this;
|
||||
if (locale != null && locale.isNotEmpty) {
|
||||
initializeDateFormatting(locale);
|
||||
@@ -19,17 +38,13 @@ extension DateTimeExtension on DateTime {
|
||||
final localDate = isUtc ? toLocal() : this;
|
||||
final now = DateTime.now();
|
||||
final yesterday = DateTime(now.year, now.month, now.day).subtract(const Duration(days: 1));
|
||||
return localDate.year == yesterday.year &&
|
||||
localDate.month == yesterday.month &&
|
||||
localDate.day == yesterday.day;
|
||||
return localDate.year == yesterday.year && localDate.month == yesterday.month && localDate.day == yesterday.day;
|
||||
}
|
||||
|
||||
bool isToday() {
|
||||
final localDate = isUtc ? toLocal() : this;
|
||||
final now = DateTime.now();
|
||||
return now.year == localDate.year &&
|
||||
now.month == localDate.month &&
|
||||
now.day == localDate.day;
|
||||
return now.year == localDate.year && now.month == localDate.month && now.day == localDate.day;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:dart_pusher_channels/dart_pusher_channels.dart';
|
||||
@@ -59,6 +60,24 @@ class WsManager {
|
||||
),
|
||||
);
|
||||
|
||||
String? extractSocketId(String? data) {
|
||||
if (data == null) return null;
|
||||
try {
|
||||
final decoded = jsonDecode(data);
|
||||
return decoded['socket_id'] as String?;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
client?.eventStream.listen((event) async {
|
||||
if (event.name == 'pusher:connection_established') {
|
||||
final data = event.data;
|
||||
final socketId = extractSocketId(data);
|
||||
await SocketIdStorage.saveSocketIdInStorage(socketId!);
|
||||
}
|
||||
});
|
||||
|
||||
await client?.connect();
|
||||
|
||||
client = client;
|
||||
@@ -77,9 +96,8 @@ class WsManager {
|
||||
EasyDebounce.debounce(
|
||||
'searchChatRoom',
|
||||
const Duration(milliseconds: 300),
|
||||
() => sl<ChatsCubit>().getChatRooms(),
|
||||
() => sl<ChatsCubit>().getChatRooms(),
|
||||
);
|
||||
log(event.rootObject.toString());
|
||||
});
|
||||
_subscribe();
|
||||
} catch (e, st) {
|
||||
|
||||
33
lib/core/widgets/bottom_sheet_action_widget.dart
Normal file
33
lib/core/widgets/bottom_sheet_action_widget.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
class BottomSheetActionWidget extends StatelessWidget {
|
||||
const BottomSheetActionWidget({super.key, required this.text, required this.iconPath, required this.onTap, this.needToPop = true, this.color});
|
||||
final String text;
|
||||
final String iconPath;
|
||||
final void Function() onTap;
|
||||
final bool needToPop;
|
||||
final Color? color;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (needToPop) Navigator.pop(context);
|
||||
onTap();
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(text, style: TextStyles.textLMedium.copyWith(color: appTheme.black)),
|
||||
),
|
||||
SvgPicture.asset(iconPath, colorFilter: color != null ? ColorFilter.mode(color!, BlendMode.srcIn) : null),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,8 @@ import 'package:eatvio_app/core/data/selectionPopupModel/selection_popup_model.d
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CustomDropDown extends StatelessWidget {
|
||||
const CustomDropDown({super.key,
|
||||
const CustomDropDown({
|
||||
super.key,
|
||||
this.alignment,
|
||||
this.width,
|
||||
this.focusNode,
|
||||
@@ -48,12 +49,7 @@ class CustomDropDown extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return alignment != null
|
||||
? Align(
|
||||
alignment: alignment ?? Alignment.center,
|
||||
child: _buildDropDownWidget(context),
|
||||
)
|
||||
: _buildDropDownWidget(context);
|
||||
return alignment != null ? Align(alignment: alignment ?? Alignment.center, child: _buildDropDownWidget(context)) : _buildDropDownWidget(context);
|
||||
}
|
||||
|
||||
DropdownButtonFormField<SelectionPopupModel> _buildDropDownWidget(BuildContext context) {
|
||||
@@ -61,7 +57,7 @@ class CustomDropDown extends StatelessWidget {
|
||||
focusNode: focusNode ?? FocusNode(),
|
||||
icon: const Icon(Icons.keyboard_arrow_down_rounded),
|
||||
autofocus: autofocus!,
|
||||
value: value,
|
||||
initialValue: value,
|
||||
isDense: true,
|
||||
isExpanded: true,
|
||||
items: items?.map((SelectionPopupModel item) {
|
||||
@@ -69,12 +65,7 @@ class CustomDropDown extends StatelessWidget {
|
||||
value: item,
|
||||
child: Text(
|
||||
item.title,
|
||||
style: hintStyle ??
|
||||
const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.black,
|
||||
),
|
||||
style: hintStyle ?? const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
@@ -90,19 +81,9 @@ class CustomDropDown extends StatelessWidget {
|
||||
|
||||
InputDecoration _buildInputDecoration(BuildContext context) {
|
||||
return InputDecoration(
|
||||
labelStyle: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
labelStyle: const TextStyle(fontSize: 16, color: Colors.black, fontWeight: FontWeight.w400),
|
||||
hintText: hintText ?? "",
|
||||
hintStyle: hintStyle ??
|
||||
TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.grey.shade400,
|
||||
height: 1,
|
||||
),
|
||||
hintStyle: hintStyle ?? TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.grey.shade400, height: 1),
|
||||
prefixIcon: prefix,
|
||||
prefixIconConstraints: prefixConstraints,
|
||||
suffixIcon: suffix,
|
||||
@@ -111,18 +92,9 @@ class CustomDropDown extends StatelessWidget {
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
|
||||
fillColor: const Color(0xFFF6F6F6),
|
||||
filled: filled,
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(20)),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
enabledBorder: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(20)),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(20)),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
border: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(20)), borderSide: BorderSide.none),
|
||||
enabledBorder: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(20)), borderSide: BorderSide.none),
|
||||
focusedBorder: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(20)), borderSide: BorderSide.none),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
class CustomDropDownButton<T> extends StatelessWidget {
|
||||
const CustomDropDownButton({
|
||||
super.key,
|
||||
required this.items,
|
||||
required this.onChanged,
|
||||
this.value,
|
||||
this.hint,
|
||||
this.hasError = false,
|
||||
});
|
||||
const CustomDropDownButton({super.key, required this.items, required this.onChanged, this.value, this.hint, this.hasError = false});
|
||||
final T? value;
|
||||
final List<DropdownMenuItem<T>> items;
|
||||
final void Function(T?)? onChanged;
|
||||
@@ -20,11 +13,7 @@ class CustomDropDownButton<T> extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return DropdownButtonFormField(
|
||||
hint: hint,
|
||||
icon: SvgPicture.asset(
|
||||
'assets/icons/arrow_down2.svg',
|
||||
width: 12,
|
||||
height: 8,
|
||||
),
|
||||
icon: SvgPicture.asset('assets/icons/arrow_down2.svg', width: 12, height: 8),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
dropdownColor: appTheme.lightGray0,
|
||||
decoration: InputDecoration(
|
||||
@@ -43,7 +32,7 @@ class CustomDropDownButton<T> extends StatelessWidget {
|
||||
fillColor: appTheme.lightGray0,
|
||||
filled: true,
|
||||
),
|
||||
value: value,
|
||||
initialValue: value,
|
||||
items: items,
|
||||
onChanged: onChanged,
|
||||
);
|
||||
|
||||
@@ -26,9 +26,13 @@ class SuccessPublishedRecipeBottomSheet extends StatelessWidget {
|
||||
s.recipe_publishing,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
Text(
|
||||
'"$title" ${s.successfully_published}',
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Text(
|
||||
'"$title" ${s.successfully_published}',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/firebase_api.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/utils/image_constant.dart';
|
||||
@@ -46,12 +47,20 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
super.initState();
|
||||
var keyboardVisibilityController = KeyboardVisibilityController();
|
||||
keyboardSubscription = keyboardVisibilityController.onChange.listen((bool visible) {
|
||||
setState(() {
|
||||
isFocused = visible;
|
||||
});
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isFocused = visible;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
keyboardSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _reconnectWebSocket(BuildContext context) async {
|
||||
try {
|
||||
await sl<WsManager>().clear();
|
||||
@@ -66,19 +75,20 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
await sl<ChatsCubit>().connectToWebSocket();
|
||||
await sl<NotificationsCubit>().getNotifications();
|
||||
await sl<NotificationsCubit>().connectToWebSocket();
|
||||
await sl<FirebaseApi>().registerPushToken();
|
||||
showSuccessToast('Успешный вход');
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
AppRouter.navigatorKey.currentContext?.go(
|
||||
'/feed-screen',
|
||||
extra: {'needToInitializeWebSocket': false},
|
||||
);
|
||||
AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var shortestSide = MediaQuery.sizeOf(context).shortestSide;
|
||||
final bool useMobileLayout = shortestSide < 600;
|
||||
if (!useMobileLayout) isFocused = true;
|
||||
final s = S.of(context);
|
||||
FlutterNativeSplash.remove();
|
||||
return BlocProvider<AuthCubit>(
|
||||
@@ -97,11 +107,7 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
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(
|
||||
@@ -132,23 +138,16 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
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),
|
||||
Center(
|
||||
child: GestureDetector(
|
||||
onTap: () => GoRouter.of(context).goNamed('forgetPasswordScreen'),
|
||||
onTap: () => GoRouter.of(context).goNamed('forgetPasswordScreen', extra: false),
|
||||
child: Text(
|
||||
'Забыли пароль?',
|
||||
style: TextStyle(
|
||||
color: appTheme.blueA400,
|
||||
decorationColor: appTheme.blueA400,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
style: TextStyle(color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -161,11 +160,7 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
onTap: () => GoRouter.of(context).goNamed('registrationScreen'),
|
||||
child: Text(
|
||||
'Зарегистрироваться',
|
||||
style: TextStyle(
|
||||
color: appTheme.blueA400,
|
||||
decorationColor: appTheme.blueA400,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
style: TextStyle(color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -176,23 +171,22 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
text: 'Нажимая "Войти" или "Зарегистрироваться", Вы соглашаетесь \n',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.black,
|
||||
),
|
||||
style: const TextStyle(fontSize: 12, color: Colors.black),
|
||||
children: [
|
||||
const TextSpan(text: 'c '),
|
||||
TextSpan(
|
||||
text: ' Публичной офертой',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: appTheme.blueA400,
|
||||
decorationColor: appTheme.blueA400,
|
||||
decoration: TextDecoration.underline,
|
||||
if (!hidePlans) ...[
|
||||
TextSpan(
|
||||
text: ' Публичной офертой',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: appTheme.blueA400,
|
||||
decorationColor: appTheme.blueA400,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')),
|
||||
),
|
||||
recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')),
|
||||
),
|
||||
const TextSpan(text: ' и '),
|
||||
const TextSpan(text: ' и '),
|
||||
],
|
||||
TextSpan(
|
||||
text: 'Политикой конфиденциальности\nи защитой персональных данных',
|
||||
style: TextStyle(
|
||||
@@ -218,17 +212,12 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
highlightElevation: 0,
|
||||
elevation: 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,
|
||||
onPressed: state is AuthStateLoading
|
||||
? null
|
||||
: () async {
|
||||
final isAuthenticated = await authCubit.getTokenByCredentials(
|
||||
emailController.text,
|
||||
passwordController.text,
|
||||
);
|
||||
final isAuthenticated = await authCubit.getTokenByCredentials(emailController.text, passwordController.text);
|
||||
if (isAuthenticated) {
|
||||
if (context.mounted) {
|
||||
_reconnectWebSocket(context);
|
||||
@@ -237,10 +226,7 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
},
|
||||
label: state is AuthStateLoading
|
||||
? const CupertinoActivityIndicator()
|
||||
: Text(
|
||||
'Войти',
|
||||
style: theme.textTheme.titleSmall!.copyWith(color: Colors.white),
|
||||
),
|
||||
: Text('Войти', style: theme.textTheme.titleSmall!.copyWith(color: Colors.white)),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
@@ -252,21 +238,71 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
highlightElevation: 0,
|
||||
elevation: 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,
|
||||
onPressed: state is AuthStateLoading
|
||||
? null
|
||||
: () {
|
||||
GoRouter.of(context).pushNamed('scanQRCodeScreen');
|
||||
: () async {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return Dialog(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('Где взять QR-код?\n', style: TextStyle(fontSize: 18)),
|
||||
Text(
|
||||
'1. Войдите на сайте $baseUrl в свой аккаунт\n\n2. Перейдите в настройки профиля\n\n3. В разделе "Доп информация" перейдите на "QR Login"',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: () {
|
||||
AppRouter.navigatorKey.currentContext?.pop();
|
||||
},
|
||||
child: Ink(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
|
||||
child: const Text(
|
||||
'Отмена',
|
||||
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w400),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: () {
|
||||
AppRouter.navigatorKey.currentContext?.pop();
|
||||
AppRouter.navigatorKey.currentContext?.pushNamed('scanQRCodeScreen');
|
||||
},
|
||||
child: Ink(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
|
||||
child: Text(
|
||||
'Перейти',
|
||||
style: TextStyle(color: appTheme.blueA400, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
label: state is AuthStateLoading
|
||||
? const CupertinoActivityIndicator()
|
||||
: const Icon(
|
||||
Icons.qr_code_scanner_rounded,
|
||||
color: Colors.white,
|
||||
),
|
||||
: const Icon(Icons.qr_code_scanner_rounded, color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -9,7 +9,8 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class ForgetPasswordScreen extends StatefulWidget {
|
||||
const ForgetPasswordScreen({super.key});
|
||||
final bool isAuth;
|
||||
const ForgetPasswordScreen({super.key, required this.isAuth});
|
||||
|
||||
@override
|
||||
State<ForgetPasswordScreen> createState() => _ForgetPasswordScreenState();
|
||||
@@ -29,7 +30,7 @@ class _ForgetPasswordScreenState extends State<ForgetPasswordScreen> {
|
||||
builder: (BuildContext context, ResetPasswordState state) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Смена пароля'),
|
||||
title: widget.isAuth ? const Text('Смена пароля') : const Text('Восстановление пароля'),
|
||||
centerTitle: false,
|
||||
),
|
||||
body: state is ResetPasswordStateLoadingResetPassword || state is ResetPasswordStateLoadingSendEmailCode
|
||||
@@ -42,9 +43,9 @@ class _ForgetPasswordScreenState extends State<ForgetPasswordScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (state is ResetPasswordStateCodeSendError || state is ResetPasswordStateEmpty) ...[
|
||||
const Text(
|
||||
'Забыли пароль? Укажите свой email,\nи мы пришлём код для сброса.',
|
||||
style: TextStyle(
|
||||
Text(
|
||||
'${widget.isAuth ? '' : 'Забыли пароль? '}Укажите свой email,\nи мы пришлём код для сброса.',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
@@ -156,7 +157,7 @@ class _ForgetPasswordScreenState extends State<ForgetPasswordScreen> {
|
||||
}
|
||||
if (state is ResetPasswordStateResetPassowrdSuccess) {
|
||||
GoRouter.of(context).pop();
|
||||
showSuccessToast('Пароль успешно сменён. Войдите в аккаунт');
|
||||
showSuccessToast('Пароль успешно сменён. ${widget.isAuth ? '' : 'Войдите в аккаунт'}');
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:io';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/firebase_api.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/token_storage.dart';
|
||||
import 'package:eatvio_app/core/utils/toasts.dart';
|
||||
@@ -92,6 +93,7 @@ class _ScanQRCodeScreenState extends State<ScanQRCodeScreen> {
|
||||
await sl<ChatsCubit>().connectToWebSocket();
|
||||
await sl<NotificationsCubit>().getNotifications();
|
||||
await sl<NotificationsCubit>().connectToWebSocket();
|
||||
await sl<FirebaseApi>().registerPushToken();
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
|
||||
@@ -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/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/feed/presentation/bloc/comments_cubit/comments_cubit.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_state.dart';
|
||||
import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart';
|
||||
import 'package:eatvio_app/core/comments/comments_cubit/comments_state.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
|
||||
@@ -15,7 +15,11 @@ abstract class ChatMessageModel with _$ChatMessageModel {
|
||||
@JsonKey(name: 'read_by_to_user', defaultValue: false) required bool isRead,
|
||||
@JsonKey(name: 'is_author_message_auth_user') required bool? isCurrentUserAuthor,
|
||||
@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;
|
||||
|
||||
factory ChatMessageModel.fromJson(Map<String, Object?> json) => _$ChatMessageModelFromJson(json);
|
||||
}
|
||||
|
||||
enum MessageState { loading, sent, error }
|
||||
|
||||
@@ -22,13 +22,9 @@ abstract class ChatRoomModel with _$ChatRoomModel {
|
||||
@freezed
|
||||
abstract class Users with _$Users {
|
||||
const factory Users({
|
||||
|
||||
@JsonKey(name: 'from_chat_user') required ChatUserModel sender,
|
||||
@JsonKey(name: 'to_chat_user') required ChatUserModel receiver,
|
||||
|
||||
}) = _Users;
|
||||
|
||||
factory Users.fromJson(Map<String, Object?> json) => _$UsersFromJson(json);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:eatvio_app/core/models/pagination_model.dart';
|
||||
import 'package:eatvio_app/features/chat/models/chat_message_model.dart';
|
||||
import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
@@ -8,29 +9,18 @@ part 'chat_room_pagination_model.g.dart';
|
||||
@freezed
|
||||
abstract class ChatRoomPaginationModel with _$ChatRoomPaginationModel {
|
||||
const factory ChatRoomPaginationModel({
|
||||
@JsonKey(name: "chatRooms") required ChatRooms chatRooms,
|
||||
@JsonKey(name: "data") required List<ChatRoom> data,
|
||||
@JsonKey(name: "number_of_unread_chat_rooms") required int unreadChatsCount,
|
||||
required Meta meta,
|
||||
}) = _ChatRoomPreviewModel;
|
||||
|
||||
factory ChatRoomPaginationModel.fromJson(Map<String, dynamic> json) => _$ChatRoomPaginationModelFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class ChatRooms with _$ChatRooms {
|
||||
const factory ChatRooms({
|
||||
@JsonKey(name: "data") required List<ChatRoom> data,
|
||||
@JsonKey(name: "last_page") required int lastPage,
|
||||
@JsonKey(name: "current_page") required int currentPage,
|
||||
}) = _ChatRooms;
|
||||
|
||||
factory ChatRooms.fromJson(Map<String, dynamic> json) => _$ChatRoomsFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class ChatRoom with _$ChatRoom {
|
||||
const factory ChatRoom({
|
||||
@JsonKey(name: "id") required String id,
|
||||
@JsonKey(name: "room_name") required String roomName,
|
||||
@JsonKey(name: "last_message", fromJson: parseLastMessage) required ChatMessageModel? lastMessage,
|
||||
@JsonKey(name: "users") required Users users,
|
||||
@JsonKey(name: "unread_messages_count") required int unreadMessagesCount,
|
||||
|
||||
@@ -6,11 +6,8 @@ part 'chat_room_settings_model.g.dart';
|
||||
@freezed
|
||||
abstract class ChatRoomSettingsModel with _$ChatRoomSettingsModel {
|
||||
const factory ChatRoomSettingsModel({
|
||||
required int id,
|
||||
@JsonKey(name: 'room_name') required String roomName,
|
||||
@JsonKey(name: 'room_name') required String name,
|
||||
required String state,
|
||||
@JsonKey(name: 'access_level') required String accessLevel,
|
||||
@JsonKey(name: 'updated_at') required String updatedAt,
|
||||
}) = _ChatRoomSettingsModel;
|
||||
|
||||
factory ChatRoomSettingsModel.fromJson(Map<String, Object?> json) => _$ChatRoomSettingsModelFromJson(json);
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:dart_pusher_channels/dart_pusher_channels.dart';
|
||||
import 'package:easy_debounce/easy_debounce.dart';
|
||||
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
|
||||
@@ -19,6 +20,7 @@ import 'package:eatvio_app/features/my_finance/models/card_model.dart';
|
||||
import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart';
|
||||
import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
final ChatRepository chatRepository;
|
||||
@@ -31,12 +33,9 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
late int lastPage;
|
||||
bool isLoading = false;
|
||||
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() {
|
||||
_timer?.cancel();
|
||||
@@ -47,14 +46,14 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
_timer?.cancel();
|
||||
}
|
||||
|
||||
Future<void> loadNextPage(String chatId) async {
|
||||
Future<void> loadNextPage(String uuid) async {
|
||||
if (currentPage < lastPage && !isLoading) {
|
||||
isLoading = true;
|
||||
|
||||
emit((state as ChatRoomStateLoaded).copyWith(isLoading: isLoading));
|
||||
|
||||
try {
|
||||
final chatMessages = await chatRepository.getChatMessages(chatId, currentPage + 1);
|
||||
final chatMessages = await chatRepository.getChatMessages(uuid, currentPage + 1);
|
||||
|
||||
isLoading = false;
|
||||
emit(
|
||||
@@ -89,17 +88,16 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> initChat(String chatId, {bool sendInitialMessage = false}) async {
|
||||
Future<void> initChat(String uuid, {bool sendInitialMessage = false}) async {
|
||||
try {
|
||||
final chatRoom = await chatRepository.getChatRoom(chatId);
|
||||
final chatRoom = await chatRepository.getChatRoom(uuid);
|
||||
ChosenPlanModel? chosenPlan;
|
||||
if (chatRoom.chosenPlanId != null) {
|
||||
chosenPlan = await chatRepository.getChosenPlan(id: chatRoom.chosenPlanId!);
|
||||
}
|
||||
final chatMessages = await chatRepository.getChatMessages(chatId, currentPage);
|
||||
final chatMessages = await chatRepository.getChatMessages(uuid, currentPage);
|
||||
lastPage = chatMessages.meta.lastPage;
|
||||
|
||||
await _subscribeToChatChannel(chatId: chatId);
|
||||
await _subscribeToChatChannel(uuid: uuid, chatId: chatRoom.id);
|
||||
emit(
|
||||
ChatRoomState.loaded(
|
||||
chatRoom: chatRoom.copyWith(chosenPlan: chosenPlan),
|
||||
@@ -158,43 +156,80 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _subscribeToChatChannel({required String chatId}) async {
|
||||
Future<void> _subscribeToChatChannel({required String uuid, required String chatId}) async {
|
||||
final client = wsManager.client;
|
||||
if (client == null) return;
|
||||
await wsManager.initPrivateDirectChatChannel(chatId);
|
||||
wsManager.subscribeDirectChatChannel();
|
||||
messagesSubscription = wsManager.directChatStream?.listen((event) {
|
||||
AudioPlayer().play((AssetSource('audio/recive_message.wav')));
|
||||
final message = ChatMessageModel.fromJson(jsonDecode(event.data)['message']);
|
||||
final message = ChatMessageModel.fromJson(jsonDecode(event.data));
|
||||
List<ChatMessageModel> messagesList = [message, ...(state as ChatRoomStateLoaded).chatMessages];
|
||||
chatRepository.emitNewMessageToChats(message);
|
||||
emit((state as ChatRoomStateLoaded).copyWith(chatMessages: messagesList, isLoading: isLoading));
|
||||
EasyDebounce.debounce(
|
||||
'searchChatRoom',
|
||||
const Duration(milliseconds: 300),
|
||||
() => sl<ChatsCubit>().getChatRooms(),
|
||||
);
|
||||
EasyDebounce.debounce('searchChatRoom', const Duration(milliseconds: 300), () => sl<ChatsCubit>().getChatRooms());
|
||||
});
|
||||
|
||||
typingSubscription = wsManager.clientTypingStream?.listen((event) {
|
||||
emit((state as ChatRoomStateLoaded).copyWith(isTyping: true, isLoading: isLoading));
|
||||
EasyDebounce.debounce(
|
||||
'client-typing', const Duration(seconds: 3), () => emit((state as ChatRoomStateLoaded).copyWith(isTyping: false, isLoading: isLoading)));
|
||||
'client-typing',
|
||||
const Duration(seconds: 3),
|
||||
() => emit((state as ChatRoomStateLoaded).copyWith(isTyping: false, isLoading: isLoading)),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> sendMessage({
|
||||
String? data,
|
||||
List<File>? files,
|
||||
required String userSlug,
|
||||
}) async {
|
||||
Future<bool> sendMessage({String? data, List<File>? files, required String userSlug}) async {
|
||||
try {
|
||||
await chatRepository.sendMessage(
|
||||
data: data,
|
||||
files: files != null ? await ImageCompressUtility.compressFilesList(List<File>.from(files)) : null,
|
||||
slug: userSlug,
|
||||
);
|
||||
final messageUuid = uuid.v4();
|
||||
final compressedFiles = files != null ? await ImageCompressUtility.compressFilesList(List<File>.from(files)) : null;
|
||||
AudioPlayer().play((AssetSource('audio/send_message.wav')));
|
||||
emit(
|
||||
(state as ChatRoomStateLoaded).copyWith(
|
||||
chatMessages: [
|
||||
ChatMessageModel(
|
||||
id: 1,
|
||||
data: data ?? '',
|
||||
mediaData: compressedFiles,
|
||||
authorId: 1,
|
||||
updatedAt: DateTime.now(),
|
||||
isRead: true,
|
||||
isCurrentUserAuthor: true,
|
||||
roomId: '',
|
||||
messageState: MessageState.loading,
|
||||
uuid: messageUuid,
|
||||
),
|
||||
...(state as ChatRoomStateLoaded).chatMessages,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
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;
|
||||
} catch (e, st) {
|
||||
log(e.toString(), stackTrace: st);
|
||||
@@ -239,7 +274,10 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
try {
|
||||
final planId = (state as ChatRoomStateLoaded).chatRoom.chosenPlan?.id;
|
||||
if (planId == null) return false;
|
||||
await chatRepository.complainAboutConsultation(id: planId, body: PlanComplainModel(description: description));
|
||||
await chatRepository.complainAboutConsultation(
|
||||
id: planId,
|
||||
body: PlanComplainModel(description: description),
|
||||
);
|
||||
return true;
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
@@ -267,4 +305,70 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
typingSubscription?.cancel();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ class ChatsCubit extends Cubit<ChatsState> {
|
||||
required this.chatRepository,
|
||||
required this.wsManager,
|
||||
required this.profileRepository,
|
||||
}) : super(ChatsState()) {
|
||||
}) : super(ChatsState()) {
|
||||
newLastMessageSubscription = chatRepository.newUserMessagesStream.listen((message) {
|
||||
_setNewLastMessage(message);
|
||||
});
|
||||
@@ -29,6 +29,7 @@ class ChatsCubit extends Cubit<ChatsState> {
|
||||
String? _query;
|
||||
int page = 1;
|
||||
late int lastPage;
|
||||
|
||||
Future<void> connectToWebSocket() async {
|
||||
await newIncomingMessagesSubscription?.cancel();
|
||||
newIncomingMessagesSubscription = wsManager.newMessagesStream?.listen((event) {
|
||||
@@ -43,10 +44,10 @@ class ChatsCubit extends Cubit<ChatsState> {
|
||||
try {
|
||||
final chatRoomsPaginationModel = await chatRepository.getChatRooms(query: query);
|
||||
final unreadChats = chatRoomsPaginationModel.unreadChatsCount;
|
||||
lastPage = chatRoomsPaginationModel.chatRooms.lastPage + 1;
|
||||
lastPage = chatRoomsPaginationModel.meta.lastPage + 1;
|
||||
emit(
|
||||
state.copyWith(
|
||||
chatRooms: chatRoomsPaginationModel.chatRooms.data,
|
||||
chatRooms: chatRoomsPaginationModel.data,
|
||||
chatsWithUnreadMessagesCount: unreadChats,
|
||||
state: BaseState.success,
|
||||
),
|
||||
@@ -65,10 +66,10 @@ class ChatsCubit extends Cubit<ChatsState> {
|
||||
try {
|
||||
final chatRoomsPaginationModel = await chatRepository.getChatRooms(query: query);
|
||||
final unreadChats = chatRoomsPaginationModel.unreadChatsCount;
|
||||
lastPage = chatRoomsPaginationModel.chatRooms.lastPage + 1;
|
||||
lastPage = chatRoomsPaginationModel.meta.lastPage + 1;
|
||||
emit(
|
||||
state.copyWith(
|
||||
chatRooms: chatRoomsPaginationModel.chatRooms.data,
|
||||
chatRooms: chatRoomsPaginationModel.data,
|
||||
chatsWithUnreadMessagesCount: unreadChats,
|
||||
state: BaseState.success,
|
||||
),
|
||||
@@ -90,7 +91,7 @@ class ChatsCubit extends Cubit<ChatsState> {
|
||||
final unreadChats = chatRoomsPaginationModel.unreadChatsCount;
|
||||
emit(
|
||||
state.copyWith(
|
||||
chatRooms: [...state.chatRooms, ...chatRoomsPaginationModel.chatRooms.data],
|
||||
chatRooms: [...state.chatRooms, ...chatRoomsPaginationModel.data],
|
||||
chatsWithUnreadMessagesCount: unreadChats,
|
||||
state: BaseState.success,
|
||||
isLoadingMoreChats: false,
|
||||
|
||||
@@ -13,6 +13,8 @@ import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.da
|
||||
import 'package:eatvio_app/features/chat/presentation/widgets/about_order_button.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/widgets/chat_bubble_widget.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/theme_helper.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/widgets/notification_banner.dart';
|
||||
@@ -32,10 +34,10 @@ import 'package:intl/intl.dart';
|
||||
import 'package:visibility_detector/visibility_detector.dart';
|
||||
|
||||
class ChatScreen extends StatefulWidget {
|
||||
final String chatId;
|
||||
final String uuid;
|
||||
final bool? sendInitialMessage;
|
||||
|
||||
const ChatScreen({required this.chatId, super.key, required this.sendInitialMessage});
|
||||
const ChatScreen({required this.uuid, super.key, required this.sendInitialMessage});
|
||||
|
||||
@override
|
||||
State<ChatScreen> createState() => _ChatScreenState();
|
||||
@@ -64,14 +66,14 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
scrollController.addListener(_onScroll);
|
||||
chatRoomCubit.initChat(widget.chatId, sendInitialMessage: widget.sendInitialMessage ?? false);
|
||||
chatRoomCubit.initChat(widget.uuid, sendInitialMessage: widget.sendInitialMessage ?? false);
|
||||
chatRoomCubit.setTimer();
|
||||
_listener = AppLifecycleListener(onHide: _onHide, onResume: _onResume);
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
if (scrollController.offset >= scrollController.position.maxScrollExtent && !scrollController.position.outOfRange) {
|
||||
chatRoomCubit.loadNextPage(widget.chatId);
|
||||
chatRoomCubit.loadNextPage(widget.uuid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +95,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
if (state is ChatRoomStateLoaded) {
|
||||
return _buildChat(context, state);
|
||||
} else if (state is ChatRoomStateError) {
|
||||
return Scaffold(appBar: AppBar(), body: DataErrorWidget(onReload: () => chatRoomCubit.initChat(widget.chatId)));
|
||||
return Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: DataErrorWidget(onReload: () => chatRoomCubit.initChat(widget.uuid)),
|
||||
);
|
||||
} else {
|
||||
return const Scaffold(body: Loader());
|
||||
}
|
||||
@@ -168,34 +173,19 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
width: 40,
|
||||
height: 40,
|
||||
alignment: Alignment.center,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFEBF4FF),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
decoration: const BoxDecoration(color: Color(0xFFEBF4FF), shape: BoxShape.circle),
|
||||
child: Text(
|
||||
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),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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(
|
||||
firstChild: Text(
|
||||
'Печатает...',
|
||||
style: CustomTextStyles.bodySmallMulishPrimary,
|
||||
),
|
||||
firstChild: Text('Печатает...', style: CustomTextStyles.bodySmallMulishPrimary),
|
||||
firstCurve: Curves.easeIn,
|
||||
secondCurve: Curves.easeOut,
|
||||
secondChild: const SizedBox.shrink(),
|
||||
@@ -251,9 +241,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
if (!result) return;
|
||||
if (!context.mounted) return;
|
||||
context.read<ChatsCubit>().reduceUnreadMessagesCount(
|
||||
chatId: state.chatRoom.id,
|
||||
count: unreadMessagesIdsCopy.length,
|
||||
);
|
||||
chatId: state.chatRoom.id,
|
||||
count: unreadMessagesIdsCopy.length,
|
||||
);
|
||||
unreadMessagesIdsCopy.clear();
|
||||
});
|
||||
}
|
||||
@@ -264,6 +254,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
text: state.chatMessages[index].data.trim(),
|
||||
isCurrentUser: isCurrentUserAuthor,
|
||||
mediaData: state.chatMessages[index].mediaData ?? [],
|
||||
messageState: state.chatMessages[index].messageState,
|
||||
messageUuid: state.chatMessages[index].uuid ?? '',
|
||||
userSlug: state.chatRoom.users.receiver.slug,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -290,6 +283,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
text: state.chatMessages[index].data.trim(),
|
||||
isCurrentUser: isCurrentUserAuthor,
|
||||
mediaData: state.chatMessages[index].mediaData ?? [],
|
||||
messageState: state.chatMessages[index].messageState,
|
||||
messageUuid: state.chatMessages[index].uuid ?? '',
|
||||
userSlug: state.chatRoom.users.receiver.slug,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -361,11 +357,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
final imageIndex = _imageIndexAt(pickedFiles, i);
|
||||
|
||||
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(
|
||||
@@ -373,14 +365,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
transitionOnUserGestures: true,
|
||||
child: ClipRRect(
|
||||
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)),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -397,15 +382,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.gray50,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
size: 20,
|
||||
Icons.close,
|
||||
color: Colors.black,
|
||||
),
|
||||
decoration: BoxDecoration(color: appTheme.gray50, shape: BoxShape.circle),
|
||||
child: const Icon(size: 20, Icons.close, color: Colors.black),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -427,7 +405,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
child: Ink(
|
||||
height: 40,
|
||||
width: 40,
|
||||
child: Transform.rotate(angle: pi / 4, child: const Icon(Icons.attach_file_rounded, size: 30, color: Color(0xFF858E99))),
|
||||
child: Transform.rotate(
|
||||
angle: pi / 4,
|
||||
child: const Icon(Icons.attach_file_rounded, size: 30, color: Color(0xFF858E99)),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
@@ -449,7 +430,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
hintStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w400, height: 1, color: Color(0xFFAEAEB2)),
|
||||
filled: true,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(100), borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1)),
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
||||
@@ -465,7 +448,27 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
const SizedBox(width: 8),
|
||||
InkWell(
|
||||
customBorder: const CircleBorder(),
|
||||
onTap: () {
|
||||
onTap: () async {
|
||||
// Проверяем, заблокирован ли пользователь
|
||||
final blockedUsersCubit = sl<BlockedUsersCubit>();
|
||||
await blockedUsersCubit.getBlockedUsers();
|
||||
final blockedState = blockedUsersCubit.state;
|
||||
|
||||
if (blockedState is BlockedUsersStateLoaded) {
|
||||
final isBlocked = blockedState.profilesList.any((profile) => profile.slug == state.chatRoom.users.receiver.slug);
|
||||
if (isBlocked) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Вы не можете отправить сообщение заблокированному пользователю'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (textEditingController.text.isNotEmpty || pickedFiles.isNotEmpty) {
|
||||
chatRoomCubit.sendMessage(
|
||||
data: textEditingController.text.isEmpty ? null : textEditingController.text.trim(),
|
||||
@@ -525,13 +528,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
const Icon(Icons.attach_file, size: 32, color: Colors.black54),
|
||||
Padding(
|
||||
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)),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -579,13 +576,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
// 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(
|
||||
onPressed: () async {
|
||||
@@ -600,13 +591,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
// 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(
|
||||
onPressed: () async {
|
||||
@@ -643,20 +628,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
// 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('Отмена')),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -665,7 +640,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 32),
|
||||
child: Text(DateFormat.yMMMMd('RU').format(date), style: const TextStyle(height: 1, color: Color(0xFF8393A1), fontWeight: FontWeight.w500)),
|
||||
child: Text(
|
||||
DateFormat.yMMMMd('RU').format(date),
|
||||
style: const TextStyle(height: 1, color: Color(0xFF8393A1), fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.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 {
|
||||
final Author author;
|
||||
@@ -133,21 +135,45 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
|
||||
InkWell(
|
||||
customBorder: const CircleBorder(),
|
||||
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) {
|
||||
await sl<ChatRepository>().sendMessage(
|
||||
data: textEditingController.text.trim(),
|
||||
slug: widget.author.authorSlug,
|
||||
);
|
||||
setState(() {
|
||||
textEditingController.clear();
|
||||
});
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
textEditingController.clear();
|
||||
});
|
||||
}
|
||||
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
|
||||
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
|
||||
'chatScreen',
|
||||
extra: {
|
||||
'chatId': chatIdOrError,
|
||||
},
|
||||
);
|
||||
if (context.mounted) {
|
||||
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
|
||||
'chatScreen',
|
||||
extra: {
|
||||
'uuid': chatIdOrError,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Ink(
|
||||
@@ -175,20 +201,44 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () async {
|
||||
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();
|
||||
XFile? image = await picker.pickImage(source: ImageSource.camera);
|
||||
if (image != null) {
|
||||
List<XFile>? images = await picker.pickMultiImage();
|
||||
if (images.isNotEmpty) {
|
||||
List<File> files = images.map((image) => File(image.path)).toList();
|
||||
await sl<ChatRepository>().sendMessage(
|
||||
files: [File(image.path)],
|
||||
files: files,
|
||||
slug: widget.author.authorSlug,
|
||||
);
|
||||
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
|
||||
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
|
||||
'chatScreen',
|
||||
extra: {
|
||||
'chatId': chatIdOrError,
|
||||
},
|
||||
);
|
||||
if (mounted) {
|
||||
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
|
||||
'chatScreen',
|
||||
extra: {
|
||||
'uuid': chatIdOrError,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: const Row(
|
||||
@@ -211,12 +261,14 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
|
||||
slug: widget.author.authorSlug,
|
||||
);
|
||||
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
|
||||
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
|
||||
'chatScreen',
|
||||
extra: {
|
||||
'chatId': chatIdOrError,
|
||||
},
|
||||
);
|
||||
if (mounted) {
|
||||
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
|
||||
'chatScreen',
|
||||
extra: {
|
||||
'uuid': chatIdOrError,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: const Row(
|
||||
@@ -261,12 +313,14 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
|
||||
slug: widget.author.authorSlug,
|
||||
);
|
||||
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
|
||||
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
|
||||
'chatScreen',
|
||||
extra: {
|
||||
'chatId': chatIdOrError,
|
||||
},
|
||||
);
|
||||
if (mounted) {
|
||||
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
|
||||
'chatScreen',
|
||||
extra: {
|
||||
'uuid': chatIdOrError,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: const Row(
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:dismissible_page/dismissible_page.dart';
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
@@ -11,7 +9,7 @@ import 'package:photo_view/photo_view_gallery.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class PhotoViewScreen extends StatefulWidget {
|
||||
final List<String> imageUrls;
|
||||
final List<dynamic> imageUrls;
|
||||
final int index;
|
||||
final bool isLocal;
|
||||
|
||||
@@ -50,14 +48,12 @@ class _PhotoViewScreenState extends State<PhotoViewScreen> {
|
||||
),
|
||||
scrollPhysics: const BouncingScrollPhysics(),
|
||||
builder: (BuildContext context, int index) {
|
||||
final imageProvider = widget.isLocal
|
||||
? FileImage(widget.imageUrls[index]) as ImageProvider<Object>
|
||||
: CachedNetworkImageProvider(widget.imageUrls[index]) as ImageProvider<Object>;
|
||||
|
||||
return PhotoViewGalleryPageOptions(
|
||||
imageProvider: widget.isLocal
|
||||
? FileImage(
|
||||
File(widget.imageUrls[index]),
|
||||
) as ImageProvider
|
||||
: CachedNetworkImageProvider(
|
||||
widget.imageUrls[index],
|
||||
),
|
||||
imageProvider: imageProvider,
|
||||
initialScale: PhotoViewComputedScale.contained,
|
||||
minScale: PhotoViewComputedScale.contained,
|
||||
heroAttributes: PhotoViewHeroAttributes(
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:dismissible_page/dismissible_page.dart';
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.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/widgets/message_action_bottom_sheet.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';
|
||||
|
||||
class ChatBubbleWidget extends StatelessWidget {
|
||||
@@ -15,6 +22,9 @@ class ChatBubbleWidget extends StatelessWidget {
|
||||
required this.isCurrentUser,
|
||||
required this.mediaData,
|
||||
required this.isRead,
|
||||
required this.messageState,
|
||||
required this.messageUuid,
|
||||
required this.userSlug
|
||||
});
|
||||
|
||||
final DateTime sendTime;
|
||||
@@ -22,7 +32,9 @@ class ChatBubbleWidget extends StatelessWidget {
|
||||
final bool isCurrentUser;
|
||||
final bool isRead;
|
||||
final List<dynamic> mediaData;
|
||||
|
||||
final MessageState messageState;
|
||||
final String messageUuid;
|
||||
final String userSlug;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.sizeOf(context);
|
||||
@@ -36,17 +48,33 @@ class ChatBubbleWidget extends StatelessWidget {
|
||||
mainAxisAlignment: isCurrentUser ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||||
children: [
|
||||
if (isCurrentUser)
|
||||
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,
|
||||
),
|
||||
switch (messageState) {
|
||||
MessageState.loading => Padding(
|
||||
padding: const EdgeInsets.only(right: 4, bottom: 4),
|
||||
child: SizedBox(width: 10, height: 10, child: CircularProgressIndicator(strokeWidth: 2, color: appTheme.blueA400)),
|
||||
),
|
||||
),
|
||||
|
||||
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(
|
||||
constraints: BoxConstraints(maxWidth: size.width * 0.7),
|
||||
decoration: BoxDecoration(
|
||||
@@ -59,8 +87,9 @@ class ChatBubbleWidget extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding:
|
||||
mediaData.isNotEmpty ? const EdgeInsets.all(8) : EdgeInsets.only(top: 8, bottom: 8, left: isCurrentUser ? 16 : 8, right: isCurrentUser ? 8 : 16),
|
||||
padding: mediaData.isNotEmpty
|
||||
? const EdgeInsets.all(8)
|
||||
: EdgeInsets.only(top: 8, bottom: 8, left: isCurrentUser ? 16 : 8, right: isCurrentUser ? 8 : 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: isCurrentUser ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -84,32 +113,19 @@ class ChatBubbleWidget extends StatelessWidget {
|
||||
throw Exception('Could not launch ${mediaData[index].toString()}');
|
||||
}
|
||||
},
|
||||
leading: const Icon(
|
||||
Icons.file_copy_outlined,
|
||||
color: Colors.white,
|
||||
),
|
||||
trailing: const Icon(
|
||||
Icons.download_outlined,
|
||||
color: Colors.white,
|
||||
),
|
||||
leading: const Icon(Icons.file_copy_outlined, color: Colors.black),
|
||||
trailing: const Icon(Icons.download_outlined, color: Colors.black),
|
||||
title: Text(
|
||||
mediaData[index].toString().replaceAll('chat_files/', ''),
|
||||
maxLines: 2,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
style: const TextStyle(color: Colors.black, fontSize: 12, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
);
|
||||
}
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
context.pushTransparentRoute(
|
||||
PhotoViewScreen(
|
||||
imageUrls: mediaData.map((e) => '$e').toList(),
|
||||
index: index,
|
||||
),
|
||||
PhotoViewScreen(imageUrls: mediaData, index: index, isLocal: mediaData[index] is File ? true : false),
|
||||
);
|
||||
},
|
||||
child: Hero(
|
||||
@@ -117,9 +133,7 @@ class ChatBubbleWidget extends StatelessWidget {
|
||||
transitionOnUserGestures: true,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: '${mediaData[index]}',
|
||||
),
|
||||
child: mediaData[index] is File ? Image.file(mediaData[index]) : CachedNetworkImage(imageUrl: '${mediaData[index]}'),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -131,11 +145,7 @@ class ChatBubbleWidget extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -145,12 +155,7 @@ class ChatBubbleWidget extends StatelessWidget {
|
||||
padding: const EdgeInsets.only(left: 4, bottom: 4),
|
||||
child: Text(
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -24,7 +24,7 @@ class _ChatEntryWidgetState extends State<ChatEntryWidget> {
|
||||
onTap: () => GoRouter.of(context).pushNamed(
|
||||
'chatScreen',
|
||||
extra: {
|
||||
'chatId': widget.chatRoom.id.toString(),
|
||||
'uuid': widget.chatRoom.id.toString(),
|
||||
},
|
||||
),
|
||||
child: Container(
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/widgets/bottom_sheet_action_widget.dart';
|
||||
import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_divider.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void showMessageActionsBottomSheet(BuildContext context, {required void Function() resend, required void Function() delete}) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => Container(
|
||||
margin: EdgeInsets.only(left: 8, right: 8, bottom: MediaQuery.of(context).padding.bottom + 20),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: appTheme.whiteA700),
|
||||
child: MessageActionsBottomSheet(resend: resend, delete: delete),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class MessageActionsBottomSheet extends StatefulWidget {
|
||||
const MessageActionsBottomSheet({super.key, required this.resend, required this.delete});
|
||||
final void Function() resend;
|
||||
final void Function() delete;
|
||||
|
||||
@override
|
||||
State<MessageActionsBottomSheet> createState() => _MessageActionsBottomSheet();
|
||||
}
|
||||
|
||||
class _MessageActionsBottomSheet extends State<MessageActionsBottomSheet> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 7),
|
||||
const BottomSheetHandlerBar(),
|
||||
BottomSheetActionWidget(text: s.resend, iconPath: 'assets/icons/resend.svg', onTap: widget.resend),
|
||||
const CustomDivider(),
|
||||
BottomSheetActionWidget(text: s.delete, iconPath: 'assets/icons/trash_can.svg', onTap: widget.delete, color: appTheme.red),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/token_storage.dart';
|
||||
import 'package:eatvio_app/features/chat/models/chat_message_model.dart';
|
||||
import 'package:eatvio_app/features/chat/models/chat_messages_model.dart';
|
||||
import 'package:eatvio_app/features/chat/models/chat_room_model.dart';
|
||||
@@ -47,10 +48,11 @@ class ChatRepository {
|
||||
}
|
||||
}
|
||||
|
||||
await chatService.sendMessageByUserSlug(
|
||||
userSlug: slug,
|
||||
await chatService.sendMessage(
|
||||
slug: slug,
|
||||
data: data,
|
||||
mediaData: multipartFiles.isEmpty ? null : multipartFiles,
|
||||
socketId: (await SocketIdStorage.getSocketIdFromStorage())!,
|
||||
);
|
||||
} catch (e, st) {
|
||||
log(e.toString(), stackTrace: st);
|
||||
@@ -58,10 +60,11 @@ class ChatRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<ChatRoomModel> getChatRoom(String chatId) async {
|
||||
Future<ChatRoomModel> getChatRoom(String uuid) async {
|
||||
try {
|
||||
return (await chatService.getChatRoom(id: chatId))['chatRoom']!;
|
||||
} catch (e) {
|
||||
return (await chatService.getChatRoom(uuid: uuid));
|
||||
} catch (e, st) {
|
||||
log(e.toString(), stackTrace: st);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -74,9 +77,9 @@ class ChatRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<ChatMessagesModel> getChatMessages(String chatId, int page) async {
|
||||
Future<ChatMessagesModel> getChatMessages(String id, int page) async {
|
||||
try {
|
||||
return (await chatService.getChatMessages(id: chatId, page: page));
|
||||
return (await chatService.getChatMessages(id: id, page: page));
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
@@ -90,9 +93,9 @@ class ChatRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> markMessagesAsRead(String chatId, List<int> messageIds) async {
|
||||
Future<void> markMessagesAsRead(String id, List<int> messageIds) async {
|
||||
try {
|
||||
await chatService.markMessagesAsRead(id: chatId, body: {'messages_ids': messageIds});
|
||||
await chatService.markMessagesAsRead(id: id, body: {'messages_ids': messageIds});
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
|
||||
@@ -13,32 +13,33 @@ part 'chat_service.g.dart';
|
||||
abstract class ChatService {
|
||||
factory ChatService(Dio dio, {String baseUrl}) = _ChatService;
|
||||
|
||||
@GET('/api/chat/index')
|
||||
@GET('/api/v2/chat/index')
|
||||
Future<ChatRoomPaginationModel> getChatRooms({
|
||||
@Query('page') required int page,
|
||||
@Query('find') String? query,
|
||||
@Query('filters[data][\$conversation_name]') String? query,
|
||||
});
|
||||
|
||||
@GET('/api/chat/show/{id}')
|
||||
Future<Map<String, ChatRoomModel>> getChatRoom({
|
||||
@Path() required String id,
|
||||
@GET('/api/v2/chat/show/{uuid}')
|
||||
Future<ChatRoomModel> getChatRoom({
|
||||
@Path() required String uuid,
|
||||
});
|
||||
|
||||
@POST('/api/v2/chat/message/send/{id}')
|
||||
@POST('/api/v2/chat/message/send/{slug}')
|
||||
@MultiPart()
|
||||
Future<void> sendMessage({
|
||||
@Path() required String id,
|
||||
@Header('X-Socket-ID') required String socketId,
|
||||
@Path() required String slug,
|
||||
@Part() String? data,
|
||||
@Part(name: 'media_data[]', contentType: 'multipart/form-data') List<MultipartFile>? mediaData,
|
||||
});
|
||||
|
||||
@GET('/api/chat/show/{id}/messages?page={page}')
|
||||
@GET('/api/v2/chat/show/{id}/messages?page={page}')
|
||||
Future<ChatMessagesModel> getChatMessages({
|
||||
@Path() required String id,
|
||||
@Path() required int page,
|
||||
});
|
||||
|
||||
@GET('/api/chat/get_by_user/{user}')
|
||||
@GET('/api/v2/chat/get_by_user/{user}')
|
||||
Future<ChatRoomModel> getChatRoomIdByUserSlug({
|
||||
@Path() required String user,
|
||||
});
|
||||
@@ -81,16 +82,8 @@ abstract class ChatService {
|
||||
@Body() required PlanComplainModel body,
|
||||
});
|
||||
|
||||
@GET('/api/v2/chat/get_by_user/{slug}')
|
||||
@GET('/api/chat/get_by_user/{slug}')
|
||||
Future<HttpResponse> getChatByUserSlug({
|
||||
@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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
|
||||
import 'package:eatvio_app/core/widgets/loader.dart';
|
||||
import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_cubit.dart';
|
||||
import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_state.dart';
|
||||
import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -194,63 +195,74 @@ class _LawViolationWidgetState extends State<_LawViolationWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const BottomSheetHandlerBar(),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
s.lawViolation,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
width: MediaQuery.sizeOf(context).width * 0.7,
|
||||
child: Text(
|
||||
s.what_do_you_want_to_complaint_about,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
...tags.mapIndexed((index, element) {
|
||||
return Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
activeColor: appTheme.blueA400,
|
||||
side: BorderSide(width: 2, color: tags[index].$1 ? appTheme.blueA400 : appTheme.blueGray),
|
||||
value: element.$1,
|
||||
onChanged: (isChecked) {
|
||||
setState(() {
|
||||
if (isChecked == null) return;
|
||||
if (index == tags.length - 1 && !isChecked) {
|
||||
context.read<ComplainingCubit>().toggleDescription();
|
||||
}
|
||||
tags[index] = (isChecked, element.$2);
|
||||
final activeTags = tags.where((elem) => elem.$1).map((e) => e.$2).toList();
|
||||
context.read<ComplainingCubit>().setTags(activeTags);
|
||||
});
|
||||
return BlocBuilder<ComplainingCubit, ComplainingState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const BottomSheetHandlerBar(),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
s.lawViolation,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
width: MediaQuery.sizeOf(context).width * 0.7,
|
||||
child: Text(
|
||||
s.what_do_you_want_to_complaint_about,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
...tags.mapIndexed((index, element) {
|
||||
return Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
activeColor: appTheme.blueA400,
|
||||
side: BorderSide(width: 2, color: tags[index].$1 ? appTheme.blueA400 : appTheme.blueGray),
|
||||
value: element.$1,
|
||||
onChanged: (isChecked) {
|
||||
setState(() {
|
||||
if (isChecked == null) return;
|
||||
if (index == tags.length - 1 && !isChecked) {
|
||||
context.read<ComplainingCubit>().toggleDescription();
|
||||
}
|
||||
tags[index] = (isChecked, element.$2);
|
||||
final activeTags = tags.where((elem) => elem.$1).map((e) => e.$2).toList();
|
||||
context.read<ComplainingCubit>().setTags(activeTags);
|
||||
});
|
||||
},
|
||||
),
|
||||
Text(
|
||||
element.$2,
|
||||
style: TextStyles.textLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
if (tags.last.$1) ...[
|
||||
const SizedBox(height: 12),
|
||||
CustomTextFormField(
|
||||
hintText: s.type_here_your_complaint,
|
||||
hasError: (state.complainInfo.description?.length ?? 10) < 10,
|
||||
onChanged: (value) {
|
||||
context.read<ComplainingCubit>().setDescription(value);
|
||||
},
|
||||
),
|
||||
Text(
|
||||
element.$2,
|
||||
style: TextStyles.textLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
if ((state.complainInfo.description?.length ?? 10) < 10) ...[
|
||||
const SizedBox(height: 4),
|
||||
const ValidationErrorWidget(error: 'Минимум 10 символов'),
|
||||
]
|
||||
],
|
||||
);
|
||||
}),
|
||||
if (tags.last.$1) ...[
|
||||
const SizedBox(height: 12),
|
||||
CustomTextFormField(
|
||||
hintText: s.type_here_your_complaint,
|
||||
onChanged: (value) {
|
||||
context.read<ComplainingCubit>().setDescription(value);
|
||||
},
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
const _ComplainButton(),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
_ComplainButton(
|
||||
isDisabled: (state.complainInfo.description?.length ?? 0) < 10,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -515,8 +527,8 @@ class _ComplaintFailedWidget extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _ComplainButton extends StatelessWidget {
|
||||
const _ComplainButton();
|
||||
|
||||
const _ComplainButton({this.isDisabled = false});
|
||||
final bool isDisabled;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
@@ -534,6 +546,10 @@ class _ComplainButton extends StatelessWidget {
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
|
||||
),
|
||||
onPressed: () async {
|
||||
if (isDisabled) {
|
||||
context.read<ComplainingCubit>().setDescription('');
|
||||
return;
|
||||
}
|
||||
await context.read<ComplainingCubit>().complain();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -72,6 +72,12 @@ class ComplainingCubit extends Cubit<ComplainingState> {
|
||||
emit(state.copyWith(wasSentSuccessfully: true));
|
||||
return;
|
||||
}
|
||||
case ComplainedContent.comment:
|
||||
if (id != null) {
|
||||
await repository.sendCommentComplain(id: id, category: category.getValue(), body: state.complainInfo);
|
||||
emit(state.copyWith(wasSentSuccessfully: true));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e, st) {
|
||||
|
||||
@@ -64,4 +64,16 @@ class ComplainingRepository {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sendCommentComplain({
|
||||
required int id,
|
||||
required String category,
|
||||
required ComplainModel body,
|
||||
}) async {
|
||||
try {
|
||||
await service.sendCommentComplain(id: id, category: category, body: body);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,5 +42,12 @@ abstract class ComplainingService {
|
||||
@Path() required String category,
|
||||
@Body() required ComplainModel body,
|
||||
});
|
||||
|
||||
@POST('/api/comments/{id}/complain/{category}')
|
||||
Future<void> sendCommentComplain({
|
||||
@Path() required int id,
|
||||
@Path() required String category,
|
||||
@Body() required ComplainModel body,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -25,24 +25,16 @@ class _LastStepScreenState extends State<LastStepScreen> {
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
'Последний шаг',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1.1,
|
||||
),
|
||||
),
|
||||
child: Text('Последний шаг', style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, height: 1.1)),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
CheckboxListTile(
|
||||
value: checkboxState,
|
||||
title: const Text(
|
||||
'Я ознакомлен и принимаю договор публичной офёрты',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
height: 1,
|
||||
),
|
||||
title: Text(
|
||||
hidePlans
|
||||
? 'Я ознакомлен и принимаю политику конфиденциальности и защиту персональных данных'
|
||||
: 'Я ознакомлен и принимаю договор публичной офёрты',
|
||||
style: const TextStyle(fontSize: 18, height: 1),
|
||||
),
|
||||
onChanged: (_) {
|
||||
setState(() {
|
||||
@@ -53,25 +45,14 @@ class _LastStepScreenState extends State<LastStepScreen> {
|
||||
activeColor: appTheme.blueA400,
|
||||
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: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: TextButton(
|
||||
onPressed: () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')),
|
||||
child: const Text(
|
||||
'Договор офёрты',
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
fontSize: 16,
|
||||
color: Color(0xFF323F4B),
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
onPressed: () async => await launchUrl(Uri.parse(hidePlans ? '$baseUrl/docs/privacy.pdf' : '$baseUrl/oferta_bs.pdf')),
|
||||
child: Text(
|
||||
hidePlans ? 'Политика конфиденциальности и пользовательское соглашение' : 'Договор офёрты',
|
||||
style: const TextStyle(height: 1, fontSize: 16, color: Color(0xFF323F4B), decoration: TextDecoration.underline),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -84,10 +65,7 @@ class _LastStepScreenState extends State<LastStepScreen> {
|
||||
return Container(
|
||||
height: 0.4,
|
||||
width: MediaQuery.of(context).size.width - 32,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black26,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
decoration: BoxDecoration(color: Colors.black26, borderRadius: BorderRadius.circular(16)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,81 +23,105 @@ class _RegistrationAsScreenState extends State<RegistrationAsScreen> {
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
'Регистрация в качестве',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1.1,
|
||||
),
|
||||
),
|
||||
child: Text('Регистрация в качестве', style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, height: 1.1)),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
RadioListTile<RegistrationAsType>(
|
||||
value: RegistrationAsType.physical,
|
||||
groupValue: registrationAsType,
|
||||
title: const Text(
|
||||
'Физического лица',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
height: 1,
|
||||
Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Radio<RegistrationAsType>(
|
||||
value: RegistrationAsType.physical,
|
||||
// ignore: deprecated_member_use
|
||||
groupValue: registrationAsType,
|
||||
activeColor: appTheme.blueA400,
|
||||
// ignore: deprecated_member_use
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
registrationAsType = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text('Физического лица', style: TextStyle(fontSize: 18, height: 1)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
activeColor: appTheme.blueA400,
|
||||
onChanged: (value) {},
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
RadioListTile<RegistrationAsType>(
|
||||
value: RegistrationAsType.selfBusy,
|
||||
groupValue: registrationAsType,
|
||||
title: const Text(
|
||||
'Самозанятого',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
height: 1,
|
||||
Row(
|
||||
children: [
|
||||
Radio<RegistrationAsType>(
|
||||
value: RegistrationAsType.selfBusy,
|
||||
// ignore: deprecated_member_use
|
||||
groupValue: registrationAsType,
|
||||
activeColor: appTheme.blueA400,
|
||||
// ignore: deprecated_member_use
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
registrationAsType = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text('Самозанятого', style: TextStyle(fontSize: 18, height: 1)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onChanged: null,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
RadioListTile<RegistrationAsType>(
|
||||
value: RegistrationAsType.iP,
|
||||
groupValue: registrationAsType,
|
||||
title: const Text(
|
||||
'Индивидуального предпринимателя',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
height: 1,
|
||||
Row(
|
||||
children: [
|
||||
Radio<RegistrationAsType>(
|
||||
value: RegistrationAsType.iP,
|
||||
// ignore: deprecated_member_use
|
||||
groupValue: registrationAsType,
|
||||
activeColor: appTheme.blueA400,
|
||||
// ignore: deprecated_member_use
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
registrationAsType = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text('Индивидуального предпринимателя', style: TextStyle(fontSize: 18, height: 1)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onChanged: null,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
RadioListTile<RegistrationAsType>(
|
||||
value: RegistrationAsType.ooo,
|
||||
groupValue: registrationAsType,
|
||||
title: const Text(
|
||||
'Общества с ограниченной ответственностью',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
height: 1,
|
||||
Row(
|
||||
children: [
|
||||
Radio<RegistrationAsType>(
|
||||
value: RegistrationAsType.ooo,
|
||||
// ignore: deprecated_member_use
|
||||
groupValue: registrationAsType,
|
||||
activeColor: appTheme.blueA400,
|
||||
// ignore: deprecated_member_use
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
registrationAsType = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text('Общества с ограниченной ответственностью', style: TextStyle(fontSize: 18, height: 1)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onChanged: null,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 16,
|
||||
),
|
||||
child: _divider(context),
|
||||
],
|
||||
),
|
||||
Padding(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), child: _divider(context)),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
'* На данный момент регистрация доступна только в качестве физического лица. Остальные варианты скоро появятся',
|
||||
),
|
||||
child: Text('* На данный момент регистрация доступна только в качестве физического лица. Остальные варианты скоро появятся'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -108,10 +132,7 @@ class _RegistrationAsScreenState extends State<RegistrationAsScreen> {
|
||||
return Container(
|
||||
height: 0.4,
|
||||
width: MediaQuery.of(context).size.width - 32,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black26,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
decoration: BoxDecoration(color: Colors.black26, borderRadius: BorderRadius.circular(16)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,42 @@
|
||||
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'rating_model.freezed.dart';
|
||||
part 'rating_model.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class RatingModel with _$RatingModel {
|
||||
abstract class RatingPaginationModel with _$RatingPaginationModel {
|
||||
factory RatingPaginationModel({
|
||||
required List<RatingModel> data,
|
||||
}) = _RatingPaginationModel;
|
||||
|
||||
factory RatingPaginationModel.fromJson(Map<String, dynamic> json) => _$RatingPaginationModelFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class RatingModel with _$RatingModel {
|
||||
factory RatingModel({
|
||||
required DateTime date,
|
||||
required String title,
|
||||
required int amount,
|
||||
@JsonKey(name: 'created_at', fromJson: parseDate) required DateTime date,
|
||||
@JsonKey(name: 'log_data') required RatingEnum type,
|
||||
@JsonKey(name: 'log_scores') required int amount,
|
||||
}) = _RatingModel;
|
||||
|
||||
factory RatingModel.fromJson(Map<String, dynamic> json) => _$RatingModelFromJson(json);
|
||||
}
|
||||
}
|
||||
|
||||
enum RatingEnum {
|
||||
@JsonValue('ADDED_RECIPE')
|
||||
addedRecipe,
|
||||
@JsonValue('ADDED_PUBLIC_DIET')
|
||||
addedPublicDiet,
|
||||
@JsonValue('ADDED_BLOG_POST')
|
||||
addedBlogPost,
|
||||
@JsonValue('INVITED_A_NEW_USER')
|
||||
invitedUser,
|
||||
@JsonValue('HAS_MEDICAL_DEGREE')
|
||||
hasMedicalDegree,
|
||||
@JsonValue('CONSULTED')
|
||||
consulted,
|
||||
@JsonValue('PRODUCT_NUTRITINAL_VALUE_APPROVED')
|
||||
addedGlossary;
|
||||
}
|
||||
|
||||
19
lib/features/consultant_rating/models/statistics_model.dart
Normal file
19
lib/features/consultant_rating/models/statistics_model.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'statistics_model.freezed.dart';
|
||||
part 'statistics_model.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class StatisticsModel with _$StatisticsModel {
|
||||
|
||||
factory StatisticsModel({
|
||||
@JsonKey(name: 'count_referal') int? countReferal,
|
||||
@JsonKey(name: 'count_blog_posts') int? blogPostsCount,
|
||||
@JsonKey(name: 'count_recipes') int? recipesCount,
|
||||
@JsonKey(name: 'count_diets') int? dietsCount,
|
||||
@JsonKey(name: 'count_consultants') int? consultationsCount,
|
||||
@JsonKey(name: 'count_products_nutritional_values') int? qwe,
|
||||
}) = _StatisticsModel;
|
||||
|
||||
factory StatisticsModel.fromJson(Map<String, dynamic> json) => _$StatisticsModelFromJson(json);
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart';
|
||||
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_state.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/rating_history_widget.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/rating_widget.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/statistic_widget.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class ConsultantRatingScreen extends StatelessWidget {
|
||||
static const name = 'consultantRatingScreen';
|
||||
@@ -15,7 +19,10 @@ class ConsultantRatingScreen extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const ConsultantRatingPageContent();
|
||||
return BlocProvider<ConsultantRatingCubit>(
|
||||
create: (context) => sl<ConsultantRatingCubit>()..getConsultantStatistics()..getScoreLogs(),
|
||||
child: const ConsultantRatingPageContent(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,52 +32,56 @@ class ConsultantRatingPageContent extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return Scaffold(
|
||||
backgroundColor: appTheme.gray100,
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
s.rating_and_statistics,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
const RatingWidget(
|
||||
rating: 1058,
|
||||
return BlocBuilder<ConsultantRatingCubit, ConsultantRatingState>(
|
||||
builder: (context, state) {
|
||||
return Scaffold(
|
||||
backgroundColor: appTheme.gray100,
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
s.rating_and_statistics,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: CustomOutlinedButton(
|
||||
text: s.copy_referral_link,
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||
buttonStyle: ButtonStyle(
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
|
||||
),
|
||||
body: state.error != null
|
||||
? DataErrorWidget(onReload: () async {
|
||||
await context.read<ConsultantRatingCubit>().getConsultantStatistics();
|
||||
if (!context.mounted) return;
|
||||
await context.read<ConsultantRatingCubit>().getScoreLogs();
|
||||
})
|
||||
: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
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),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_state.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/repositories/consultant_rating_repository.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class ConsultantRatingCubit extends Cubit<ConsultantRatingState> {
|
||||
final ConsultantRatingRepository repository;
|
||||
ConsultantRatingCubit(this.repository) : super(ConsultantRatingState());
|
||||
|
||||
Future<void> getConsultantStatistics({String? date}) async {
|
||||
try {
|
||||
emit(state.copyWith(isStatisticsLoading: true));
|
||||
final statistics = await repository.getConsultantStatistics(date: date);
|
||||
emit(state.copyWith(isStatisticsLoading: false, statistics: statistics, error: null));
|
||||
} catch (e, st) {
|
||||
emit(state.copyWith(isStatisticsLoading: true, error: e.toString()));
|
||||
log(e.toString(), stackTrace: st);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> getScoreLogs({String? date}) async {
|
||||
try {
|
||||
emit(state.copyWith(isRatingLoading: true));
|
||||
final scoreLogs = await repository.getScoreLogs(date: date);
|
||||
emit(state.copyWith(isRatingLoading: false, ratings: scoreLogs, error: null));
|
||||
} catch (e, st) {
|
||||
emit(state.copyWith(isRatingLoading: true, error: e.toString()));
|
||||
log(e.toString(), stackTrace: st);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/models/statistics_model.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'consultant_rating_state.freezed.dart';
|
||||
|
||||
@freezed
|
||||
abstract class ConsultantRatingState with _$ConsultantRatingState {
|
||||
factory ConsultantRatingState({
|
||||
@Default(false) bool isStatisticsLoading,
|
||||
@Default(false) bool isRatingLoading,
|
||||
@Default(null) StatisticsModel? statistics,
|
||||
@Default(null) String? error,
|
||||
@Default(<RatingModel>[]) List<RatingModel> ratings,
|
||||
}) = _ConsultantRatingState;
|
||||
}
|
||||
@@ -1,42 +1,29 @@
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/utils/date_time_utils.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_drop_down_button.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class PeriodFilterWidget extends StatefulWidget {
|
||||
const PeriodFilterWidget({super.key});
|
||||
|
||||
const PeriodFilterWidget({super.key, required this.isStatistic});
|
||||
final bool isStatistic;
|
||||
@override
|
||||
State<PeriodFilterWidget> createState() => _PeriodFilterWidgetState();
|
||||
}
|
||||
|
||||
class _PeriodFilterWidgetState extends State<PeriodFilterWidget> {
|
||||
final List<String> periodDropDownTitles = [];
|
||||
int periodDropDownIndex = 0;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final s = S.of(context);
|
||||
if (periodDropDownTitles.isEmpty) {
|
||||
periodDropDownTitles.addAll([
|
||||
s.for_whole_period_of_time,
|
||||
s.for_a_year,
|
||||
s.for_a_month,
|
||||
s.for_a_week,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
PeriodFilterStatus _currentState = PeriodFilterStatus.all;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CustomDropDownButton(
|
||||
CustomDropDownButton<PeriodFilterStatus>(
|
||||
hint: Text(
|
||||
s.choose_diet_duration,
|
||||
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
|
||||
@@ -44,24 +31,59 @@ class _PeriodFilterWidgetState extends State<PeriodFilterWidget> {
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
periodDropDownIndex = periodDropDownTitles.indexOf(value);
|
||||
_currentState = value;
|
||||
});
|
||||
if (widget.isStatistic) {
|
||||
if (value == PeriodFilterStatus.all) {
|
||||
context.read<ConsultantRatingCubit>().getConsultantStatistics();
|
||||
return;
|
||||
}
|
||||
context.read<ConsultantRatingCubit>().getConsultantStatistics(date: value.value);
|
||||
return;
|
||||
}
|
||||
if (value == PeriodFilterStatus.all) {
|
||||
context.read<ConsultantRatingCubit>().getScoreLogs();
|
||||
return;
|
||||
}
|
||||
context.read<ConsultantRatingCubit>().getScoreLogs(date: value.value);
|
||||
return;
|
||||
}
|
||||
},
|
||||
items: periodDropDownTitles
|
||||
items: PeriodFilterStatus.values
|
||||
.mapIndexed(
|
||||
(index, value) => DropdownMenuItem(
|
||||
value: periodDropDownTitles[index],
|
||||
value: PeriodFilterStatus.values[index],
|
||||
child: Text(
|
||||
periodDropDownTitles[index],
|
||||
PeriodFilterStatus.values[index].name,
|
||||
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
value: periodDropDownTitles[periodDropDownIndex],
|
||||
value: _currentState,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum PeriodFilterStatus {
|
||||
all,
|
||||
month,
|
||||
week,
|
||||
year;
|
||||
|
||||
String get name => switch (this) {
|
||||
PeriodFilterStatus.all => 'Все периоды',
|
||||
PeriodFilterStatus.month => 'За месяц',
|
||||
PeriodFilterStatus.week => 'За неделю',
|
||||
PeriodFilterStatus.year => 'За год',
|
||||
};
|
||||
|
||||
String? get value => switch (this) {
|
||||
PeriodFilterStatus.all => null,
|
||||
PeriodFilterStatus.month => DateTime.now().subtract(const Duration(days: 30)).format('yyyy-MM-dd'),
|
||||
PeriodFilterStatus.week => DateTime.now().subtract(const Duration(days: 7)).format('yyyy-MM-dd'),
|
||||
PeriodFilterStatus.year => DateTime.now().subtract(const Duration(days: 365)).format('yyyy-MM-dd'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ class RatingHistoryItem extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 80,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.gray100,
|
||||
@@ -29,7 +28,7 @@ class RatingHistoryItem extends StatelessWidget {
|
||||
date.format('dd.MM.yyyy'),
|
||||
style: TextStyles.textXSSemibold.copyWith(color: appTheme.gray),
|
||||
),
|
||||
const Spacer(),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/widgets/loader.dart';
|
||||
import 'package:eatvio_app/core/widgets/main_background_bouble.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/period_filter_widget.dart';
|
||||
@@ -9,7 +10,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
class RatingHistoryWidget extends StatelessWidget {
|
||||
const RatingHistoryWidget({super.key, required this.ratingHistory});
|
||||
const RatingHistoryWidget({super.key, required this.ratingHistory, required this.isLoading});
|
||||
final bool isLoading;
|
||||
final List<RatingModel> ratingHistory;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -20,50 +22,60 @@ class RatingHistoryWidget extends StatelessWidget {
|
||||
right: 16,
|
||||
top: 16,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
s.rating_history,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
const Spacer(),
|
||||
SvgPicture.asset(
|
||||
'assets/icons/question_mark.svg',
|
||||
height: 22,
|
||||
width: 22,
|
||||
colorFilter: ColorFilter.mode(
|
||||
appTheme.blueA400,
|
||||
BlendMode.srcIn,
|
||||
child: isLoading
|
||||
? const Center(child: Loader())
|
||||
: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
s.rating_history,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
const Spacer(),
|
||||
SvgPicture.asset(
|
||||
'assets/icons/question_mark.svg',
|
||||
height: 22,
|
||||
width: 22,
|
||||
colorFilter: ColorFilter.mode(
|
||||
appTheme.blueA400,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
const PeriodFilterWidget(),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
ListView.builder(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: index == ratingHistory.length - 1 ? EdgeInsets.zero : const EdgeInsets.only(bottom: 12),
|
||||
child: RatingHistoryItem(
|
||||
title: ratingHistory[index].title,
|
||||
amount: ratingHistory[index].amount,
|
||||
date: ratingHistory[index].date,
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: ratingHistory.length,
|
||||
shrinkWrap: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
const PeriodFilterWidget(isStatistic: false),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
ListView.builder(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: index == ratingHistory.length - 1 ? EdgeInsets.zero : const EdgeInsets.only(bottom: 12),
|
||||
child: RatingHistoryItem(
|
||||
title: switch (ratingHistory[index].type) {
|
||||
RatingEnum.addedRecipe => 'Добавление рецепта',
|
||||
RatingEnum.addedPublicDiet => 'Добавление публичной диеты',
|
||||
RatingEnum.addedBlogPost => 'Добавление поста',
|
||||
RatingEnum.invitedUser => 'Регистрация по реф. ссылке',
|
||||
RatingEnum.hasMedicalDegree => 'Получение мед. степени',
|
||||
RatingEnum.consulted => 'Консультация',
|
||||
RatingEnum.addedGlossary => 'Добавление продукта в глоссарий',
|
||||
},
|
||||
amount: ratingHistory[index].amount,
|
||||
date: ratingHistory[index].date,
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: ratingHistory.length,
|
||||
shrinkWrap: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/widgets/loader.dart';
|
||||
import 'package:eatvio_app/core/widgets/main_background_bouble.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/models/statistics_model.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/period_filter_widget.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/statistic_bouble.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
@@ -8,14 +10,15 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
class StatisticWidget extends StatelessWidget {
|
||||
const StatisticWidget({super.key});
|
||||
|
||||
const StatisticWidget({super.key, required this.isLoading, required this.statistics});
|
||||
final bool isLoading;
|
||||
final StatisticsModel? statistics;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return MainBackgroundBouble(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
child: isLoading ? const Center(child: Loader()) : Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
@@ -38,15 +41,17 @@ class StatisticWidget extends StatelessWidget {
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
const PeriodFilterWidget(),
|
||||
const PeriodFilterWidget(
|
||||
isStatistic: true,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: StatisticBouble(number: 12, title: s.invited_people_count)),
|
||||
Expanded(child: StatisticBouble(number: statistics?.countReferal ?? 0, title: s.invited_people_count)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: StatisticBouble(number: 12, title: s.published_posts_count)),
|
||||
Expanded(child: StatisticBouble(number: statistics?.blogPostsCount ?? 0, title: s.published_posts_count)),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
@@ -54,9 +59,9 @@ class StatisticWidget extends StatelessWidget {
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: StatisticBouble(number: 12, title: s.published_recipes_count)),
|
||||
Expanded(child: StatisticBouble(number: statistics?.recipesCount ?? 0, title: s.published_recipes_count)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: StatisticBouble(number: 12, title: s.published_diets_count)),
|
||||
Expanded(child: StatisticBouble(number: statistics?.dietsCount ?? 0, title: s.published_diets_count)),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
@@ -64,9 +69,9 @@ class StatisticWidget extends StatelessWidget {
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: StatisticBouble(number: 12, title: s.consultations_count)),
|
||||
Expanded(child: StatisticBouble(number: statistics?.consultationsCount ?? 0, title: s.consultations_count)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: StatisticBouble(number: 12, title: s.active_consultations_count)),
|
||||
Expanded(child: StatisticBouble(number: statistics?.qwe ?? 0, title: s.active_consultations_count)),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/models/statistics_model.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/services/consultant_rating_service.dart';
|
||||
|
||||
class ConsultantRatingRepository {
|
||||
final ConsultantRatingService service;
|
||||
|
||||
ConsultantRatingRepository(this.service);
|
||||
|
||||
Future<StatisticsModel> getConsultantStatistics({String? date}) async {
|
||||
try {
|
||||
return await service.getConsultantStatistics(date: date);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<RatingModel>> getScoreLogs({String? date}) async {
|
||||
try {
|
||||
final data = (await service.getScoreLogs(date: date)).data;
|
||||
return data;
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/models/statistics_model.dart';
|
||||
|
||||
import 'package:retrofit/retrofit.dart';
|
||||
|
||||
part 'consultant_rating_service.g.dart';
|
||||
|
||||
@RestApi()
|
||||
abstract class ConsultantRatingService {
|
||||
factory ConsultantRatingService(Dio dio, {String baseUrl}) = _ConsultantRatingService;
|
||||
|
||||
@GET('/api/consultants/rating/statistics')
|
||||
Future<StatisticsModel> getConsultantStatistics({@Query('filters[duration][\$gte]') String? date});
|
||||
|
||||
@GET('/api/consultants/score/logs')
|
||||
Future<RatingPaginationModel> getScoreLogs({@Query('filters[duration][\$gte]') String? date});
|
||||
}
|
||||
@@ -14,10 +14,7 @@ import 'package:go_router/go_router.dart';
|
||||
class ConsultationCardWidget extends StatefulWidget {
|
||||
final ConsultationModel consultationsModel;
|
||||
|
||||
const ConsultationCardWidget({
|
||||
super.key,
|
||||
required this.consultationsModel,
|
||||
});
|
||||
const ConsultationCardWidget({super.key, required this.consultationsModel});
|
||||
|
||||
@override
|
||||
State<ConsultationCardWidget> createState() => _ConsultationCardWidgetState();
|
||||
@@ -32,13 +29,7 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
|
||||
'Дневник заполнен, прокомментирован консультантом и после этого клиент просмотрел комментарии.',
|
||||
];
|
||||
|
||||
final List<Color> legendColors = const [
|
||||
Colors.white,
|
||||
Color(0xFFF0C497),
|
||||
Color(0xFFFF9500),
|
||||
Color(0xFF2F80ED),
|
||||
Color(0xFF4CD964),
|
||||
];
|
||||
final List<Color> legendColors = const [Colors.white, Color(0xFFF0C497), Color(0xFFFF9500), Color(0xFF2F80ED), Color(0xFF4CD964)];
|
||||
|
||||
late final List<(int, DiaryModel)> givenDaysWithDayNumber;
|
||||
final List<List<DiaryStateEnum>> legendStates = [
|
||||
@@ -46,7 +37,7 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
|
||||
[DiaryStateEnum.started, DiaryStateEnum.lastDayStarted],
|
||||
[DiaryStateEnum.finished, DiaryStateEnum.lastDayFinished],
|
||||
[DiaryStateEnum.commented, DiaryStateEnum.lastDayCommented],
|
||||
[DiaryStateEnum.reviewedByUser]
|
||||
[DiaryStateEnum.reviewedByUser],
|
||||
];
|
||||
@override
|
||||
void initState() {
|
||||
@@ -72,32 +63,19 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 30,
|
||||
backgroundImage: NetworkImage(baseUrl + widget.consultationsModel.userProfilePhotoUrl),
|
||||
),
|
||||
CircleAvatar(radius: 30, backgroundImage: NetworkImage(baseUrl + widget.consultationsModel.userProfilePhotoUrl)),
|
||||
const SizedBox(width: 12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.consultationsModel.userName,
|
||||
style: const TextStyle(
|
||||
height: 1,
|
||||
color: Color(0xFF323F4B),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: const TextStyle(height: 1, color: Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.consultationsModel.planTitle,
|
||||
style: const TextStyle(
|
||||
height: 1,
|
||||
color: Color(0xFF88949F),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
style: const TextStyle(height: 1, color: Color(0xFF88949F), fontSize: 14, fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -127,12 +105,7 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
|
||||
children: [
|
||||
Text(
|
||||
'День ${widget.consultationsModel.currentDiaryDay} из ${widget.consultationsModel.duration}',
|
||||
style: const TextStyle(
|
||||
height: 1,
|
||||
color: Color(0xFF323F4B),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
style: const TextStyle(height: 1, color: Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w500),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
@@ -149,12 +122,7 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
|
||||
children: [
|
||||
const Text(
|
||||
'Легенда',
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Color(0xFF323F4B),
|
||||
),
|
||||
style: TextStyle(height: 1, fontSize: 20, fontWeight: FontWeight.w700, color: Color(0xFF323F4B)),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ListView.separated(
|
||||
@@ -170,11 +138,7 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Icon(
|
||||
Icons.help_outline_rounded,
|
||||
color: Color(0xFF323F4B),
|
||||
size: 20,
|
||||
),
|
||||
child: const Icon(Icons.help_outline_rounded, color: Color(0xFF323F4B), size: 20),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -185,7 +149,8 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
|
||||
children: List.generate(widget.consultationsModel.duration, (index) {
|
||||
final day = givenDaysWithDayNumber.firstWhereOrNull((e) => e.$1 == index);
|
||||
final hasActivity = day != null && day.$2.state != DiaryStateEnum.init;
|
||||
final canConsultantOpenThisDay = day != null &&
|
||||
final canConsultantOpenThisDay =
|
||||
day != null &&
|
||||
(day.$2.state != DiaryStateEnum.init && day.$2.state != DiaryStateEnum.started && day.$2.state != DiaryStateEnum.lastDayStarted);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 12),
|
||||
@@ -195,81 +160,74 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
|
||||
InkWell(
|
||||
onTap: () {
|
||||
if (!hasActivity || !canConsultantOpenThisDay) {
|
||||
showErrorDialog(context,
|
||||
title: s.you_can_not_open_this_day, description: '${widget.consultationsModel.userName} ${s.has_not_filled_it_yet}');
|
||||
showErrorDialog(
|
||||
context,
|
||||
title: s.you_can_not_open_this_day,
|
||||
description: '${widget.consultationsModel.userName} ${s.has_not_filled_it_yet}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
GoRouter.of(context).push(UserDiaryScreen.path, extra: {
|
||||
'diaryId': day.$2.id,
|
||||
'currentDiaryDay': index + 1,
|
||||
});
|
||||
GoRouter.of(context).push(UserDiaryScreen.path, extra: {'diaryId': day.$2.id, 'currentDiaryDay': index + 1});
|
||||
},
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
child: Ink(
|
||||
decoration: BoxDecoration(
|
||||
color: hasActivity ? appTheme.dividerColor : appTheme.dividerColor.withValues(alpha: 0.4),
|
||||
borderRadius: BorderRadius.circular(100)),
|
||||
color: hasActivity ? appTheme.dividerColor : appTheme.dividerColor.withValues(alpha: 0.4),
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
),
|
||||
width: 58,
|
||||
height: 32,
|
||||
child: Center(
|
||||
child: Text(
|
||||
(index + 1).toString(),
|
||||
style: TextStyles.textMSemibold.copyWith(
|
||||
color: hasActivity ? appTheme.gray : appTheme.gray.withValues(alpha: 0.4),
|
||||
),
|
||||
style: TextStyles.textMSemibold.copyWith(color: hasActivity ? appTheme.gray : appTheme.gray.withValues(alpha: 0.4)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (hasActivity) ...[
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
switch (day.$2.state) {
|
||||
DiaryStateEnum.init => const SizedBox.shrink(),
|
||||
DiaryStateEnum.started => Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[1]),
|
||||
),
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[1]),
|
||||
),
|
||||
DiaryStateEnum.lastDayStarted => Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[1]),
|
||||
),
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[1]),
|
||||
),
|
||||
DiaryStateEnum.finished => Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[2]),
|
||||
),
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[2]),
|
||||
),
|
||||
DiaryStateEnum.lastDayFinished => Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[2]),
|
||||
),
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[2]),
|
||||
),
|
||||
DiaryStateEnum.commented => Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[3]),
|
||||
),
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[3]),
|
||||
),
|
||||
DiaryStateEnum.lastDayCommented => Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[3]),
|
||||
),
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[3]),
|
||||
),
|
||||
DiaryStateEnum.reviewedByUser => Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[4]),
|
||||
),
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(shape: BoxShape.circle, color: legendColors[4]),
|
||||
),
|
||||
},
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
] else
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -280,11 +238,7 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'${widget.consultationsModel.consultationOptionPrice.toStringAsFixed(0)} ₽',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF019A3E),
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: const TextStyle(color: Color(0xFF019A3E), fontSize: 20, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
@@ -293,36 +247,26 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
onTap: () {
|
||||
final lastActiveDay = givenDaysWithDayNumber
|
||||
.where(
|
||||
(e) => e.$2.state == DiaryStateEnum.finished || e.$2.state == DiaryStateEnum.lastDayFinished,
|
||||
)
|
||||
.where((e) => e.$2.state == DiaryStateEnum.finished || e.$2.state == DiaryStateEnum.lastDayFinished)
|
||||
.lastOrNull;
|
||||
if (lastActiveDay != null) {
|
||||
GoRouter.of(context).push(UserDiaryScreen.path, extra: {
|
||||
'diaryId': lastActiveDay.$2.id,
|
||||
'currentDiaryDay': lastActiveDay.$1,
|
||||
});
|
||||
GoRouter.of(context).push(UserDiaryScreen.path, extra: lastActiveDay.$2.id);
|
||||
GoRouter.of(context).push(UserDiaryScreen.path, extra: {'diaryId': lastActiveDay.$2.id, 'currentDiaryDay': lastActiveDay.$1});
|
||||
return;
|
||||
}
|
||||
showErrorDialog(context,
|
||||
title: s.you_can_not_open_this_day, description: '${widget.consultationsModel.userName} ${s.has_not_filled_it_yet}');
|
||||
showErrorDialog(
|
||||
context,
|
||||
title: s.you_can_not_open_this_day,
|
||||
description: '${widget.consultationsModel.userName} ${s.has_not_filled_it_yet}',
|
||||
);
|
||||
},
|
||||
child: Ink(
|
||||
width: MediaQuery.of(context).size.width - 32 - 52 - 8,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: appTheme.blueA400,
|
||||
),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'Комментировать отчёт',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -334,16 +278,8 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
|
||||
child: Ink(
|
||||
height: 52,
|
||||
width: 52,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: const Color(0xFFF6F6F6),
|
||||
),
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
Icons.chat_outlined,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFF6F6F6)),
|
||||
child: const Center(child: Icon(Icons.chat_outlined, size: 24)),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -361,21 +297,13 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
|
||||
Container(
|
||||
height: 20,
|
||||
width: 20,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: color,
|
||||
),
|
||||
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Flexible(
|
||||
child: Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
height: 1,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF323F4B),
|
||||
),
|
||||
style: const TextStyle(height: 1, fontSize: 16, fontWeight: FontWeight.w500, color: Color(0xFF323F4B)),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -50,6 +50,7 @@ class ConsultationService {
|
||||
else if (state != null && stateComparison != null)
|
||||
'filters[last_state][${stateComparison.value}]': state,
|
||||
if (dateComparison != null && date != null) 'filters[start_date][${dateComparison.value}]': date,
|
||||
'sort[]': 'created_at:desc',
|
||||
},
|
||||
);
|
||||
return ConsultationPaginationModel.fromJson(response.data);
|
||||
@@ -77,7 +78,8 @@ class ConsultationService {
|
||||
final response = await dio.get(
|
||||
'/api/finances/my_finances',
|
||||
queryParameters: {
|
||||
if (dateComparison != null && date != null) 'filters[end_date]': date,
|
||||
if (date != null && dateComparison != null) 'filters[start_date][${dateComparison.value}]': date,
|
||||
'sort[]': 'start_date:desc',
|
||||
},
|
||||
);
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
@@ -96,7 +98,8 @@ class ConsultationService {
|
||||
final response = await dio.get(
|
||||
'/api/finances/my_purchases',
|
||||
queryParameters: {
|
||||
if (dateComparison != null && date != null) 'filters[start_date]': date,
|
||||
if (date != null && dateComparison != null) 'filters[start_date][${dateComparison.value}]': date,
|
||||
'sort[]': 'start_date:desc',
|
||||
},
|
||||
);
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
|
||||
@@ -57,7 +57,7 @@ class RecipeRepository {
|
||||
'type': recipe.type,
|
||||
'state': 'active',
|
||||
'title': recipe.title,
|
||||
'tags': recipe.tags,
|
||||
'tags[]': recipe.tags ?? [],
|
||||
'description_md': recipe.description,
|
||||
'video_url': recipe.videoUrl,
|
||||
'ingredients': ingredientsMap,
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:eatvio_app/core/utils/toasts.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
|
||||
import 'package:eatvio_app/features/dev_tools/test_cases.dart';
|
||||
import 'package:eatvio_app/core/env_config.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
@@ -97,10 +98,15 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(padding: const EdgeInsets.all(8.0), child: Row(children: [Text('BaseUrl: $baseUrlForTest')])),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(children: [Text('BaseUrl: $baseUrlForTest')]),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: CustomTextFormField(controller: controller, hintText: 'type new base url')),
|
||||
Expanded(
|
||||
child: CustomTextFormField(controller: controller, hintText: 'type new base url'),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
@@ -116,13 +122,13 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
CustomOutlinedButton(
|
||||
text: baseUrl == 'https://dev2.eatvio.com' ? 'Set production base url' : 'Set dev2 base url',
|
||||
text: baseUrl == EnvConfig.devBaseUrl ? 'Set production base url' : 'Set dev2 base url',
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
if (baseUrl == 'https://eatvio.ru') {
|
||||
baseUrlForTest = 'https://dev2.eatvio.com';
|
||||
if (baseUrl == EnvConfig.baseUrl) {
|
||||
baseUrlForTest = EnvConfig.devBaseUrl;
|
||||
} else {
|
||||
baseUrlForTest = 'https://eatvio.ru';
|
||||
baseUrlForTest = EnvConfig.baseUrl;
|
||||
}
|
||||
baseUrl = baseUrlForTest.toLowerCase();
|
||||
sl<Dio>().options.baseUrl = baseUrl;
|
||||
@@ -132,7 +138,7 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
|
||||
buttonStyle: const ButtonStyle(),
|
||||
),
|
||||
SizedBox(height: baseUrlForTest != baseUrl ? 40 : 10),
|
||||
if (baseUrl.toLowerCase() != 'https://eatvio.ru')
|
||||
if (baseUrl.toLowerCase() != EnvConfig.baseUrl.toLowerCase())
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: testCases.length,
|
||||
@@ -175,12 +181,7 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Test Failed'),
|
||||
content: SingleChildScrollView(child: Text(result.error ?? 'No error details.')),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('OK'))],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -197,12 +198,7 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Test Failed'),
|
||||
content: SingleChildScrollView(child: Text('$e\n$stack')),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('OK'))],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -216,10 +212,10 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
|
||||
TestState.fail => Container(width: 20, height: 20, color: Colors.red),
|
||||
TestState.notRun => Container(width: 20, height: 20, color: Colors.grey),
|
||||
TestState.loading => const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: SizedBox(height: 20, width: 20, child: Center(child: CircularProgressIndicator())),
|
||||
),
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: SizedBox(height: 20, width: 20, child: Center(child: CircularProgressIndicator())),
|
||||
),
|
||||
},
|
||||
],
|
||||
),
|
||||
|
||||
@@ -35,6 +35,7 @@ import 'package:eatvio_app/features/recipe/services/about_recipe_service.dart';
|
||||
import 'package:eatvio_app/features/recipe/services/my_recipes_service.dart';
|
||||
import 'package:eatvio_app/features/registration/services/registration_service.dart';
|
||||
import 'package:eatvio_app/features/settings/services/settings_service.dart';
|
||||
import 'package:eatvio_app/core/env_config.dart';
|
||||
|
||||
class TestResult {
|
||||
final bool success;
|
||||
@@ -477,7 +478,9 @@ class AuthServiceTest implements TestCases {
|
||||
final dio = sl<Dio>();
|
||||
final authService = AuthService(dio, baseUrl: baseUrl);
|
||||
// Use dummy credentials, expecting failure but no crash
|
||||
await authService.getTokenByCredentials(credentials: const CredentialsModel(email: 'reshetnick.mike+user1@gmail.com', password: '1234567890'));
|
||||
await authService.getTokenByCredentials(
|
||||
credentials: CredentialsModel(email: EnvConfig.testEmail, password: EnvConfig.testPassword),
|
||||
);
|
||||
return TestResult(false, 'Should not succeed');
|
||||
} catch (e) {
|
||||
return TestResult(true, 'Exception expected for dummy credentials');
|
||||
|
||||
@@ -1,784 +0,0 @@
|
||||
// dart format width=80
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'consultant_review_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$ConsultantReviewModel {
|
||||
String get planName;
|
||||
int get planDuration;
|
||||
int get currentPlanDay;
|
||||
ConsultantComments get consultantComments;
|
||||
|
||||
/// Create a copy of ConsultantReviewModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$ConsultantReviewModelCopyWith<ConsultantReviewModel> get copyWith =>
|
||||
_$ConsultantReviewModelCopyWithImpl<ConsultantReviewModel>(
|
||||
this as ConsultantReviewModel, _$identity);
|
||||
|
||||
/// Serializes this ConsultantReviewModel to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is ConsultantReviewModel &&
|
||||
(identical(other.planName, planName) ||
|
||||
other.planName == planName) &&
|
||||
(identical(other.planDuration, planDuration) ||
|
||||
other.planDuration == planDuration) &&
|
||||
(identical(other.currentPlanDay, currentPlanDay) ||
|
||||
other.currentPlanDay == currentPlanDay) &&
|
||||
(identical(other.consultantComments, consultantComments) ||
|
||||
other.consultantComments == consultantComments));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType, planName, planDuration, currentPlanDay, consultantComments);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ConsultantReviewModel(planName: $planName, planDuration: $planDuration, currentPlanDay: $currentPlanDay, consultantComments: $consultantComments)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $ConsultantReviewModelCopyWith<$Res> {
|
||||
factory $ConsultantReviewModelCopyWith(ConsultantReviewModel value,
|
||||
$Res Function(ConsultantReviewModel) _then) =
|
||||
_$ConsultantReviewModelCopyWithImpl;
|
||||
@useResult
|
||||
$Res call(
|
||||
{String planName,
|
||||
int planDuration,
|
||||
int currentPlanDay,
|
||||
ConsultantComments consultantComments});
|
||||
|
||||
$ConsultantCommentsCopyWith<$Res> get consultantComments;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$ConsultantReviewModelCopyWithImpl<$Res>
|
||||
implements $ConsultantReviewModelCopyWith<$Res> {
|
||||
_$ConsultantReviewModelCopyWithImpl(this._self, this._then);
|
||||
|
||||
final ConsultantReviewModel _self;
|
||||
final $Res Function(ConsultantReviewModel) _then;
|
||||
|
||||
/// Create a copy of ConsultantReviewModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? planName = null,
|
||||
Object? planDuration = null,
|
||||
Object? currentPlanDay = null,
|
||||
Object? consultantComments = null,
|
||||
}) {
|
||||
return _then(_self.copyWith(
|
||||
planName: null == planName
|
||||
? _self.planName
|
||||
: planName // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
planDuration: null == planDuration
|
||||
? _self.planDuration
|
||||
: planDuration // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
currentPlanDay: null == currentPlanDay
|
||||
? _self.currentPlanDay
|
||||
: currentPlanDay // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
consultantComments: null == consultantComments
|
||||
? _self.consultantComments
|
||||
: consultantComments // ignore: cast_nullable_to_non_nullable
|
||||
as ConsultantComments,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of ConsultantReviewModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$ConsultantCommentsCopyWith<$Res> get consultantComments {
|
||||
return $ConsultantCommentsCopyWith<$Res>(_self.consultantComments, (value) {
|
||||
return _then(_self.copyWith(consultantComments: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _ConsultantReviewModel implements ConsultantReviewModel {
|
||||
_ConsultantReviewModel(
|
||||
{required this.planName,
|
||||
required this.planDuration,
|
||||
required this.currentPlanDay,
|
||||
required this.consultantComments});
|
||||
factory _ConsultantReviewModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$ConsultantReviewModelFromJson(json);
|
||||
|
||||
@override
|
||||
final String planName;
|
||||
@override
|
||||
final int planDuration;
|
||||
@override
|
||||
final int currentPlanDay;
|
||||
@override
|
||||
final ConsultantComments consultantComments;
|
||||
|
||||
/// Create a copy of ConsultantReviewModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$ConsultantReviewModelCopyWith<_ConsultantReviewModel> get copyWith =>
|
||||
__$ConsultantReviewModelCopyWithImpl<_ConsultantReviewModel>(
|
||||
this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$ConsultantReviewModelToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _ConsultantReviewModel &&
|
||||
(identical(other.planName, planName) ||
|
||||
other.planName == planName) &&
|
||||
(identical(other.planDuration, planDuration) ||
|
||||
other.planDuration == planDuration) &&
|
||||
(identical(other.currentPlanDay, currentPlanDay) ||
|
||||
other.currentPlanDay == currentPlanDay) &&
|
||||
(identical(other.consultantComments, consultantComments) ||
|
||||
other.consultantComments == consultantComments));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType, planName, planDuration, currentPlanDay, consultantComments);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ConsultantReviewModel(planName: $planName, planDuration: $planDuration, currentPlanDay: $currentPlanDay, consultantComments: $consultantComments)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$ConsultantReviewModelCopyWith<$Res>
|
||||
implements $ConsultantReviewModelCopyWith<$Res> {
|
||||
factory _$ConsultantReviewModelCopyWith(_ConsultantReviewModel value,
|
||||
$Res Function(_ConsultantReviewModel) _then) =
|
||||
__$ConsultantReviewModelCopyWithImpl;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{String planName,
|
||||
int planDuration,
|
||||
int currentPlanDay,
|
||||
ConsultantComments consultantComments});
|
||||
|
||||
@override
|
||||
$ConsultantCommentsCopyWith<$Res> get consultantComments;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$ConsultantReviewModelCopyWithImpl<$Res>
|
||||
implements _$ConsultantReviewModelCopyWith<$Res> {
|
||||
__$ConsultantReviewModelCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _ConsultantReviewModel _self;
|
||||
final $Res Function(_ConsultantReviewModel) _then;
|
||||
|
||||
/// Create a copy of ConsultantReviewModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$Res call({
|
||||
Object? planName = null,
|
||||
Object? planDuration = null,
|
||||
Object? currentPlanDay = null,
|
||||
Object? consultantComments = null,
|
||||
}) {
|
||||
return _then(_ConsultantReviewModel(
|
||||
planName: null == planName
|
||||
? _self.planName
|
||||
: planName // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
planDuration: null == planDuration
|
||||
? _self.planDuration
|
||||
: planDuration // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
currentPlanDay: null == currentPlanDay
|
||||
? _self.currentPlanDay
|
||||
: currentPlanDay // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
consultantComments: null == consultantComments
|
||||
? _self.consultantComments
|
||||
: consultantComments // ignore: cast_nullable_to_non_nullable
|
||||
as ConsultantComments,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of ConsultantReviewModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$ConsultantCommentsCopyWith<$Res> get consultantComments {
|
||||
return $ConsultantCommentsCopyWith<$Res>(_self.consultantComments, (value) {
|
||||
return _then(_self.copyWith(consultantComments: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$ConsultantComments {
|
||||
String? get dayComment;
|
||||
List<MealComment> get mealsComments;
|
||||
List<PortionComment> get portionsComments;
|
||||
|
||||
/// Create a copy of ConsultantComments
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$ConsultantCommentsCopyWith<ConsultantComments> get copyWith =>
|
||||
_$ConsultantCommentsCopyWithImpl<ConsultantComments>(
|
||||
this as ConsultantComments, _$identity);
|
||||
|
||||
/// Serializes this ConsultantComments to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is ConsultantComments &&
|
||||
(identical(other.dayComment, dayComment) ||
|
||||
other.dayComment == dayComment) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other.mealsComments, mealsComments) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other.portionsComments, portionsComments));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
dayComment,
|
||||
const DeepCollectionEquality().hash(mealsComments),
|
||||
const DeepCollectionEquality().hash(portionsComments));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ConsultantComments(dayComment: $dayComment, mealsComments: $mealsComments, portionsComments: $portionsComments)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $ConsultantCommentsCopyWith<$Res> {
|
||||
factory $ConsultantCommentsCopyWith(
|
||||
ConsultantComments value, $Res Function(ConsultantComments) _then) =
|
||||
_$ConsultantCommentsCopyWithImpl;
|
||||
@useResult
|
||||
$Res call(
|
||||
{String? dayComment,
|
||||
List<MealComment> mealsComments,
|
||||
List<PortionComment> portionsComments});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$ConsultantCommentsCopyWithImpl<$Res>
|
||||
implements $ConsultantCommentsCopyWith<$Res> {
|
||||
_$ConsultantCommentsCopyWithImpl(this._self, this._then);
|
||||
|
||||
final ConsultantComments _self;
|
||||
final $Res Function(ConsultantComments) _then;
|
||||
|
||||
/// Create a copy of ConsultantComments
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? dayComment = freezed,
|
||||
Object? mealsComments = null,
|
||||
Object? portionsComments = null,
|
||||
}) {
|
||||
return _then(_self.copyWith(
|
||||
dayComment: freezed == dayComment
|
||||
? _self.dayComment
|
||||
: dayComment // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
mealsComments: null == mealsComments
|
||||
? _self.mealsComments
|
||||
: mealsComments // ignore: cast_nullable_to_non_nullable
|
||||
as List<MealComment>,
|
||||
portionsComments: null == portionsComments
|
||||
? _self.portionsComments
|
||||
: portionsComments // ignore: cast_nullable_to_non_nullable
|
||||
as List<PortionComment>,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _ConsultantComments implements ConsultantComments {
|
||||
_ConsultantComments(
|
||||
{required this.dayComment,
|
||||
required final List<MealComment> mealsComments,
|
||||
required final List<PortionComment> portionsComments})
|
||||
: _mealsComments = mealsComments,
|
||||
_portionsComments = portionsComments;
|
||||
factory _ConsultantComments.fromJson(Map<String, dynamic> json) =>
|
||||
_$ConsultantCommentsFromJson(json);
|
||||
|
||||
@override
|
||||
final String? dayComment;
|
||||
final List<MealComment> _mealsComments;
|
||||
@override
|
||||
List<MealComment> get mealsComments {
|
||||
if (_mealsComments is EqualUnmodifiableListView) return _mealsComments;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_mealsComments);
|
||||
}
|
||||
|
||||
final List<PortionComment> _portionsComments;
|
||||
@override
|
||||
List<PortionComment> get portionsComments {
|
||||
if (_portionsComments is EqualUnmodifiableListView)
|
||||
return _portionsComments;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_portionsComments);
|
||||
}
|
||||
|
||||
/// Create a copy of ConsultantComments
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$ConsultantCommentsCopyWith<_ConsultantComments> get copyWith =>
|
||||
__$ConsultantCommentsCopyWithImpl<_ConsultantComments>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$ConsultantCommentsToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _ConsultantComments &&
|
||||
(identical(other.dayComment, dayComment) ||
|
||||
other.dayComment == dayComment) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._mealsComments, _mealsComments) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._portionsComments, _portionsComments));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
dayComment,
|
||||
const DeepCollectionEquality().hash(_mealsComments),
|
||||
const DeepCollectionEquality().hash(_portionsComments));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ConsultantComments(dayComment: $dayComment, mealsComments: $mealsComments, portionsComments: $portionsComments)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$ConsultantCommentsCopyWith<$Res>
|
||||
implements $ConsultantCommentsCopyWith<$Res> {
|
||||
factory _$ConsultantCommentsCopyWith(
|
||||
_ConsultantComments value, $Res Function(_ConsultantComments) _then) =
|
||||
__$ConsultantCommentsCopyWithImpl;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{String? dayComment,
|
||||
List<MealComment> mealsComments,
|
||||
List<PortionComment> portionsComments});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$ConsultantCommentsCopyWithImpl<$Res>
|
||||
implements _$ConsultantCommentsCopyWith<$Res> {
|
||||
__$ConsultantCommentsCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _ConsultantComments _self;
|
||||
final $Res Function(_ConsultantComments) _then;
|
||||
|
||||
/// Create a copy of ConsultantComments
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$Res call({
|
||||
Object? dayComment = freezed,
|
||||
Object? mealsComments = null,
|
||||
Object? portionsComments = null,
|
||||
}) {
|
||||
return _then(_ConsultantComments(
|
||||
dayComment: freezed == dayComment
|
||||
? _self.dayComment
|
||||
: dayComment // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
mealsComments: null == mealsComments
|
||||
? _self._mealsComments
|
||||
: mealsComments // ignore: cast_nullable_to_non_nullable
|
||||
as List<MealComment>,
|
||||
portionsComments: null == portionsComments
|
||||
? _self._portionsComments
|
||||
: portionsComments // ignore: cast_nullable_to_non_nullable
|
||||
as List<PortionComment>,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$MealComment {
|
||||
String get comment;
|
||||
int get typeNo;
|
||||
|
||||
/// Create a copy of MealComment
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$MealCommentCopyWith<MealComment> get copyWith =>
|
||||
_$MealCommentCopyWithImpl<MealComment>(this as MealComment, _$identity);
|
||||
|
||||
/// Serializes this MealComment to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is MealComment &&
|
||||
(identical(other.comment, comment) || other.comment == comment) &&
|
||||
(identical(other.typeNo, typeNo) || other.typeNo == typeNo));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, comment, typeNo);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'MealComment(comment: $comment, typeNo: $typeNo)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $MealCommentCopyWith<$Res> {
|
||||
factory $MealCommentCopyWith(
|
||||
MealComment value, $Res Function(MealComment) _then) =
|
||||
_$MealCommentCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({String comment, int typeNo});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$MealCommentCopyWithImpl<$Res> implements $MealCommentCopyWith<$Res> {
|
||||
_$MealCommentCopyWithImpl(this._self, this._then);
|
||||
|
||||
final MealComment _self;
|
||||
final $Res Function(MealComment) _then;
|
||||
|
||||
/// Create a copy of MealComment
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? comment = null,
|
||||
Object? typeNo = null,
|
||||
}) {
|
||||
return _then(_self.copyWith(
|
||||
comment: null == comment
|
||||
? _self.comment
|
||||
: comment // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
typeNo: null == typeNo
|
||||
? _self.typeNo
|
||||
: typeNo // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _MealComment implements MealComment {
|
||||
_MealComment({required this.comment, required this.typeNo});
|
||||
factory _MealComment.fromJson(Map<String, dynamic> json) =>
|
||||
_$MealCommentFromJson(json);
|
||||
|
||||
@override
|
||||
final String comment;
|
||||
@override
|
||||
final int typeNo;
|
||||
|
||||
/// Create a copy of MealComment
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$MealCommentCopyWith<_MealComment> get copyWith =>
|
||||
__$MealCommentCopyWithImpl<_MealComment>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$MealCommentToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _MealComment &&
|
||||
(identical(other.comment, comment) || other.comment == comment) &&
|
||||
(identical(other.typeNo, typeNo) || other.typeNo == typeNo));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, comment, typeNo);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'MealComment(comment: $comment, typeNo: $typeNo)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$MealCommentCopyWith<$Res>
|
||||
implements $MealCommentCopyWith<$Res> {
|
||||
factory _$MealCommentCopyWith(
|
||||
_MealComment value, $Res Function(_MealComment) _then) =
|
||||
__$MealCommentCopyWithImpl;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({String comment, int typeNo});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$MealCommentCopyWithImpl<$Res> implements _$MealCommentCopyWith<$Res> {
|
||||
__$MealCommentCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _MealComment _self;
|
||||
final $Res Function(_MealComment) _then;
|
||||
|
||||
/// Create a copy of MealComment
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$Res call({
|
||||
Object? comment = null,
|
||||
Object? typeNo = null,
|
||||
}) {
|
||||
return _then(_MealComment(
|
||||
comment: null == comment
|
||||
? _self.comment
|
||||
: comment // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
typeNo: null == typeNo
|
||||
? _self.typeNo
|
||||
: typeNo // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$PortionComment {
|
||||
@JsonKey(name: 'id')
|
||||
int get portionId;
|
||||
String get comment;
|
||||
|
||||
/// Create a copy of PortionComment
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$PortionCommentCopyWith<PortionComment> get copyWith =>
|
||||
_$PortionCommentCopyWithImpl<PortionComment>(
|
||||
this as PortionComment, _$identity);
|
||||
|
||||
/// Serializes this PortionComment to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is PortionComment &&
|
||||
(identical(other.portionId, portionId) ||
|
||||
other.portionId == portionId) &&
|
||||
(identical(other.comment, comment) || other.comment == comment));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, portionId, comment);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PortionComment(portionId: $portionId, comment: $comment)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $PortionCommentCopyWith<$Res> {
|
||||
factory $PortionCommentCopyWith(
|
||||
PortionComment value, $Res Function(PortionComment) _then) =
|
||||
_$PortionCommentCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({@JsonKey(name: 'id') int portionId, String comment});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$PortionCommentCopyWithImpl<$Res>
|
||||
implements $PortionCommentCopyWith<$Res> {
|
||||
_$PortionCommentCopyWithImpl(this._self, this._then);
|
||||
|
||||
final PortionComment _self;
|
||||
final $Res Function(PortionComment) _then;
|
||||
|
||||
/// Create a copy of PortionComment
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? portionId = null,
|
||||
Object? comment = null,
|
||||
}) {
|
||||
return _then(_self.copyWith(
|
||||
portionId: null == portionId
|
||||
? _self.portionId
|
||||
: portionId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
comment: null == comment
|
||||
? _self.comment
|
||||
: comment // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _PortionComment implements PortionComment {
|
||||
_PortionComment(
|
||||
{@JsonKey(name: 'id') required this.portionId, required this.comment});
|
||||
factory _PortionComment.fromJson(Map<String, dynamic> json) =>
|
||||
_$PortionCommentFromJson(json);
|
||||
|
||||
@override
|
||||
@JsonKey(name: 'id')
|
||||
final int portionId;
|
||||
@override
|
||||
final String comment;
|
||||
|
||||
/// Create a copy of PortionComment
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$PortionCommentCopyWith<_PortionComment> get copyWith =>
|
||||
__$PortionCommentCopyWithImpl<_PortionComment>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$PortionCommentToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _PortionComment &&
|
||||
(identical(other.portionId, portionId) ||
|
||||
other.portionId == portionId) &&
|
||||
(identical(other.comment, comment) || other.comment == comment));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, portionId, comment);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PortionComment(portionId: $portionId, comment: $comment)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$PortionCommentCopyWith<$Res>
|
||||
implements $PortionCommentCopyWith<$Res> {
|
||||
factory _$PortionCommentCopyWith(
|
||||
_PortionComment value, $Res Function(_PortionComment) _then) =
|
||||
__$PortionCommentCopyWithImpl;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({@JsonKey(name: 'id') int portionId, String comment});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$PortionCommentCopyWithImpl<$Res>
|
||||
implements _$PortionCommentCopyWith<$Res> {
|
||||
__$PortionCommentCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _PortionComment _self;
|
||||
final $Res Function(_PortionComment) _then;
|
||||
|
||||
/// Create a copy of PortionComment
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$Res call({
|
||||
Object? portionId = null,
|
||||
Object? comment = null,
|
||||
}) {
|
||||
return _then(_PortionComment(
|
||||
portionId: null == portionId
|
||||
? _self.portionId
|
||||
: portionId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
comment: null == comment
|
||||
? _self.comment
|
||||
: comment // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// dart format on
|
||||
@@ -1,67 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'consultant_review_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_ConsultantReviewModel _$ConsultantReviewModelFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_ConsultantReviewModel(
|
||||
planName: json['planName'] as String,
|
||||
planDuration: (json['planDuration'] as num).toInt(),
|
||||
currentPlanDay: (json['currentPlanDay'] as num).toInt(),
|
||||
consultantComments: ConsultantComments.fromJson(
|
||||
json['consultantComments'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ConsultantReviewModelToJson(
|
||||
_ConsultantReviewModel instance) =>
|
||||
<String, dynamic>{
|
||||
'planName': instance.planName,
|
||||
'planDuration': instance.planDuration,
|
||||
'currentPlanDay': instance.currentPlanDay,
|
||||
'consultantComments': instance.consultantComments,
|
||||
};
|
||||
|
||||
_ConsultantComments _$ConsultantCommentsFromJson(Map<String, dynamic> json) =>
|
||||
_ConsultantComments(
|
||||
dayComment: json['dayComment'] as String?,
|
||||
mealsComments: (json['mealsComments'] as List<dynamic>)
|
||||
.map((e) => MealComment.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
portionsComments: (json['portionsComments'] as List<dynamic>)
|
||||
.map((e) => PortionComment.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ConsultantCommentsToJson(_ConsultantComments instance) =>
|
||||
<String, dynamic>{
|
||||
'dayComment': instance.dayComment,
|
||||
'mealsComments': instance.mealsComments,
|
||||
'portionsComments': instance.portionsComments,
|
||||
};
|
||||
|
||||
_MealComment _$MealCommentFromJson(Map<String, dynamic> json) => _MealComment(
|
||||
comment: json['comment'] as String,
|
||||
typeNo: (json['typeNo'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$MealCommentToJson(_MealComment instance) =>
|
||||
<String, dynamic>{
|
||||
'comment': instance.comment,
|
||||
'typeNo': instance.typeNo,
|
||||
};
|
||||
|
||||
_PortionComment _$PortionCommentFromJson(Map<String, dynamic> json) =>
|
||||
_PortionComment(
|
||||
portionId: (json['id'] as num).toInt(),
|
||||
comment: json['comment'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PortionCommentToJson(_PortionComment instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.portionId,
|
||||
'comment': instance.comment,
|
||||
};
|
||||
@@ -136,15 +136,7 @@ class DiaryCubit extends Cubit<DiaryState> {
|
||||
final fifthMeal = MealModel(id: 0, time: '0', typeNumber: 5, satiety: 0, mood: 0, portions: fifthMealPortions ?? [], photos: []);
|
||||
emit(
|
||||
DiaryState.loadedTomorrowWithDiet(
|
||||
diary: diaryDay.copyWith(
|
||||
meals: [
|
||||
firstMeal,
|
||||
secondMeal,
|
||||
thirdMeal,
|
||||
fourthMeal,
|
||||
fifthMeal,
|
||||
],
|
||||
),
|
||||
diary: diaryDay.copyWith(meals: [firstMeal, secondMeal, thirdMeal, fourthMeal, fifthMeal]),
|
||||
selectedDay: selectedDay,
|
||||
selectedMeal: selectedMeal,
|
||||
),
|
||||
@@ -156,7 +148,7 @@ class DiaryCubit extends Cubit<DiaryState> {
|
||||
|
||||
Future<void> fetchDiary([bool needLoading = false, int? diaryId, int? currentDay]) async {
|
||||
this.diaryId ??= diaryId;
|
||||
|
||||
final isConsultantView = this.diaryId != null;
|
||||
if (needLoading) {
|
||||
emit(const DiaryState.loading());
|
||||
selectedMeal = 1;
|
||||
@@ -179,17 +171,22 @@ class DiaryCubit extends Cubit<DiaryState> {
|
||||
int? currentPlanDay;
|
||||
String? planName;
|
||||
if (diaryDay.chosenPlanId != null) {
|
||||
consultantReview = await diaryRepository.getDiaryComments(planId: diaryDay.chosenPlanId!, diaryId: diaryDay.id);
|
||||
final chosenPlanModel = await chatRepository.getChosenPlan(id: diaryDay.chosenPlanId!);
|
||||
if (!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);
|
||||
planDuration = chosenPlanModel.plan.duration;
|
||||
planName = chosenPlanModel.plan.name;
|
||||
currentPlanDay = currentDay ?? DateTime.now().difference(chosenPlanModel.startDate).inDays + 1;
|
||||
currentPlanDay = consultantReview.currentPlanDay;
|
||||
}
|
||||
final nutrition = await getNutrition(diaryDay.id);
|
||||
|
||||
final isDiaryFinished = diaryDay.typeOfDiary.diaryState == DiaryStateEnum.finished || diaryDay.typeOfDiary.diaryState == DiaryStateEnum.lastDayFinished;
|
||||
|
||||
final isDiaryCommented = diaryDay.typeOfDiary.diaryState == DiaryStateEnum.commented ||
|
||||
final isDiaryCommented =
|
||||
diaryDay.typeOfDiary.diaryState == DiaryStateEnum.commented ||
|
||||
diaryDay.typeOfDiary.diaryState == DiaryStateEnum.lastDayCommented ||
|
||||
diaryDay.typeOfDiary.diaryState == DiaryStateEnum.reviewedByUser;
|
||||
emit(
|
||||
@@ -198,7 +195,7 @@ class DiaryCubit extends Cubit<DiaryState> {
|
||||
nutrition: nutrition,
|
||||
selectedDay: selectedDay,
|
||||
selectedMeal: selectedMeal,
|
||||
isConsultantView: this.diaryId != null,
|
||||
isConsultantView: isConsultantView,
|
||||
consultantReview: consultantReview,
|
||||
isDiaryFinished: isDiaryFinished,
|
||||
isDiaryCommented: isDiaryCommented,
|
||||
@@ -206,7 +203,7 @@ class DiaryCubit extends Cubit<DiaryState> {
|
||||
chosenPlanDuration: planDuration,
|
||||
chosenPlanName: planName,
|
||||
isMoodEnabled: isMoodEnabled,
|
||||
isWakeUpAndRetireEnabled: isWakeUpAndRetireTimeEnabled
|
||||
isWakeUpAndRetireEnabled: isWakeUpAndRetireTimeEnabled,
|
||||
),
|
||||
);
|
||||
} catch (e, st) {
|
||||
@@ -215,17 +212,8 @@ class DiaryCubit extends Cubit<DiaryState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> finishDiaryDay({
|
||||
required int diaryId,
|
||||
required String wakeUp,
|
||||
required String retire,
|
||||
}) async {
|
||||
await diaryRepository.finishDiaryDay(
|
||||
diaryId: diaryId,
|
||||
wakeUp: wakeUp,
|
||||
retire: retire,
|
||||
comment: comment.isEmpty ? null : comment,
|
||||
);
|
||||
Future<void> finishDiaryDay({required int diaryId, required String wakeUp, required String retire}) async {
|
||||
await diaryRepository.finishDiaryDay(diaryId: diaryId, wakeUp: wakeUp, retire: retire, comment: comment.isEmpty ? null : comment);
|
||||
await fetchDiary();
|
||||
}
|
||||
|
||||
@@ -241,6 +229,7 @@ class DiaryCubit extends Cubit<DiaryState> {
|
||||
required int quantity,
|
||||
int? productNutritionalValueId,
|
||||
}) async {
|
||||
emit((state as DiaryStateLoaded).copyWith(isLoadingAddingProductInMeal: true));
|
||||
await diaryRepository.addPortionToMeal(
|
||||
diaryId: diaryId,
|
||||
mealId: mealId,
|
||||
|
||||
@@ -29,6 +29,7 @@ abstract class DiaryState with _$DiaryState {
|
||||
@Default(false) bool isDiaryCommented,
|
||||
@Default(false) bool isWakeUpAndRetireEnabled,
|
||||
@Default(false) bool isMoodEnabled,
|
||||
@Default(false) bool isLoadingAddingProductInMeal,
|
||||
}) = DiaryStateLoaded;
|
||||
|
||||
const factory DiaryState.loadedTomorrowWithDiet({
|
||||
|
||||
@@ -14,12 +14,7 @@ class AddProductModal extends StatefulWidget {
|
||||
final DiaryStateLoaded state;
|
||||
final DiaryCubit diaryCubit;
|
||||
|
||||
const AddProductModal({
|
||||
super.key,
|
||||
this.portion,
|
||||
required this.state,
|
||||
required this.diaryCubit,
|
||||
});
|
||||
const AddProductModal({super.key, this.portion, required this.state, required this.diaryCubit});
|
||||
|
||||
@override
|
||||
State<AddProductModal> createState() => _AddProductModalState();
|
||||
@@ -58,75 +53,62 @@ class _AddProductModalState extends State<AddProductModal> {
|
||||
return StatefulBuilder(
|
||||
builder: (BuildContext context, void Function(void Function()) setState) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16) +
|
||||
EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
padding: const EdgeInsets.all(16) + EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Добавление блюда',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF323F4B),
|
||||
),
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.8 - 32,
|
||||
width: MediaQuery.of(context).size.width * 0.76 - 32,
|
||||
child: CustomTextFormField(
|
||||
controller: productNameController,
|
||||
autofocus: true,
|
||||
hintText: 'Введите название продукта/блюда',
|
||||
onChanged: (query) {
|
||||
EasyDebounce.debounce(
|
||||
'glossary-search',
|
||||
const Duration(milliseconds: 250),
|
||||
() async {
|
||||
selectedGlossary = null;
|
||||
if (query.isEmpty) {
|
||||
glossariesList = [];
|
||||
setState(() {});
|
||||
return;
|
||||
}
|
||||
glossariesList = await widget.diaryCubit.getGlossaryListByQuery(query);
|
||||
EasyDebounce.debounce('glossary-search', const Duration(milliseconds: 250), () async {
|
||||
selectedGlossary = null;
|
||||
if (query.isEmpty) {
|
||||
glossariesList = [];
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
glossariesList = await widget.diaryCubit.getGlossaryListByQuery(query);
|
||||
setState(() {});
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.2 - 4,
|
||||
width: MediaQuery.of(context).size.width * 0.24 - 4,
|
||||
child: CustomTextFormField(
|
||||
controller: quantityController,
|
||||
keyboardType: TextInputType.number,
|
||||
hintText: selectedGlossary == null
|
||||
? '100'
|
||||
: selectedGlossary!.grInPortion == 0
|
||||
? '100'
|
||||
: '1',
|
||||
? '100'
|
||||
: '1',
|
||||
suffixText: selectedGlossary == null
|
||||
? 'гр'
|
||||
: selectedGlossary?.grInPortion == 0
|
||||
? 'гр'
|
||||
: 'шт',
|
||||
? 'гр'
|
||||
: 'шт',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
glossariesList.isEmpty ? const SizedBox(height: 16) : const SizedBox.shrink(),
|
||||
Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 256,
|
||||
),
|
||||
constraints: const BoxConstraints(maxHeight: 256),
|
||||
child: ListView.separated(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
@@ -141,28 +123,12 @@ class _AddProductModalState extends State<AddProductModal> {
|
||||
setState(() {});
|
||||
FocusManager.instance.primaryFocus?.nextFocus();
|
||||
},
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
title: Text(
|
||||
glossariesList[i].name,
|
||||
style: const TextStyle(
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
title: Text(glossariesList[i].name, style: const TextStyle(height: 1)),
|
||||
subtitle: glossariesList[i].grInPortion == 0
|
||||
? null
|
||||
: Text(
|
||||
'${glossariesList[i].grInPortion} г',
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.add_rounded,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
: Text('${glossariesList[i].grInPortion} г', style: TextStyle(height: 1, color: Colors.grey.shade600)),
|
||||
trailing: Icon(Icons.add_rounded, color: Colors.grey.shade400),
|
||||
contentPadding: const EdgeInsets.only(left: 12),
|
||||
);
|
||||
},
|
||||
@@ -188,14 +154,8 @@ class _AddProductModalState extends State<AddProductModal> {
|
||||
child: Ink(
|
||||
width: 52,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: const Color(0xFFF6F6F6),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.delete_outline_rounded,
|
||||
color: Colors.red,
|
||||
),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFF6F6F6)),
|
||||
child: const Icon(Icons.delete_outline_rounded, color: Colors.red),
|
||||
),
|
||||
),
|
||||
if (widget.portion != null) const SizedBox(width: 8),
|
||||
@@ -209,8 +169,8 @@ class _AddProductModalState extends State<AddProductModal> {
|
||||
portion: selectedGlossary == null
|
||||
? 'gr'
|
||||
: selectedGlossary!.grInPortion == 0
|
||||
? 'gr'
|
||||
: 'pcs',
|
||||
? 'gr'
|
||||
: 'pcs',
|
||||
quantity: int.parse(quantityController.text),
|
||||
productNutritionalValueId: selectedGlossary?.id,
|
||||
);
|
||||
@@ -223,8 +183,8 @@ class _AddProductModalState extends State<AddProductModal> {
|
||||
portion: selectedGlossary == null
|
||||
? 'gr'
|
||||
: selectedGlossary!.grInPortion == 0
|
||||
? 'gr'
|
||||
: 'pcs',
|
||||
? 'gr'
|
||||
: 'pcs',
|
||||
quantity: int.parse(quantityController.text),
|
||||
productNutritionalValueId: selectedGlossary?.id,
|
||||
portionState: widget.portion!.portionState,
|
||||
@@ -237,13 +197,8 @@ class _AddProductModalState extends State<AddProductModal> {
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
builder: (context) => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
child: GlossaryModerationRequestModalWidget(
|
||||
productName: productNameController.text,
|
||||
diaryCubit: widget.diaryCubit,
|
||||
),
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: GlossaryModerationRequestModalWidget(productName: productNameController.text, diaryCubit: widget.diaryCubit),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -252,18 +207,11 @@ class _AddProductModalState extends State<AddProductModal> {
|
||||
child: Ink(
|
||||
width: MediaQuery.of(context).size.width - (widget.portion == null ? 32 : 92),
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: appTheme.blueA400,
|
||||
),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
|
||||
child: Center(
|
||||
child: Text(
|
||||
widget.portion == null ? 'Добавить' : 'Сохранить',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/widgets/product_loader_widget.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/widgets/product_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -25,6 +26,11 @@ class _ProductsListWidgetState extends State<ProductsListWidget> {
|
||||
ProductWidget(portion: state.diary.meals[mealIndex].portions[i]),
|
||||
i < state.diary.meals[mealIndex].portions.length - 1 ? const SizedBox(height: 8) : const SizedBox.shrink(),
|
||||
],
|
||||
if (state.isLoadingAddingProductInMeal)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 8),
|
||||
child: ProductLoaderWidget(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -297,4 +297,12 @@ class DiaryRepository {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> viewCommentOfDiaryDayByConsultant({required int diaryId}) async {
|
||||
try {
|
||||
await diaryService.viewCommentOfDiaryDayByConsultant(diaryId: diaryId);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,4 +150,9 @@ abstract class DiaryService {
|
||||
@Path() required int planId,
|
||||
@Path() required int diaryId,
|
||||
});
|
||||
|
||||
@POST('/api/diary/{diaryId}/consultant/comment/view')
|
||||
Future<void> viewCommentOfDiaryDayByConsultant({
|
||||
@Path() required int diaryId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ import 'package:eatvio_app/features/diets/presentation/widgets/info_bouble.dart'
|
||||
import 'package:eatvio_app/core/widgets/main_background_bouble.dart';
|
||||
import 'package:eatvio_app/features/diets/presentation/widgets/start_diet_bottom_sheet.dart';
|
||||
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_cubit.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_state.dart';
|
||||
import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart';
|
||||
import 'package:eatvio_app/core/comments/comments_cubit/comments_state.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
|
||||
@@ -10,7 +10,11 @@ class ExpandableText extends StatefulWidget {
|
||||
final String text;
|
||||
final int maxLines;
|
||||
|
||||
const ExpandableText({super.key, required this.text, this.maxLines = 3});
|
||||
const ExpandableText({
|
||||
super.key,
|
||||
required this.text,
|
||||
this.maxLines = 5, // по умолчанию 5 строк
|
||||
});
|
||||
|
||||
@override
|
||||
State<ExpandableText> createState() => _ExpandableTextState();
|
||||
@@ -18,6 +22,29 @@ class ExpandableText extends StatefulWidget {
|
||||
|
||||
class _ExpandableTextState extends State<ExpandableText> {
|
||||
bool isExpanded = false;
|
||||
bool showExpandButton = false;
|
||||
|
||||
final GlobalKey _textKey = GlobalKey();
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _checkTextOverflow());
|
||||
}
|
||||
|
||||
void _checkTextOverflow() {
|
||||
final textRenderObject = _textKey.currentContext?.findRenderObject();
|
||||
if (textRenderObject is RenderBox) {
|
||||
final maxLinesHeight = widget.maxLines * TextStyles.textMMediumAuto.fontSize! * 1.2;
|
||||
final actualHeight = textRenderObject.size.height;
|
||||
|
||||
if (actualHeight > maxLinesHeight && !showExpandButton) {
|
||||
setState(() {
|
||||
showExpandButton = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void toggleExpansion() {
|
||||
setState(() {
|
||||
@@ -33,52 +60,63 @@ class _ExpandableTextState extends State<ExpandableText> {
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Stack(
|
||||
children: [
|
||||
Text(
|
||||
widget.text,
|
||||
style: TextStyles.textMMediumAuto.copyWith(color: appTheme.black),
|
||||
// style: {
|
||||
// "*": Style(
|
||||
// margin: Margins.all(0),
|
||||
// color: Colors.black87,
|
||||
// maxLines: isExpanded ? null : widget.maxLines,
|
||||
// textOverflow: TextOverflow.clip,
|
||||
// ),
|
||||
// },
|
||||
),
|
||||
if (!isExpanded)
|
||||
Positioned.fill(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
height: 35,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Colors.white.withValues(alpha: 0.0), Colors.white.withValues(alpha: 0.6), Colors.white],
|
||||
child: AnimatedSize(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInOut,
|
||||
child: Stack(
|
||||
children: [
|
||||
Text(
|
||||
widget.text,
|
||||
key: _textKey,
|
||||
style: TextStyles.textMMediumAuto.copyWith(color: appTheme.black),
|
||||
maxLines: isExpanded ? null : widget.maxLines,
|
||||
overflow: TextOverflow.clip,
|
||||
),
|
||||
if (!isExpanded && showExpandButton)
|
||||
Positioned.fill(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
height: 35,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.white.withAlpha(0),
|
||||
Colors.white.withAlpha((255 * 0.6).toInt()),
|
||||
Colors.white,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: GestureDetector(
|
||||
onTap: toggleExpansion,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(isExpanded ? s.hide : s.expand, style: TextStyles.textMMedium.copyWith(color: appTheme.blueA400)),
|
||||
Transform.rotate(angle: isExpanded ? math.pi : 0, child: SvgPicture.asset('assets/icons/arrow_down.svg', width: 12, height: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showExpandButton)
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: GestureDetector(
|
||||
onTap: toggleExpansion,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
isExpanded ? s.hide : s.expand,
|
||||
style: TextStyles.textMMedium.copyWith(color: appTheme.blueA400),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Transform.rotate(
|
||||
angle: isExpanded ? math.pi : 0,
|
||||
child: SvgPicture.asset('assets/icons/arrow_down.svg', width: 12, height: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:easy_debounce/easy_debounce.dart';
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/widgets/add_item_button.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_search_bar.dart';
|
||||
import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart';
|
||||
@@ -32,24 +33,12 @@ class FeedScreen extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<RecipesCubit>(
|
||||
create: (context) => sl<RecipesCubit>()..loadRecipesList(),
|
||||
),
|
||||
BlocProvider<FeedProfileCubit>(
|
||||
create: (context) => sl<FeedProfileCubit>()..getProfile(),
|
||||
),
|
||||
BlocProvider<FeedCubit>(
|
||||
create: (context) => sl<FeedCubit>()..loadFeedBlogPostList(),
|
||||
),
|
||||
BlocProvider<DietsCubit>(
|
||||
create: (context) => sl<DietsCubit>()..loadDiets(),
|
||||
),
|
||||
BlocProvider<PlansCubit>(
|
||||
create: (context) => sl<PlansCubit>()..loadPlans(),
|
||||
),
|
||||
BlocProvider<ConsultantsCubit>(
|
||||
create: (context) => sl<ConsultantsCubit>()..getConsultants(),
|
||||
),
|
||||
BlocProvider<RecipesCubit>(create: (context) => sl<RecipesCubit>()..loadRecipesList()),
|
||||
BlocProvider<FeedProfileCubit>(create: (context) => sl<FeedProfileCubit>()..getProfile()),
|
||||
BlocProvider<FeedCubit>(create: (context) => sl<FeedCubit>()..loadFeedBlogPostList()),
|
||||
BlocProvider<DietsCubit>(create: (context) => sl<DietsCubit>()..loadDiets()),
|
||||
if (!hidePlans) BlocProvider<PlansCubit>(create: (context) => sl<PlansCubit>()..loadPlans()),
|
||||
BlocProvider<ConsultantsCubit>(create: (context) => sl<ConsultantsCubit>()..getConsultants()),
|
||||
],
|
||||
child: const FeedScreenContent(),
|
||||
);
|
||||
@@ -69,7 +58,8 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
|
||||
final dietsController = TextEditingController();
|
||||
final plansController = TextEditingController();
|
||||
final consultantsController = TextEditingController();
|
||||
final List<String> chipsLabelList = ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты'];
|
||||
|
||||
late final List<String> chipsLabelList;
|
||||
int _selectedChipIndex = 0;
|
||||
final blogPostsScrollController = ScrollController();
|
||||
final recipesScrollController = ScrollController();
|
||||
@@ -78,12 +68,13 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
|
||||
final consultantsScrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
void initState() {
|
||||
// Initialize chipsLabelList based on hidePlans setting
|
||||
chipsLabelList = hidePlans ? ['Все', 'Рецепты', 'Диеты', 'Консультанты'] : ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты'];
|
||||
|
||||
consultantsScrollController.addListener(_onScrollConsultants);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
void _onScrollConsultants() {
|
||||
if (consultantsScrollController.offset >= consultantsScrollController.position.maxScrollExtent && !consultantsScrollController.position.outOfRange) {}
|
||||
@@ -108,45 +99,36 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
|
||||
heroTag: 'recipesTagsFilter',
|
||||
backgroundColor: appTheme.blueA400,
|
||||
shape: const CircleBorder(),
|
||||
child: const Icon(
|
||||
Icons.filter_list_rounded,
|
||||
color: Colors.white,
|
||||
),
|
||||
child: const Icon(Icons.filter_list_rounded, color: Colors.white),
|
||||
onPressed: () async {
|
||||
final recipesCubit = context.read<RecipesCubit>();
|
||||
await showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
builder: (context) => TagsModalWidget(
|
||||
recipesCubit: recipesCubit,
|
||||
),
|
||||
builder: (context) => TagsModalWidget(recipesCubit: recipesCubit),
|
||||
);
|
||||
recipesCubit.loadRecipesList();
|
||||
},
|
||||
)
|
||||
: null,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
toolbarHeight: 0,
|
||||
),
|
||||
appBar: AppBar(backgroundColor: Colors.white, toolbarHeight: 0),
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
body: NestedScrollView(
|
||||
controller: switch (_selectedChipIndex) {
|
||||
0 => blogPostsScrollController,
|
||||
1 => recipesScrollController,
|
||||
2 => dietsScrollController,
|
||||
3 => plansScrollController,
|
||||
4 => consultantsScrollController,
|
||||
_ => blogPostsScrollController
|
||||
3 when !hidePlans => plansScrollController,
|
||||
3 when hidePlans => consultantsScrollController,
|
||||
4 when !hidePlans => consultantsScrollController,
|
||||
_ => blogPostsScrollController,
|
||||
},
|
||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||
return [
|
||||
BlocBuilder<FeedProfileCubit, ProfileState>(
|
||||
builder: (context, state) {
|
||||
return SliverAppBar(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(bottom: Radius.circular(12)),
|
||||
),
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(bottom: Radius.circular(12))),
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.white,
|
||||
automaticallyImplyLeading: false,
|
||||
@@ -172,78 +154,71 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
|
||||
searchController: switch (_selectedChipIndex) {
|
||||
1 => recipesController,
|
||||
2 => dietsController,
|
||||
3 => plansController,
|
||||
4 => consultantsController,
|
||||
3 when !hidePlans => plansController,
|
||||
3 when hidePlans => consultantsController,
|
||||
4 when !hidePlans => consultantsController,
|
||||
_ => postsController,
|
||||
},
|
||||
onChanged: (query) {
|
||||
if (query.length > 2) {
|
||||
EasyDebounce.debounce(
|
||||
'searchFeed',
|
||||
const Duration(milliseconds: 500),
|
||||
() {
|
||||
switch (_selectedChipIndex) {
|
||||
case 0:
|
||||
context.read<FeedCubit>().loadFeedBlogPostList(query: query);
|
||||
break;
|
||||
case 1:
|
||||
context.read<RecipesCubit>().loadRecipesList(query: query);
|
||||
break;
|
||||
case 2:
|
||||
context.read<DietsCubit>().loadDiets(query: query);
|
||||
break;
|
||||
case 3:
|
||||
context.read<PlansCubit>().loadPlans(query: query);
|
||||
break;
|
||||
case 4:
|
||||
context.read<ConsultantsCubit>().getConsultants();
|
||||
break;
|
||||
}
|
||||
},
|
||||
);
|
||||
EasyDebounce.debounce('searchFeed', const Duration(milliseconds: 500), () {
|
||||
switch (_selectedChipIndex) {
|
||||
case 0:
|
||||
context.read<FeedCubit>().loadFeedBlogPostList(query: query);
|
||||
break;
|
||||
case 1:
|
||||
context.read<RecipesCubit>().loadRecipesList(query: query);
|
||||
break;
|
||||
case 2:
|
||||
context.read<DietsCubit>().loadDiets(query: query);
|
||||
break;
|
||||
case 3 when !hidePlans:
|
||||
context.read<PlansCubit>().loadPlans(query: query);
|
||||
break;
|
||||
case 3 when hidePlans:
|
||||
case 4 when !hidePlans:
|
||||
context.read<ConsultantsCubit>().getConsultants();
|
||||
break;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
EasyDebounce.debounce(
|
||||
'searchFeed',
|
||||
const Duration(milliseconds: 500),
|
||||
() {
|
||||
switch (_selectedChipIndex) {
|
||||
case 0:
|
||||
context.read<FeedCubit>().loadFeedBlogPostList();
|
||||
break;
|
||||
case 1:
|
||||
context.read<RecipesCubit>().loadRecipesList();
|
||||
break;
|
||||
case 2:
|
||||
context.read<DietsCubit>().loadDiets();
|
||||
break;
|
||||
case 3:
|
||||
context.read<PlansCubit>().loadPlans();
|
||||
break;
|
||||
case 4:
|
||||
context.read<ConsultantsCubit>().getConsultants();
|
||||
break;
|
||||
}
|
||||
},
|
||||
);
|
||||
EasyDebounce.debounce('searchFeed', const Duration(milliseconds: 500), () {
|
||||
switch (_selectedChipIndex) {
|
||||
case 0:
|
||||
context.read<FeedCubit>().loadFeedBlogPostList();
|
||||
break;
|
||||
case 1:
|
||||
context.read<RecipesCubit>().loadRecipesList();
|
||||
break;
|
||||
case 2:
|
||||
context.read<DietsCubit>().loadDiets();
|
||||
break;
|
||||
case 3 when !hidePlans:
|
||||
context.read<PlansCubit>().loadPlans();
|
||||
break;
|
||||
case 3 when hidePlans:
|
||||
case 4 when !hidePlans:
|
||||
context.read<ConsultantsCubit>().getConsultants();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
switch (_selectedChipIndex) {
|
||||
1 => AddItemButton(
|
||||
onTap: () => GoRouter.of(context).pushNamed(RecipeCreateScreen.name),
|
||||
),
|
||||
1 => AddItemButton(onTap: () => GoRouter.of(context).pushNamed(RecipeCreateScreen.name)),
|
||||
2 => AddItemButton(
|
||||
onTap: state.profile?.roles.contains('consultant') ?? false
|
||||
? () async {
|
||||
await GoRouter.of(context).pushNamed(CreateDietScreen.name);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
3 => AddItemButton(
|
||||
onTap: state.profile?.roles.contains('consultant') ?? false ? () => GoRouter.of(context).pushNamed(CreatePlanScreen.name) : null,
|
||||
),
|
||||
_ => const AddItemButton()
|
||||
onTap: state.profile?.roles.contains('consultant') ?? false
|
||||
? () async {
|
||||
await GoRouter.of(context).pushNamed(CreateDietScreen.name);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
3 when !hidePlans => AddItemButton(
|
||||
onTap: state.profile?.roles.contains('consultant') ?? false ? () => GoRouter.of(context).pushNamed(CreatePlanScreen.name) : null,
|
||||
),
|
||||
_ => const AddItemButton(),
|
||||
},
|
||||
],
|
||||
),
|
||||
@@ -256,8 +231,9 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
|
||||
0 => const BlogPosts(),
|
||||
1 => const Recipes(),
|
||||
2 => const Diets(),
|
||||
3 => const Plans(),
|
||||
4 => const Consultants(),
|
||||
3 when !hidePlans => const Plans(),
|
||||
3 when hidePlans => const Consultants(),
|
||||
4 when !hidePlans => const Consultants(),
|
||||
_ => Center(child: Text(chipsLabelList[_selectedChipIndex])),
|
||||
},
|
||||
),
|
||||
|
||||
@@ -245,9 +245,9 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
|
||||
const SizedBox(width: 8),
|
||||
FeedIconButton(
|
||||
svgIcon: SvgPicture.asset(
|
||||
height: 20,
|
||||
width: 20,
|
||||
'assets/icons/share.svg',
|
||||
height: 15,
|
||||
width: 12,
|
||||
'assets/icons/repost.svg',
|
||||
colorFilter: ColorFilter.mode(appTheme.gray600, BlendMode.srcIn),
|
||||
),
|
||||
onTap: () {
|
||||
|
||||
@@ -2,12 +2,7 @@ import 'package:eatvio_app/features/feed/presentation/widgets/choice_chip_widget
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class ChipGroup extends StatelessWidget {
|
||||
const ChipGroup({
|
||||
super.key,
|
||||
required this.selectedChipIndex,
|
||||
required this.chipsLabelList,
|
||||
required this.setSelectedChipIndex,
|
||||
});
|
||||
const ChipGroup({super.key, required this.selectedChipIndex, required this.chipsLabelList, required this.setSelectedChipIndex});
|
||||
final int selectedChipIndex;
|
||||
final List<String> chipsLabelList;
|
||||
final void Function(int) setSelectedChipIndex;
|
||||
@@ -22,7 +17,7 @@ class ChipGroup extends StatelessWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
...List.generate(
|
||||
5,
|
||||
chipsLabelList.length,
|
||||
(index) => Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: ChoiceChipWidget(
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import 'package:eatvio_app/core/comments/presentation/comments_actions_bottom_sheet.dart';
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/enums/complained_content.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/utils/date_time_utils.dart';
|
||||
import 'package:eatvio_app/core/utils/toasts.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
|
||||
import 'package:eatvio_app/features/complaining/presentation/complaining_bottom_sheet.dart';
|
||||
import 'package:eatvio_app/features/feed/models/comment_model.dart';
|
||||
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_cubit.dart';
|
||||
import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
@@ -53,98 +58,133 @@ class _CommentsWidgetState extends State<CommentsWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
Container _comment(S s, Comment comment, [double radius = 18, bool isChild = false]) {
|
||||
return Container(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
decoration: BoxDecoration(
|
||||
color: widget.replyId == comment.id ? appTheme.blueA400.withValues(alpha: 0.1) : Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'profileScreen',
|
||||
extra: Author(
|
||||
authorSlug: comment.authorSlug,
|
||||
authorName: comment.authorName,
|
||||
authorProfileIcon: comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : baseUrl + comment.authorProfileIcon,
|
||||
authorIsMe: (sl<ProfileAvatarCubit>().state as ProfileAvatarStateLoaded).profile.slug == comment.authorSlug ? true : false,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: CircleAvatar(
|
||||
foregroundImage: NetworkImage(
|
||||
comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : baseUrl + comment.authorProfileIcon,
|
||||
),
|
||||
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
||||
radius: radius,
|
||||
Widget _comment(S s, Comment comment, [double radius = 18, bool isChild = false]) {
|
||||
final isMe = (sl<ProfileAvatarCubit>().state as ProfileAvatarStateLoaded).profile.slug == comment.authorSlug;
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
showCommentsActionsBottomSheet(
|
||||
context,
|
||||
commentText: comment.body,
|
||||
reply: !isChild
|
||||
? () {
|
||||
context.read<CommentsCubit>().setReplyId(comment.id);
|
||||
}
|
||||
: null,
|
||||
copy: () {
|
||||
Clipboard.setData(ClipboardData(text: comment.body));
|
||||
showInfoToast(s.link_was_copied);
|
||||
},
|
||||
edit: (String text) {
|
||||
context.read<CommentsCubit>().updateComment(id: comment.id, body: text);
|
||||
},
|
||||
delete: () {
|
||||
context.read<CommentsCubit>().deleteComment(id: comment.id);
|
||||
},
|
||||
complain: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => ComplainingBottomSheet(
|
||||
complainedContentType: ComplainedContent.comment,
|
||||
id: comment.id,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
comment.authorName,
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
color: appTheme.gray,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
);
|
||||
},
|
||||
isMyComment: isMe,
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
decoration: BoxDecoration(
|
||||
color: widget.replyId == comment.id ? appTheme.blueA400.withValues(alpha: 0.1) : Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'profileScreen',
|
||||
extra: Author(
|
||||
authorSlug: comment.authorSlug,
|
||||
authorName: comment.authorName,
|
||||
authorProfileIcon: comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : baseUrl + comment.authorProfileIcon,
|
||||
authorIsMe: isMe,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: CircleAvatar(
|
||||
foregroundImage: NetworkImage(
|
||||
comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : baseUrl + comment.authorProfileIcon,
|
||||
),
|
||||
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
||||
radius: radius,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
SizedBox(
|
||||
width: isChild ? MediaQuery.of(context).size.width * 0.72 : MediaQuery.of(context).size.width * 0.8,
|
||||
child: Text(
|
||||
comment.body,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
comment.authorName,
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
color: appTheme.black,
|
||||
fontSize: 16,
|
||||
color: appTheme.gray,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
comment.createdAt.format('dd MMMM', 'ru'),
|
||||
const SizedBox(height: 4),
|
||||
SizedBox(
|
||||
width: isChild ? MediaQuery.of(context).size.width * 0.72 : MediaQuery.of(context).size.width * 0.8,
|
||||
child: Text(
|
||||
comment.body,
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
color: appTheme.gray,
|
||||
fontSize: 14,
|
||||
color: appTheme.black,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
if (!isChild)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context.read<CommentsCubit>().setReplyId(comment.id);
|
||||
},
|
||||
child: Text(
|
||||
s.answer,
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
color: appTheme.gray,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
comment.createdAt.format('dd MMMM', 'ru'),
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
color: appTheme.gray,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 16),
|
||||
if (!isChild)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context.read<CommentsCubit>().setReplyId(comment.id);
|
||||
},
|
||||
child: Text(
|
||||
s.answer,
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
color: appTheme.gray,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
class DietsWidget extends StatefulWidget {
|
||||
class DietsWidget extends StatefulWidget {
|
||||
final Diet diet;
|
||||
final int index;
|
||||
const DietsWidget({
|
||||
|
||||
@@ -231,7 +231,7 @@ class _RecipesWidgetState extends State<RecipesWidget> {
|
||||
final box = context.findRenderObject() as RenderBox?;
|
||||
SharePlus.instance.share(
|
||||
ShareParams(
|
||||
uri: Uri.parse('$baseUrl/ru/blog_post/${widget.recipe.id}'),
|
||||
uri: Uri.parse('$baseUrl/ru/recipe/${widget.recipe.id}'),
|
||||
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -31,9 +31,7 @@ class ConsultationsArchiveCubit extends Cubit<ConsultationsArchiveState> {
|
||||
dateComparison: state.periodFilterStatus == PeriodFilterStatus.all ? null : FiltersComparisonEnum.greaterThanOrEqual,
|
||||
stateComparison: state.consultationFilterStatus == ConsultationFilterStatus.all
|
||||
? null
|
||||
: state.consultationFilterStatus == ConsultationFilterStatus.inProgress
|
||||
? FiltersComparisonEnum.notInArray
|
||||
: FiltersComparisonEnum.equal,
|
||||
: FiltersComparisonEnum.equal,
|
||||
);
|
||||
emit(state.copyWith(consultations: consultations, isLoading: false));
|
||||
} catch (e) {
|
||||
@@ -67,20 +65,17 @@ class ConsultationsArchiveCubit extends Cubit<ConsultationsArchiveState> {
|
||||
enum ConsultationFilterStatus {
|
||||
all,
|
||||
canceled,
|
||||
finished,
|
||||
inProgress;
|
||||
finished;
|
||||
|
||||
String get name => switch (this) {
|
||||
ConsultationFilterStatus.all => 'Все консультации',
|
||||
ConsultationFilterStatus.canceled => 'Отменён',
|
||||
ConsultationFilterStatus.finished => 'Завершён',
|
||||
ConsultationFilterStatus.inProgress => 'В процессе',
|
||||
};
|
||||
|
||||
String? get value => switch (this) {
|
||||
ConsultationFilterStatus.all => null,
|
||||
ConsultationFilterStatus.canceled => 'canceled_by_the_initiator',
|
||||
ConsultationFilterStatus.finished => 'charge_was_collected',
|
||||
ConsultationFilterStatus.inProgress => 'active',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ abstract class ConsultationsArchiveState with _$ConsultationsArchiveState {
|
||||
@Default([]) List<ConsultationModel> consultations,
|
||||
@Default(false) isLoading,
|
||||
@Default(null) String? error,
|
||||
@Default(null) PeriodFilterStatus? periodFilterStatus,
|
||||
@Default(PeriodFilterStatus.all) PeriodFilterStatus? periodFilterStatus,
|
||||
@Default(null) ConsultationFilterStatus? consultationFilterStatus,
|
||||
required StatisticModel statisticModel,
|
||||
}) = _ConsultationsArchiveState;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user