Compare commits

..

2 Commits

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

View File

@@ -11,29 +11,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
- name: Checkout env repository
uses: actions/checkout@v2
with:
repository: mso7nueh/eatvio_app_env
path: env_repo
token: ${{ vars.MSO7NUEH_TOKEN }}
ref: main
- name: Copy .env file
run: |
cp env_repo/.env .env
- name: Load environment variables from .env
run: |
set -a
source .env
set +a
# Export all variables to GITHUB_ENV
while IFS= read -r line; do
if [[ $line =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then
echo "$line" >> $GITHUB_ENV
fi
done < .env
- name: Unlock login keychain
run: |
@@ -41,25 +18,12 @@ jobs:
env:
KEYCHAIN_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }}
- name: Setup API key
run: |
echo "${API_APPSTORE_KEY}" | tr -d '\n' | base64 --decode > ios/AuthKey.p8
chmod 600 ios/AuthKey.p8
ls -la ios/AuthKey.p8
echo "File size: $(wc -c < ios/AuthKey.p8) bytes"
echo "Contents of ios directory:"
ls -la ios/
env:
API_APPSTORE_KEY: ${{ vars.API_APPSTORE_KEY }}
- name: Flutter build
run: |
flutter clean
flutter pub get
flutter pub upgrade
flutter pub run build_runner build --delete-conflicting-outputs
# Configure Flutter for release mode
flutter build ios --config-only --release
cd ios
pod update
fastlane beta
@@ -67,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 }}

View File

@@ -3,7 +3,7 @@ name: Eatvio App Android Test
on: [ pull_request ]
env:
flutter_version: "3.35.1"
flutter_version: "3.32.5"
java_version: "21.x"
flutter_channel: "stable"
my_local_cache_dir: "/opt/hostedtoolcache/"
@@ -16,30 +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: |
cp env_repo/.env .env
- name: Load environment variables from .env
run: |
set -a
source .env
set +a
# Export all variables to GITHUB_ENV
while IFS= read -r line; do
if [[ $line =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then
echo "$line" >> $GITHUB_ENV
fi
done < .env
- name: Create key.properties file
run: |
cat > android/key.properties <<EOF
@@ -143,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: |

4
.gitignore vendored
View File

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

View File

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

View File

@@ -19,17 +19,17 @@ android {
ndkVersion = "27.0.12077973"
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"
minSdkVersion flutter.minSdkVersion
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName

View File

@@ -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

View File

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

View File

@@ -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-9.0.0-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-all.zip

View File

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

View File

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

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

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

View File

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

View File

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

View File

@@ -370,7 +370,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -388,7 +388,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 72;
CURRENT_PROJECT_VERSION = 39;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -406,11 +406,10 @@
"$(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";
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
@@ -467,7 +466,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -519,7 +518,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -539,7 +538,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 72;
CURRENT_PROJECT_VERSION = 39;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -557,11 +556,10 @@
"$(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";
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@@ -578,7 +576,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 72;
CURRENT_PROJECT_VERSION = 39;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -596,11 +594,10 @@
"$(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";
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";

View File

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

View File

@@ -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>72</string>
<string>39</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>

View File

@@ -18,69 +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}"
# Configure Flutter for release mode
UI.message "Configuring Flutter for release mode..."
sh("cd .. && flutter build ios --config-only --release")
match(type: "appstore", readonly: true)
match(type: "appstore")
increment_build_number(xcodeproj: "Runner.xcodeproj")
build_app(
workspace: "Runner.xcworkspace",
scheme: "Runner",
export_method: "app-store",
codesigning_identity: "Apple Distribution",
)
app_store_connect_api_key(
key_id: "325JS9JCZH",
issuer_id: "dfa27d1d-2bb8-4d4d-847e-0b32f9b880b3",
key_filepath: auth_key_path,
duration: 1200,
in_house: false
)
begin
upload_to_testflight(
skip_waiting_for_build_processing: true,
skip_submission: true,
distribute_external: false,
notify_external_testers: false,
changelog: "TestFlight build #{get_build_number(xcodeproj: 'Runner.xcodeproj')}",
verbose: true
)
rescue => ex
UI.error "Upload to TestFlight failed: #{ex.message}"
UI.error "Full error: #{ex.backtrace.join("\n")}"
raise ex
end
build_app(workspace: "Runner.xcworkspace", scheme: "Runner", export_method: "app-store", codesigning_identity: "Apple Distribution")
upload_to_testflight
end
end

View File

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

View File

@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/firebase_api.dart';
@@ -24,7 +26,6 @@ import 'package:eatvio_app/features/consultations/presentation/pages/consultatio
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart';
import 'package:eatvio_app/features/menu/presentation/pages/blocked_users_screen.dart';
import 'package:eatvio_app/features/menu/presentation/pages/notifications_settings_screen.dart';
import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart';
import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart';
import 'package:eatvio_app/features/create_diet/presentation/diet_preview_screen.dart';
@@ -112,13 +113,21 @@ class AppRouter {
name: 'authScreen',
builder: (context, state) => const AuthScreen(),
routes: [
GoRoute(path: 'registration-screen', name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()),
GoRoute(
path: 'registration-screen',
name: 'registrationScreen',
builder: (context, state) => const RegistrationScreen(),
),
GoRoute(
path: 'forget-password-screen',
name: 'forgetPasswordScreen',
builder: (context, state) => ForgetPasswordScreen(isAuth: state.extra as bool),
builder: (context, state) => const ForgetPasswordScreen(),
),
GoRoute(
path: 'scan-qr-code-screen',
name: 'scanQRCodeScreen',
builder: (context, state) => const ScanQRCodeScreen(),
),
GoRoute(path: 'scan-qr-code-screen', name: 'scanQRCodeScreen', builder: (context, state) => const ScanQRCodeScreen()),
],
),
ShellRoute(
@@ -156,23 +165,19 @@ class AppRouter {
GoRoute(
path: '/diary-screen',
name: 'diaryScreen',
pageBuilder: (context, state) => const NoTransitionPage(child: DiaryScreen()),
pageBuilder: (context, state) => const NoTransitionPage(
child: DiaryScreen(),
),
),
GoRoute(
path: ConsultationsScreen.path,
name: ConsultationsScreen.name,
pageBuilder: (context, state) => const NoTransitionPage(child: ConsultationsScreen()),
),
path: ConsultationsScreen.path,
name: ConsultationsScreen.name,
pageBuilder: (context, state) => const NoTransitionPage(child: ConsultationsScreen())),
GoRoute(path: '/chat-select-screen', name: 'chatSelectScreen', pageBuilder: (context, state) => const NoTransitionPage(child: ChatsScreen())),
GoRoute(
path: '/chat-select-screen',
name: 'chatSelectScreen',
pageBuilder: (context, state) => const NoTransitionPage(child: ChatsScreen()),
),
GoRoute(
path: '/notifications-screen',
name: 'notificationsScreen',
pageBuilder: (context, state) => const NoTransitionPage(child: NotificationsScreen()),
),
path: '/notifications-screen',
name: 'notificationsScreen',
pageBuilder: (context, state) => const NoTransitionPage(child: NotificationsScreen())),
GoRoute(
path: '/menu-screen',
name: 'menuScreen',
@@ -189,16 +194,32 @@ class AppRouter {
),
],
),
GoRoute(name: 'blockedUsersScreen', path: '/blocked-users-screen', builder: (context, state) => const BlockedUsersScreen()),
GoRoute(
name: 'blockedUsersScreen',
path: '/blocked-users-screen',
builder: (context, state) => const BlockedUsersScreen(),
),
GoRoute(
path: '/welcome-screen',
name: 'welcomeScreen',
builder: (context, state) => const WelcomeScreen(),
redirect: (context, state) async {
try {
if (await TokenStorage.haveTokenInStorage()) {
final firebaseApi = sl<FirebaseApi>();
final fbToken = await firebaseApi.getToken();
final deviceInfo = DeviceInfoPlugin();
await sl<Dio>().post(
'/api/profile/notification/token/register',
data: {
'platform': Platform.isIOS ? 'ios' : 'android',
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
'token': fbToken,
},
);
}
final sharedPreferences = await SharedPreferences.getInstance();
final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed') ?? false;
await sl<FirebaseApi>().registerPushToken();
if (welcomeScreenWasShowed == false) {
return null;
} else if (await TokenStorage.haveTokenInStorage()) {
@@ -207,7 +228,7 @@ class AppRouter {
return '/auth-screen';
}
} catch (e) {
return '/feed-screen';
rethrow;
}
},
),
@@ -232,10 +253,26 @@ class AppRouter {
}
},
),
GoRoute(path: ConsultationsArchiveScreen.path, name: ConsultationsArchiveScreen.name, builder: (context, state) => const ConsultationsArchiveScreen()),
GoRoute(path: ConsultantFinancesScreen.path, name: ConsultantFinancesScreen.name, builder: (context, state) => const ConsultantFinancesScreen()),
GoRoute(path: UserPurchasesScreen.path, name: UserPurchasesScreen.name, builder: (context, state) => const UserPurchasesScreen()),
GoRoute(path: '/user-finance-screen', name: 'userFinanceScreen', builder: (context, state) => const UserFinanceScreen()),
GoRoute(
path: ConsultationsArchiveScreen.path,
name: ConsultationsArchiveScreen.name,
builder: (context, state) => const ConsultationsArchiveScreen(),
),
GoRoute(
path: ConsultantFinancesScreen.path,
name: ConsultantFinancesScreen.name,
builder: (context, state) => const ConsultantFinancesScreen(),
),
GoRoute(
path: UserPurchasesScreen.path,
name: UserPurchasesScreen.name,
builder: (context, state) => const UserPurchasesScreen(),
),
GoRoute(
path: '/user-finance-screen',
name: 'userFinanceScreen',
builder: (context, state) => const UserFinanceScreen(),
),
GoRoute(
path: CardSettingsScreen.path,
name: CardSettingsScreen.name,
@@ -250,7 +287,11 @@ class AppRouter {
builder: (context, state) {
final slug = state.uri.queryParameters['slug'];
if (slug != null) {
return AboutPlanScreen(slug: slug, navigateToComments: false, index: null);
return AboutPlanScreen(
slug: slug,
navigateToComments: false,
index: null,
);
}
final data = state.extra as Map<String, dynamic>;
@@ -264,23 +305,29 @@ class AppRouter {
builder: (context, state) {
final slug = state.uri.queryParameters['slug'];
if (slug != null) {
return AboutRecipeScreen(recipeSlug: slug, navigateToComments: false, index: null);
return AboutRecipeScreen(
recipeSlug: slug,
navigateToComments: false,
index: null,
);
}
final data = state.extra as Map<String, dynamic>;
return AboutRecipeScreen(recipeSlug: data['recipe'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
return AboutRecipeScreen(
recipeSlug: data['recipe'] as String,
index: data['index'] as int?,
navigateToComments: data['navigateToComments'] as bool,
);
},
),
GoRoute(
path: RecipeCreateScreen.path,
name: RecipeCreateScreen.name,
builder: (context, state) => RecipeCreateScreen(recipe: state.extra != null ? state.extra as Recipe : null),
),
GoRoute(
path: '/profile-screen',
name: 'profileScreen',
builder: (context, state) => ProfileScreen(author: state.extra as Author),
builder: (context, state) => RecipeCreateScreen(
recipe: state.extra != null ? state.extra as Recipe : null,
),
),
GoRoute(path: '/profile-screen', name: 'profileScreen', builder: (context, state) => ProfileScreen(author: state.extra as Author)),
GoRoute(
path: '/subs-screen/:slug',
name: 'subsScreen',
@@ -292,7 +339,11 @@ class AppRouter {
builder: (context, state) {
final slug = state.uri.queryParameters['slug'];
if (slug != null) {
return AboutDietScreen(slug: slug, index: null, navigateToComments: false);
return AboutDietScreen(
slug: slug,
index: null,
navigateToComments: false,
);
}
final data = state.extra as Map<String, dynamic>;
return AboutDietScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
@@ -304,26 +355,30 @@ class AppRouter {
builder: (context, state) => BlogPostCreateScreen(feedBlogPostModel: state.extra != null ? state.extra as FeedBlogPostModel : null),
),
GoRoute(
path: '/blog-post-screen',
name: 'blogPostScreen',
builder: (context, state) {
final id = state.uri.queryParameters['id'];
final parsedId = int.tryParse(id ?? '');
if (parsedId != null) {
return BlogPostScreen(id: parsedId);
}
path: '/blog-post-screen',
name: 'blogPostScreen',
builder: (context, state) {
final id = state.uri.queryParameters['id'];
final parsedId = int.tryParse(id ?? '');
if (parsedId != null) {
return BlogPostScreen(
id: parsedId,
);
}
return BlogPostScreen(id: state.extra as int);
},
),
return BlogPostScreen(id: state.extra as int);
}),
GoRoute(
path: '/chat-screen',
name: 'chatScreen',
builder: (context, state) {
final data = state.extra as Map<String, dynamic>;
final uuid = data['uuid'] as String;
final chatId = data['chatId'] as String;
final sendInitialMessage = data['sendInitialMessage'] as bool?;
return ChatScreen(uuid: uuid, sendInitialMessage: sendInitialMessage);
return ChatScreen(
chatId: chatId,
sendInitialMessage: sendInitialMessage,
);
},
),
GoRoute(
@@ -356,11 +411,7 @@ class AppRouter {
name: 'physicalFormScreen',
builder: (context, state) => PhysicalFormScreen(settingsEntity: state.extra as SettingsEntity),
),
GoRoute(
path: 'pressure-screen',
name: 'pressureScreen',
builder: (context, state) => PressureScreen(settingsEntity: state.extra as SettingsEntity),
),
GoRoute(path: 'pressure-screen', name: 'pressureScreen', builder: (context, state) => PressureScreen(settingsEntity: state.extra as SettingsEntity)),
GoRoute(
path: 'change-password-screen',
name: 'changePasswordScreen',
@@ -377,7 +428,9 @@ class AppRouter {
GoRoute(
path: '/stepper-become-consultant-screen',
name: 'stepperBecomeConsultantScreen',
builder: (context, state) => StepperBecomeConsultantScreen(slug: state.extra == null ? null : state.extra as String),
builder: (context, state) => StepperBecomeConsultantScreen(
slug: state.extra == null ? null : state.extra as String,
),
),
GoRoute(path: '/calendar-screen', name: 'calendarScreen', builder: (context, state) => const CalendarScreen()),
GoRoute(path: '/support-screen', name: 'supportScreen', builder: (context, state) => const SupportScreen()),
@@ -405,11 +458,7 @@ class AppRouter {
GoRoute(path: CreatePlanScreen.path, name: CreatePlanScreen.name, builder: (context, state) => const CreatePlanScreen()),
GoRoute(path: MyDietsScreen.path, name: MyDietsScreen.name, builder: (context, state) => const MyDietsScreen()),
GoRoute(path: MyPlansScreen.path, name: MyPlansScreen.name, builder: (context, state) => const MyPlansScreen()),
GoRoute(
path: UserDetailsScreen.path,
name: UserDetailsScreen.name,
builder: (context, state) => UserDetailsScreen(slug: state.extra as String),
),
GoRoute(path: UserDetailsScreen.path, name: UserDetailsScreen.name, builder: (context, state) => UserDetailsScreen(slug: state.extra as String)),
GoRoute(path: DevToolsScreen.path, name: DevToolsScreen.name, builder: (context, state) => const DevToolsScreen()),
GoRoute(path: ConsultantRatingScreen.path, name: ConsultantRatingScreen.name, builder: (context, state) => const ConsultantRatingScreen()),
GoRoute(
@@ -419,7 +468,10 @@ class AppRouter {
final data = state.extra as Map<String, dynamic>;
final planId = data['planId'] as int;
final successfulPayment = data['successfulPayment'] as bool?;
return InfoByOrderScreen(planId: planId, successfulPayment: successfulPayment);
return InfoByOrderScreen(
planId: planId,
successfulPayment: successfulPayment,
);
},
),
GoRoute(
@@ -429,7 +481,10 @@ class AppRouter {
final data = state.extra as Map<String, dynamic>;
final planId = data['planId'] as int;
final url = data['url'] as String;
return PaymentScreen(url: url, planId: planId);
return PaymentScreen(
url: url,
planId: planId,
);
},
),
GoRoute(
@@ -439,7 +494,10 @@ class AppRouter {
final data = state.extra as Map<String, int>;
final diaryId = data['diaryId'] as int;
final currentPlanDay = data['currentDiaryDay'] as int;
return UserDiaryScreen(diaryId: diaryId, currentPlanDay: currentPlanDay);
return UserDiaryScreen(
diaryId: diaryId,
currentPlanDay: currentPlanDay,
);
},
),
GoRoute(
@@ -447,9 +505,16 @@ class AppRouter {
name: 'verifyScreen',
builder: (context, state) => VerifyScreen(passKey: state.extra as String),
),
GoRoute(path: '/verify-email-screen', name: 'verifyEmailScreen', builder: (context, state) => const VerifyEmailScreen()),
GoRoute(path: MyRecipesScreen.path, name: MyRecipesScreen.name, builder: (context, state) => const MyRecipesScreen()),
GoRoute(path: '/notifications-settings-screen', name: 'notificationsSettingsScreen', builder: (context, state) => const NotificationsSettingsScreen()),
GoRoute(
path: '/verify-email-screen',
name: 'verifyEmailScreen',
builder: (context, state) => const VerifyEmailScreen(),
),
GoRoute(
path: MyRecipesScreen.path,
name: MyRecipesScreen.name,
builder: (context, state) => const MyRecipesScreen(),
),
],
);
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,5 @@
import 'package:eatvio_app/core/env_config.dart';
String baseUrl = 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];

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,3 @@
import 'dart:developer';
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
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);
}
}
}
}

View File

@@ -1,13 +1,8 @@
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';
import 'package:eatvio_app/core/interceptors/token_interceptor.dart';
import 'package:eatvio_app/core/views/cubit/views_cubit.dart';
import 'package:eatvio_app/core/views/repository/views_repository.dart';
import 'package:eatvio_app/core/views/service/views_service.dart';
import 'package:eatvio_app/core/websockets/ws_manager.dart';
import 'package:eatvio_app/core/websockets/ws_service.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart';
@@ -23,9 +18,6 @@ import 'package:eatvio_app/features/cancel_plan_bottom_sheet/service/cancel_plan
import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_cubit.dart';
import 'package:eatvio_app/features/complaining/repositories/complaining_repository.dart';
import 'package:eatvio_app/features/complaining/services/complaining_service.dart';
import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart';
import 'package:eatvio_app/features/consultant_rating/repositories/consultant_rating_repository.dart';
import 'package:eatvio_app/features/consultant_rating/services/consultant_rating_service.dart';
import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_cubit.dart';
import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart';
import 'package:eatvio_app/features/consultations/services/consultation_service.dart';
@@ -46,11 +38,8 @@ import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_cubit.dart';
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart';
import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart';
import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart';
import 'package:eatvio_app/features/menu/repositories/notification_settings_repository.dart';
import 'package:eatvio_app/features/menu/services/blocked_users_service.dart';
import 'package:eatvio_app/features/menu/services/notification_settings_service.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
import 'package:eatvio_app/features/calendar/presentation/bloc/calendar_cubit.dart';
@@ -67,7 +56,7 @@ import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
import 'package:eatvio_app/features/diary/repositories/diary_repository.dart';
import 'package:eatvio_app/features/diary/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';
@@ -116,6 +105,7 @@ import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/se
import 'package:eatvio_app/features/settings/repositories/service_repository.dart';
import 'package:eatvio_app/features/settings/services/settings_service.dart';
import 'package:eatvio_app/features/user_details/presentation/cubit/user_details_cubit.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:get_it/get_it.dart';
import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart' as profile_plans;
@@ -138,20 +128,18 @@ 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(), consultationsRepository: sl()));
sl.registerFactory<SettingsCubit>(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl(), consultantRepository: sl()));
sl.registerFactory<RegisterConsultantCubit>(() => RegisterConsultantCubit(consultantRepository: sl()));
sl.registerFactory<DiaryCubit>(
() => DiaryCubit(
chatRepository: sl(),
diaryRepository: sl(),
glossaryRepository: sl(),
profileRepository: sl(),
dietRepository: sl(),
settingsRepository: sl(),
),
);
sl.registerFactory<DiaryCubit>(() => DiaryCubit(
chatRepository: sl(),
diaryRepository: sl(),
glossaryRepository: sl(),
profileRepository: sl(),
dietRepository: sl(),
settingsRepository: sl(),
));
sl.registerFactory<AboutDietCubit>(() => AboutDietCubit(dietsRepository: sl(), profileRepository: sl()));
sl.registerFactory<BlogPostCreateCubit>(() => BlogPostCreateCubit(blogPostRepository: sl()));
sl.registerFactory<SubCubit>(() => SubCubit(profileRepository: sl()));
@@ -180,9 +168,6 @@ Future<void> init() async {
sl.registerFactory<ResetPasswordCubit>(() => ResetPasswordCubit(resetPasswordRepository: sl()));
sl.registerFactory<BlogPostCubit>(() => BlogPostCubit(sl()));
sl.registerFactory<BlockedUsersCubit>(() => BlockedUsersCubit(blockedUsersRepository: sl()));
sl.registerFactory<NotificationSettingsCubit>(() => NotificationSettingsCubit(notificationSettingsRepository: sl()));
sl.registerFactory<ConsultantRatingCubit>(() => ConsultantRatingCubit(sl()));
sl.registerFactory<ViewsCubit>(() => ViewsCubit(viewsRepository: sl()));
// Repository
sl.registerLazySingleton<ChatRepository>(() => ChatRepository(chatService: sl()));
@@ -213,10 +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()));
sl.registerLazySingleton<NotificationSettingsRepository>(() => NotificationSettingsRepository(sl()));
sl.registerFactory<ConsultantRatingRepository>(() => ConsultantRatingRepository(sl()));
sl.registerFactory<ViewsRepository>(() => ViewsRepository(viewsService: sl()));
// Services
sl.registerLazySingleton<ChatService>(() => ChatService(sl()));
@@ -245,19 +226,28 @@ Future<void> init() async {
sl.registerLazySingleton<CancelPlanService>(() => CancelPlanService(sl()));
sl.registerLazySingleton<CreatePlanService>(() => CreatePlanService(sl()));
sl.registerLazySingleton<NotificationsService>(() => NotificationsService(sl()));
sl.registerLazySingleton<MyRecipesService>(() => MyRecipesService(sl()));
sl.registerLazySingleton<FirebaseApi>(() => FirebaseApi());
sl.registerLazySingleton<ResetPasswordService>(() => ResetPasswordService(sl()));
sl.registerLazySingleton<BlockedUserService>(() => BlockedUserService(sl()));
sl.registerLazySingleton<CommentsService>(() => CommentsService(sl()));
sl.registerLazySingleton<MyRecipesService>(() => MyRecipesService(sl()));
sl.registerLazySingleton<ConsultantRatingService>(() => ConsultantRatingService(sl()));
sl.registerLazySingleton<NotificationSettingsService>(() => NotificationSettingsService(sl()));
sl.registerLazySingleton<ViewsService>(() => ViewsService(sl()));
// External
sl.registerLazySingleton(
() =>
Dio(BaseOptions(baseUrl: baseUrl, headers: {'accept': 'application/json', 'content-type': 'application/json'}))
..interceptors.addAll([TokenInterceptor(), PhoneVerifyErrorInterceptor()]),
() => Dio(
BaseOptions(
baseUrl: baseUrl,
headers: {
'accept': 'application/json',
'content-type': 'application/json',
},
validateStatus: (status) => status != null && status < 500,
),
)..interceptors.addAll(
[
TokenInterceptor(),
PhoneVerifyErrorInterceptor(),
],
),
);
sl.registerLazySingleton(() async => const FlutterSecureStorage());
}

View File

@@ -1,5 +1,3 @@
import 'dart:io';
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
import 'package:eatvio_app/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();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
import 'dart:convert';
import 'dart:developer';
import '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) {

View File

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

View File

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

View File

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

View File

@@ -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,

View File

@@ -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';
@@ -47,20 +46,12 @@ class _AuthScreenState extends State<AuthScreen> {
super.initState();
var keyboardVisibilityController = KeyboardVisibilityController();
keyboardSubscription = keyboardVisibilityController.onChange.listen((bool visible) {
if (mounted) {
setState(() {
isFocused = visible;
});
}
setState(() {
isFocused = visible;
});
});
}
@override
void dispose() {
keyboardSubscription.cancel();
super.dispose();
}
Future<void> _reconnectWebSocket(BuildContext context) async {
try {
await sl<WsManager>().clear();
@@ -75,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>(
@@ -107,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(
@@ -138,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,
),
),
),
),
@@ -160,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,
),
),
),
],
@@ -171,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(
@@ -212,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);
@@ -226,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),
@@ -238,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
@@ -253,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,
@@ -268,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,
),
),
),
),
@@ -284,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,
),
),
),
),
@@ -302,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,
),
),
),
],

View File

@@ -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('Пароль успешно сменён. Войдите в аккаунт');
}
},
),

View File

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

View File

@@ -7,8 +7,8 @@ import 'package:eatvio_app/core/widgets/custom_popup.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/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';

View File

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

View File

@@ -15,11 +15,7 @@ abstract class ChatMessageModel with _$ChatMessageModel {
@JsonKey(name: 'read_by_to_user', defaultValue: false) required bool isRead,
@JsonKey(name: '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 }

View File

@@ -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);
}

View File

@@ -1,4 +1,3 @@
import 'package:eatvio_app/core/models/pagination_model.dart';
import 'package:eatvio_app/features/chat/models/chat_message_model.dart';
import 'package:eatvio_app/features/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,

View File

@@ -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);

View File

@@ -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);
}
}
}
}
}

View File

@@ -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,

View File

@@ -13,8 +13,6 @@ import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.da
import 'package:eatvio_app/features/chat/presentation/widgets/about_order_button.dart';
import 'package:eatvio_app/features/chat/presentation/widgets/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)),
),
);
}

View File

@@ -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(

View File

@@ -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(

View File

@@ -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,
),
),
),
],

View File

@@ -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(

View File

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

View File

@@ -2,7 +2,6 @@ import 'dart:async';
import 'dart:developer';
import 'dart: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;
}

View File

@@ -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,
});
}

View File

@@ -9,7 +9,6 @@ import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/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();
},
);

View File

@@ -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) {

View File

@@ -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;
}
}
}

View File

@@ -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,
});
}

View File

@@ -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),
),
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,7 +14,10 @@ import 'package:go_router/go_router.dart';
class ConsultationCardWidget extends StatefulWidget {
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),
),
),
),
],

View File

@@ -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>;

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,6 @@ import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/custom_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())),
),
},
],
),

View File

@@ -35,7 +35,6 @@ import 'package:eatvio_app/features/recipe/services/about_recipe_service.dart';
import 'package:eatvio_app/features/recipe/services/my_recipes_service.dart';
import 'package:eatvio_app/features/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');

View File

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

View File

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

View File

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

View File

@@ -136,7 +136,15 @@ class DiaryCubit extends Cubit<DiaryState> {
final fifthMeal = MealModel(id: 0, time: '0', typeNumber: 5, satiety: 0, mood: 0, portions: fifthMealPortions ?? [], photos: []);
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,

View File

@@ -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({

View File

@@ -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);

View File

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

View File

@@ -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();
}

View File

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

View File

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

View File

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

View File

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

View File

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

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