forked from eatvio/eatvio_app
Compare commits
2 Commits
issue-642
...
notificati
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26ff548752 | ||
|
|
43e9098d8d |
@@ -11,30 +11,6 @@ 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: |
|
||||
mkdir -p env
|
||||
cp env_repo/.env env/.env
|
||||
|
||||
- name: Load environment variables from .env
|
||||
run: |
|
||||
set -a
|
||||
source env/.env
|
||||
set +a
|
||||
# Export all variables to GITHUB_ENV
|
||||
while IFS= read -r line; do
|
||||
if [[ $line =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then
|
||||
echo "$line" >> $GITHUB_ENV
|
||||
fi
|
||||
done < env/.env
|
||||
|
||||
- name: Unlock login keychain
|
||||
run: |
|
||||
@@ -42,17 +18,6 @@ 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
|
||||
@@ -66,8 +31,6 @@ 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 }}
|
||||
API_APPSTORE_KEY: ${{ vars.API_APPSTORE_KEY }}
|
||||
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ vars.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }}
|
||||
@@ -3,7 +3,7 @@ name: Eatvio App Android Test
|
||||
on: [ pull_request ]
|
||||
|
||||
env:
|
||||
flutter_version: "3.32.8"
|
||||
flutter_version: "3.32.5"
|
||||
java_version: "21.x"
|
||||
flutter_channel: "stable"
|
||||
my_local_cache_dir: "/opt/hostedtoolcache/"
|
||||
@@ -16,31 +16,6 @@ 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: |
|
||||
mkdir -p env
|
||||
cp env_repo/.env env/.env
|
||||
|
||||
- name: Load environment variables from .env
|
||||
run: |
|
||||
set -a
|
||||
source env/.env
|
||||
set +a
|
||||
# Export all variables to GITHUB_ENV
|
||||
while IFS= read -r line; do
|
||||
if [[ $line =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then
|
||||
echo "$line" >> $GITHUB_ENV
|
||||
fi
|
||||
done < env/.env
|
||||
|
||||
- name: Create key.properties file
|
||||
run: |
|
||||
cat > android/key.properties <<EOF
|
||||
@@ -144,11 +119,7 @@ jobs:
|
||||
run: flutter test
|
||||
|
||||
- name: Build apk
|
||||
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"
|
||||
run: flutter build apk --release
|
||||
|
||||
- name: Rename and move apk
|
||||
run: |
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -12,10 +12,5 @@
|
||||
/android/app/.cxx
|
||||
*.g.dart
|
||||
*.freezed.dart
|
||||
*.gr.dart
|
||||
*.mocks.dart
|
||||
android/build/
|
||||
ios/XCBuildData/
|
||||
.env
|
||||
env.example
|
||||
env/.env
|
||||
|
||||
84
ENV_SETUP.md
84
ENV_SETUP.md
@@ -1,84 +0,0 @@
|
||||
# Настройка переменных окружения
|
||||
|
||||
Этот проект использует переменные окружения для хранения секретных данных и конфигурации.
|
||||
|
||||
## Установка
|
||||
|
||||
1. Скопируйте файл `env.example` в `.env`:
|
||||
```bash
|
||||
cp env.example .env
|
||||
```
|
||||
|
||||
2. Отредактируйте файл `.env` и замените значения на ваши собственные секреты.
|
||||
|
||||
## Переменные окружения
|
||||
|
||||
### API URLs
|
||||
- `BASE_URL` - основной URL API (по умолчанию: https://eatvio.ru)
|
||||
- `DEV_BASE_URL` - URL API для разработки (по умолчанию: https://dev2.eatvio.com)
|
||||
|
||||
### Sentry Configuration
|
||||
- `SENTRY_DSN` - DSN для Sentry мониторинга
|
||||
|
||||
### Firebase Configuration
|
||||
- `FIREBASE_PROJECT_ID` - ID проекта Firebase
|
||||
- `FIREBASE_DATABASE_URL` - URL базы данных Firebase
|
||||
- `FIREBASE_STORAGE_BUCKET` - bucket для хранения файлов
|
||||
- `FIREBASE_MESSAGING_SENDER_ID` - ID отправителя сообщений
|
||||
|
||||
### Firebase Android Configuration
|
||||
- `FIREBASE_ANDROID_API_KEY` - API ключ для Android
|
||||
- `FIREBASE_ANDROID_APP_ID` - ID приложения для Android
|
||||
|
||||
### Firebase iOS Configuration
|
||||
- `FIREBASE_IOS_API_KEY` - API ключ для iOS
|
||||
- `FIREBASE_IOS_APP_ID` - ID приложения для iOS
|
||||
- `FIREBASE_IOS_BUNDLE_ID` - Bundle ID для iOS
|
||||
|
||||
### App Store URLs
|
||||
- `APP_STORE_URL` - URL App Store
|
||||
- `RUSTORE_URL` - URL RuStore
|
||||
|
||||
### Support URLs
|
||||
- `TELEGRAM_SUPPORT_URL` - URL поддержки в Telegram
|
||||
- `SUPPORT_EMAIL` - email для поддержки
|
||||
|
||||
### Test Credentials (только для разработки)
|
||||
- `TEST_EMAIL` - тестовый email
|
||||
- `TEST_PASSWORD` - тестовый пароль
|
||||
|
||||
### Email Validation API
|
||||
- `EMAIL_VALIDATION_API_URL` - URL API для валидации email
|
||||
|
||||
### Connection Check URLs
|
||||
- `GOOGLE_CHECK_URL` - URL для проверки соединения (Google)
|
||||
- `YANDEX_CHECK_URL` - URL для проверки соединения (Yandex)
|
||||
|
||||
## Безопасность
|
||||
|
||||
⚠️ **ВАЖНО**: Файл `.env` добавлен в `.gitignore` и не должен попадать в репозиторий. Все секретные данные должны храниться только в этом файле.
|
||||
|
||||
## Использование в коде
|
||||
|
||||
Для доступа к переменным окружения используйте класс `EnvConfig`:
|
||||
|
||||
```dart
|
||||
import 'package:eatvio_app/core/env_config.dart';
|
||||
|
||||
String baseUrl = EnvConfig.baseUrl;
|
||||
String sentryDsn = EnvConfig.sentryDsn;
|
||||
```
|
||||
|
||||
## Миграция
|
||||
|
||||
Все секреты были перенесены из кода в переменные окружения:
|
||||
|
||||
1. **Sentry DSN** - из `main.dart`
|
||||
2. **Firebase конфигурация** - из `firebase_options.dart`
|
||||
3. **API URLs** - из `const.dart`
|
||||
4. **URL для проверки соединения** - из `internet_connection_checker_screen.dart`
|
||||
5. **URL для обновления приложения** - из `update_screen.dart`
|
||||
6. **URL поддержки Telegram** - из `support_screen.dart`
|
||||
7. **API для валидации email** - из `registration_repository.dart`
|
||||
8. **Тестовые учетные данные** - из `test_cases.dart`
|
||||
9. **Email поддержки** - из `support_screen.dart`
|
||||
@@ -19,17 +19,17 @@ android {
|
||||
ndkVersion = "27.0.12077973"
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
sourceCompatibility = JavaVersion.VERSION_21
|
||||
targetCompatibility = JavaVersion.VERSION_21
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||
jvmTarget = 21
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.eatvioapp.app"
|
||||
minSdk = 23
|
||||
minSdk = flutter.minSdkVersion
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
|
||||
@@ -2,12 +2,9 @@
|
||||
|
||||
<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="Итвио"
|
||||
android:label="Eatvio"
|
||||
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=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -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.12-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-all.zip
|
||||
|
||||
@@ -22,7 +22,7 @@ plugins {
|
||||
// START: FlutterFire Configuration
|
||||
id "com.google.gms.google-services" version "4.4.2" apply false
|
||||
// END: FlutterFire Configuration
|
||||
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
|
||||
id "org.jetbrains.kotlin.android" version "2.0.21" apply false
|
||||
}
|
||||
|
||||
include ":app"
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<svg width="19" height="14" viewBox="0 0 19 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.56039 6.73215L7.06827 10.2925C7.22519 10.452 7.3026 10.6385 7.30049 10.8521C7.29855 11.0656 7.21569 11.2555 7.0519 11.4218C6.88812 11.577 6.70269 11.6573 6.49563 11.6629C6.28874 11.6682 6.1034 11.5879 5.93961 11.4218L1.19568 6.60653C1.0048 6.41278 0.909363 6.1868 0.909363 5.92858C0.909363 5.67037 1.0048 5.44439 1.19568 5.25064L5.93961 0.435368C6.08581 0.287154 6.26675 0.211261 6.48243 0.207689C6.69829 0.204297 6.88812 0.280189 7.0519 0.435368C7.21569 0.601618 7.29759 0.792601 7.29759 1.00831C7.29759 1.22385 7.21569 1.41474 7.0519 1.58099L3.56039 5.12501H13.4583C14.8441 5.12501 16.0261 5.62162 17.0045 6.61483C17.983 7.60787 18.4722 8.80769 18.4722 10.2143V13.1607C18.4722 13.3888 18.3964 13.5797 18.2448 13.7334C18.0933 13.8873 17.9052 13.9643 17.6806 13.9643C17.4559 13.9643 17.2678 13.8873 17.1164 13.7334C16.9647 13.5797 16.8889 13.3888 16.8889 13.1607V10.2143C16.8889 9.25269 16.5539 8.43197 15.884 7.75215C15.2143 7.07215 14.4057 6.73215 13.4583 6.73215H3.56039Z" fill="#2F80ED"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.377 11L15.3193 6.94225C15.1641 6.78709 15.0865 6.60892 15.0865 6.40775C15.0865 6.20642 15.1641 6.02817 15.3193 5.873C15.4744 5.72817 15.6542 5.65734 15.8587 5.6605C16.0631 5.66384 16.2345 5.73467 16.373 5.873L20.8673 10.3673C20.9609 10.4609 21.0269 10.5597 21.0653 10.6635C21.1038 10.7673 21.123 10.8795 21.123 11C21.123 11.1205 21.1038 11.2327 21.0653 11.3365C21.0269 11.4403 20.9609 11.5391 20.8673 11.6328L16.373 16.127C16.2178 16.282 16.0422 16.357 15.8462 16.352C15.6501 16.3468 15.4744 16.2718 15.3193 16.127C15.1641 15.9718 15.0856 15.7946 15.0837 15.5953C15.0817 15.3959 15.1551 15.2218 15.3038 15.073L19.377 11ZM14.0693 11.75H7.25C6.3525 11.75 5.5865 12.0673 4.952 12.702C4.31733 13.3365 4 14.1025 4 15V17.75C4 17.9628 3.92817 18.141 3.7845 18.2845C3.641 18.4282 3.46283 18.5 3.25 18.5C3.03717 18.5 2.859 18.4282 2.7155 18.2845C2.57183 18.141 2.5 17.9628 2.5 17.75V15C2.5 13.6872 2.9635 12.5673 3.8905 11.6405C4.81733 10.7135 5.93717 10.25 7.25 10.25H14.0693L10.7615 6.94225C10.6065 6.78709 10.529 6.60892 10.529 6.40775C10.529 6.20642 10.6065 6.02817 10.7615 5.873C10.9167 5.72817 11.0965 5.65734 11.301 5.6605C11.5055 5.66384 11.677 5.73467 11.8155 5.873L16.3095 10.3673C16.4032 10.4609 16.4692 10.5597 16.5078 10.6635C16.5463 10.7673 16.5655 10.8795 16.5655 11C16.5655 11.1205 16.5463 11.2327 16.5078 11.3365C16.4692 11.4403 16.4032 11.5391 16.3095 11.6328L11.8155 16.127C11.6603 16.282 11.4847 16.357 11.2885 16.352C11.0923 16.3468 10.9167 16.2718 10.7615 16.127C10.6065 15.9718 10.528 15.7946 10.526 15.5953C10.524 15.3959 10.5974 15.2218 10.7463 15.073L14.0693 11.75Z" fill="#2F80ED"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,3 +1,3 @@
|
||||
source "https://rubygems.org"
|
||||
|
||||
gem "fastlane", "~> 2.217"
|
||||
gem "fastlane"
|
||||
|
||||
@@ -213,14 +213,10 @@
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||
@@ -287,14 +283,10 @@
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||
@@ -396,7 +388,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 68;
|
||||
CURRENT_PROJECT_VERSION = 39;
|
||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
@@ -414,7 +406,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
MARKETING_VERSION = 1.2.3;
|
||||
MARKETING_VERSION = 1.0.50;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
|
||||
@@ -546,7 +538,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 68;
|
||||
CURRENT_PROJECT_VERSION = 39;
|
||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
@@ -564,7 +556,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
MARKETING_VERSION = 1.2.3;
|
||||
MARKETING_VERSION = 1.0.50;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
|
||||
@@ -584,7 +576,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 68;
|
||||
CURRENT_PROJECT_VERSION = 39;
|
||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
@@ -602,7 +594,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
MARKETING_VERSION = 1.2.3;
|
||||
MARKETING_VERSION = 1.0.50;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Итвио</string>
|
||||
<string>eatvio_app</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
@@ -21,7 +21,7 @@
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>68</string>
|
||||
<string>39</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
|
||||
@@ -18,68 +18,9 @@ default_platform(:ios)
|
||||
platform :ios do
|
||||
desc "Push a new beta build to TestFlight"
|
||||
lane :beta do
|
||||
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}"
|
||||
|
||||
match(type: "appstore", readonly: true)
|
||||
match(type: "appstore")
|
||||
increment_build_number(xcodeproj: "Runner.xcodeproj")
|
||||
|
||||
build_date = Time.now.getlocal("+03:00").strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
build_app(
|
||||
workspace: "Runner.xcworkspace",
|
||||
scheme: "Runner",
|
||||
export_method: "app-store",
|
||||
codesigning_identity: "Apple Distribution",
|
||||
xcargs: "--dart-define=HIDE_PLANS=true --dart-define=BUILD_DATE='#{build_date}'"
|
||||
)
|
||||
|
||||
|
||||
|
||||
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
|
||||
build_app(workspace: "Runner.xcworkspace", scheme: "Runner", export_method: "app-store", codesigning_identity: "Apple Distribution")
|
||||
upload_to_testflight
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,24 +5,22 @@
|
||||
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000227">
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000206">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: match" time="8.292217">
|
||||
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.195115">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: increment_build_number" time="0.201631">
|
||||
<testcase classname="fastlane.lanes" name="2: build_app" time="116.686863">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase 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 classname="fastlane.lanes" name="3: upload_to_testflight" time="338.642203">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
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';
|
||||
@@ -119,9 +121,7 @@ class AppRouter {
|
||||
GoRoute(
|
||||
path: 'forget-password-screen',
|
||||
name: 'forgetPasswordScreen',
|
||||
builder: (context, state) => ForgetPasswordScreen(
|
||||
isAuth: state.extra as bool,
|
||||
),
|
||||
builder: (context, state) => const ForgetPasswordScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'scan-qr-code-screen',
|
||||
@@ -205,9 +205,21 @@ class AppRouter {
|
||||
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()) {
|
||||
@@ -216,7 +228,7 @@ class AppRouter {
|
||||
return '/auth-screen';
|
||||
}
|
||||
} catch (e) {
|
||||
return '/feed-screen';
|
||||
rethrow;
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -361,10 +373,10 @@ class AppRouter {
|
||||
name: 'chatScreen',
|
||||
builder: (context, state) {
|
||||
final data = state.extra as Map<String, dynamic>;
|
||||
final uuid = data['uuid'] as String;
|
||||
final chatId = data['chatId'] as String;
|
||||
final sendInitialMessage = data['sendInitialMessage'] as bool?;
|
||||
return ChatScreen(
|
||||
uuid: uuid,
|
||||
chatId: chatId,
|
||||
sendInitialMessage: sendInitialMessage,
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'comment_update_model.freezed.dart';
|
||||
part 'comment_update_model.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class CommentUpdateModel with _$CommentUpdateModel {
|
||||
|
||||
factory CommentUpdateModel({required String body}) = _CommentUpdateModel;
|
||||
|
||||
factory CommentUpdateModel.fromJson(Map<String, dynamic> json) => _$CommentUpdateModelFromJson(json);
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/widgets/bottom_sheet_action_widget.dart';
|
||||
import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_divider.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
void showCommentsActionsBottomSheet(
|
||||
BuildContext context, {
|
||||
required void Function()? reply,
|
||||
required void Function() copy,
|
||||
required void Function(String text) edit,
|
||||
required void Function() delete,
|
||||
required void Function() complain,
|
||||
required String commentText,
|
||||
required bool isMyComment,
|
||||
}) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => Container(
|
||||
margin: EdgeInsets.only(left: 8, right: 8, bottom: MediaQuery.of(context).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,
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import 'package:eatvio_app/core/comments/models/comment_update_model.dart';
|
||||
import 'package:eatvio_app/core/comments/services/comments_service.dart';
|
||||
|
||||
class CommentsRepository {
|
||||
final CommentsService service;
|
||||
|
||||
CommentsRepository({required this.service});
|
||||
|
||||
Future<void> deleteComment({required int id}) async {
|
||||
try {
|
||||
return service.deleteComment(id);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateComment({required int id, required String body}) async {
|
||||
try {
|
||||
return service.updateComment(id, CommentUpdateModel(body: body));
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/comments/models/comment_update_model.dart';
|
||||
import 'package:retrofit/retrofit.dart';
|
||||
|
||||
part 'comments_service.g.dart';
|
||||
|
||||
@RestApi()
|
||||
abstract class CommentsService {
|
||||
factory CommentsService(Dio dio, {String baseUrl}) = _CommentsService;
|
||||
|
||||
@DELETE('/api/comments/{id}')
|
||||
Future<void> deleteComment(@Path('id') int id);
|
||||
|
||||
@PATCH('/api/comments/{id}')
|
||||
Future<void> updateComment(@Path('id') int id, @Body() CommentUpdateModel body);
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
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: '');
|
||||
String baseUrl = 'https://eatvio.ru';
|
||||
// String baseUrl = 'https://dev2.eatvio.com';
|
||||
|
||||
const List<int> durationValues = [7, 14, 21];
|
||||
|
||||
|
||||
@@ -1 +1,7 @@
|
||||
enum ComplainedContent { diet, plan, recipe, profile, posts, comment }
|
||||
enum ComplainedContent {
|
||||
diet,
|
||||
plan,
|
||||
recipe,
|
||||
profile,
|
||||
posts
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
|
||||
class EnvConfig {
|
||||
static String get baseUrl => dotenv.env['BASE_URL'] ?? '';
|
||||
static String get devBaseUrl => dotenv.env['DEV_BASE_URL'] ?? '';
|
||||
|
||||
// Sentry Configuration
|
||||
static String get sentryDsn => dotenv.env['SENTRY_DSN'] ?? '';
|
||||
|
||||
// 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,10 +1,3 @@
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/token_storage.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
|
||||
class FirebaseApi {
|
||||
@@ -14,22 +7,4 @@ 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,6 +1,4 @@
|
||||
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';
|
||||
@@ -58,7 +56,7 @@ import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
|
||||
import 'package:eatvio_app/features/diary/repositories/diary_repository.dart';
|
||||
import 'package:eatvio_app/features/diary/services/diary_service.dart';
|
||||
import 'package:eatvio_app/features/diets/presentation/cubit/about_diet_cubit.dart';
|
||||
import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/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';
|
||||
@@ -130,7 +128,7 @@ 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(), commentsRepository: sl()));
|
||||
sl.registerFactory<CommentsCubit>(() => CommentsCubit(feedRepository: 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()));
|
||||
@@ -200,7 +198,6 @@ 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()));
|
||||
|
||||
// Services
|
||||
sl.registerLazySingleton<ChatService>(() => ChatService(sl()));
|
||||
@@ -233,7 +230,6 @@ Future<void> init() async {
|
||||
sl.registerLazySingleton<FirebaseApi>(() => FirebaseApi());
|
||||
sl.registerLazySingleton<ResetPasswordService>(() => ResetPasswordService(sl()));
|
||||
sl.registerLazySingleton<BlockedUserService>(() => BlockedUserService(sl()));
|
||||
sl.registerLazySingleton<CommentsService>(() => CommentsService(sl()));
|
||||
|
||||
// External
|
||||
sl.registerLazySingleton(
|
||||
@@ -244,6 +240,7 @@ Future<void> init() async {
|
||||
'accept': 'application/json',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
validateStatus: (status) => status != null && status < 500,
|
||||
),
|
||||
)..interceptors.addAll(
|
||||
[
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
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';
|
||||
@@ -13,10 +11,6 @@ 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();
|
||||
|
||||
@@ -21,25 +21,3 @@ abstract class TokenStorage {
|
||||
|
||||
static Future<void> removeTokenInStorage() async => (await const FlutterSecureStorage().delete(key: 'token'));
|
||||
}
|
||||
|
||||
abstract class SocketIdStorage {
|
||||
static Future<bool> haveSocketIdInStorage() async {
|
||||
try {
|
||||
if (await const FlutterSecureStorage().read(key: 'socketId') == null) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<String?> getSocketIdFromStorage() async => await const FlutterSecureStorage().read(key: 'socketId');
|
||||
|
||||
static Future<void> saveSocketIdInStorage(String socketId) async {
|
||||
await const FlutterSecureStorage().write(key: 'socketId', value: socketId);
|
||||
}
|
||||
|
||||
static Future<void> removeSocketIdInStorage() async => (await const FlutterSecureStorage().delete(key: 'socketId'));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:dart_pusher_channels/dart_pusher_channels.dart';
|
||||
@@ -60,24 +59,6 @@ class WsManager {
|
||||
),
|
||||
);
|
||||
|
||||
String? extractSocketId(String? data) {
|
||||
if (data == null) return null;
|
||||
try {
|
||||
final decoded = jsonDecode(data);
|
||||
return decoded['socket_id'] as String?;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
client?.eventStream.listen((event) async {
|
||||
if (event.name == 'pusher:connection_established') {
|
||||
final data = event.data;
|
||||
final socketId = extractSocketId(data);
|
||||
await SocketIdStorage.saveSocketIdInStorage(socketId!);
|
||||
}
|
||||
});
|
||||
|
||||
await client?.connect();
|
||||
|
||||
client = client;
|
||||
@@ -96,8 +77,9 @@ class WsManager {
|
||||
EasyDebounce.debounce(
|
||||
'searchChatRoom',
|
||||
const Duration(milliseconds: 300),
|
||||
() => sl<ChatsCubit>().getChatRooms(),
|
||||
() => sl<ChatsCubit>().getChatRooms(),
|
||||
);
|
||||
log(event.rootObject.toString());
|
||||
});
|
||||
_subscribe();
|
||||
} catch (e, st) {
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
class BottomSheetActionWidget extends StatelessWidget {
|
||||
const BottomSheetActionWidget({super.key, required this.text, required this.iconPath, required this.onTap, this.needToPop = true, this.color});
|
||||
final String text;
|
||||
final String iconPath;
|
||||
final void Function() onTap;
|
||||
final bool needToPop;
|
||||
final Color? color;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (needToPop) Navigator.pop(context);
|
||||
onTap();
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(text, style: TextStyles.textLMedium.copyWith(color: appTheme.black)),
|
||||
),
|
||||
SvgPicture.asset(iconPath, colorFilter: color != null ? ColorFilter.mode(color!, BlendMode.srcIn) : null),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -26,13 +26,9 @@ class SuccessPublishedRecipeBottomSheet extends StatelessWidget {
|
||||
s.recipe_publishing,
|
||||
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),
|
||||
),
|
||||
Text(
|
||||
'"$title" ${s.successfully_published}',
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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';
|
||||
@@ -67,20 +66,19 @@ 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>(
|
||||
@@ -99,7 +97,11 @@ 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(
|
||||
@@ -130,16 +132,23 @@ 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', extra: false),
|
||||
onTap: () => GoRouter.of(context).goNamed('forgetPasswordScreen'),
|
||||
child: Text(
|
||||
'Забыли пароль?',
|
||||
style: TextStyle(color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline),
|
||||
style: TextStyle(
|
||||
color: appTheme.blueA400,
|
||||
decorationColor: appTheme.blueA400,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -152,7 +161,11 @@ 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -163,22 +176,23 @@ 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 '),
|
||||
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')),
|
||||
TextSpan(
|
||||
text: ' Публичной офертой',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: appTheme.blueA400,
|
||||
decorationColor: appTheme.blueA400,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
const TextSpan(text: ' и '),
|
||||
],
|
||||
recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')),
|
||||
),
|
||||
const TextSpan(text: ' и '),
|
||||
TextSpan(
|
||||
text: 'Политикой конфиденциальности\nи защитой персональных данных',
|
||||
style: TextStyle(
|
||||
@@ -204,12 +218,17 @@ 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);
|
||||
@@ -218,7 +237,10 @@ 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),
|
||||
@@ -230,7 +252,9 @@ 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
|
||||
@@ -245,10 +269,14 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
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 Text(
|
||||
'Где взять QR-код?\n',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'1. Войдите на сайте $baseUrl в свой аккаунт\n\n2. Перейдите в настройки профиля\n\n3. В разделе "Доп информация" перейдите на "QR Login"'),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
@@ -260,10 +288,15 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
},
|
||||
child: Ink(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Text(
|
||||
'Отмена',
|
||||
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w400),
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -276,10 +309,15 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
},
|
||||
child: Ink(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'Перейти',
|
||||
style: TextStyle(color: appTheme.blueA400, fontWeight: FontWeight.w600),
|
||||
style: TextStyle(
|
||||
color: appTheme.blueA400,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -294,7 +332,10 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
},
|
||||
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,8 +9,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class ForgetPasswordScreen extends StatefulWidget {
|
||||
final bool isAuth;
|
||||
const ForgetPasswordScreen({super.key, required this.isAuth});
|
||||
const ForgetPasswordScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ForgetPasswordScreen> createState() => _ForgetPasswordScreenState();
|
||||
@@ -30,7 +29,7 @@ class _ForgetPasswordScreenState extends State<ForgetPasswordScreen> {
|
||||
builder: (BuildContext context, ResetPasswordState state) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: widget.isAuth ? const Text('Смена пароля') : const Text('Восстановление пароля'),
|
||||
title: const Text('Смена пароля'),
|
||||
centerTitle: false,
|
||||
),
|
||||
body: state is ResetPasswordStateLoadingResetPassword || state is ResetPasswordStateLoadingSendEmailCode
|
||||
@@ -43,9 +42,9 @@ class _ForgetPasswordScreenState extends State<ForgetPasswordScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (state is ResetPasswordStateCodeSendError || state is ResetPasswordStateEmpty) ...[
|
||||
Text(
|
||||
'${widget.isAuth ? '' : 'Забыли пароль? '}Укажите свой email,\nи мы пришлём код для сброса.',
|
||||
style: const TextStyle(
|
||||
const Text(
|
||||
'Забыли пароль? Укажите свой email,\nи мы пришлём код для сброса.',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
@@ -157,7 +156,7 @@ class _ForgetPasswordScreenState extends State<ForgetPasswordScreen> {
|
||||
}
|
||||
if (state is ResetPasswordStateResetPassowrdSuccess) {
|
||||
GoRouter.of(context).pop();
|
||||
showSuccessToast('Пароль успешно сменён. ${widget.isAuth ? '' : 'Войдите в аккаунт'}');
|
||||
showSuccessToast('Пароль успешно сменён. Войдите в аккаунт');
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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/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/comments_cubit/comments_cubit.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_state.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
|
||||
@@ -15,11 +15,7 @@ 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,9 +22,13 @@ 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,4 +1,3 @@
|
||||
import 'package:eatvio_app/core/models/pagination_model.dart';
|
||||
import 'package:eatvio_app/features/chat/models/chat_message_model.dart';
|
||||
import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
@@ -9,18 +8,29 @@ part 'chat_room_pagination_model.g.dart';
|
||||
@freezed
|
||||
abstract class ChatRoomPaginationModel with _$ChatRoomPaginationModel {
|
||||
const factory ChatRoomPaginationModel({
|
||||
@JsonKey(name: "data") required List<ChatRoom> data,
|
||||
@JsonKey(name: "chatRooms") required ChatRooms chatRooms,
|
||||
@JsonKey(name: "number_of_unread_chat_rooms") required int unreadChatsCount,
|
||||
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,8 +6,11 @@ part 'chat_room_settings_model.g.dart';
|
||||
@freezed
|
||||
abstract class ChatRoomSettingsModel with _$ChatRoomSettingsModel {
|
||||
const factory ChatRoomSettingsModel({
|
||||
@JsonKey(name: 'room_name') required String name,
|
||||
required int id,
|
||||
@JsonKey(name: 'room_name') required String roomName,
|
||||
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,7 +4,6 @@ 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';
|
||||
@@ -20,7 +19,6 @@ import 'package:eatvio_app/features/my_finance/models/card_model.dart';
|
||||
import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart';
|
||||
import 'package:eatvio_app/features/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;
|
||||
@@ -33,9 +31,12 @@ 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();
|
||||
@@ -46,14 +47,14 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
_timer?.cancel();
|
||||
}
|
||||
|
||||
Future<void> loadNextPage(String uuid) async {
|
||||
Future<void> loadNextPage(String chatId) async {
|
||||
if (currentPage < lastPage && !isLoading) {
|
||||
isLoading = true;
|
||||
|
||||
emit((state as ChatRoomStateLoaded).copyWith(isLoading: isLoading));
|
||||
|
||||
try {
|
||||
final chatMessages = await chatRepository.getChatMessages(uuid, currentPage + 1);
|
||||
final chatMessages = await chatRepository.getChatMessages(chatId, currentPage + 1);
|
||||
|
||||
isLoading = false;
|
||||
emit(
|
||||
@@ -88,16 +89,17 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> initChat(String uuid, {bool sendInitialMessage = false}) async {
|
||||
Future<void> initChat(String chatId, {bool sendInitialMessage = false}) async {
|
||||
try {
|
||||
final chatRoom = await chatRepository.getChatRoom(uuid);
|
||||
final chatRoom = await chatRepository.getChatRoom(chatId);
|
||||
ChosenPlanModel? chosenPlan;
|
||||
if (chatRoom.chosenPlanId != null) {
|
||||
chosenPlan = await chatRepository.getChosenPlan(id: chatRoom.chosenPlanId!);
|
||||
}
|
||||
final chatMessages = await chatRepository.getChatMessages(uuid, currentPage);
|
||||
final chatMessages = await chatRepository.getChatMessages(chatId, currentPage);
|
||||
lastPage = chatMessages.meta.lastPage;
|
||||
await _subscribeToChatChannel(uuid: uuid, chatId: chatRoom.id);
|
||||
|
||||
await _subscribeToChatChannel(chatId: chatId);
|
||||
emit(
|
||||
ChatRoomState.loaded(
|
||||
chatRoom: chatRoom.copyWith(chosenPlan: chosenPlan),
|
||||
@@ -156,80 +158,43 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _subscribeToChatChannel({required String uuid, required String chatId}) async {
|
||||
Future<void> _subscribeToChatChannel({required String chatId}) async {
|
||||
final client = wsManager.client;
|
||||
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));
|
||||
final message = ChatMessageModel.fromJson(jsonDecode(event.data)['message']);
|
||||
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 {
|
||||
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,
|
||||
],
|
||||
),
|
||||
await chatRepository.sendMessage(
|
||||
data: data,
|
||||
files: files != null ? await ImageCompressUtility.compressFilesList(List<File>.from(files)) : null,
|
||||
slug: userSlug,
|
||||
);
|
||||
|
||||
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();
|
||||
AudioPlayer().play((AssetSource('audio/send_message.wav')));
|
||||
return true;
|
||||
} catch (e, st) {
|
||||
log(e.toString(), stackTrace: st);
|
||||
@@ -274,10 +239,7 @@ 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());
|
||||
@@ -305,70 +267,4 @@ 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,7 +29,6 @@ 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) {
|
||||
@@ -44,10 +43,10 @@ class ChatsCubit extends Cubit<ChatsState> {
|
||||
try {
|
||||
final chatRoomsPaginationModel = await chatRepository.getChatRooms(query: query);
|
||||
final unreadChats = chatRoomsPaginationModel.unreadChatsCount;
|
||||
lastPage = chatRoomsPaginationModel.meta.lastPage + 1;
|
||||
lastPage = chatRoomsPaginationModel.chatRooms.lastPage + 1;
|
||||
emit(
|
||||
state.copyWith(
|
||||
chatRooms: chatRoomsPaginationModel.data,
|
||||
chatRooms: chatRoomsPaginationModel.chatRooms.data,
|
||||
chatsWithUnreadMessagesCount: unreadChats,
|
||||
state: BaseState.success,
|
||||
),
|
||||
@@ -66,10 +65,10 @@ class ChatsCubit extends Cubit<ChatsState> {
|
||||
try {
|
||||
final chatRoomsPaginationModel = await chatRepository.getChatRooms(query: query);
|
||||
final unreadChats = chatRoomsPaginationModel.unreadChatsCount;
|
||||
lastPage = chatRoomsPaginationModel.meta.lastPage + 1;
|
||||
lastPage = chatRoomsPaginationModel.chatRooms.lastPage + 1;
|
||||
emit(
|
||||
state.copyWith(
|
||||
chatRooms: chatRoomsPaginationModel.data,
|
||||
chatRooms: chatRoomsPaginationModel.chatRooms.data,
|
||||
chatsWithUnreadMessagesCount: unreadChats,
|
||||
state: BaseState.success,
|
||||
),
|
||||
@@ -91,7 +90,7 @@ class ChatsCubit extends Cubit<ChatsState> {
|
||||
final unreadChats = chatRoomsPaginationModel.unreadChatsCount;
|
||||
emit(
|
||||
state.copyWith(
|
||||
chatRooms: [...state.chatRooms, ...chatRoomsPaginationModel.data],
|
||||
chatRooms: [...state.chatRooms, ...chatRoomsPaginationModel.chatRooms.data],
|
||||
chatsWithUnreadMessagesCount: unreadChats,
|
||||
state: BaseState.success,
|
||||
isLoadingMoreChats: false,
|
||||
|
||||
@@ -13,8 +13,6 @@ import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.da
|
||||
import 'package:eatvio_app/features/chat/presentation/widgets/about_order_button.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/widgets/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';
|
||||
@@ -34,10 +32,10 @@ import 'package:intl/intl.dart';
|
||||
import 'package:visibility_detector/visibility_detector.dart';
|
||||
|
||||
class ChatScreen extends StatefulWidget {
|
||||
final String uuid;
|
||||
final String chatId;
|
||||
final bool? sendInitialMessage;
|
||||
|
||||
const ChatScreen({required this.uuid, super.key, required this.sendInitialMessage});
|
||||
const ChatScreen({required this.chatId, super.key, required this.sendInitialMessage});
|
||||
|
||||
@override
|
||||
State<ChatScreen> createState() => _ChatScreenState();
|
||||
@@ -66,14 +64,14 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
scrollController.addListener(_onScroll);
|
||||
chatRoomCubit.initChat(widget.uuid, sendInitialMessage: widget.sendInitialMessage ?? false);
|
||||
chatRoomCubit.initChat(widget.chatId, 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.uuid);
|
||||
chatRoomCubit.loadNextPage(widget.chatId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,10 +93,7 @@ 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.uuid)),
|
||||
);
|
||||
return Scaffold(appBar: AppBar(), body: DataErrorWidget(onReload: () => chatRoomCubit.initChat(widget.chatId)));
|
||||
} else {
|
||||
return const Scaffold(body: Loader());
|
||||
}
|
||||
@@ -173,19 +168,34 @@ 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(),
|
||||
@@ -241,9 +251,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();
|
||||
});
|
||||
}
|
||||
@@ -254,9 +264,6 @@ 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -283,9 +290,6 @@ 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,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -357,7 +361,11 @@ 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(
|
||||
@@ -365,7 +373,14 @@ 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -382,8 +397,15 @@ 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -405,10 +427,7 @@ 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),
|
||||
@@ -430,9 +449,7 @@ 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),
|
||||
@@ -448,27 +465,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
const SizedBox(width: 8),
|
||||
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 == state.chatRoom.users.receiver.slug);
|
||||
if (isBlocked) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Вы не можете отправить сообщение заблокированному пользователю'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onTap: () {
|
||||
if (textEditingController.text.isNotEmpty || pickedFiles.isNotEmpty) {
|
||||
chatRoomCubit.sendMessage(
|
||||
data: textEditingController.text.isEmpty ? null : textEditingController.text.trim(),
|
||||
@@ -528,7 +525,13 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
const Icon(Icons.attach_file, size: 32, color: Colors.black54),
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -576,7 +579,13 @@ 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 {
|
||||
@@ -591,7 +600,13 @@ 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 {
|
||||
@@ -628,10 +643,20 @@ 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('Отмена'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -640,10 +665,7 @@ 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,8 +12,6 @@ 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;
|
||||
@@ -135,45 +133,21 @@ 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,
|
||||
);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
textEditingController.clear();
|
||||
});
|
||||
}
|
||||
setState(() {
|
||||
textEditingController.clear();
|
||||
});
|
||||
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
|
||||
if (context.mounted) {
|
||||
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
|
||||
'chatScreen',
|
||||
extra: {
|
||||
'uuid': chatIdOrError,
|
||||
},
|
||||
);
|
||||
}
|
||||
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
|
||||
'chatScreen',
|
||||
extra: {
|
||||
'chatId': chatIdOrError,
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Ink(
|
||||
@@ -201,44 +175,20 @@ 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();
|
||||
List<XFile>? images = await picker.pickMultiImage();
|
||||
if (images.isNotEmpty) {
|
||||
List<File> files = images.map((image) => File(image.path)).toList();
|
||||
XFile? image = await picker.pickImage(source: ImageSource.camera);
|
||||
if (image != null) {
|
||||
await sl<ChatRepository>().sendMessage(
|
||||
files: files,
|
||||
files: [File(image.path)],
|
||||
slug: widget.author.authorSlug,
|
||||
);
|
||||
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
|
||||
if (mounted) {
|
||||
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
|
||||
'chatScreen',
|
||||
extra: {
|
||||
'uuid': chatIdOrError,
|
||||
},
|
||||
);
|
||||
}
|
||||
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
|
||||
'chatScreen',
|
||||
extra: {
|
||||
'chatId': chatIdOrError,
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Row(
|
||||
@@ -261,14 +211,12 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
|
||||
slug: widget.author.authorSlug,
|
||||
);
|
||||
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
|
||||
if (mounted) {
|
||||
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
|
||||
'chatScreen',
|
||||
extra: {
|
||||
'uuid': chatIdOrError,
|
||||
},
|
||||
);
|
||||
}
|
||||
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
|
||||
'chatScreen',
|
||||
extra: {
|
||||
'chatId': chatIdOrError,
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Row(
|
||||
@@ -313,14 +261,12 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
|
||||
slug: widget.author.authorSlug,
|
||||
);
|
||||
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
|
||||
if (mounted) {
|
||||
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
|
||||
'chatScreen',
|
||||
extra: {
|
||||
'uuid': chatIdOrError,
|
||||
},
|
||||
);
|
||||
}
|
||||
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
|
||||
'chatScreen',
|
||||
extra: {
|
||||
'chatId': chatIdOrError,
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Row(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
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';
|
||||
@@ -9,7 +11,7 @@ import 'package:photo_view/photo_view_gallery.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class PhotoViewScreen extends StatefulWidget {
|
||||
final List<dynamic> imageUrls;
|
||||
final List<String> imageUrls;
|
||||
final int index;
|
||||
final bool isLocal;
|
||||
|
||||
@@ -48,12 +50,14 @@ 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: imageProvider,
|
||||
imageProvider: widget.isLocal
|
||||
? FileImage(
|
||||
File(widget.imageUrls[index]),
|
||||
) as ImageProvider
|
||||
: CachedNetworkImageProvider(
|
||||
widget.imageUrls[index],
|
||||
),
|
||||
initialScale: PhotoViewComputedScale.contained,
|
||||
minScale: PhotoViewComputedScale.contained,
|
||||
heroAttributes: PhotoViewHeroAttributes(
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
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 {
|
||||
@@ -22,9 +15,6 @@ 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;
|
||||
@@ -32,9 +22,7 @@ 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);
|
||||
@@ -48,33 +36,17 @@ class ChatBubbleWidget extends StatelessWidget {
|
||||
mainAxisAlignment: isCurrentUser ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||||
children: [
|
||||
if (isCurrentUser)
|
||||
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)),
|
||||
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.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(
|
||||
@@ -87,9 +59,8 @@ 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: [
|
||||
@@ -113,19 +84,32 @@ class ChatBubbleWidget extends StatelessWidget {
|
||||
throw Exception('Could not launch ${mediaData[index].toString()}');
|
||||
}
|
||||
},
|
||||
leading: const Icon(Icons.file_copy_outlined, color: Colors.black),
|
||||
trailing: const Icon(Icons.download_outlined, color: Colors.black),
|
||||
leading: const Icon(
|
||||
Icons.file_copy_outlined,
|
||||
color: Colors.white,
|
||||
),
|
||||
trailing: const Icon(
|
||||
Icons.download_outlined,
|
||||
color: Colors.white,
|
||||
),
|
||||
title: Text(
|
||||
mediaData[index].toString().replaceAll('chat_files/', ''),
|
||||
maxLines: 2,
|
||||
style: const TextStyle(color: Colors.black, fontSize: 12, overflow: TextOverflow.ellipsis),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
context.pushTransparentRoute(
|
||||
PhotoViewScreen(imageUrls: mediaData, index: index, isLocal: mediaData[index] is File ? true : false),
|
||||
PhotoViewScreen(
|
||||
imageUrls: mediaData.map((e) => '$e').toList(),
|
||||
index: index,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Hero(
|
||||
@@ -133,7 +117,9 @@ class ChatBubbleWidget extends StatelessWidget {
|
||||
transitionOnUserGestures: true,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: mediaData[index] is File ? Image.file(mediaData[index]) : CachedNetworkImage(imageUrl: '${mediaData[index]}'),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: '${mediaData[index]}',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -145,7 +131,11 @@ class ChatBubbleWidget extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
if (mediaData.isNotEmpty && text.isNotEmpty) const SizedBox(height: 8),
|
||||
if (text.isNotEmpty) Text(text, style: TextStyles.textFS13FW400.copyWith(color: appTheme.black, fontSize: 16)),
|
||||
if (text.isNotEmpty)
|
||||
Text(
|
||||
text,
|
||||
style: TextStyles.textFS13FW400.copyWith(color: appTheme.black, fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -155,7 +145,12 @@ class ChatBubbleWidget extends StatelessWidget {
|
||||
padding: const EdgeInsets.only(left: 4, bottom: 4),
|
||||
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: {
|
||||
'uuid': widget.chatRoom.id.toString(),
|
||||
'chatId': widget.chatRoom.id.toString(),
|
||||
},
|
||||
),
|
||||
child: Container(
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/widgets/bottom_sheet_action_widget.dart';
|
||||
import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_divider.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void showMessageActionsBottomSheet(BuildContext context, {required void Function() resend, required void Function() delete}) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => Container(
|
||||
margin: EdgeInsets.only(left: 8, right: 8, bottom: MediaQuery.of(context).padding.bottom + 20),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: appTheme.whiteA700),
|
||||
child: MessageActionsBottomSheet(resend: resend, delete: delete),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class MessageActionsBottomSheet extends StatefulWidget {
|
||||
const MessageActionsBottomSheet({super.key, required this.resend, required this.delete});
|
||||
final void Function() resend;
|
||||
final void Function() delete;
|
||||
|
||||
@override
|
||||
State<MessageActionsBottomSheet> createState() => _MessageActionsBottomSheet();
|
||||
}
|
||||
|
||||
class _MessageActionsBottomSheet extends State<MessageActionsBottomSheet> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 7),
|
||||
const BottomSheetHandlerBar(),
|
||||
BottomSheetActionWidget(text: s.resend, iconPath: 'assets/icons/resend.svg', onTap: widget.resend),
|
||||
const CustomDivider(),
|
||||
BottomSheetActionWidget(text: s.delete, iconPath: 'assets/icons/trash_can.svg', onTap: widget.delete, color: appTheme.red),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ 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';
|
||||
@@ -48,11 +47,10 @@ class ChatRepository {
|
||||
}
|
||||
}
|
||||
|
||||
await chatService.sendMessage(
|
||||
slug: slug,
|
||||
await chatService.sendMessageByUserSlug(
|
||||
userSlug: slug,
|
||||
data: data,
|
||||
mediaData: multipartFiles.isEmpty ? null : multipartFiles,
|
||||
socketId: (await SocketIdStorage.getSocketIdFromStorage())!,
|
||||
);
|
||||
} catch (e, st) {
|
||||
log(e.toString(), stackTrace: st);
|
||||
@@ -60,11 +58,10 @@ class ChatRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<ChatRoomModel> getChatRoom(String uuid) async {
|
||||
Future<ChatRoomModel> getChatRoom(String chatId) async {
|
||||
try {
|
||||
return (await chatService.getChatRoom(uuid: uuid));
|
||||
} catch (e, st) {
|
||||
log(e.toString(), stackTrace: st);
|
||||
return (await chatService.getChatRoom(id: chatId))['chatRoom']!;
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -77,9 +74,9 @@ class ChatRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<ChatMessagesModel> getChatMessages(String id, int page) async {
|
||||
Future<ChatMessagesModel> getChatMessages(String chatId, int page) async {
|
||||
try {
|
||||
return (await chatService.getChatMessages(id: id, page: page));
|
||||
return (await chatService.getChatMessages(id: chatId, page: page));
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
@@ -93,9 +90,9 @@ class ChatRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> markMessagesAsRead(String id, List<int> messageIds) async {
|
||||
Future<void> markMessagesAsRead(String chatId, List<int> messageIds) async {
|
||||
try {
|
||||
await chatService.markMessagesAsRead(id: id, body: {'messages_ids': messageIds});
|
||||
await chatService.markMessagesAsRead(id: chatId, body: {'messages_ids': messageIds});
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
|
||||
@@ -13,33 +13,32 @@ part 'chat_service.g.dart';
|
||||
abstract class ChatService {
|
||||
factory ChatService(Dio dio, {String baseUrl}) = _ChatService;
|
||||
|
||||
@GET('/api/v2/chat/index')
|
||||
@GET('/api/chat/index')
|
||||
Future<ChatRoomPaginationModel> getChatRooms({
|
||||
@Query('page') required int page,
|
||||
@Query('filters[data][\$conversation_name]') String? query,
|
||||
@Query('find') String? query,
|
||||
});
|
||||
|
||||
@GET('/api/v2/chat/show/{uuid}')
|
||||
Future<ChatRoomModel> getChatRoom({
|
||||
@Path() required String uuid,
|
||||
@GET('/api/chat/show/{id}')
|
||||
Future<Map<String, ChatRoomModel>> getChatRoom({
|
||||
@Path() required String id,
|
||||
});
|
||||
|
||||
@POST('/api/v2/chat/message/send/{slug}')
|
||||
@POST('/api/v2/chat/message/send/{id}')
|
||||
@MultiPart()
|
||||
Future<void> sendMessage({
|
||||
@Header('X-Socket-ID') required String socketId,
|
||||
@Path() required String slug,
|
||||
@Path() required String id,
|
||||
@Part() String? data,
|
||||
@Part(name: 'media_data[]', contentType: 'multipart/form-data') List<MultipartFile>? mediaData,
|
||||
});
|
||||
|
||||
@GET('/api/v2/chat/show/{id}/messages?page={page}')
|
||||
@GET('/api/chat/show/{id}/messages?page={page}')
|
||||
Future<ChatMessagesModel> getChatMessages({
|
||||
@Path() required String id,
|
||||
@Path() required int page,
|
||||
});
|
||||
|
||||
@GET('/api/v2/chat/get_by_user/{user}')
|
||||
@GET('/api/chat/get_by_user/{user}')
|
||||
Future<ChatRoomModel> getChatRoomIdByUserSlug({
|
||||
@Path() required String user,
|
||||
});
|
||||
@@ -82,8 +81,16 @@ abstract class ChatService {
|
||||
@Body() required PlanComplainModel body,
|
||||
});
|
||||
|
||||
@GET('/api/chat/get_by_user/{slug}')
|
||||
@GET('/api/v2/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,7 +9,6 @@ import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
|
||||
import 'package:eatvio_app/core/widgets/loader.dart';
|
||||
import 'package:eatvio_app/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';
|
||||
@@ -195,74 +194,63 @@ class _LawViolationWidgetState extends State<_LawViolationWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
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);
|
||||
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);
|
||||
});
|
||||
},
|
||||
),
|
||||
if ((state.complainInfo.description?.length ?? 10) < 10) ...[
|
||||
const SizedBox(height: 4),
|
||||
const ValidationErrorWidget(error: 'Минимум 10 символов'),
|
||||
]
|
||||
Text(
|
||||
element.$2,
|
||||
style: TextStyles.textLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
_ComplainButton(
|
||||
isDisabled: (state.complainInfo.description?.length ?? 0) < 10,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
if (tags.last.$1) ...[
|
||||
const SizedBox(height: 12),
|
||||
CustomTextFormField(
|
||||
hintText: s.type_here_your_complaint,
|
||||
onChanged: (value) {
|
||||
context.read<ComplainingCubit>().setDescription(value);
|
||||
},
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
const _ComplainButton(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -527,8 +515,8 @@ class _ComplaintFailedWidget extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _ComplainButton extends StatelessWidget {
|
||||
const _ComplainButton({this.isDisabled = false});
|
||||
final bool isDisabled;
|
||||
const _ComplainButton();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
@@ -546,10 +534,6 @@ class _ComplainButton extends StatelessWidget {
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
|
||||
),
|
||||
onPressed: () async {
|
||||
if (isDisabled) {
|
||||
context.read<ComplainingCubit>().setDescription('');
|
||||
return;
|
||||
}
|
||||
await context.read<ComplainingCubit>().complain();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -72,12 +72,6 @@ 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,16 +64,4 @@ 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,12 +42,5 @@ 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,16 +25,24 @@ 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: Text(
|
||||
hidePlans
|
||||
? 'Я ознакомлен и принимаю политику конфиденциальности и защиту персональных данных'
|
||||
: 'Я ознакомлен и принимаю договор публичной офёрты',
|
||||
style: const TextStyle(fontSize: 18, height: 1),
|
||||
title: const Text(
|
||||
'Я ознакомлен и принимаю договор публичной офёрты',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
onChanged: (_) {
|
||||
setState(() {
|
||||
@@ -45,14 +53,25 @@ 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(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),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -65,7 +84,10 @@ 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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,10 @@ 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();
|
||||
@@ -29,7 +32,13 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
|
||||
'Дневник заполнен, прокомментирован консультантом и после этого клиент просмотрел комментарии.',
|
||||
];
|
||||
|
||||
final List<Color> legendColors = const [Colors.white, Color(0xFFF0C497), Color(0xFFFF9500), Color(0xFF2F80ED), Color(0xFF4CD964)];
|
||||
final List<Color> legendColors = const [
|
||||
Colors.white,
|
||||
Color(0xFFF0C497),
|
||||
Color(0xFFFF9500),
|
||||
Color(0xFF2F80ED),
|
||||
Color(0xFF4CD964),
|
||||
];
|
||||
|
||||
late final List<(int, DiaryModel)> givenDaysWithDayNumber;
|
||||
final List<List<DiaryStateEnum>> legendStates = [
|
||||
@@ -37,7 +46,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() {
|
||||
@@ -63,19 +72,32 @@ 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -105,7 +127,12 @@ 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: () {
|
||||
@@ -122,7 +149,12 @@ 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(
|
||||
@@ -138,7 +170,11 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Icon(Icons.help_outline_rounded, color: Color(0xFF323F4B), size: 20),
|
||||
child: const Icon(
|
||||
Icons.help_outline_rounded,
|
||||
color: Color(0xFF323F4B),
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -149,8 +185,7 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
|
||||
children: List.generate(widget.consultationsModel.duration, (index) {
|
||||
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),
|
||||
@@ -160,74 +195,81 @@ 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -238,7 +280,11 @@ 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(
|
||||
@@ -247,26 +293,36 @@ 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: {
|
||||
'diaryId': lastActiveDay.$2.id,
|
||||
'currentDiaryDay': lastActiveDay.$1,
|
||||
});
|
||||
GoRouter.of(context).push(UserDiaryScreen.path, extra: lastActiveDay.$2.id);
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -278,8 +334,16 @@ 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -297,13 +361,21 @@ 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,7 +50,6 @@ 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);
|
||||
@@ -78,8 +77,7 @@ class ConsultationService {
|
||||
final response = await dio.get(
|
||||
'/api/finances/my_finances',
|
||||
queryParameters: {
|
||||
if (date != null && dateComparison != null) 'filters[start_date][${dateComparison.value}]': date,
|
||||
'sort[]': 'start_date:desc',
|
||||
if (dateComparison != null && date != null) 'filters[end_date]': date,
|
||||
},
|
||||
);
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
@@ -98,8 +96,7 @@ class ConsultationService {
|
||||
final response = await dio.get(
|
||||
'/api/finances/my_purchases',
|
||||
queryParameters: {
|
||||
if (date != null && dateComparison != null) 'filters[start_date][${dateComparison.value}]': date,
|
||||
'sort[]': 'start_date:desc',
|
||||
if (dateComparison != null && date != null) 'filters[start_date]': date,
|
||||
},
|
||||
);
|
||||
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,7 +9,6 @@ import 'package:eatvio_app/core/utils/toasts.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_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';
|
||||
|
||||
@@ -98,15 +97,10 @@ 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(() {
|
||||
@@ -122,13 +116,13 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
CustomOutlinedButton(
|
||||
text: baseUrl == EnvConfig.devBaseUrl ? 'Set production base url' : 'Set dev2 base url',
|
||||
text: baseUrl == 'https://dev2.eatvio.com' ? 'Set production base url' : 'Set dev2 base url',
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
if (baseUrl == EnvConfig.baseUrl) {
|
||||
baseUrlForTest = EnvConfig.devBaseUrl;
|
||||
if (baseUrl == 'https://eatvio.ru') {
|
||||
baseUrlForTest = 'https://dev2.eatvio.com';
|
||||
} else {
|
||||
baseUrlForTest = EnvConfig.baseUrl;
|
||||
baseUrlForTest = 'https://eatvio.ru';
|
||||
}
|
||||
baseUrl = baseUrlForTest.toLowerCase();
|
||||
sl<Dio>().options.baseUrl = baseUrl;
|
||||
@@ -138,7 +132,7 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
|
||||
buttonStyle: const ButtonStyle(),
|
||||
),
|
||||
SizedBox(height: baseUrlForTest != baseUrl ? 40 : 10),
|
||||
if (baseUrl.toLowerCase() != EnvConfig.baseUrl.toLowerCase())
|
||||
if (baseUrl.toLowerCase() != 'https://eatvio.ru')
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: testCases.length,
|
||||
@@ -181,7 +175,12 @@ 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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -198,7 +197,12 @@ 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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -212,10 +216,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,7 +35,6 @@ import 'package:eatvio_app/features/recipe/services/about_recipe_service.dart';
|
||||
import 'package:eatvio_app/features/recipe/services/my_recipes_service.dart';
|
||||
import 'package:eatvio_app/features/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;
|
||||
@@ -478,9 +477,7 @@ 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: CredentialsModel(email: EnvConfig.testEmail, password: EnvConfig.testPassword),
|
||||
);
|
||||
await authService.getTokenByCredentials(credentials: const CredentialsModel(email: 'reshetnick.mike+user1@gmail.com', password: '1234567890'));
|
||||
return TestResult(false, 'Should not succeed');
|
||||
} catch (e) {
|
||||
return TestResult(true, 'Exception expected for dummy credentials');
|
||||
|
||||
784
lib/features/diary/models/consultant_review_model.freezed.dart
Normal file
784
lib/features/diary/models/consultant_review_model.freezed.dart
Normal file
@@ -0,0 +1,784 @@
|
||||
// dart format width=80
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'consultant_review_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$ConsultantReviewModel {
|
||||
String get planName;
|
||||
int get planDuration;
|
||||
int get currentPlanDay;
|
||||
ConsultantComments get consultantComments;
|
||||
|
||||
/// Create a copy of ConsultantReviewModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$ConsultantReviewModelCopyWith<ConsultantReviewModel> get copyWith =>
|
||||
_$ConsultantReviewModelCopyWithImpl<ConsultantReviewModel>(
|
||||
this as ConsultantReviewModel, _$identity);
|
||||
|
||||
/// Serializes this ConsultantReviewModel to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is ConsultantReviewModel &&
|
||||
(identical(other.planName, planName) ||
|
||||
other.planName == planName) &&
|
||||
(identical(other.planDuration, planDuration) ||
|
||||
other.planDuration == planDuration) &&
|
||||
(identical(other.currentPlanDay, currentPlanDay) ||
|
||||
other.currentPlanDay == currentPlanDay) &&
|
||||
(identical(other.consultantComments, consultantComments) ||
|
||||
other.consultantComments == consultantComments));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType, planName, planDuration, currentPlanDay, consultantComments);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ConsultantReviewModel(planName: $planName, planDuration: $planDuration, currentPlanDay: $currentPlanDay, consultantComments: $consultantComments)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $ConsultantReviewModelCopyWith<$Res> {
|
||||
factory $ConsultantReviewModelCopyWith(ConsultantReviewModel value,
|
||||
$Res Function(ConsultantReviewModel) _then) =
|
||||
_$ConsultantReviewModelCopyWithImpl;
|
||||
@useResult
|
||||
$Res call(
|
||||
{String planName,
|
||||
int planDuration,
|
||||
int currentPlanDay,
|
||||
ConsultantComments consultantComments});
|
||||
|
||||
$ConsultantCommentsCopyWith<$Res> get consultantComments;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$ConsultantReviewModelCopyWithImpl<$Res>
|
||||
implements $ConsultantReviewModelCopyWith<$Res> {
|
||||
_$ConsultantReviewModelCopyWithImpl(this._self, this._then);
|
||||
|
||||
final ConsultantReviewModel _self;
|
||||
final $Res Function(ConsultantReviewModel) _then;
|
||||
|
||||
/// Create a copy of ConsultantReviewModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? planName = null,
|
||||
Object? planDuration = null,
|
||||
Object? currentPlanDay = null,
|
||||
Object? consultantComments = null,
|
||||
}) {
|
||||
return _then(_self.copyWith(
|
||||
planName: null == planName
|
||||
? _self.planName
|
||||
: planName // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
planDuration: null == planDuration
|
||||
? _self.planDuration
|
||||
: planDuration // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
currentPlanDay: null == currentPlanDay
|
||||
? _self.currentPlanDay
|
||||
: currentPlanDay // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
consultantComments: null == consultantComments
|
||||
? _self.consultantComments
|
||||
: consultantComments // ignore: cast_nullable_to_non_nullable
|
||||
as ConsultantComments,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of ConsultantReviewModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$ConsultantCommentsCopyWith<$Res> get consultantComments {
|
||||
return $ConsultantCommentsCopyWith<$Res>(_self.consultantComments, (value) {
|
||||
return _then(_self.copyWith(consultantComments: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _ConsultantReviewModel implements ConsultantReviewModel {
|
||||
_ConsultantReviewModel(
|
||||
{required this.planName,
|
||||
required this.planDuration,
|
||||
required this.currentPlanDay,
|
||||
required this.consultantComments});
|
||||
factory _ConsultantReviewModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$ConsultantReviewModelFromJson(json);
|
||||
|
||||
@override
|
||||
final String planName;
|
||||
@override
|
||||
final int planDuration;
|
||||
@override
|
||||
final int currentPlanDay;
|
||||
@override
|
||||
final ConsultantComments consultantComments;
|
||||
|
||||
/// Create a copy of ConsultantReviewModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$ConsultantReviewModelCopyWith<_ConsultantReviewModel> get copyWith =>
|
||||
__$ConsultantReviewModelCopyWithImpl<_ConsultantReviewModel>(
|
||||
this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$ConsultantReviewModelToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _ConsultantReviewModel &&
|
||||
(identical(other.planName, planName) ||
|
||||
other.planName == planName) &&
|
||||
(identical(other.planDuration, planDuration) ||
|
||||
other.planDuration == planDuration) &&
|
||||
(identical(other.currentPlanDay, currentPlanDay) ||
|
||||
other.currentPlanDay == currentPlanDay) &&
|
||||
(identical(other.consultantComments, consultantComments) ||
|
||||
other.consultantComments == consultantComments));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType, planName, planDuration, currentPlanDay, consultantComments);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ConsultantReviewModel(planName: $planName, planDuration: $planDuration, currentPlanDay: $currentPlanDay, consultantComments: $consultantComments)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$ConsultantReviewModelCopyWith<$Res>
|
||||
implements $ConsultantReviewModelCopyWith<$Res> {
|
||||
factory _$ConsultantReviewModelCopyWith(_ConsultantReviewModel value,
|
||||
$Res Function(_ConsultantReviewModel) _then) =
|
||||
__$ConsultantReviewModelCopyWithImpl;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{String planName,
|
||||
int planDuration,
|
||||
int currentPlanDay,
|
||||
ConsultantComments consultantComments});
|
||||
|
||||
@override
|
||||
$ConsultantCommentsCopyWith<$Res> get consultantComments;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$ConsultantReviewModelCopyWithImpl<$Res>
|
||||
implements _$ConsultantReviewModelCopyWith<$Res> {
|
||||
__$ConsultantReviewModelCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _ConsultantReviewModel _self;
|
||||
final $Res Function(_ConsultantReviewModel) _then;
|
||||
|
||||
/// Create a copy of ConsultantReviewModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$Res call({
|
||||
Object? planName = null,
|
||||
Object? planDuration = null,
|
||||
Object? currentPlanDay = null,
|
||||
Object? consultantComments = null,
|
||||
}) {
|
||||
return _then(_ConsultantReviewModel(
|
||||
planName: null == planName
|
||||
? _self.planName
|
||||
: planName // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
planDuration: null == planDuration
|
||||
? _self.planDuration
|
||||
: planDuration // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
currentPlanDay: null == currentPlanDay
|
||||
? _self.currentPlanDay
|
||||
: currentPlanDay // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
consultantComments: null == consultantComments
|
||||
? _self.consultantComments
|
||||
: consultantComments // ignore: cast_nullable_to_non_nullable
|
||||
as ConsultantComments,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of ConsultantReviewModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$ConsultantCommentsCopyWith<$Res> get consultantComments {
|
||||
return $ConsultantCommentsCopyWith<$Res>(_self.consultantComments, (value) {
|
||||
return _then(_self.copyWith(consultantComments: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$ConsultantComments {
|
||||
String? get dayComment;
|
||||
List<MealComment> get mealsComments;
|
||||
List<PortionComment> get portionsComments;
|
||||
|
||||
/// Create a copy of ConsultantComments
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$ConsultantCommentsCopyWith<ConsultantComments> get copyWith =>
|
||||
_$ConsultantCommentsCopyWithImpl<ConsultantComments>(
|
||||
this as ConsultantComments, _$identity);
|
||||
|
||||
/// Serializes this ConsultantComments to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is ConsultantComments &&
|
||||
(identical(other.dayComment, dayComment) ||
|
||||
other.dayComment == dayComment) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other.mealsComments, mealsComments) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other.portionsComments, portionsComments));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
dayComment,
|
||||
const DeepCollectionEquality().hash(mealsComments),
|
||||
const DeepCollectionEquality().hash(portionsComments));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ConsultantComments(dayComment: $dayComment, mealsComments: $mealsComments, portionsComments: $portionsComments)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $ConsultantCommentsCopyWith<$Res> {
|
||||
factory $ConsultantCommentsCopyWith(
|
||||
ConsultantComments value, $Res Function(ConsultantComments) _then) =
|
||||
_$ConsultantCommentsCopyWithImpl;
|
||||
@useResult
|
||||
$Res call(
|
||||
{String? dayComment,
|
||||
List<MealComment> mealsComments,
|
||||
List<PortionComment> portionsComments});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$ConsultantCommentsCopyWithImpl<$Res>
|
||||
implements $ConsultantCommentsCopyWith<$Res> {
|
||||
_$ConsultantCommentsCopyWithImpl(this._self, this._then);
|
||||
|
||||
final ConsultantComments _self;
|
||||
final $Res Function(ConsultantComments) _then;
|
||||
|
||||
/// Create a copy of ConsultantComments
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? dayComment = freezed,
|
||||
Object? mealsComments = null,
|
||||
Object? portionsComments = null,
|
||||
}) {
|
||||
return _then(_self.copyWith(
|
||||
dayComment: freezed == dayComment
|
||||
? _self.dayComment
|
||||
: dayComment // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
mealsComments: null == mealsComments
|
||||
? _self.mealsComments
|
||||
: mealsComments // ignore: cast_nullable_to_non_nullable
|
||||
as List<MealComment>,
|
||||
portionsComments: null == portionsComments
|
||||
? _self.portionsComments
|
||||
: portionsComments // ignore: cast_nullable_to_non_nullable
|
||||
as List<PortionComment>,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _ConsultantComments implements ConsultantComments {
|
||||
_ConsultantComments(
|
||||
{required this.dayComment,
|
||||
required final List<MealComment> mealsComments,
|
||||
required final List<PortionComment> portionsComments})
|
||||
: _mealsComments = mealsComments,
|
||||
_portionsComments = portionsComments;
|
||||
factory _ConsultantComments.fromJson(Map<String, dynamic> json) =>
|
||||
_$ConsultantCommentsFromJson(json);
|
||||
|
||||
@override
|
||||
final String? dayComment;
|
||||
final List<MealComment> _mealsComments;
|
||||
@override
|
||||
List<MealComment> get mealsComments {
|
||||
if (_mealsComments is EqualUnmodifiableListView) return _mealsComments;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_mealsComments);
|
||||
}
|
||||
|
||||
final List<PortionComment> _portionsComments;
|
||||
@override
|
||||
List<PortionComment> get portionsComments {
|
||||
if (_portionsComments is EqualUnmodifiableListView)
|
||||
return _portionsComments;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_portionsComments);
|
||||
}
|
||||
|
||||
/// Create a copy of ConsultantComments
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$ConsultantCommentsCopyWith<_ConsultantComments> get copyWith =>
|
||||
__$ConsultantCommentsCopyWithImpl<_ConsultantComments>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$ConsultantCommentsToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _ConsultantComments &&
|
||||
(identical(other.dayComment, dayComment) ||
|
||||
other.dayComment == dayComment) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._mealsComments, _mealsComments) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._portionsComments, _portionsComments));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
dayComment,
|
||||
const DeepCollectionEquality().hash(_mealsComments),
|
||||
const DeepCollectionEquality().hash(_portionsComments));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ConsultantComments(dayComment: $dayComment, mealsComments: $mealsComments, portionsComments: $portionsComments)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$ConsultantCommentsCopyWith<$Res>
|
||||
implements $ConsultantCommentsCopyWith<$Res> {
|
||||
factory _$ConsultantCommentsCopyWith(
|
||||
_ConsultantComments value, $Res Function(_ConsultantComments) _then) =
|
||||
__$ConsultantCommentsCopyWithImpl;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{String? dayComment,
|
||||
List<MealComment> mealsComments,
|
||||
List<PortionComment> portionsComments});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$ConsultantCommentsCopyWithImpl<$Res>
|
||||
implements _$ConsultantCommentsCopyWith<$Res> {
|
||||
__$ConsultantCommentsCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _ConsultantComments _self;
|
||||
final $Res Function(_ConsultantComments) _then;
|
||||
|
||||
/// Create a copy of ConsultantComments
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$Res call({
|
||||
Object? dayComment = freezed,
|
||||
Object? mealsComments = null,
|
||||
Object? portionsComments = null,
|
||||
}) {
|
||||
return _then(_ConsultantComments(
|
||||
dayComment: freezed == dayComment
|
||||
? _self.dayComment
|
||||
: dayComment // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
mealsComments: null == mealsComments
|
||||
? _self._mealsComments
|
||||
: mealsComments // ignore: cast_nullable_to_non_nullable
|
||||
as List<MealComment>,
|
||||
portionsComments: null == portionsComments
|
||||
? _self._portionsComments
|
||||
: portionsComments // ignore: cast_nullable_to_non_nullable
|
||||
as List<PortionComment>,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$MealComment {
|
||||
String get comment;
|
||||
int get typeNo;
|
||||
|
||||
/// Create a copy of MealComment
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$MealCommentCopyWith<MealComment> get copyWith =>
|
||||
_$MealCommentCopyWithImpl<MealComment>(this as MealComment, _$identity);
|
||||
|
||||
/// Serializes this MealComment to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is MealComment &&
|
||||
(identical(other.comment, comment) || other.comment == comment) &&
|
||||
(identical(other.typeNo, typeNo) || other.typeNo == typeNo));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, comment, typeNo);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'MealComment(comment: $comment, typeNo: $typeNo)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $MealCommentCopyWith<$Res> {
|
||||
factory $MealCommentCopyWith(
|
||||
MealComment value, $Res Function(MealComment) _then) =
|
||||
_$MealCommentCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({String comment, int typeNo});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$MealCommentCopyWithImpl<$Res> implements $MealCommentCopyWith<$Res> {
|
||||
_$MealCommentCopyWithImpl(this._self, this._then);
|
||||
|
||||
final MealComment _self;
|
||||
final $Res Function(MealComment) _then;
|
||||
|
||||
/// Create a copy of MealComment
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? comment = null,
|
||||
Object? typeNo = null,
|
||||
}) {
|
||||
return _then(_self.copyWith(
|
||||
comment: null == comment
|
||||
? _self.comment
|
||||
: comment // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
typeNo: null == typeNo
|
||||
? _self.typeNo
|
||||
: typeNo // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _MealComment implements MealComment {
|
||||
_MealComment({required this.comment, required this.typeNo});
|
||||
factory _MealComment.fromJson(Map<String, dynamic> json) =>
|
||||
_$MealCommentFromJson(json);
|
||||
|
||||
@override
|
||||
final String comment;
|
||||
@override
|
||||
final int typeNo;
|
||||
|
||||
/// Create a copy of MealComment
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$MealCommentCopyWith<_MealComment> get copyWith =>
|
||||
__$MealCommentCopyWithImpl<_MealComment>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$MealCommentToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _MealComment &&
|
||||
(identical(other.comment, comment) || other.comment == comment) &&
|
||||
(identical(other.typeNo, typeNo) || other.typeNo == typeNo));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, comment, typeNo);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'MealComment(comment: $comment, typeNo: $typeNo)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$MealCommentCopyWith<$Res>
|
||||
implements $MealCommentCopyWith<$Res> {
|
||||
factory _$MealCommentCopyWith(
|
||||
_MealComment value, $Res Function(_MealComment) _then) =
|
||||
__$MealCommentCopyWithImpl;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({String comment, int typeNo});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$MealCommentCopyWithImpl<$Res> implements _$MealCommentCopyWith<$Res> {
|
||||
__$MealCommentCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _MealComment _self;
|
||||
final $Res Function(_MealComment) _then;
|
||||
|
||||
/// Create a copy of MealComment
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$Res call({
|
||||
Object? comment = null,
|
||||
Object? typeNo = null,
|
||||
}) {
|
||||
return _then(_MealComment(
|
||||
comment: null == comment
|
||||
? _self.comment
|
||||
: comment // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
typeNo: null == typeNo
|
||||
? _self.typeNo
|
||||
: typeNo // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$PortionComment {
|
||||
@JsonKey(name: 'id')
|
||||
int get portionId;
|
||||
String get comment;
|
||||
|
||||
/// Create a copy of PortionComment
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$PortionCommentCopyWith<PortionComment> get copyWith =>
|
||||
_$PortionCommentCopyWithImpl<PortionComment>(
|
||||
this as PortionComment, _$identity);
|
||||
|
||||
/// Serializes this PortionComment to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is PortionComment &&
|
||||
(identical(other.portionId, portionId) ||
|
||||
other.portionId == portionId) &&
|
||||
(identical(other.comment, comment) || other.comment == comment));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, portionId, comment);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PortionComment(portionId: $portionId, comment: $comment)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $PortionCommentCopyWith<$Res> {
|
||||
factory $PortionCommentCopyWith(
|
||||
PortionComment value, $Res Function(PortionComment) _then) =
|
||||
_$PortionCommentCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({@JsonKey(name: 'id') int portionId, String comment});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$PortionCommentCopyWithImpl<$Res>
|
||||
implements $PortionCommentCopyWith<$Res> {
|
||||
_$PortionCommentCopyWithImpl(this._self, this._then);
|
||||
|
||||
final PortionComment _self;
|
||||
final $Res Function(PortionComment) _then;
|
||||
|
||||
/// Create a copy of PortionComment
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? portionId = null,
|
||||
Object? comment = null,
|
||||
}) {
|
||||
return _then(_self.copyWith(
|
||||
portionId: null == portionId
|
||||
? _self.portionId
|
||||
: portionId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
comment: null == comment
|
||||
? _self.comment
|
||||
: comment // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _PortionComment implements PortionComment {
|
||||
_PortionComment(
|
||||
{@JsonKey(name: 'id') required this.portionId, required this.comment});
|
||||
factory _PortionComment.fromJson(Map<String, dynamic> json) =>
|
||||
_$PortionCommentFromJson(json);
|
||||
|
||||
@override
|
||||
@JsonKey(name: 'id')
|
||||
final int portionId;
|
||||
@override
|
||||
final String comment;
|
||||
|
||||
/// Create a copy of PortionComment
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$PortionCommentCopyWith<_PortionComment> get copyWith =>
|
||||
__$PortionCommentCopyWithImpl<_PortionComment>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$PortionCommentToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _PortionComment &&
|
||||
(identical(other.portionId, portionId) ||
|
||||
other.portionId == portionId) &&
|
||||
(identical(other.comment, comment) || other.comment == comment));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, portionId, comment);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PortionComment(portionId: $portionId, comment: $comment)';
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$PortionCommentCopyWith<$Res>
|
||||
implements $PortionCommentCopyWith<$Res> {
|
||||
factory _$PortionCommentCopyWith(
|
||||
_PortionComment value, $Res Function(_PortionComment) _then) =
|
||||
__$PortionCommentCopyWithImpl;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({@JsonKey(name: 'id') int portionId, String comment});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$PortionCommentCopyWithImpl<$Res>
|
||||
implements _$PortionCommentCopyWith<$Res> {
|
||||
__$PortionCommentCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _PortionComment _self;
|
||||
final $Res Function(_PortionComment) _then;
|
||||
|
||||
/// Create a copy of PortionComment
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$Res call({
|
||||
Object? portionId = null,
|
||||
Object? comment = null,
|
||||
}) {
|
||||
return _then(_PortionComment(
|
||||
portionId: null == portionId
|
||||
? _self.portionId
|
||||
: portionId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
comment: null == comment
|
||||
? _self.comment
|
||||
: comment // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// dart format on
|
||||
67
lib/features/diary/models/consultant_review_model.g.dart
Normal file
67
lib/features/diary/models/consultant_review_model.g.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'consultant_review_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_ConsultantReviewModel _$ConsultantReviewModelFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_ConsultantReviewModel(
|
||||
planName: json['planName'] as String,
|
||||
planDuration: (json['planDuration'] as num).toInt(),
|
||||
currentPlanDay: (json['currentPlanDay'] as num).toInt(),
|
||||
consultantComments: ConsultantComments.fromJson(
|
||||
json['consultantComments'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ConsultantReviewModelToJson(
|
||||
_ConsultantReviewModel instance) =>
|
||||
<String, dynamic>{
|
||||
'planName': instance.planName,
|
||||
'planDuration': instance.planDuration,
|
||||
'currentPlanDay': instance.currentPlanDay,
|
||||
'consultantComments': instance.consultantComments,
|
||||
};
|
||||
|
||||
_ConsultantComments _$ConsultantCommentsFromJson(Map<String, dynamic> json) =>
|
||||
_ConsultantComments(
|
||||
dayComment: json['dayComment'] as String?,
|
||||
mealsComments: (json['mealsComments'] as List<dynamic>)
|
||||
.map((e) => MealComment.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
portionsComments: (json['portionsComments'] as List<dynamic>)
|
||||
.map((e) => PortionComment.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ConsultantCommentsToJson(_ConsultantComments instance) =>
|
||||
<String, dynamic>{
|
||||
'dayComment': instance.dayComment,
|
||||
'mealsComments': instance.mealsComments,
|
||||
'portionsComments': instance.portionsComments,
|
||||
};
|
||||
|
||||
_MealComment _$MealCommentFromJson(Map<String, dynamic> json) => _MealComment(
|
||||
comment: json['comment'] as String,
|
||||
typeNo: (json['typeNo'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$MealCommentToJson(_MealComment instance) =>
|
||||
<String, dynamic>{
|
||||
'comment': instance.comment,
|
||||
'typeNo': instance.typeNo,
|
||||
};
|
||||
|
||||
_PortionComment _$PortionCommentFromJson(Map<String, dynamic> json) =>
|
||||
_PortionComment(
|
||||
portionId: (json['id'] as num).toInt(),
|
||||
comment: json['comment'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PortionCommentToJson(_PortionComment instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.portionId,
|
||||
'comment': instance.comment,
|
||||
};
|
||||
@@ -136,7 +136,15 @@ class DiaryCubit extends Cubit<DiaryState> {
|
||||
final fifthMeal = MealModel(id: 0, time: '0', typeNumber: 5, satiety: 0, mood: 0, portions: fifthMealPortions ?? [], photos: []);
|
||||
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,
|
||||
),
|
||||
@@ -148,7 +156,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;
|
||||
@@ -171,22 +179,17 @@ class DiaryCubit extends Cubit<DiaryState> {
|
||||
int? currentPlanDay;
|
||||
String? planName;
|
||||
if (diaryDay.chosenPlanId != null) {
|
||||
final chosenPlanModel = await chatRepository.getChosenPlan(id: diaryDay.chosenPlanId!);
|
||||
if (!isConsultantView &&
|
||||
(diaryDay.typeOfDiary.diaryState == DiaryStateEnum.lastDayCommented || diaryDay.typeOfDiary.diaryState == DiaryStateEnum.commented)) {
|
||||
await diaryRepository.viewCommentOfDiaryDayByConsultant(diaryId: diaryDay.id);
|
||||
}
|
||||
consultantReview = await diaryRepository.getDiaryComments(planId: diaryDay.chosenPlanId!, diaryId: diaryDay.id);
|
||||
final chosenPlanModel = await chatRepository.getChosenPlan(id: diaryDay.chosenPlanId!);
|
||||
planDuration = chosenPlanModel.plan.duration;
|
||||
planName = chosenPlanModel.plan.name;
|
||||
currentPlanDay = consultantReview.currentPlanDay;
|
||||
currentPlanDay = currentDay ?? DateTime.now().difference(chosenPlanModel.startDate).inDays + 1;
|
||||
}
|
||||
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(
|
||||
@@ -195,7 +198,7 @@ class DiaryCubit extends Cubit<DiaryState> {
|
||||
nutrition: nutrition,
|
||||
selectedDay: selectedDay,
|
||||
selectedMeal: selectedMeal,
|
||||
isConsultantView: isConsultantView,
|
||||
isConsultantView: this.diaryId != null,
|
||||
consultantReview: consultantReview,
|
||||
isDiaryFinished: isDiaryFinished,
|
||||
isDiaryCommented: isDiaryCommented,
|
||||
@@ -203,7 +206,7 @@ class DiaryCubit extends Cubit<DiaryState> {
|
||||
chosenPlanDuration: planDuration,
|
||||
chosenPlanName: planName,
|
||||
isMoodEnabled: isMoodEnabled,
|
||||
isWakeUpAndRetireEnabled: isWakeUpAndRetireTimeEnabled,
|
||||
isWakeUpAndRetireEnabled: isWakeUpAndRetireTimeEnabled
|
||||
),
|
||||
);
|
||||
} catch (e, st) {
|
||||
@@ -212,8 +215,17 @@ class DiaryCubit extends Cubit<DiaryState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> finishDiaryDay({required int diaryId, required String wakeUp, required String retire}) async {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -229,7 +241,6 @@ 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,7 +29,6 @@ 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({
|
||||
|
||||
@@ -24,8 +24,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class DiaryScreen extends StatelessWidget {
|
||||
const DiaryScreen({super.key});
|
||||
|
||||
const DiaryScreen({super.key, this.date});
|
||||
final DateTime? date;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const DiaryScreenContent();
|
||||
@@ -52,9 +52,10 @@ class UserDiaryScreen extends StatelessWidget {
|
||||
}
|
||||
|
||||
class DiaryScreenContent extends StatelessWidget {
|
||||
const DiaryScreenContent({super.key, this.diaryId, this.currentPlanDay});
|
||||
const DiaryScreenContent({super.key, this.diaryId, this.currentPlanDay, this.date});
|
||||
final int? diaryId;
|
||||
final int? currentPlanDay;
|
||||
final DateTime? date;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
|
||||
@@ -8,8 +8,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:table_calendar/table_calendar.dart';
|
||||
|
||||
class CalendarWidget extends StatefulWidget {
|
||||
const CalendarWidget({super.key});
|
||||
|
||||
const CalendarWidget({super.key,});
|
||||
@override
|
||||
State<CalendarWidget> createState() => _CalendarWidgetState();
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ProductLoaderWidget extends StatefulWidget {
|
||||
const ProductLoaderWidget({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ProductLoaderWidget> createState() => _ProductLoaderWidgetState();
|
||||
}
|
||||
|
||||
class _ProductLoaderWidgetState extends State<ProductLoaderWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
horizontalTitleGap: 0,
|
||||
contentPadding: const EdgeInsets.only(right: 24, left: 12),
|
||||
title: const CupertinoActivityIndicator(),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
tileColor: const Color(0xFFF6FAFF),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
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';
|
||||
@@ -26,11 +25,6 @@ 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,12 +297,4 @@ class DiaryRepository {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> viewCommentOfDiaryDayByConsultant({required int diaryId}) async {
|
||||
try {
|
||||
await diaryService.viewCommentOfDiaryDayByConsultant(diaryId: diaryId);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,9 +150,4 @@ 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/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/comments_cubit/comments_cubit.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_state.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
|
||||
@@ -10,11 +10,7 @@ class ExpandableText extends StatefulWidget {
|
||||
final String text;
|
||||
final int maxLines;
|
||||
|
||||
const ExpandableText({
|
||||
super.key,
|
||||
required this.text,
|
||||
this.maxLines = 5, // по умолчанию 5 строк
|
||||
});
|
||||
const ExpandableText({super.key, required this.text, this.maxLines = 3});
|
||||
|
||||
@override
|
||||
State<ExpandableText> createState() => _ExpandableTextState();
|
||||
@@ -22,29 +18,6 @@ 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(() {
|
||||
@@ -60,63 +33,52 @@ class _ExpandableTextState extends State<ExpandableText> {
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
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,
|
||||
],
|
||||
),
|
||||
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],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
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,25 +1,20 @@
|
||||
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/bloc/comments_cubit/comments_state.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) {
|
||||
@@ -32,7 +27,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;
|
||||
}
|
||||
@@ -83,34 +78,4 @@ 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,7 +12,6 @@ abstract class CommentsState with _$CommentsState {
|
||||
const factory CommentsState.loaded({
|
||||
required CommentModel comment,
|
||||
int? replyId,
|
||||
int? editedCommentId,
|
||||
}) = CommentsStateLoaded;
|
||||
|
||||
const factory CommentsState.error({
|
||||
@@ -1,5 +1,4 @@
|
||||
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';
|
||||
@@ -33,12 +32,24 @@ 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()),
|
||||
if (!hidePlans) 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(),
|
||||
),
|
||||
BlocProvider<PlansCubit>(
|
||||
create: (context) => sl<PlansCubit>()..loadPlans(),
|
||||
),
|
||||
BlocProvider<ConsultantsCubit>(
|
||||
create: (context) => sl<ConsultantsCubit>()..getConsultants(),
|
||||
),
|
||||
],
|
||||
child: const FeedScreenContent(),
|
||||
);
|
||||
@@ -58,8 +69,7 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
|
||||
final dietsController = TextEditingController();
|
||||
final plansController = TextEditingController();
|
||||
final consultantsController = TextEditingController();
|
||||
|
||||
late final List<String> chipsLabelList;
|
||||
final List<String> chipsLabelList = ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты'];
|
||||
int _selectedChipIndex = 0;
|
||||
final blogPostsScrollController = ScrollController();
|
||||
final recipesScrollController = ScrollController();
|
||||
@@ -68,13 +78,12 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
|
||||
final consultantsScrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// Initialize chipsLabelList based on hidePlans setting
|
||||
chipsLabelList = hidePlans ? ['Все', 'Рецепты', 'Диеты', 'Консультанты'] : ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты'];
|
||||
|
||||
void initState() {
|
||||
consultantsScrollController.addListener(_onScrollConsultants);
|
||||
super.initState();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
void _onScrollConsultants() {
|
||||
if (consultantsScrollController.offset >= consultantsScrollController.position.maxScrollExtent && !consultantsScrollController.position.outOfRange) {}
|
||||
@@ -99,36 +108,45 @@ 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 when !hidePlans => plansScrollController,
|
||||
3 when hidePlans => consultantsScrollController,
|
||||
4 when !hidePlans => consultantsScrollController,
|
||||
_ => blogPostsScrollController,
|
||||
3 => plansScrollController,
|
||||
4 => 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,
|
||||
@@ -154,71 +172,78 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
|
||||
searchController: switch (_selectedChipIndex) {
|
||||
1 => recipesController,
|
||||
2 => dietsController,
|
||||
3 when !hidePlans => plansController,
|
||||
3 when hidePlans => consultantsController,
|
||||
4 when !hidePlans => consultantsController,
|
||||
3 => plansController,
|
||||
4 => 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 when !hidePlans:
|
||||
context.read<PlansCubit>().loadPlans(query: query);
|
||||
break;
|
||||
case 3 when hidePlans:
|
||||
case 4 when !hidePlans:
|
||||
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:
|
||||
context.read<PlansCubit>().loadPlans(query: query);
|
||||
break;
|
||||
case 4:
|
||||
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 when !hidePlans:
|
||||
context.read<PlansCubit>().loadPlans();
|
||||
break;
|
||||
case 3 when hidePlans:
|
||||
case 4 when !hidePlans:
|
||||
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:
|
||||
context.read<PlansCubit>().loadPlans();
|
||||
break;
|
||||
case 4:
|
||||
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 when !hidePlans => 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 => AddItemButton(
|
||||
onTap: state.profile?.roles.contains('consultant') ?? false ? () => GoRouter.of(context).pushNamed(CreatePlanScreen.name) : null,
|
||||
),
|
||||
_ => const AddItemButton()
|
||||
},
|
||||
],
|
||||
),
|
||||
@@ -231,9 +256,8 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
|
||||
0 => const BlogPosts(),
|
||||
1 => const Recipes(),
|
||||
2 => const Diets(),
|
||||
3 when !hidePlans => const Plans(),
|
||||
3 when hidePlans => const Consultants(),
|
||||
4 when !hidePlans => const Consultants(),
|
||||
3 => const Plans(),
|
||||
4 => const Consultants(),
|
||||
_ => Center(child: Text(chipsLabelList[_selectedChipIndex])),
|
||||
},
|
||||
),
|
||||
|
||||
@@ -2,7 +2,12 @@ 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;
|
||||
@@ -17,7 +22,7 @@ class ChipGroup extends StatelessWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
...List.generate(
|
||||
chipsLabelList.length,
|
||||
5,
|
||||
(index) => Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: ChoiceChipWidget(
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
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/core/comments/comments_cubit/comments_cubit.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/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';
|
||||
|
||||
@@ -58,133 +53,98 @@ class _CommentsWidgetState extends State<CommentsWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
);
|
||||
},
|
||||
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,
|
||||
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,
|
||||
),
|
||||
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
||||
radius: radius,
|
||||
);
|
||||
},
|
||||
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(width: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
comment.authorName,
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
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(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(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
comment.createdAt.format('dd MMMM', 'ru'),
|
||||
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'),
|
||||
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,
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -31,7 +31,9 @@ class ConsultationsArchiveCubit extends Cubit<ConsultationsArchiveState> {
|
||||
dateComparison: state.periodFilterStatus == PeriodFilterStatus.all ? null : FiltersComparisonEnum.greaterThanOrEqual,
|
||||
stateComparison: state.consultationFilterStatus == ConsultationFilterStatus.all
|
||||
? null
|
||||
: FiltersComparisonEnum.equal,
|
||||
: state.consultationFilterStatus == ConsultationFilterStatus.inProgress
|
||||
? FiltersComparisonEnum.notInArray
|
||||
: FiltersComparisonEnum.equal,
|
||||
);
|
||||
emit(state.copyWith(consultations: consultations, isLoading: false));
|
||||
} catch (e) {
|
||||
@@ -65,17 +67,20 @@ class ConsultationsArchiveCubit extends Cubit<ConsultationsArchiveState> {
|
||||
enum ConsultationFilterStatus {
|
||||
all,
|
||||
canceled,
|
||||
finished;
|
||||
finished,
|
||||
inProgress;
|
||||
|
||||
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(PeriodFilterStatus.all) PeriodFilterStatus? periodFilterStatus,
|
||||
@Default(null) PeriodFilterStatus? periodFilterStatus,
|
||||
@Default(null) ConsultationFilterStatus? consultationFilterStatus,
|
||||
required StatisticModel statisticModel,
|
||||
}) = _ConsultationsArchiveState;
|
||||
|
||||
@@ -10,7 +10,7 @@ abstract class ConsultantFinancesState with _$ConsultantFinancesState {
|
||||
@Default([]) List<ConsultantFinanceModel> financesModels,
|
||||
@Default(false) isLoading,
|
||||
@Default(null) String? error,
|
||||
@Default(PeriodFilterStatus.all) PeriodFilterStatus? periodFilterStatus,
|
||||
@Default(null) PeriodFilterStatus? periodFilterStatus,
|
||||
}) = _ConsultantFinancesState;
|
||||
|
||||
}
|
||||
|
||||
@@ -10,6 +10,6 @@ abstract class UserPurchasesState with _$UserPurchasesState {
|
||||
@Default([]) List<UserPurchasesModel> purchasesModels,
|
||||
@Default(false) isLoading,
|
||||
@Default(null) String? error,
|
||||
@Default(PeriodFilterStatus.all) PeriodFilterStatus? periodFilterStatus,
|
||||
@Default(null) PeriodFilterStatus? periodFilterStatus,
|
||||
}) = _UserPurchasesState;
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ class _ConsultantFinancesScreenState extends State<ConsultantFinancesScreen> {
|
||||
if (value == null) return;
|
||||
context.read<ConsultantFinancesCubit>().setFinancesPeriod(value);
|
||||
},
|
||||
value: state.periodFilterStatus ?? PeriodFilterStatus.all,
|
||||
value: state.periodFilterStatus ?? PeriodFilterStatus.values.first,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (state.financesModels.isNotEmpty)
|
||||
@@ -129,7 +129,7 @@ class _ConsultantFinancesScreenState extends State<ConsultantFinancesScreen> {
|
||||
);
|
||||
}),
|
||||
Text(
|
||||
state.financesModels[index].startDate.format('dd.MM.yy'),
|
||||
state.financesModels[index].startDate.format('yyyy.MM.dd'),
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF6D6F71),
|
||||
height: 1,
|
||||
|
||||
@@ -195,7 +195,7 @@ class _ConsultationsArchiveScreenState extends State<ConsultationsArchiveScreen>
|
||||
),
|
||||
),
|
||||
Text(
|
||||
state.consultations[index].startDate.format('dd.MM.yy'),
|
||||
state.consultations[index].startDate.format('yyyy.MM.dd'),
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF6D6F71),
|
||||
height: 1,
|
||||
|
||||
@@ -10,7 +10,6 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:eatvio_app/core/env_config.dart';
|
||||
|
||||
class InternetConnectionCheckerScreen extends StatefulWidget {
|
||||
const InternetConnectionCheckerScreen({super.key, required this.child});
|
||||
@@ -67,7 +66,12 @@ class _InternetConnectionCheckerScreenState extends State<InternetConnectionChec
|
||||
return false;
|
||||
}
|
||||
|
||||
final uris = [Uri.parse(EnvConfig.googleCheckUrl), Uri.parse(EnvConfig.yandexCheckUrl), Uri.parse(EnvConfig.devBaseUrl), Uri.parse(EnvConfig.baseUrl)];
|
||||
final uris = [
|
||||
Uri.parse('https://google.com'),
|
||||
Uri.parse('https://yandex.ru'),
|
||||
Uri.parse('https://dev2.eatvio.com'),
|
||||
Uri.parse('https://eatvio.ru'),
|
||||
];
|
||||
for (final uri in uris) {
|
||||
try {
|
||||
final response = await http.get(uri).timeout(const Duration(seconds: 3));
|
||||
@@ -94,9 +98,13 @@ class _InternetConnectionCheckerScreenState extends State<InternetConnectionChec
|
||||
if (!_hasInternet)
|
||||
NoInternetScreen(
|
||||
onTryAgain: () async {
|
||||
EasyDebounce.debounce('internet connection check', const Duration(seconds: 2), () async {
|
||||
await _checkConnection();
|
||||
});
|
||||
EasyDebounce.debounce(
|
||||
'internet connection check',
|
||||
const Duration(seconds: 2),
|
||||
() async {
|
||||
await _checkConnection();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -121,7 +129,9 @@ class NoInternetScreen extends StatelessWidget {
|
||||
child: Text(
|
||||
'Отсутствует подключение \nк интернету',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
style: TextStyles.headingXLSemibold.copyWith(
|
||||
color: appTheme.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -130,7 +140,9 @@ class NoInternetScreen extends StatelessWidget {
|
||||
child: Text(
|
||||
'Проверьте соединение с сетью\nи попробуйте ещё раз',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyles.textMMedium.copyWith(color: appTheme.gray),
|
||||
style: TextStyles.textMMedium.copyWith(
|
||||
color: appTheme.gray,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
@@ -141,7 +153,10 @@ class NoInternetScreen extends StatelessWidget {
|
||||
height: 52,
|
||||
text: 'Попробовать ещё раз',
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||
buttonStyle: ButtonStyle(backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), side: const WidgetStatePropertyAll(BorderSide.none)),
|
||||
buttonStyle: ButtonStyle(
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/token_storage.dart';
|
||||
import 'package:eatvio_app/core/utils/image_constant.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
@@ -15,7 +14,6 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
class MenuScreen extends StatelessWidget {
|
||||
final ProfileModel profileModel;
|
||||
@@ -39,20 +37,18 @@ class MenuScreen extends StatelessWidget {
|
||||
if (profileModel.roles.contains('consultant')) _divider(context),
|
||||
if (profileModel.roles.contains('consultant'))
|
||||
_tile(leading: ImageConstant.icnPlans, title: 'Мои планы', onTap: () => GoRouter.of(context).push(MyPlansScreen.path)),
|
||||
if (!hidePlans) ...[
|
||||
_divider(context),
|
||||
_tile(
|
||||
leading: 'assets/icons/finance.svg',
|
||||
title: 'Мои финансы',
|
||||
onTap: () {
|
||||
if (profileModel.roles.contains('consultant')) {
|
||||
GoRouter.of(context).push(ConsultantFinancesScreen.path);
|
||||
return;
|
||||
}
|
||||
GoRouter.of(context).push(UserPurchasesScreen.path);
|
||||
},
|
||||
),
|
||||
],
|
||||
_divider(context),
|
||||
_tile(
|
||||
leading: 'assets/icons/finance.svg',
|
||||
title: 'Мои финансы',
|
||||
onTap: () {
|
||||
if (profileModel.roles.contains('consultant')) {
|
||||
GoRouter.of(context).push(ConsultantFinancesScreen.path);
|
||||
return;
|
||||
}
|
||||
GoRouter.of(context).push(UserPurchasesScreen.path);
|
||||
},
|
||||
),
|
||||
_divider(context),
|
||||
_tile(leading: ImageConstant.icnGlossary, title: 'Глоссарий', onTap: () => GoRouter.of(context).push('/glossary-screen')),
|
||||
_divider(context),
|
||||
@@ -69,8 +65,6 @@ class MenuScreen extends StatelessWidget {
|
||||
// ),
|
||||
_divider(context),
|
||||
_tile(leading: ImageConstant.icnExit, title: 'Выход', onTap: _onTapLogout(context)),
|
||||
const SizedBox(height: 10),
|
||||
const AppInfo(),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -128,42 +122,3 @@ class MenuScreen extends StatelessWidget {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class AppInfo extends StatefulWidget {
|
||||
const AppInfo({super.key});
|
||||
|
||||
@override
|
||||
State<AppInfo> createState() => _AppInfoState();
|
||||
}
|
||||
|
||||
class _AppInfoState extends State<AppInfo> {
|
||||
String version = '';
|
||||
String buildNumber = '';
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
PackageInfo.fromPlatform().then((PackageInfo packageInfo) {
|
||||
setState(() {
|
||||
version = packageInfo.version;
|
||||
buildNumber = packageInfo.buildNumber;
|
||||
});
|
||||
});
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsGeometry.only(left: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Версия приложения: $version ($buildNumber)', style: TextStyles.textXSSemibold.copyWith(color: appTheme.gray)),
|
||||
if (buildDate != '') Text('Дата сборки: $buildDate', style: TextStyles.textXSSemibold.copyWith(color: appTheme.gray)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
|
||||
|
||||
sealed class BaseNotificationModel {
|
||||
abstract final NotificationType type;
|
||||
abstract final bool isRead;
|
||||
abstract final DateTime createdAt;
|
||||
}
|
||||
|
||||
class PlanSubscriptionStartedToAuthorNotification implements BaseNotificationModel {
|
||||
@@ -12,8 +13,6 @@ class PlanSubscriptionStartedToAuthorNotification implements BaseNotificationMod
|
||||
final String planName;
|
||||
final String customerName;
|
||||
final String chatId;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
|
||||
PlanSubscriptionStartedToAuthorNotification({
|
||||
required this.type,
|
||||
@@ -21,17 +20,15 @@ class PlanSubscriptionStartedToAuthorNotification implements BaseNotificationMod
|
||||
required this.customerName,
|
||||
required this.chatId,
|
||||
required this.isRead,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory PlanSubscriptionStartedToAuthorNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
|
||||
factory PlanSubscriptionStartedToAuthorNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return PlanSubscriptionStartedToAuthorNotification(
|
||||
type: type,
|
||||
planName: json['plan_name'],
|
||||
customerName: json['user_name'],
|
||||
chatId: json['chat_id'],
|
||||
isRead: isRead ?? false,
|
||||
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -43,24 +40,20 @@ class PlanSubscriptionCanceledToAuthorNotification implements BaseNotificationMo
|
||||
final bool isRead;
|
||||
final String planName;
|
||||
final String customerName;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
|
||||
PlanSubscriptionCanceledToAuthorNotification({
|
||||
required this.type,
|
||||
required this.planName,
|
||||
required this.customerName,
|
||||
required this.isRead,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory PlanSubscriptionCanceledToAuthorNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
|
||||
factory PlanSubscriptionCanceledToAuthorNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return PlanSubscriptionCanceledToAuthorNotification(
|
||||
type: type,
|
||||
planName: json['plan_name'],
|
||||
customerName: json['user_name'],
|
||||
isRead: isRead ?? false,
|
||||
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -72,18 +65,20 @@ class ChosenPlanStartedToClientNotification implements BaseNotificationModel {
|
||||
final bool isRead;
|
||||
final String planName;
|
||||
final int planId;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
|
||||
ChosenPlanStartedToClientNotification({required this.type, required this.planName, required this.planId, required this.isRead, required this.createdAt});
|
||||
ChosenPlanStartedToClientNotification({
|
||||
required this.type,
|
||||
required this.planName,
|
||||
required this.planId,
|
||||
required this.isRead,
|
||||
});
|
||||
|
||||
factory ChosenPlanStartedToClientNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
|
||||
factory ChosenPlanStartedToClientNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return ChosenPlanStartedToClientNotification(
|
||||
type: type,
|
||||
planName: json['plan_name'],
|
||||
planId: json['plan_id'],
|
||||
isRead: isRead ?? false,
|
||||
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -94,18 +89,15 @@ class ChosenPlanTomorrowReminderToClientNotification implements BaseNotification
|
||||
@override
|
||||
final bool isRead;
|
||||
final String planName;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
|
||||
ChosenPlanTomorrowReminderToClientNotification({required this.type, required this.planName, required this.isRead, required this.createdAt});
|
||||
ChosenPlanTomorrowReminderToClientNotification({
|
||||
required this.type,
|
||||
required this.planName,
|
||||
required this.isRead,
|
||||
});
|
||||
|
||||
factory ChosenPlanTomorrowReminderToClientNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
|
||||
return ChosenPlanTomorrowReminderToClientNotification(
|
||||
type: type,
|
||||
planName: json['plan_name'],
|
||||
isRead: isRead ?? false,
|
||||
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
|
||||
);
|
||||
factory ChosenPlanTomorrowReminderToClientNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return ChosenPlanTomorrowReminderToClientNotification(type: type, planName: json['plan_name'], isRead: isRead ?? false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,17 +106,16 @@ class DiaryReminderNotification implements BaseNotificationModel {
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
final DateTime date;
|
||||
|
||||
DiaryReminderNotification({required this.type, required this.isRead, required this.createdAt});
|
||||
DiaryReminderNotification({
|
||||
required this.type,
|
||||
required this.isRead,
|
||||
required this.date,
|
||||
});
|
||||
|
||||
factory DiaryReminderNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
|
||||
return DiaryReminderNotification(
|
||||
type: type,
|
||||
isRead: isRead ?? false,
|
||||
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
|
||||
);
|
||||
factory DiaryReminderNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return DiaryReminderNotification(type: type, isRead: isRead ?? false, date: parseDate(json['date']));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,17 +125,18 @@ class ConsultantLeftCommentNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final bool isRead;
|
||||
final DateTime date;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
|
||||
ConsultantLeftCommentNotification({required this.type, required this.date, required this.isRead, required this.createdAt});
|
||||
ConsultantLeftCommentNotification({
|
||||
required this.type,
|
||||
required this.date,
|
||||
required this.isRead,
|
||||
});
|
||||
|
||||
factory ConsultantLeftCommentNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
|
||||
factory ConsultantLeftCommentNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return ConsultantLeftCommentNotification(
|
||||
type: type,
|
||||
date: parseDate(json['diary_date'] ?? DateTime.now().toIso8601String()),
|
||||
date: parseDate(json['diary_date']),
|
||||
isRead: isRead ?? false,
|
||||
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -154,16 +146,16 @@ class PersonalizedDietIsDefinedNotification implements BaseNotificationModel {
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
|
||||
PersonalizedDietIsDefinedNotification({required this.type, required this.isRead, required this.createdAt});
|
||||
PersonalizedDietIsDefinedNotification({
|
||||
required this.type,
|
||||
required this.isRead,
|
||||
});
|
||||
|
||||
factory PersonalizedDietIsDefinedNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
|
||||
factory PersonalizedDietIsDefinedNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return PersonalizedDietIsDefinedNotification(
|
||||
type: type,
|
||||
isRead: isRead ?? false,
|
||||
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -174,16 +166,17 @@ class ChosenPlanFinishedToClientNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final bool isRead;
|
||||
final String planName;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
ChosenPlanFinishedToClientNotification({required this.type, required this.planName, required this.isRead, required this.createdAt});
|
||||
ChosenPlanFinishedToClientNotification({
|
||||
required this.type,
|
||||
required this.planName,
|
||||
required this.isRead,
|
||||
});
|
||||
|
||||
factory ChosenPlanFinishedToClientNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
|
||||
factory ChosenPlanFinishedToClientNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return ChosenPlanFinishedToClientNotification(
|
||||
type: type,
|
||||
planName: json['plan_name'],
|
||||
isRead: isRead ?? false,
|
||||
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -194,16 +187,17 @@ class PlanSubscriptionCanceledToCustomerNotification implements BaseNotification
|
||||
@override
|
||||
final bool isRead;
|
||||
final String planName;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
PlanSubscriptionCanceledToCustomerNotification({required this.type, required this.planName, required this.isRead, required this.createdAt});
|
||||
PlanSubscriptionCanceledToCustomerNotification({
|
||||
required this.type,
|
||||
required this.planName,
|
||||
required this.isRead,
|
||||
});
|
||||
|
||||
factory PlanSubscriptionCanceledToCustomerNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
|
||||
factory PlanSubscriptionCanceledToCustomerNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return PlanSubscriptionCanceledToCustomerNotification(
|
||||
type: type,
|
||||
planName: json['plan_name'],
|
||||
isRead: isRead ?? false,
|
||||
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -215,23 +209,19 @@ class ChosenPlanStartedToConsultantNotification implements BaseNotificationModel
|
||||
final bool isRead;
|
||||
final String planName;
|
||||
final String customerName;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
ChosenPlanStartedToConsultantNotification({
|
||||
required this.type,
|
||||
required this.isRead,
|
||||
required this.planName,
|
||||
required this.customerName,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory ChosenPlanStartedToConsultantNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
|
||||
factory ChosenPlanStartedToConsultantNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return ChosenPlanStartedToConsultantNotification(
|
||||
type: type,
|
||||
isRead: isRead ?? false,
|
||||
planName: json['plan_name'],
|
||||
customerName: json['client_name'],
|
||||
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -243,23 +233,19 @@ class ChosenPlanTomorrowReminderToConsultantNotification implements BaseNotifica
|
||||
final bool isRead;
|
||||
final String planName;
|
||||
final String customerName;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
ChosenPlanTomorrowReminderToConsultantNotification({
|
||||
required this.type,
|
||||
required this.planName,
|
||||
required this.customerName,
|
||||
required this.isRead,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory ChosenPlanTomorrowReminderToConsultantNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
|
||||
factory ChosenPlanTomorrowReminderToConsultantNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return ChosenPlanTomorrowReminderToConsultantNotification(
|
||||
type: type,
|
||||
planName: json['plan_name'],
|
||||
customerName: json['client_name'],
|
||||
isRead: isRead ?? false,
|
||||
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -271,23 +257,19 @@ class PlanSubscriptionPaidToAuthorNotification implements BaseNotificationModel
|
||||
final bool isRead;
|
||||
final String planName;
|
||||
final String customerName;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
PlanSubscriptionPaidToAuthorNotification({
|
||||
required this.type,
|
||||
required this.isRead,
|
||||
required this.planName,
|
||||
required this.customerName,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory PlanSubscriptionPaidToAuthorNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
|
||||
factory PlanSubscriptionPaidToAuthorNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return PlanSubscriptionPaidToAuthorNotification(
|
||||
type: type,
|
||||
isRead: isRead ?? false,
|
||||
planName: json['plan_name'],
|
||||
customerName: json['user_name'],
|
||||
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -300,531 +282,21 @@ class ChosenPlanFinishedToConsultantNotification implements BaseNotificationMode
|
||||
final String planName;
|
||||
final String customerName;
|
||||
final int planId;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
ChosenPlanFinishedToConsultantNotification({
|
||||
required this.type,
|
||||
required this.planName,
|
||||
required this.customerName,
|
||||
required this.planId,
|
||||
required this.isRead,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory ChosenPlanFinishedToConsultantNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
|
||||
factory ChosenPlanFinishedToConsultantNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return ChosenPlanFinishedToConsultantNotification(
|
||||
type: type,
|
||||
planName: json['plan_name'],
|
||||
customerName: json['client_name'],
|
||||
planId: json['plan_id'],
|
||||
isRead: isRead ?? false,
|
||||
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChosenPlanLastDayFinishedForUser implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
|
||||
ChosenPlanLastDayFinishedForUser({required this.type, required this.isRead, required this.createdAt});
|
||||
|
||||
factory ChosenPlanLastDayFinishedForUser.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
|
||||
return ChosenPlanLastDayFinishedForUser(
|
||||
type: type,
|
||||
isRead: isRead ?? false,
|
||||
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChosenPlanConsultantLastDayStarted implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
final String planName;
|
||||
final int planId;
|
||||
final String chatId;
|
||||
final String chatUrl;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
|
||||
ChosenPlanConsultantLastDayStarted({
|
||||
required this.type,
|
||||
required this.planName,
|
||||
required this.planId,
|
||||
required this.chatId,
|
||||
required this.chatUrl,
|
||||
required this.isRead,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory ChosenPlanConsultantLastDayStarted.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
|
||||
return ChosenPlanConsultantLastDayStarted(
|
||||
type: type,
|
||||
planName: json['plan_name'],
|
||||
planId: json['plan_id'],
|
||||
chatId: json['chat_id'],
|
||||
chatUrl: json['chat_url'],
|
||||
isRead: isRead ?? false,
|
||||
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ComplaintToModerationForUser implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
final String message;
|
||||
final int entityId;
|
||||
final String entityType;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
|
||||
ComplaintToModerationForUser({
|
||||
required this.type,
|
||||
required this.message,
|
||||
required this.entityId,
|
||||
required this.entityType,
|
||||
required this.isRead,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory ComplaintToModerationForUser.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
|
||||
return ComplaintToModerationForUser(
|
||||
type: type,
|
||||
message: json['message'],
|
||||
entityId: json['entity_id'],
|
||||
entityType: json['entity_type'],
|
||||
isRead: isRead ?? false,
|
||||
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CommentReplied implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
final String entityName;
|
||||
final int commentedEntityId;
|
||||
final String commentedEntityType;
|
||||
final String author;
|
||||
final int authorId;
|
||||
final int parentCommentId;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
|
||||
CommentReplied({
|
||||
required this.type,
|
||||
required this.entityName,
|
||||
required this.commentedEntityId,
|
||||
required this.commentedEntityType,
|
||||
required this.author,
|
||||
required this.authorId,
|
||||
required this.parentCommentId,
|
||||
required this.isRead,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory CommentReplied.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
|
||||
return CommentReplied(
|
||||
type: type,
|
||||
entityName: json['entity_name'],
|
||||
commentedEntityId: json['commented_entity_id'],
|
||||
commentedEntityType: json['commented_entity_type'],
|
||||
author: json['author'],
|
||||
authorId: json['author_id'],
|
||||
parentCommentId: json['parrent_comment_id'],
|
||||
isRead: isRead ?? false,
|
||||
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ScoreAdded implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
final String scoreKey;
|
||||
final int scorableId;
|
||||
final String scorableType;
|
||||
final String scorableName;
|
||||
final String scorableSlug;
|
||||
final int scoreValue;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
|
||||
ScoreAdded({
|
||||
required this.type,
|
||||
required this.scoreKey,
|
||||
required this.scorableId,
|
||||
required this.scorableType,
|
||||
required this.scorableName,
|
||||
required this.scorableSlug,
|
||||
required this.scoreValue,
|
||||
required this.isRead,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory ScoreAdded.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
|
||||
return ScoreAdded(
|
||||
type: type,
|
||||
scoreKey: json['score_key'],
|
||||
scorableId: json['scorable_id'],
|
||||
scorableType: json['scorable_type'],
|
||||
scorableName: json['scorable_name'] ?? '',
|
||||
scorableSlug: json['scorable_slug'] ?? '',
|
||||
scoreValue: json['score_value'],
|
||||
isRead: isRead ?? false,
|
||||
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EntityCommented implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
final String entityName;
|
||||
final int entityId;
|
||||
final String entityType;
|
||||
final String entityTranslatedType;
|
||||
final String entityUrl;
|
||||
final String authorName;
|
||||
final int authorId;
|
||||
final String authorSlug;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
|
||||
EntityCommented({
|
||||
required this.type,
|
||||
required this.entityName,
|
||||
required this.entityId,
|
||||
required this.entityType,
|
||||
required this.entityTranslatedType,
|
||||
required this.entityUrl,
|
||||
required this.authorName,
|
||||
required this.authorId,
|
||||
required this.authorSlug,
|
||||
required this.isRead,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory EntityCommented.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
|
||||
return EntityCommented(
|
||||
type: type,
|
||||
entityName: json['entity_name'],
|
||||
entityId: json['entity_id'],
|
||||
entityType: json['entity_type'],
|
||||
entityTranslatedType: json['entity_traslated_type'],
|
||||
entityUrl: json['entity_url'],
|
||||
authorName: json['author_name'],
|
||||
authorId: json['author_id'],
|
||||
authorSlug: json['author_slug'],
|
||||
isRead: isRead ?? false,
|
||||
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PlanSubscriptionExpiredNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
final String planName;
|
||||
final String customerName;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
|
||||
PlanSubscriptionExpiredNotification({
|
||||
required this.type,
|
||||
required this.planName,
|
||||
required this.customerName,
|
||||
required this.isRead,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory PlanSubscriptionExpiredNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
|
||||
return PlanSubscriptionExpiredNotification(
|
||||
type: type,
|
||||
planName: json['plan_name'],
|
||||
customerName: json['user_name'],
|
||||
isRead: isRead ?? false,
|
||||
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PlanSubscriptionRenewedNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
final String planName;
|
||||
final String customerName;
|
||||
final String renewalDate;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
|
||||
PlanSubscriptionRenewedNotification({
|
||||
required this.type,
|
||||
required this.planName,
|
||||
required this.customerName,
|
||||
required this.renewalDate,
|
||||
required this.isRead,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory PlanSubscriptionRenewedNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
|
||||
return PlanSubscriptionRenewedNotification(
|
||||
type: type,
|
||||
planName: json['plan_name'],
|
||||
customerName: json['user_name'],
|
||||
renewalDate: json['renewal_date'],
|
||||
isRead: isRead ?? false,
|
||||
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ConsultantAssignedNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
final String consultantName;
|
||||
final String planName;
|
||||
final int planId;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
|
||||
ConsultantAssignedNotification({
|
||||
required this.type,
|
||||
required this.consultantName,
|
||||
required this.planName,
|
||||
required this.planId,
|
||||
required this.isRead,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory ConsultantAssignedNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
|
||||
return ConsultantAssignedNotification(
|
||||
type: type,
|
||||
consultantName: json['consultant_name'],
|
||||
planName: json['plan_name'],
|
||||
planId: json['plan_id'],
|
||||
isRead: isRead ?? false,
|
||||
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ConsultantUnassignedNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
final String consultantName;
|
||||
final String planName;
|
||||
final int planId;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
|
||||
ConsultantUnassignedNotification({
|
||||
required this.type,
|
||||
required this.consultantName,
|
||||
required this.planName,
|
||||
required this.planId,
|
||||
required this.isRead,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory ConsultantUnassignedNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
|
||||
return ConsultantUnassignedNotification(
|
||||
type: type,
|
||||
consultantName: json['consultant_name'],
|
||||
planName: json['plan_name'],
|
||||
planId: json['plan_id'],
|
||||
isRead: isRead ?? false,
|
||||
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PaymentFailedNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
final String planName;
|
||||
final String errorMessage;
|
||||
final String paymentMethod;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
|
||||
PaymentFailedNotification({
|
||||
required this.type,
|
||||
required this.planName,
|
||||
required this.errorMessage,
|
||||
required this.paymentMethod,
|
||||
required this.isRead,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory PaymentFailedNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
|
||||
return PaymentFailedNotification(
|
||||
type: type,
|
||||
planName: json['plan_name'],
|
||||
errorMessage: json['error_message'],
|
||||
paymentMethod: json['payment_method'],
|
||||
isRead: isRead ?? false,
|
||||
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PaymentSuccessfulNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
final String planName;
|
||||
final double amount;
|
||||
final String currency;
|
||||
final String transactionId;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
|
||||
PaymentSuccessfulNotification({
|
||||
required this.type,
|
||||
required this.planName,
|
||||
required this.amount,
|
||||
required this.currency,
|
||||
required this.transactionId,
|
||||
required this.isRead,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory PaymentSuccessfulNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
|
||||
return PaymentSuccessfulNotification(
|
||||
type: type,
|
||||
planName: json['plan_name'],
|
||||
amount: (json['amount'] as num).toDouble(),
|
||||
currency: json['currency'],
|
||||
transactionId: json['transaction_id'],
|
||||
isRead: isRead ?? false,
|
||||
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GoalAchievedNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
final String goalName;
|
||||
final String goalType;
|
||||
final double achievedValue;
|
||||
final double targetValue;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
|
||||
GoalAchievedNotification({
|
||||
required this.type,
|
||||
required this.goalName,
|
||||
required this.goalType,
|
||||
required this.achievedValue,
|
||||
required this.targetValue,
|
||||
required this.isRead,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory GoalAchievedNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
|
||||
return GoalAchievedNotification(
|
||||
type: type,
|
||||
goalName: json['goal_name'],
|
||||
goalType: json['goal_type'],
|
||||
achievedValue: (json['achieved_value'] as num).toDouble(),
|
||||
targetValue: (json['target_value'] as num).toDouble(),
|
||||
isRead: isRead ?? false,
|
||||
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class WorkoutReminderNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
final String workoutName;
|
||||
final String workoutType;
|
||||
final int duration;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
|
||||
WorkoutReminderNotification({
|
||||
required this.type,
|
||||
required this.workoutName,
|
||||
required this.workoutType,
|
||||
required this.duration,
|
||||
required this.isRead,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory WorkoutReminderNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
|
||||
return WorkoutReminderNotification(
|
||||
type: type,
|
||||
workoutName: json['workout_name'],
|
||||
workoutType: json['workout_type'],
|
||||
duration: json['duration'],
|
||||
isRead: isRead ?? false,
|
||||
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MealReminderNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
final String mealName;
|
||||
final String mealType;
|
||||
final int calories;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
|
||||
MealReminderNotification({
|
||||
required this.type,
|
||||
required this.mealName,
|
||||
required this.mealType,
|
||||
required this.calories,
|
||||
required this.isRead,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory MealReminderNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
|
||||
return MealReminderNotification(
|
||||
type: type,
|
||||
mealName: json['meal_name'],
|
||||
mealType: json['meal_type'],
|
||||
calories: json['calories'],
|
||||
isRead: isRead ?? false,
|
||||
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -834,10 +306,11 @@ class UnknownNotification implements BaseNotificationModel {
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
|
||||
UnknownNotification({required this.type, this.isRead = true, DateTime? createdAt}) : createdAt = createdAt ?? DateTime.fromMillisecondsSinceEpoch(0);
|
||||
UnknownNotification({
|
||||
required this.type,
|
||||
this.isRead = true,
|
||||
});
|
||||
}
|
||||
|
||||
enum NotificationType {
|
||||
@@ -854,58 +327,22 @@ enum NotificationType {
|
||||
chosenPlanTomorrowReminderToConsultantNotification,
|
||||
planSubscriptionPaidToAuthor,
|
||||
chosenPlanFinishedToConsultant,
|
||||
chosenPlanLastDayFinishedForUser,
|
||||
// Новые типы
|
||||
chosenPlanConsultantLastDayStarted,
|
||||
complaintToModerationForUser,
|
||||
commentReplied,
|
||||
scoreAdded,
|
||||
entityCommented,
|
||||
// Дополнительные типы уведомлений
|
||||
planSubscriptionExpired,
|
||||
planSubscriptionRenewed,
|
||||
consultantAssigned,
|
||||
consultantUnassigned,
|
||||
paymentFailed,
|
||||
paymentSuccessful,
|
||||
goalAchieved,
|
||||
workoutReminder,
|
||||
mealReminder,
|
||||
unknown;
|
||||
|
||||
static NotificationType getTypeFromString(String type) => switch (type) {
|
||||
'App\\Notifications\\Plan\\PlanSubscriptionStartedToAuthor' => NotificationType.planSubscriptionStartedToAuthor,
|
||||
'App\\Notifications\\Plan\\PlanSubscriptionCanceledToAuthor' => NotificationType.planSubscriptionCanceledToAuthor,
|
||||
'App\\Notifications\\Plan\\ChosenPlanStartedToClientNotification' => NotificationType.chosenPlanStartedToClientNotification,
|
||||
'App\\Notifications\\Plan\\ChosenPlanTomorrowReminderToClientNotification' => NotificationType.chosenPlanStartedToClientNotification,
|
||||
'App\\Notifications\\DiaryReminderNotification' => NotificationType.diaryReminderNotification,
|
||||
'App\\Notifications\\Plan\\ConsultantLeftComment' => NotificationType.consultantLeftComment,
|
||||
'App\\Notifications\\Plan\\PersonalizedDietIsDefined' => NotificationType.personalizedDietIsDefined,
|
||||
'App\\Notifications\\Plan\\ChosenPlanFinishedToClient' => NotificationType.chosenPlanFinishedToClient,
|
||||
'App\\Notifications\\Plan\\PlanSubscriptionCanceledToCustomer' => NotificationType.planSubscriptionCanceledToCustomer,
|
||||
'App\\Notifications\\Plan\\ChosenPlanStartedToConsultantNotification' => NotificationType.chosenPlanStartedToConsultantNotification,
|
||||
'App\\Notifications\\Plan\\ChosenPlanTomorrowReminderToConsultantNotification' => NotificationType.chosenPlanTomorrowReminderToConsultantNotification,
|
||||
'App\\Notifications\\Plan\\PlanSubscriptionPaidToAuthor' => NotificationType.planSubscriptionPaidToAuthor,
|
||||
'App\\Notifications\\Plan\\ChosenPlanFinishedToConsultant' => NotificationType.chosenPlanFinishedToConsultant,
|
||||
'App\\Notifications\\ChosenPlan\\LastDayFinishedForUser' => NotificationType.chosenPlanLastDayFinishedForUser,
|
||||
// Новые маппинги
|
||||
'App\\Notifications\\ChosenPlan\\LastDayStartedForConsultant' => NotificationType.chosenPlanConsultantLastDayStarted,
|
||||
'App\\Notifications\\ComplaintToModerationNotify' => NotificationType.complaintToModerationForUser,
|
||||
'App\\Notifications\\Comment\\Replied' => NotificationType.commentReplied,
|
||||
'App\\Notifications\\Score\\ScoreAdded' => NotificationType.scoreAdded,
|
||||
'App\\Notifications\\Comment\\EntityCommented' => NotificationType.entityCommented,
|
||||
// Дополнительные маппинги
|
||||
'App\\Notifications\\Plan\\PlanSubscriptionExpired' => NotificationType.planSubscriptionExpired,
|
||||
'App\\Notifications\\Plan\\PlanSubscriptionRenewed' => NotificationType.planSubscriptionRenewed,
|
||||
'App\\Notifications\\Plan\\ConsultantAssigned' => NotificationType.consultantAssigned,
|
||||
'App\\Notifications\\Plan\\ConsultantUnassigned' => NotificationType.consultantUnassigned,
|
||||
'App\\Notifications\\Payment\\PaymentFailed' => NotificationType.paymentFailed,
|
||||
'App\\Notifications\\Payment\\PaymentSuccessful' => NotificationType.paymentSuccessful,
|
||||
'App\\Notifications\\Goal\\GoalAchieved' => NotificationType.goalAchieved,
|
||||
'App\\Notifications\\Workout\\WorkoutReminder' => NotificationType.workoutReminder,
|
||||
'App\\Notifications\\Meal\\MealReminder' => NotificationType.mealReminder,
|
||||
_ => NotificationType.unknown,
|
||||
};
|
||||
'App\\Notifications\\Plan\\PlanSubscriptionStartedToAuthor' => NotificationType.planSubscriptionStartedToAuthor,
|
||||
'App\\Notifications\\Plan\\PlanSubscriptionCanceledToAuthor' => NotificationType.planSubscriptionCanceledToAuthor,
|
||||
'App\\Notifications\\Plan\\ChosenPlanStartedToClientNotification' => NotificationType.chosenPlanStartedToClientNotification,
|
||||
'App\\Notifications\\Plan\\ChosenPlanTomorrowReminderToClientNotification' => NotificationType.chosenPlanStartedToClientNotification,
|
||||
'App\\Notifications\\DiaryReminderNotification' => NotificationType.diaryReminderNotification,
|
||||
'App\\Notifications\\Plan\\ConsultantLeftComment' => NotificationType.consultantLeftComment,
|
||||
'App\\Notifications\\Plan\\PersonalizedDietIsDefined' => NotificationType.personalizedDietIsDefined,
|
||||
'App\\Notifications\\Plan\\ChosenPlanFinishedToClient' => NotificationType.chosenPlanFinishedToClient,
|
||||
'App\\Notifications\\Plan\\PlanSubscriptionCanceledToCustomer' => NotificationType.planSubscriptionCanceledToCustomer,
|
||||
'App\\Notifications\\Plan\\ChosenPlanStartedToConsultantNotification' => NotificationType.chosenPlanStartedToConsultantNotification,
|
||||
'App\\Notifications\\Plan\\ChosenPlanTomorrowReminderToConsultantNotification' => NotificationType.chosenPlanTomorrowReminderToConsultantNotification,
|
||||
'App\\Notifications\\Plan\\PlanSubscriptionPaidToAuthor' => NotificationType.planSubscriptionPaidToAuthor,
|
||||
'App\\Notifications\\Plan\\ChosenPlanFinishedToConsultant' => NotificationType.chosenPlanFinishedToConsultant,
|
||||
_ => NotificationType.unknown,
|
||||
};
|
||||
}
|
||||
|
||||
DateTime parseDate(String date) => DateTime.parse(date).toLocal();
|
||||
|
||||
@@ -13,116 +13,36 @@ class NotificationsCubit extends Cubit<NotificationsState> {
|
||||
final WsManager wsManager;
|
||||
final NotificationsRepository notificationsRepository;
|
||||
StreamSubscription? _newNotificationsSubscription;
|
||||
NotificationsCubit({required this.wsManager, required this.notificationsRepository}) : super(const NotificationsState.empty());
|
||||
NotificationsCubit({
|
||||
required this.wsManager,
|
||||
required this.notificationsRepository,
|
||||
}) : super(const NotificationsState.empty());
|
||||
|
||||
Future<void> connectToWebSocket() async {
|
||||
await _newNotificationsSubscription?.cancel();
|
||||
_newNotificationsSubscription = wsManager.newNotificationsStream?.listen((event) {
|
||||
_handleNotification(event.data);
|
||||
|
||||
_handleNotification(event.data, event.rootObject['created_at']);
|
||||
});
|
||||
}
|
||||
|
||||
BaseNotificationModel _parseNotification(Map<String, dynamic> notificationJson, NotificationType type) {
|
||||
try {
|
||||
final createdAt = notificationJson['created_at'];
|
||||
final isRead = notificationJson['read_at'] != null;
|
||||
|
||||
return switch (type) {
|
||||
NotificationType.planSubscriptionStartedToAuthor => PlanSubscriptionStartedToAuthorNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.planSubscriptionCanceledToAuthor => PlanSubscriptionCanceledToAuthorNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.chosenPlanStartedToClientNotification => ChosenPlanStartedToClientNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.chosenPlanTomorrowReminderToClientNotification => ChosenPlanTomorrowReminderToClientNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.chosenPlanConsultantLastDayStarted => ChosenPlanConsultantLastDayStarted.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.diaryReminderNotification => DiaryReminderNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.consultantLeftComment => ConsultantLeftCommentNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.personalizedDietIsDefined => PersonalizedDietIsDefinedNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.chosenPlanFinishedToClient => ChosenPlanFinishedToClientNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.planSubscriptionCanceledToCustomer => PlanSubscriptionCanceledToAuthorNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.chosenPlanStartedToConsultantNotification => ChosenPlanStartedToConsultantNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.chosenPlanTomorrowReminderToConsultantNotification => ChosenPlanTomorrowReminderToConsultantNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.planSubscriptionPaidToAuthor => PlanSubscriptionPaidToAuthorNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.chosenPlanFinishedToConsultant => ChosenPlanFinishedToConsultantNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.chosenPlanLastDayFinishedForUser => ChosenPlanLastDayFinishedForUser.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
// New notification types
|
||||
NotificationType.complaintToModerationForUser => ComplaintToModerationForUser.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.commentReplied => CommentReplied.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.scoreAdded => ScoreAdded.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.entityCommented => EntityCommented.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
// Новые типы уведомлений
|
||||
NotificationType.planSubscriptionExpired => PlanSubscriptionExpiredNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.planSubscriptionRenewed => PlanSubscriptionRenewedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.consultantAssigned => ConsultantAssignedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.consultantUnassigned => ConsultantUnassignedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.paymentFailed => PaymentFailedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.paymentSuccessful => PaymentSuccessfulNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.goalAchieved => GoalAchievedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.workoutReminder => WorkoutReminderNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.mealReminder => MealReminderNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.planSubscriptionStartedToAuthor => PlanSubscriptionStartedToAuthorNotification.fromJson(notificationJson, type),
|
||||
NotificationType.planSubscriptionCanceledToAuthor => PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson, type),
|
||||
NotificationType.chosenPlanStartedToClientNotification => ChosenPlanStartedToClientNotification.fromJson(notificationJson, type),
|
||||
NotificationType.chosenPlanTomorrowReminderToClientNotification => ChosenPlanTomorrowReminderToClientNotification.fromJson(notificationJson, type),
|
||||
NotificationType.diaryReminderNotification => DiaryReminderNotification.fromJson(notificationJson, type),
|
||||
NotificationType.consultantLeftComment => ConsultantLeftCommentNotification.fromJson(notificationJson, type),
|
||||
NotificationType.personalizedDietIsDefined => PersonalizedDietIsDefinedNotification.fromJson(notificationJson, type),
|
||||
NotificationType.chosenPlanFinishedToClient => ChosenPlanFinishedToClientNotification.fromJson(notificationJson, type),
|
||||
NotificationType.planSubscriptionCanceledToCustomer => PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson, type),
|
||||
NotificationType.chosenPlanStartedToConsultantNotification => ChosenPlanStartedToConsultantNotification.fromJson(notificationJson, type),
|
||||
NotificationType.chosenPlanTomorrowReminderToConsultantNotification =>
|
||||
ChosenPlanTomorrowReminderToConsultantNotification.fromJson(notificationJson, type),
|
||||
NotificationType.planSubscriptionPaidToAuthor => PlanSubscriptionPaidToAuthorNotification.fromJson(notificationJson, type),
|
||||
NotificationType.chosenPlanFinishedToConsultant => ChosenPlanFinishedToConsultantNotification.fromJson(notificationJson, type),
|
||||
NotificationType.unknown => UnknownNotification(type: type),
|
||||
};
|
||||
} catch (e) {
|
||||
@@ -130,86 +50,39 @@ class NotificationsCubit extends Cubit<NotificationsState> {
|
||||
}
|
||||
}
|
||||
|
||||
void _handleNotification(String notificationData) {
|
||||
void _handleNotification(String notificationData, String createdAt) {
|
||||
final json = jsonDecode(notificationData) as Map<String, dynamic>;
|
||||
json.addAll({'date': createdAt});
|
||||
final stringifiedType = json['notification_class_name'];
|
||||
final type = NotificationType.getTypeFromString(stringifiedType);
|
||||
final notification = _parseNotification(json, type);
|
||||
_notifications.insert(0, notification);
|
||||
|
||||
final currentState = state;
|
||||
if (currentState is NotificationsStateLoaded) {
|
||||
emit(currentState.copyWith(notifications: _notifications, notificationsCount: _notifications.length, hasUnreadNotifications: true));
|
||||
}
|
||||
emit(
|
||||
NotificationsState.loaded(
|
||||
notifications: _notifications,
|
||||
notificationsCount: _notifications.length,
|
||||
hasUnreadNotifications: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> getNotifications() async {
|
||||
try {
|
||||
final result = await notificationsRepository.getNotifications(page: 1);
|
||||
if (result != null) {
|
||||
final notifications = result['notifications'] as List<BaseNotificationModel>;
|
||||
final pagination = result['pagination'] as Map<String, dynamic>;
|
||||
|
||||
_notifications.clear();
|
||||
_notifications.addAll(notifications);
|
||||
|
||||
final currentPage = pagination['current_page'] as int;
|
||||
final lastPage = pagination['last_page'] as int;
|
||||
final hasNextPage = currentPage < lastPage;
|
||||
|
||||
emit(
|
||||
NotificationsState.loaded(
|
||||
notifications: _notifications,
|
||||
notificationsCount: _notifications.length,
|
||||
hasUnreadNotifications: notifications.where((elem) => elem.isRead == false).isNotEmpty,
|
||||
currentPage: currentPage,
|
||||
hasNextPage: hasNextPage,
|
||||
),
|
||||
);
|
||||
}
|
||||
final notifications = await notificationsRepository.getNotifications(page: 2);
|
||||
_notifications.clear();
|
||||
_notifications.addAll(notifications ?? []);
|
||||
emit(
|
||||
NotificationsState.loaded(
|
||||
notifications: _notifications,
|
||||
notificationsCount: _notifications.length,
|
||||
hasUnreadNotifications: (notifications ?? []).where((elem) => elem.isRead == false).isNotEmpty,
|
||||
),
|
||||
);
|
||||
} catch (e, st) {
|
||||
log(e.toString(), stackTrace: st);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadMoreNotifications() async {
|
||||
final currentState = state;
|
||||
if (currentState is NotificationsStateLoaded && currentState.hasNextPage && !currentState.isLoadingMore) {
|
||||
try {
|
||||
emit(currentState.copyWith(isLoadingMore: true));
|
||||
|
||||
final nextPage = currentState.currentPage + 1;
|
||||
final result = await notificationsRepository.getNotifications(page: nextPage);
|
||||
|
||||
if (result != null) {
|
||||
final newNotifications = result['notifications'] as List<BaseNotificationModel>;
|
||||
final pagination = result['pagination'] as Map<String, dynamic>;
|
||||
|
||||
_notifications.addAll(newNotifications);
|
||||
|
||||
final currentPage = pagination['current_page'] as int;
|
||||
final lastPage = pagination['last_page'] as int;
|
||||
final hasNextPage = currentPage < lastPage;
|
||||
|
||||
emit(
|
||||
currentState.copyWith(
|
||||
notifications: _notifications,
|
||||
notificationsCount: _notifications.length,
|
||||
currentPage: currentPage,
|
||||
hasNextPage: hasNextPage,
|
||||
isLoadingMore: false,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
emit(currentState.copyWith(isLoadingMore: false));
|
||||
}
|
||||
} catch (e, st) {
|
||||
log(e.toString(), stackTrace: st);
|
||||
emit(currentState.copyWith(isLoadingMore: false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> markNotificationsAsRead() async {
|
||||
final currentState = state;
|
||||
if (currentState is NotificationsStateLoaded) {
|
||||
|
||||
@@ -14,9 +14,6 @@ abstract class NotificationsState with _$NotificationsState {
|
||||
required int notificationsCount,
|
||||
@Default(false) bool isLoading,
|
||||
@Default(false) bool hasUnreadNotifications,
|
||||
@Default(1) int currentPage,
|
||||
@Default(true) bool hasNextPage,
|
||||
@Default(false) bool isLoadingMore,
|
||||
}) = NotificationsStateLoaded;
|
||||
|
||||
const factory NotificationsState.error({
|
||||
|
||||
@@ -10,38 +10,9 @@ import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class NotificationsScreen extends StatefulWidget {
|
||||
class NotificationsScreen extends StatelessWidget {
|
||||
const NotificationsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<NotificationsScreen> createState() => _NotificationsScreenState();
|
||||
}
|
||||
|
||||
class _NotificationsScreenState extends State<NotificationsScreen> {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController.addListener(_onScroll);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) {
|
||||
final cubit = context.read<NotificationsCubit>();
|
||||
final state = cubit.state;
|
||||
if (state is NotificationsStateLoaded && state.hasNextPage && !state.isLoadingMore) {
|
||||
cubit.loadMoreNotifications();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
@@ -49,65 +20,62 @@ class _NotificationsScreenState extends State<NotificationsScreen> {
|
||||
builder: (BuildContext context, NotificationsState state) {
|
||||
return Scaffold(
|
||||
backgroundColor: appTheme.gray100,
|
||||
appBar: AppBar(title: const Text('Уведомления')),
|
||||
appBar: AppBar(
|
||||
title: const Text('Уведомления'),
|
||||
),
|
||||
body: state is NotificationsStateLoaded
|
||||
? Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.whiteA700,
|
||||
borderRadius: const BorderRadius.only(topLeft: Radius.circular(12), topRight: Radius.circular(12)),
|
||||
? Stack(children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.whiteA700,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(12),
|
||||
topRight: Radius.circular(12),
|
||||
),
|
||||
child: state.isLoading
|
||||
? const Center(child: Loader())
|
||||
: CustomScrollView(
|
||||
controller: _scrollController,
|
||||
slivers: [
|
||||
SliverList.separated(
|
||||
separatorBuilder: (context, index) =>
|
||||
const Padding(padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16), child: CustomDivider()),
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: NotificationWidget(notification: state.notifications[index]),
|
||||
);
|
||||
},
|
||||
itemCount: state.notifications.length,
|
||||
),
|
||||
child: state.isLoading
|
||||
? const Center(child: Loader())
|
||||
: CustomScrollView(
|
||||
slivers: [
|
||||
SliverList.separated(
|
||||
separatorBuilder: (context, index) => const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
child: CustomDivider(),
|
||||
),
|
||||
if (state.isLoadingMore)
|
||||
const SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Center(child: Loader()),
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: NotificationWidget(
|
||||
notification: state.notifications[index],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: state.notifications.length,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16),
|
||||
child: CustomOutlinedButton(
|
||||
text: s.markAsRead,
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||
buttonStyle:
|
||||
ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
|
||||
onPressed: () async {
|
||||
context.read<NotificationsCubit>().markNotificationsAsRead();
|
||||
},
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16),
|
||||
child: CustomOutlinedButton(
|
||||
text: s.markAsRead,
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||
buttonStyle: ButtonStyle(
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
|
||||
),
|
||||
onPressed: () async {
|
||||
context.read<NotificationsCubit>().markNotificationsAsRead();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
)
|
||||
])
|
||||
: const Placeholder(),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -15,49 +15,45 @@ class NotificationWidget extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return switch (notification) {
|
||||
PlanSubscriptionStartedToAuthorNotification() => _PlanSubscriptionStartedToAuthorWidget(
|
||||
notification: notification as PlanSubscriptionStartedToAuthorNotification,
|
||||
),
|
||||
notification: notification as PlanSubscriptionStartedToAuthorNotification,
|
||||
),
|
||||
PlanSubscriptionCanceledToAuthorNotification() => _PlanSubscriptionCanceledToAuthorWidget(
|
||||
notification: notification as PlanSubscriptionCanceledToAuthorNotification,
|
||||
),
|
||||
ChosenPlanStartedToClientNotification() => _ChosenPlanStartedToClientWidget(notification: notification as ChosenPlanStartedToClientNotification),
|
||||
notification: notification as PlanSubscriptionCanceledToAuthorNotification,
|
||||
),
|
||||
ChosenPlanStartedToClientNotification() => _ChosenPlanStartedToClientWidget(
|
||||
notification: notification as ChosenPlanStartedToClientNotification,
|
||||
),
|
||||
ChosenPlanTomorrowReminderToClientNotification() => _ChosenPlanTomorrowReminderToClientWidget(
|
||||
notification: notification as ChosenPlanTomorrowReminderToClientNotification,
|
||||
),
|
||||
DiaryReminderNotification() => _DiaryReminderWidget(notification: notification as DiaryReminderNotification),
|
||||
ConsultantLeftCommentNotification() => _ConsultantLeftCommentWidget(notification: notification as ConsultantLeftCommentNotification),
|
||||
PersonalizedDietIsDefinedNotification() => _PersonalizedDietIsDefinedWidget(notification: notification as PersonalizedDietIsDefinedNotification),
|
||||
ChosenPlanFinishedToClientNotification() => _ChosenPlanFinishedToClientWidget(notification: notification as ChosenPlanFinishedToClientNotification),
|
||||
notification: notification as ChosenPlanTomorrowReminderToClientNotification,
|
||||
),
|
||||
DiaryReminderNotification() => _DiaryReminderWidget(
|
||||
notification: notification as DiaryReminderNotification,
|
||||
),
|
||||
ConsultantLeftCommentNotification() => _ConsultantLeftCommentWidget(
|
||||
notification: notification as ConsultantLeftCommentNotification,
|
||||
),
|
||||
PersonalizedDietIsDefinedNotification() => _PersonalizedDietIsDefinedWidget(
|
||||
notification: notification as PersonalizedDietIsDefinedNotification,
|
||||
),
|
||||
ChosenPlanFinishedToClientNotification() => _ChosenPlanFinishedToClientWidget(
|
||||
notification: notification as ChosenPlanFinishedToClientNotification,
|
||||
),
|
||||
PlanSubscriptionCanceledToCustomerNotification() => _PlanSubscriptionCanceledToCustomerWidget(
|
||||
notification: notification as PlanSubscriptionCanceledToCustomerNotification,
|
||||
),
|
||||
notification: notification as PlanSubscriptionCanceledToCustomerNotification,
|
||||
),
|
||||
ChosenPlanStartedToConsultantNotification() => _ChosenPlanStartedToConsultantWidget(
|
||||
notification: notification as ChosenPlanStartedToConsultantNotification,
|
||||
),
|
||||
notification: notification as ChosenPlanStartedToConsultantNotification,
|
||||
),
|
||||
ChosenPlanTomorrowReminderToConsultantNotification() => _ChosenPlanTomorrowReminderToConsultantWidget(
|
||||
notification: notification as ChosenPlanTomorrowReminderToConsultantNotification,
|
||||
),
|
||||
PlanSubscriptionPaidToAuthorNotification() => _PlanSubscriptionPaidToAuthorWidget(notification: notification as PlanSubscriptionPaidToAuthorNotification),
|
||||
notification: notification as ChosenPlanTomorrowReminderToConsultantNotification,
|
||||
),
|
||||
PlanSubscriptionPaidToAuthorNotification() => _PlanSubscriptionPaidToAuthorWidget(
|
||||
notification: notification as PlanSubscriptionPaidToAuthorNotification,
|
||||
),
|
||||
ChosenPlanFinishedToConsultantNotification() => _ChosenPlanFinishedToConsultantWidget(
|
||||
notification: notification as ChosenPlanFinishedToConsultantNotification,
|
||||
),
|
||||
ChosenPlanLastDayFinishedForUser() => _ChosenPlanLastDayFinishedForUserWidget(notification: notification as ChosenPlanLastDayFinishedForUser),
|
||||
ChosenPlanConsultantLastDayStarted() => _ChosenPlanConsultantLastDayStartedWidget(notification: notification as ChosenPlanConsultantLastDayStarted),
|
||||
ComplaintToModerationForUser() => _ComplaintToModerationForUserWidget(notification: notification as ComplaintToModerationForUser),
|
||||
CommentReplied() => _CommentRepliedWidget(notification: notification as CommentReplied),
|
||||
ScoreAdded() => _ScoreAddedWidget(notification: notification as ScoreAdded),
|
||||
EntityCommented() => _EntityCommentedWidget(notification: notification as EntityCommented),
|
||||
// Новые типы уведомлений
|
||||
PlanSubscriptionExpiredNotification() => _PlanSubscriptionExpiredWidget(notification: notification as PlanSubscriptionExpiredNotification),
|
||||
PlanSubscriptionRenewedNotification() => _PlanSubscriptionRenewedWidget(notification: notification as PlanSubscriptionRenewedNotification),
|
||||
ConsultantAssignedNotification() => _ConsultantAssignedWidget(notification: notification as ConsultantAssignedNotification),
|
||||
ConsultantUnassignedNotification() => _ConsultantUnassignedWidget(notification: notification as ConsultantUnassignedNotification),
|
||||
PaymentFailedNotification() => _PaymentFailedWidget(notification: notification as PaymentFailedNotification),
|
||||
PaymentSuccessfulNotification() => _PaymentSuccessfulWidget(notification: notification as PaymentSuccessfulNotification),
|
||||
GoalAchievedNotification() => _GoalAchievedWidget(notification: notification as GoalAchievedNotification),
|
||||
WorkoutReminderNotification() => _WorkoutReminderWidget(notification: notification as WorkoutReminderNotification),
|
||||
MealReminderNotification() => _MealReminderWidget(notification: notification as MealReminderNotification),
|
||||
UnknownNotification() => _UnknownNotificationWidget(notification: notification as UnknownNotification),
|
||||
notification: notification as ChosenPlanFinishedToConsultantNotification,
|
||||
),
|
||||
UnknownNotification() => const SizedBox.shrink(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -71,8 +67,7 @@ class _PlanSubscriptionStartedToAuthorWidget extends StatelessWidget {
|
||||
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: s.userSubscribedToPlan(notification.customerName, notification.planName),
|
||||
createdAt: notification.createdAt,
|
||||
notificationsText: s.fillYasterday,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -86,8 +81,7 @@ class _PlanSubscriptionCanceledToAuthorWidget extends StatelessWidget {
|
||||
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: s.userCanceldThePlanSubscription(notification.customerName, notification.planName),
|
||||
createdAt: notification.createdAt,
|
||||
notificationsText: s.fillYasterday,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -99,7 +93,10 @@ class _ChosenPlanStartedToClientWidget extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
|
||||
return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: s.planStarted(notification.planName), createdAt: notification.createdAt);
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: s.fillYasterday,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,8 +109,7 @@ class _ChosenPlanTomorrowReminderToClientWidget extends StatelessWidget {
|
||||
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: s.tomorrowYourPlanWillStart(notification.planName),
|
||||
createdAt: notification.createdAt,
|
||||
notificationsText: s.fillYasterday,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -124,7 +120,13 @@ class _DiaryReminderWidget extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: s.fillYasterday, createdAt: notification.createdAt);
|
||||
return _BaseNotificationWidget(
|
||||
onTap: () {
|
||||
GoRouter.of(context).go('/diary');
|
||||
},
|
||||
isRead: notification.isRead,
|
||||
notificationsText: s.fillYasterday,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +140,6 @@ class _ConsultantLeftCommentWidget extends StatelessWidget {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: s.consultantLeftComment(notification.date.format('dd.MM.yyyy')),
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -148,7 +149,7 @@ class _PersonalizedDietIsDefinedWidget extends StatelessWidget {
|
||||
final PersonalizedDietIsDefinedNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: 'Персональная диета определена', createdAt: notification.createdAt);
|
||||
return Container();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,7 +163,6 @@ class _ChosenPlanFinishedToClientWidget extends StatelessWidget {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: '${s.congratsYouHaveFinishedPlan} «${notification.planName}»',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -177,7 +177,6 @@ class _PlanSubscriptionCanceledToCustomerWidget extends StatelessWidget {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: s.youCanceledSubscriptionToPlan(notification.planName),
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -192,7 +191,6 @@ class _ChosenPlanStartedToConsultantWidget extends StatelessWidget {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: s.planWithUserStarted(notification.planName, notification.customerName),
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -207,7 +205,6 @@ class _ChosenPlanTomorrowReminderToConsultantWidget extends StatelessWidget {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: s.tommorowPlanWithUserWillStart(notification.planName, notification.customerName),
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -222,7 +219,6 @@ class _PlanSubscriptionPaidToAuthorWidget extends StatelessWidget {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: s.userPaidThePlan(notification.customerName, notification.planName),
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -233,341 +229,62 @@ class _ChosenPlanFinishedToConsultantWidget extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('${s.congratsYouHaveFinishedPlan} «${notification.planName}»', style: TextStyles.textMSemibold.copyWith(color: appTheme.black)),
|
||||
const SizedBox(height: 12),
|
||||
Text(s.userConfirmedPlanCompletionWithoutSum, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
|
||||
const SizedBox(height: 12),
|
||||
CustomOutlinedButton(
|
||||
text: s.collectCharge,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.green)),
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pushNamed(CardSettingsScreen.name, extra: notification.planId);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!notification.isRead)
|
||||
Container(
|
||||
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
|
||||
width: 8,
|
||||
height: 8,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${notification.createdAt.day} ${_getMonthName(notification.createdAt.month)} ${notification.createdAt.hour.toString().padLeft(2, '0')}:${notification.createdAt.minute.toString().padLeft(2, '0')}',
|
||||
style: TextStyles.textSMedium.copyWith(color: appTheme.gray600),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _getMonthName(int month) {
|
||||
switch (month) {
|
||||
case 1:
|
||||
return 'янв';
|
||||
case 2:
|
||||
return 'фев';
|
||||
case 3:
|
||||
return 'мар';
|
||||
case 4:
|
||||
return 'апр';
|
||||
case 5:
|
||||
return 'май';
|
||||
case 6:
|
||||
return 'июн';
|
||||
case 7:
|
||||
return 'июл';
|
||||
case 8:
|
||||
return 'авг';
|
||||
case 9:
|
||||
return 'сен';
|
||||
case 10:
|
||||
return 'окт';
|
||||
case 11:
|
||||
return 'ноя';
|
||||
case 12:
|
||||
return 'дек';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
return Column(children: [
|
||||
Text(
|
||||
'${s.congratsYouHaveFinishedPlan} «${notification.planName}»',
|
||||
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
s.userConfirmedPlanCompletionWithoutSum,
|
||||
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
CustomOutlinedButton(
|
||||
text: s.collectCharge,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.green)),
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pushNamed(CardSettingsScreen.name, extra: notification.planId);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// CardSettingsScreen
|
||||
class _BaseNotificationWidget extends StatelessWidget {
|
||||
const _BaseNotificationWidget({required this.notificationsText, required this.isRead, required this.createdAt});
|
||||
const _BaseNotificationWidget({required this.notificationsText, required this.isRead, this.onTap});
|
||||
final String notificationsText;
|
||||
final bool isRead;
|
||||
final DateTime createdAt;
|
||||
final Function()? onTap;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(notificationsText, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: MediaQuery.sizeOf(context).width * 0.6,
|
||||
child: Text(
|
||||
notificationsText,
|
||||
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
|
||||
),
|
||||
if (!isRead)
|
||||
Container(
|
||||
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
|
||||
width: 8,
|
||||
height: 8,
|
||||
),
|
||||
if (!isRead) ...[
|
||||
const Spacer(),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.blueA400,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${createdAt.day} ${_getMonthName(createdAt.month)} ${createdAt.hour.toString().padLeft(2, '0')}:${createdAt.minute.toString().padLeft(2, '0')}',
|
||||
style: TextStyles.textSMedium.copyWith(color: appTheme.gray600),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _getMonthName(int month) {
|
||||
switch (month) {
|
||||
case 1:
|
||||
return 'янв';
|
||||
case 2:
|
||||
return 'фев';
|
||||
case 3:
|
||||
return 'мар';
|
||||
case 4:
|
||||
return 'апр';
|
||||
case 5:
|
||||
return 'май';
|
||||
case 6:
|
||||
return 'июн';
|
||||
case 7:
|
||||
return 'июл';
|
||||
case 8:
|
||||
return 'авг';
|
||||
case 9:
|
||||
return 'сен';
|
||||
case 10:
|
||||
return 'окт';
|
||||
case 11:
|
||||
return 'ноя';
|
||||
case 12:
|
||||
return 'дек';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _ChosenPlanLastDayFinishedForUserWidget extends StatelessWidget {
|
||||
const _ChosenPlanLastDayFinishedForUserWidget({required this.notification});
|
||||
final ChosenPlanLastDayFinishedForUser notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: 'Последний день плана завершен', createdAt: notification.createdAt);
|
||||
}
|
||||
}
|
||||
|
||||
class _ChosenPlanConsultantLastDayStartedWidget extends StatelessWidget {
|
||||
const _ChosenPlanConsultantLastDayStartedWidget({required this.notification});
|
||||
final ChosenPlanConsultantLastDayStarted notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Последний день плана начался: ${notification.planName}',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ComplaintToModerationForUserWidget extends StatelessWidget {
|
||||
const _ComplaintToModerationForUserWidget({required this.notification});
|
||||
final ComplaintToModerationForUser notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: 'Жалоба отправлена на модерацию', createdAt: notification.createdAt);
|
||||
}
|
||||
}
|
||||
|
||||
class _CommentRepliedWidget extends StatelessWidget {
|
||||
const _CommentRepliedWidget({required this.notification});
|
||||
final CommentReplied notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Ответ на комментарий: ${notification.entityName}',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ScoreAddedWidget extends StatelessWidget {
|
||||
const _ScoreAddedWidget({required this.notification});
|
||||
final ScoreAdded notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Добавлены баллы: ${notification.scoreValue}',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EntityCommentedWidget extends StatelessWidget {
|
||||
const _EntityCommentedWidget({required this.notification});
|
||||
final EntityCommented notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Комментарий к ${notification.entityTranslatedType}: ${notification.entityName}',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _UnknownNotificationWidget extends StatelessWidget {
|
||||
const _UnknownNotificationWidget({required this.notification});
|
||||
final UnknownNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Неизвестное уведомление: ${notification.type.toString()}',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Новые виджеты для дополнительных типов уведомлений
|
||||
class _PlanSubscriptionExpiredWidget extends StatelessWidget {
|
||||
const _PlanSubscriptionExpiredWidget({required this.notification});
|
||||
final PlanSubscriptionExpiredNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Подписка на план "${notification.planName}" истекла',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PlanSubscriptionRenewedWidget extends StatelessWidget {
|
||||
const _PlanSubscriptionRenewedWidget({required this.notification});
|
||||
final PlanSubscriptionRenewedNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Подписка на план "${notification.planName}" продлена до ${notification.renewalDate}',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ConsultantAssignedWidget extends StatelessWidget {
|
||||
const _ConsultantAssignedWidget({required this.notification});
|
||||
final ConsultantAssignedNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'К плану "${notification.planName}" назначен консультант ${notification.consultantName}',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ConsultantUnassignedWidget extends StatelessWidget {
|
||||
const _ConsultantUnassignedWidget({required this.notification});
|
||||
final ConsultantUnassignedNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Консультант ${notification.consultantName} отстранен от плана "${notification.planName}"',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PaymentFailedWidget extends StatelessWidget {
|
||||
const _PaymentFailedWidget({required this.notification});
|
||||
final PaymentFailedNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Ошибка оплаты плана "${notification.planName}": ${notification.errorMessage}',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PaymentSuccessfulWidget extends StatelessWidget {
|
||||
const _PaymentSuccessfulWidget({required this.notification});
|
||||
final PaymentSuccessfulNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Успешная оплата плана "${notification.planName}" на сумму ${notification.amount} ${notification.currency}',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GoalAchievedWidget extends StatelessWidget {
|
||||
const _GoalAchievedWidget({required this.notification});
|
||||
final GoalAchievedNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Достигнута цель "${notification.goalName}": ${notification.achievedValue}/${notification.targetValue}',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WorkoutReminderWidget extends StatelessWidget {
|
||||
const _WorkoutReminderWidget({required this.notification});
|
||||
final WorkoutReminderNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Напоминание о тренировке "${notification.workoutName}" (${notification.duration} мин)',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MealReminderWidget extends StatelessWidget {
|
||||
const _MealReminderWidget({required this.notification});
|
||||
final MealReminderNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Напоминание о приеме пищи "${notification.mealName}" (${notification.calories} ккал)',
|
||||
createdAt: notification.createdAt,
|
||||
width: 8,
|
||||
height: 8,
|
||||
)
|
||||
]
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import 'dart:developer';
|
||||
import 'package:eatvio_app/features/notifications/models/notification_model.dart';
|
||||
import 'package:eatvio_app/features/notifications/service/notifications_service.dart';
|
||||
|
||||
class NotificationsRepository {
|
||||
final NotificationsService notificationsService;
|
||||
NotificationsRepository({required this.notificationsService});
|
||||
|
||||
Future<Map<String, dynamic>?> getNotifications({required int page}) {
|
||||
Future<List<BaseNotificationModel>?> getNotifications({required int page}) {
|
||||
try {
|
||||
return notificationsService.getNotifications(page: page);
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,192 +5,50 @@ class NotificationsService {
|
||||
final Dio dio;
|
||||
NotificationsService(this.dio);
|
||||
|
||||
Future<Map<String, dynamic>?> getNotifications({required int page}) async {
|
||||
Future<List<BaseNotificationModel>?> getNotifications({required int page}) async {
|
||||
try {
|
||||
final response = await dio.get('/api/profile/notifications?page=$page');
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data['data'] as List<dynamic>;
|
||||
|
||||
final notifications = data.map((notificationJson) {
|
||||
try {
|
||||
final stringifiedType = notificationJson['data']['notification_class_name'];
|
||||
final readDate = notificationJson['read_at'];
|
||||
final createdAt = notificationJson['created_at'];
|
||||
final type = NotificationType.getTypeFromString(stringifiedType);
|
||||
|
||||
return switch (type) {
|
||||
NotificationType.planSubscriptionStartedToAuthor => PlanSubscriptionStartedToAuthorNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.planSubscriptionCanceledToAuthor => PlanSubscriptionCanceledToAuthorNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.chosenPlanStartedToClientNotification => ChosenPlanStartedToClientNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.chosenPlanTomorrowReminderToClientNotification => ChosenPlanTomorrowReminderToClientNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.planSubscriptionStartedToAuthor =>
|
||||
PlanSubscriptionStartedToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.planSubscriptionCanceledToAuthor =>
|
||||
PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.chosenPlanStartedToClientNotification =>
|
||||
ChosenPlanStartedToClientNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.chosenPlanTomorrowReminderToClientNotification =>
|
||||
ChosenPlanTomorrowReminderToClientNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.diaryReminderNotification => DiaryReminderNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.consultantLeftComment => ConsultantLeftCommentNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.personalizedDietIsDefined => PersonalizedDietIsDefinedNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.chosenPlanFinishedToClient => ChosenPlanFinishedToClientNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.planSubscriptionCanceledToCustomer => PlanSubscriptionCanceledToAuthorNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.chosenPlanStartedToConsultantNotification => ChosenPlanStartedToConsultantNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.chosenPlanTomorrowReminderToConsultantNotification => ChosenPlanTomorrowReminderToConsultantNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.planSubscriptionPaidToAuthor => PlanSubscriptionPaidToAuthorNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.chosenPlanFinishedToConsultant => ChosenPlanFinishedToConsultantNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.chosenPlanLastDayFinishedForUser => ChosenPlanLastDayFinishedForUser.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
// Новые типы уведомлений
|
||||
NotificationType.chosenPlanConsultantLastDayStarted => ChosenPlanConsultantLastDayStarted.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.complaintToModerationForUser => ComplaintToModerationForUser.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.commentReplied => CommentReplied.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt),
|
||||
NotificationType.scoreAdded => ScoreAdded.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt),
|
||||
NotificationType.entityCommented => EntityCommented.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt),
|
||||
// Новые типы уведомлений
|
||||
NotificationType.planSubscriptionExpired => PlanSubscriptionExpiredNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.planSubscriptionRenewed => PlanSubscriptionRenewedNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.consultantAssigned => ConsultantAssignedNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.consultantUnassigned => ConsultantUnassignedNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.paymentFailed => PaymentFailedNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.paymentSuccessful => PaymentSuccessfulNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.goalAchieved => GoalAchievedNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.workoutReminder => WorkoutReminderNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.mealReminder => MealReminderNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.unknown => UnknownNotification(type: type, createdAt: parseDate(createdAt)),
|
||||
(notificationJson['data'] as Map<String, dynamic>).putIfAbsent('date', () => notificationJson['created_at']), type,
|
||||
isRead: readDate != null),
|
||||
NotificationType.consultantLeftComment => ConsultantLeftCommentNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.personalizedDietIsDefined =>
|
||||
PersonalizedDietIsDefinedNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.chosenPlanFinishedToClient =>
|
||||
ChosenPlanFinishedToClientNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.planSubscriptionCanceledToCustomer =>
|
||||
PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.chosenPlanStartedToConsultantNotification =>
|
||||
ChosenPlanStartedToConsultantNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.chosenPlanTomorrowReminderToConsultantNotification =>
|
||||
ChosenPlanTomorrowReminderToConsultantNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.planSubscriptionPaidToAuthor =>
|
||||
PlanSubscriptionPaidToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.chosenPlanFinishedToConsultant =>
|
||||
ChosenPlanFinishedToConsultantNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.unknown => UnknownNotification(type: type),
|
||||
};
|
||||
} catch (e) {
|
||||
return UnknownNotification(type: NotificationType.unknown, createdAt: DateTime.now());
|
||||
return UnknownNotification(type: NotificationType.unknown);
|
||||
}
|
||||
}).toList();
|
||||
|
||||
// Извлекаем информацию о пагинации из корня ответа
|
||||
final currentPage = response.data['current_page'] as int;
|
||||
final lastPage = response.data['last_page'] as int;
|
||||
final perPage = response.data['per_page'] as int;
|
||||
final total = response.data['total'] as int;
|
||||
|
||||
return {
|
||||
'notifications': notifications,
|
||||
'pagination': {'current_page': currentPage, 'last_page': lastPage, 'per_page': perPage, 'total': total},
|
||||
};
|
||||
return notifications.where((notification) => notification.type != NotificationType.unknown).toList();
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
@@ -206,5 +64,3 @@ class NotificationsService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DateTime parseDate(String date) => DateTime.parse(date).toLocal();
|
||||
|
||||
@@ -15,8 +15,8 @@ import 'package:eatvio_app/features/diets/presentation/widgets/author_card.dart'
|
||||
import 'package:eatvio_app/features/diets/presentation/widgets/expandable_text.dart';
|
||||
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/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/comments_cubit/comments_cubit.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_state.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart';
|
||||
import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_cubit.dart';
|
||||
@@ -208,7 +208,7 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
|
||||
GoRouter.of(context).pushReplacementNamed(
|
||||
'chatScreen',
|
||||
extra: {
|
||||
'uuid': chosenPlan.chatId,
|
||||
'chatId': chosenPlan.chatId,
|
||||
'sendInitialMessage': true,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -14,12 +14,9 @@ abstract class ProfileModel with _$ProfileModel {
|
||||
@JsonKey(name: "email") required String email,
|
||||
@JsonKey(name: "rating_place") int? ratingPlace,
|
||||
@JsonKey(name: "email_verified_at") DateTime? emailVerifiedAt,
|
||||
@JsonKey(name: "phone_confirmation_state") required String phoneConfirmationState,
|
||||
@JsonKey(name: "created_at") DateTime? createdAt,
|
||||
@JsonKey(name: "updated_at") DateTime? updatedAt,
|
||||
@JsonKey(name: "is_public_profile", fromJson: parseBool) required bool isPublicProfile,
|
||||
@JsonKey(name: "is_blocked_by_me", fromJson: parseBool) required bool isBlockedByMe,
|
||||
@JsonKey(name: "is_blocked_by_user", fromJson: parseBool, defaultValue: false) required bool isBlockedByUser,
|
||||
@JsonKey(name: 'is_entity_subscribed_by_me') required bool isEntitySubscribedByMe,
|
||||
@JsonKey(name: "slug") required String slug,
|
||||
@JsonKey(name: "profile_photo_url") required String profilePhotoUrl,
|
||||
|
||||
@@ -25,7 +25,6 @@ class ProfileCubit extends Cubit<ProfileState> {
|
||||
}) : super(const ProfileState.loading());
|
||||
|
||||
Future<void> getProfileBySlug(String slug) async {
|
||||
emit(const ProfileState.loading());
|
||||
try {
|
||||
final profile = await profileRepository.getProfileBySlug(slug);
|
||||
emit(
|
||||
@@ -45,7 +44,6 @@ class ProfileCubit extends Cubit<ProfileState> {
|
||||
Future<void> getMyProfile({
|
||||
String period = '1',
|
||||
}) async {
|
||||
emit(const ProfileState.loading());
|
||||
_currentPeriod = period;
|
||||
try {
|
||||
final profile = await profileRepository.getMyProfile();
|
||||
@@ -125,7 +123,7 @@ class ProfileCubit extends Cubit<ProfileState> {
|
||||
AppRouter.navigatorKey.currentContext?.pushNamed(
|
||||
'chatScreen',
|
||||
extra: {
|
||||
'uuid': chatIdOrError,
|
||||
'chatId': chatIdOrError,
|
||||
},
|
||||
);
|
||||
return chatIdOrError;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -69,7 +69,7 @@ class _SubActionButtonWidgetState extends State<SubActionButtonWidget> {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'chatScreen',
|
||||
extra: {
|
||||
'uuid': await sl<ProfileCubit>().getChatRoomIdBySlug(
|
||||
'chatId': await sl<ProfileCubit>().getChatRoomIdBySlug(
|
||||
author: Author(
|
||||
authorSlug: widget.slug,
|
||||
authorName: widget.name,
|
||||
|
||||
@@ -203,12 +203,4 @@ class ProfileRepository {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteAccount() async {
|
||||
try {
|
||||
await profileService.deleteAccount();
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +119,4 @@ abstract class ProfileService {
|
||||
Future<void> tryVerifyEmailByCode({
|
||||
@Body() required String code,
|
||||
});
|
||||
|
||||
@DELETE('/api/profile/delete_my_account')
|
||||
Future<void> deleteAccount();
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ import 'package:eatvio_app/features/diets/presentation/widgets/author_card.dart'
|
||||
import 'package:eatvio_app/features/diets/presentation/widgets/expandable_text.dart';
|
||||
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/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/comments_cubit/comments_cubit.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/comments_cubit/comments_state.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart';
|
||||
import 'package:eatvio_app/features/recipe/presentation/cubit/about_recipe_cubit.dart';
|
||||
@@ -205,14 +205,14 @@ class _AboutRecipeScreenState extends State<AboutRecipeScreen> {
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
leading: BackButton(
|
||||
onPressed: () {
|
||||
if (context.canPop()) {
|
||||
GoRouter.of(context).pop();
|
||||
return;
|
||||
}
|
||||
GoRouter.of(context).go('/feed-screen');
|
||||
},
|
||||
),
|
||||
onPressed: () {
|
||||
if (context.canPop()) {
|
||||
GoRouter.of(context).pop();
|
||||
return;
|
||||
}
|
||||
GoRouter.of(context).go('/feed-screen');
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
recipeState.recipe.title ?? '',
|
||||
maxLines: 2,
|
||||
@@ -356,7 +356,6 @@ class _AboutRecipeScreenState extends State<AboutRecipeScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ExpandableText(text: recipeState.recipe.description),
|
||||
const SizedBox(height: 20),
|
||||
Padding(
|
||||
@@ -401,14 +400,14 @@ class _AboutRecipeScreenState extends State<AboutRecipeScreen> {
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
leading: BackButton(
|
||||
onPressed: () {
|
||||
if (context.canPop()) {
|
||||
GoRouter.of(context).pop();
|
||||
return;
|
||||
}
|
||||
GoRouter.of(context).go('/feed-screen');
|
||||
},
|
||||
),
|
||||
onPressed: () {
|
||||
if (context.canPop()) {
|
||||
GoRouter.of(context).pop();
|
||||
return;
|
||||
}
|
||||
GoRouter.of(context).go('/feed-screen');
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
'',
|
||||
maxLines: 2,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user