forked from eatvio/eatvio_app
Compare commits
118 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87c0d4ec2a | ||
|
|
17be198a23 | ||
| d87d0e5e2a | |||
|
|
5275273830 | ||
|
|
584f00a1ea | ||
|
|
37cedb2238 | ||
|
|
09d025252d | ||
|
|
086e6cf94e | ||
|
|
766d5ba248 | ||
|
|
e7ce9d8f90 | ||
|
|
7861cdce3c | ||
|
|
2865852124 | ||
|
|
d18ad86590 | ||
|
|
f1945202fe | ||
|
|
b65fca8116 | ||
|
|
27376ca54d | ||
|
|
01f1c39450 | ||
|
|
24fe984e6b | ||
|
|
053cf781c3 | ||
|
|
46d0e31f5f | ||
|
|
a0b29f42df | ||
|
|
96b3bd1637 | ||
|
|
fc09fa36b8 | ||
|
|
68ea8da6e3 | ||
|
|
052c4fedca | ||
|
|
ad777c695b | ||
|
|
e6df89c98a | ||
|
|
fef4451f4e | ||
|
|
c68f86eeb7 | ||
|
|
fafc2aa1f7 | ||
|
|
ca49415ef6 | ||
|
|
2600b8cc79 | ||
| e1f25a63bc | |||
|
|
087436da1c | ||
|
|
c295c06565 | ||
|
|
b146295996 | ||
|
|
41f77bd373 | ||
|
|
6ac93d42df | ||
|
|
1a6fb39d84 | ||
|
|
d4a8cbc637 | ||
|
|
2300bc4ab6 | ||
|
|
b4776c3be3 | ||
|
|
06e07a92b7 | ||
|
|
0fce3f9d30 | ||
|
|
4bc8cba941 | ||
|
|
8c348f61b1 | ||
|
|
0446e64409 | ||
|
|
74b5c6732d | ||
|
|
b840c936bf | ||
|
|
f3dbbf3537 | ||
|
|
ff49562e86 | ||
|
|
8b7180c2da | ||
|
|
5c911707a3 | ||
|
|
aa604b667b | ||
|
|
0a5d78be1f | ||
|
|
f1f5c79661 | ||
|
|
1c517807c4 | ||
|
|
cad54913d1 | ||
|
|
89f0a46192 | ||
|
|
65542020a7 | ||
|
|
b6b1c8b3c1 | ||
|
|
bf6e23e96a | ||
|
|
952cf76164 | ||
|
|
b665692779 | ||
|
|
54ac20978f | ||
|
|
b9dc5c0136 | ||
|
|
39fce4b0f9 | ||
|
|
73ff67aa13 | ||
|
|
dcf2805df7 | ||
|
|
d13eb78f0e | ||
|
|
5f21a65028 | ||
|
|
12bf9f9e5f | ||
|
|
1680bd2556 | ||
|
|
91d5118f60 | ||
|
|
d8324c1635 | ||
|
|
0f1dbe16f1 | ||
|
|
020682ae80 | ||
|
|
e54476656b | ||
|
|
dd56924076 | ||
|
|
b47da44cf2 | ||
|
|
964550a325 | ||
|
|
7ffa061f3e | ||
|
|
997da4f940 | ||
|
|
ca1777ece0 | ||
|
|
1a427d6f5d | ||
|
|
f190e70f3a | ||
|
|
bc6469a6ae | ||
|
|
e3ff359e1b | ||
|
|
b5edf13bd9 | ||
|
|
c05d027e3d | ||
|
|
bae79b60fd | ||
|
|
6061e413b2 | ||
|
|
5a8fa7089c | ||
|
|
886cfa8f1c | ||
|
|
a416912d2a | ||
|
|
f46392ec8e | ||
|
|
766d06fc4d | ||
|
|
6f4023dd0b | ||
|
|
f892dfed9f | ||
|
|
98803e56cf | ||
|
|
4391870e72 | ||
|
|
7fd24f57e3 | ||
|
|
384a25c4a6 | ||
|
|
88558d62ef | ||
|
|
09b04118aa | ||
|
|
f76b142117 | ||
|
|
13e66edd27 | ||
|
|
5b74ae45b3 | ||
|
|
3d2634b96a | ||
|
|
1281abaf24 | ||
|
|
7dfac3f792 | ||
|
|
9d4687553a | ||
|
|
442da40622 | ||
|
|
ccf586234b | ||
|
|
4f249499a9 | ||
|
|
2f74387df2 | ||
| ed4db5b59e | |||
|
|
96e80ced38 |
@@ -21,20 +21,19 @@ jobs:
|
||||
|
||||
- name: Copy .env file
|
||||
run: |
|
||||
mkdir -p env
|
||||
cp env_repo/.env env/.env
|
||||
cp env_repo/.env .env
|
||||
|
||||
- name: Load environment variables from .env
|
||||
run: |
|
||||
set -a
|
||||
source env/.env
|
||||
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/.env
|
||||
done < .env
|
||||
|
||||
- name: Unlock login keychain
|
||||
run: |
|
||||
@@ -59,6 +58,8 @@ jobs:
|
||||
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
|
||||
|
||||
@@ -3,7 +3,7 @@ name: Eatvio App Android Test
|
||||
on: [ pull_request ]
|
||||
|
||||
env:
|
||||
flutter_version: "3.32.8"
|
||||
flutter_version: "3.35.1"
|
||||
java_version: "21.x"
|
||||
flutter_channel: "stable"
|
||||
my_local_cache_dir: "/opt/hostedtoolcache/"
|
||||
@@ -26,20 +26,19 @@ jobs:
|
||||
|
||||
- name: Copy .env file
|
||||
run: |
|
||||
mkdir -p env
|
||||
cp env_repo/.env env/.env
|
||||
cp env_repo/.env .env
|
||||
|
||||
- name: Load environment variables from .env
|
||||
run: |
|
||||
set -a
|
||||
source env/.env
|
||||
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/.env
|
||||
done < .env
|
||||
|
||||
- name: Create key.properties file
|
||||
run: |
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -16,6 +16,7 @@
|
||||
*.mocks.dart
|
||||
android/build/
|
||||
ios/XCBuildData/
|
||||
.env
|
||||
env.example
|
||||
env/.env
|
||||
.env
|
||||
android/key.properties
|
||||
app/upload-keystore.jks
|
||||
@@ -54,6 +54,15 @@ cp env.example .env
|
||||
- `GOOGLE_CHECK_URL` - URL для проверки соединения (Google)
|
||||
- `YANDEX_CHECK_URL` - URL для проверки соединения (Yandex)
|
||||
|
||||
### VK ID Configuration
|
||||
- `VK_CLIENT_ID` - ID приложения VK (числовой)
|
||||
- **Примечание**: VK_CLIENT_ID - это числовой "ID приложения" из консоли VK ID, НЕ "Защищённый ключ" или "Сервисный ключ доступа"
|
||||
|
||||
### Yandex ID Configuration
|
||||
- `YANDEX_CLIENT_ID` - ID приложения Yandex OAuth
|
||||
- `YANDEX_CLIENT_SECRET` - секретный ключ приложения Yandex OAuth
|
||||
- `YANDEX_REDIRECT_URI` - URI для перенаправления (должен совпадать с настройками в Yandex OAuth приложении)
|
||||
|
||||
## Безопасность
|
||||
|
||||
⚠️ **ВАЖНО**: Файл `.env` добавлен в `.gitignore` и не должен попадать в репозиторий. Все секретные данные должны храниться только в этом файле.
|
||||
|
||||
1
android/app/agconnect-services.json
Normal file
1
android/app/agconnect-services.json
Normal file
@@ -0,0 +1 @@
|
||||
{"agcgw":{"url":"connect-drru.hispace.dbankcloud.ru","backurl":"connect-drru.hispace.dbankcloud.cn","websocketurl":"connect-ws-drru.hispace.dbankcloud.ru","websocketbackurl":"connect-ws-drru.hispace.dbankcloud.cn"},"agcgw_all":{"SG":"connect-dra.dbankcloud.cn","SG_back":"connect-dra.hispace.hicloud.com","CN":"connect-drcn.dbankcloud.cn","CN_back":"connect-drcn.hispace.hicloud.com","RU":"connect-drru.hispace.dbankcloud.ru","RU_back":"connect-drru.hispace.dbankcloud.cn","DE":"connect-dre.dbankcloud.cn","DE_back":"connect-dre.hispace.hicloud.com"},"websocketgw_all":{"SG":"connect-ws-dra.hispace.dbankcloud.cn","SG_back":"connect-ws-dra.hispace.dbankcloud.com","CN":"connect-ws-drcn.hispace.dbankcloud.cn","CN_back":"connect-ws-drcn.hispace.dbankcloud.com","RU":"connect-ws-drru.hispace.dbankcloud.ru","RU_back":"connect-ws-drru.hispace.dbankcloud.cn","DE":"connect-ws-dre.hispace.dbankcloud.cn","DE_back":"connect-ws-dre.hispace.dbankcloud.com"},"client":{"cp_id":"20007000036693451","product_id":"461323198429446894","client_id":"1595824543740483264","client_secret":"2A1727B0D31AA9BE1F5D9E8A5CDA1A67B9EF084AC9FB68D6C633A3CCB08ECDEE","project_id":"461323198429446894","app_id":"113238569","api_key":"DQEDAJNRITp9M3uc1rPHyT8PzLc7bnVcKJAeavZZk0TQNMx9ifvBebr+JOmZe9tEvjnLwv7c1hAHXmPGR/gWG86iABDrCVotq+NGnw==","package_name":"com.eatvioapp.app"},"oauth_client":{"client_id":"113238569","client_type":1},"app_info":{"app_id":"113238569","package_name":"com.eatvioapp.app"},"service":{"analytics":{"collector_url":"datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com","collector_url_cn":"datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn","collector_url_de":"datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn","collector_url_ru":"datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com","collector_url_sg":"datacollector-dra.dt.hicloud.com,datacollector-dra.dt.dbankcloud.cn","resource_id":"p1","channel_id":""},"ml":{"mlservice_url":"ml-api-drru.ai.dbankcloud.ru,ml-api-drru.ai.dbankcloud.cn"},"cloudstorage":{"storage_url":"https://agc-storage-drcn.platform.dbankcloud.cn","storage_url_ru":"https://agc-storage-drru.cloud.huawei.ru","storage_url_sg":"https://ops-dra.agcstorage.link","storage_url_de":"https://ops-dre.agcstorage.link","storage_url_cn":"https://agc-storage-drcn.platform.dbankcloud.cn","storage_url_ru_back":"https://agc-storage-drru.cloud.huawei.ru","storage_url_sg_back":"https://agc-storage-dra.cloud.huawei.asia","storage_url_de_back":"https://agc-storage-dre.cloud.huawei.eu","storage_url_cn_back":"https://agc-storage-drcn.cloud.huawei.com.cn"},"search":{"url":"https://search-drru.cloud.huawei.ru"},"edukit":{"edu_url":"edukit.cloud.huawei.com.cn","dh_url":"edukit.cloud.huawei.com.cn"}},"region":"RU","configuration_version":"3.0","appInfos":[{"package_name":"com.eatvioapp.app","client":{"app_id":"113238569"},"app_info":{"package_name":"com.eatvioapp.app","app_id":"113238569"},"oauth_client":{"client_type":1,"client_id":"113238569"}}]}
|
||||
@@ -5,6 +5,7 @@ plugins {
|
||||
// END: FlutterFire Configuration
|
||||
id "kotlin-android"
|
||||
id "dev.flutter.flutter-gradle-plugin"
|
||||
id 'com.huawei.agconnect'
|
||||
}
|
||||
|
||||
def keystoreProperties = new Properties()
|
||||
@@ -13,6 +14,8 @@ if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
|
||||
def yandexClientId = project.hasProperty('YANDEX_CLIENT_ID') ? project.property('YANDEX_CLIENT_ID') : (System.getenv('YANDEX_CLIENT_ID') ?: "")
|
||||
|
||||
android {
|
||||
namespace = "com.eatvioapp.app"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
@@ -29,10 +32,14 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.eatvioapp.app"
|
||||
minSdk = 23
|
||||
minSdkVersion flutter.minSdkVersion
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
manifestPlaceholders = [
|
||||
YANDEX_CLIENT_ID: yandexClientId,
|
||||
applicationName: "io.flutter.app.FlutterApplication"
|
||||
]
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
@@ -47,6 +54,9 @@ android {
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig signingConfigs.release
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
debug {
|
||||
signingConfig signingConfigs.debug
|
||||
@@ -57,6 +67,15 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.huawei.agconnect:agconnect-core:1.9.1.301'
|
||||
implementation 'com.huawei.hms:push:6.12.0.300'
|
||||
// Huawei Analytics dependency to satisfy com.huawei.hianalytics.* references during R8
|
||||
implementation 'com.huawei.hms:hianalytics:6.12.0.300'
|
||||
// BouncyCastle provider required by Huawei secure utils used transitively by HMS
|
||||
implementation 'org.bouncycastle:bcprov-jdk15to18:1.78.1'
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
|
||||
21
android/app/proguard-rules.pro
vendored
Normal file
21
android/app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
-keep class com.huawei.** { *; }
|
||||
-dontwarn com.huawei.**
|
||||
|
||||
-keep class org.bouncycastle.** { *; }
|
||||
-dontwarn org.bouncycastle.**
|
||||
|
||||
-keep class io.flutter.** { *; }
|
||||
-dontwarn io.flutter.**
|
||||
|
||||
-keep class kotlinx.coroutines.** { *; }
|
||||
-dontwarn kotlinx.coroutines.**
|
||||
|
||||
-keep class **.R$* { *; }
|
||||
-keepclassmembers class ** { @androidx.annotation.Keep *; }
|
||||
-keep @androidx.annotation.Keep class * { *; }
|
||||
|
||||
-keep public class * extends android.app.Application
|
||||
-keep public class * extends android.content.ContentProvider
|
||||
|
||||
-keep class * implements android.os.Parcelable { *; }
|
||||
-keepclassmembers enum * { *; }
|
||||
@@ -11,6 +11,16 @@
|
||||
android:usesCleartextTraffic="true"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<service
|
||||
android:name=".HmsPushService"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.huawei.push.action.MESSAGING_EVENT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<meta-data
|
||||
android:name="push_kit_auto_init_enabled"
|
||||
android:value="true" />
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.eatvioapp.app
|
||||
|
||||
import android.util.Log
|
||||
import com.huawei.hms.push.HmsMessageService
|
||||
import com.huawei.hms.push.RemoteMessage
|
||||
|
||||
class HmsPushService : HmsMessageService() {
|
||||
override fun onNewToken(token: String?) {
|
||||
super.onNewToken(token)
|
||||
Log.i("HMS", "onNewToken: ${token ?: "null"}")
|
||||
}
|
||||
|
||||
override fun onMessageReceived(message: RemoteMessage?) {
|
||||
super.onMessageReceived(message)
|
||||
Log.i("HMS", "onMessageReceived: ${message?.data}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,71 @@
|
||||
package com.eatvioapp.app
|
||||
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import com.huawei.agconnect.config.AGConnectServicesConfig
|
||||
import com.huawei.hms.aaid.HmsInstanceId
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MainActivity: FlutterActivity() {
|
||||
private val channelName = "com.eatvioapp/hms_push"
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channelName)
|
||||
.setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"getHuaweiToken" -> {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
// Try to read appId from agconnect-services.json
|
||||
var appId: String? = try {
|
||||
AGConnectServicesConfig.fromContext(applicationContext)
|
||||
.getString("client/app_id")
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
// Fallback to manifest meta-data if needed
|
||||
if (appId.isNullOrBlank()) {
|
||||
try {
|
||||
val pm: PackageManager = applicationContext.packageManager
|
||||
val ai: ApplicationInfo = pm.getApplicationInfo(applicationContext.packageName, PackageManager.GET_META_DATA)
|
||||
val raw = ai.metaData?.getString("com.huawei.hms.client.appid")
|
||||
if (!raw.isNullOrBlank()) {
|
||||
appId = raw.removePrefix("appid=")
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
// ignore and handle below
|
||||
}
|
||||
}
|
||||
|
||||
if (appId.isNullOrBlank()) {
|
||||
runOnUiThread {
|
||||
result.error("HMS_TOKEN_ERROR", "Missing Huawei appId. Ensure agconnect-services.json or manifest meta-data is configured.", null)
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
val token = HmsInstanceId.getInstance(applicationContext)
|
||||
.getToken(appId, "HCM")
|
||||
runOnUiThread {
|
||||
result.success(token)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
runOnUiThread {
|
||||
result.error("HMS_TOKEN_ERROR", e.message, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,21 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven {url 'https://developer.huawei.com/repo/'}
|
||||
gradlePluginPortal()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.12.0'
|
||||
classpath 'com.huawei.agconnect:agcp:1.9.1.301'
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven {url 'https://developer.huawei.com/repo/'}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +27,32 @@ subprojects {
|
||||
project.evaluationDependsOn(":app")
|
||||
}
|
||||
|
||||
// Temporary safeguard: set missing namespaces for Android library modules based on their manifest package
|
||||
subprojects { Project subproject ->
|
||||
subproject.pluginManager.withPlugin('com.android.library') {
|
||||
def androidExt = subproject.extensions.findByName('android')
|
||||
if (androidExt != null) {
|
||||
def hasNamespaceProp = androidExt.hasProperty('namespace')
|
||||
def currentNamespace = hasNamespaceProp ? androidExt.namespace : null
|
||||
if (hasNamespaceProp && !currentNamespace) {
|
||||
def manifestFile = file("${subproject.projectDir}/src/main/AndroidManifest.xml")
|
||||
if (manifestFile.exists()) {
|
||||
try {
|
||||
def manifest = new groovy.xml.XmlSlurper().parse(manifestFile)
|
||||
def manifestPackage = manifest.@package?.text()
|
||||
if (manifestPackage) {
|
||||
androidExt.namespace = manifestPackage
|
||||
subproject.logger.lifecycle("[namespace-fix] Set namespace for ${subproject.path} to '${manifestPackage}' from AndroidManifest.xml")
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
subproject.logger.warn("[namespace-fix] Failed to read manifest for ${subproject.path}: ${t.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("clean", Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-all.zip
|
||||
|
||||
@@ -12,17 +12,18 @@ pluginManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url 'https://developer.huawei.com/repo/' }
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "8.7.3" apply false
|
||||
id "com.android.application" version "8.12.0" apply false
|
||||
// START: FlutterFire Configuration
|
||||
id "com.google.gms.google-services" version "4.4.2" apply false
|
||||
id "com.google.gms.google-services" version "4.4.3" apply false
|
||||
// END: FlutterFire Configuration
|
||||
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
|
||||
id "org.jetbrains.kotlin.android" version "2.2.10" apply false
|
||||
}
|
||||
|
||||
include ":app"
|
||||
|
||||
1
assets/icons/consultant_rating_icon.svg
Normal file
1
assets/icons/consultant_rating_icon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#2F80ED"><path d="M759.23-353.08q8.92-8.92 21.19-8.92 12.27 0 21.19 8.92l63.39 64q8.92 9.07 8.92 21.15 0 12.08-8.92 21.01-8.92 8.92-21.01 8.92-12.08 0-21.14-8.92l-64-63.39q-8.93-8.92-8.73-21.38.19-12.46 9.11-21.39Zm33.85-472.22q8.92 9 8.92 21.38 0 12.38-8.92 21.3l-63.39 63.39q-8.92 8.92-21.19 8.92-12.27 0-21.19-9-8.92-9.01-8.92-21.39t8.92-21.3l63.61-63.38q9.07-8.93 21.15-8.93 12.08 0 21.01 9.01ZM167-825q9.01-8.92 21.39-8.92t21.3 8.92l63.39 63.62q8.92 9.06 8.92 21.14 0 12.09-9 21.01-9.01 8.92-21.39 8.92t-21.3-8.92l-63.39-63.39q-8.92-8.92-8.92-21.19 0-12.27 9-21.19Zm33.77 472.31q8.92 8.92 8.92 21.19 0 12.27-8.92 21.19l-63.39 63.39q-8.92 8.92-21.19 8.92-12.27 0-21.19-9-8.92-9.01-8.92-21.39t8.92-21.3l63.62-63.39q8.92-8.92 21.07-8.73 12.16.2 21.08 9.12Zm135.54 86.85L480-352.23l144.08 87.77-37.62-163.62 126.39-109.07-166.39-14.93L480-706.54l-66.08 153.46-166.38 14.54 126.38 110.46-37.61 162.24ZM480.38-486ZM480-281.31 306.69-177.08q-8.69 5.08-17.42 4.27-8.73-.81-15.81-5.88-7.07-5.08-10.92-13.27-3.85-8.19-1.23-18.12L307.23-406 154.08-538.39q-7.69-6.83-9.62-15.57-1.92-8.73 1.31-17.04 3.23-8.31 9.28-13.62 6.05-5.32 16.64-6.84l201.7-17.39 78.46-185.3q3.84-9.31 11.65-13.77 7.81-4.46 16.5-4.46 8.69 0 16.5 4.46 7.81 4.46 11.65 13.77L587-608.85l201.69 17.39q10.68 1.51 16.78 6.82 6.1 5.3 9.14 13.64 3.24 8.31 1.31 17.04-1.92 8.74-9.61 15.57L653.15-406l45.93 196.31q2.61 9.92-1.23 17.92-3.85 8-10.93 13.08-7.07 5.07-15.8 5.88-8.73.81-17.43-4.27L480-281.31Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
11
assets/icons/mail_logo.svg
Normal file
11
assets/icons/mail_logo.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1713_218)">
|
||||
<path d="M7.48034 0.0116882H20.547C24.6707 0.0116882 28.0137 3.35463 28.0137 7.47836V20.545C28.0137 24.6687 24.6707 28.0117 20.547 28.0117H7.48034C3.35661 28.0117 0.0136719 24.6687 0.0136719 20.545V7.47836C0.0136717 3.35463 3.35661 0.0116882 7.48034 0.0116882Z" fill="#0077FF"/>
|
||||
<path d="M14.0133 3.43393C19.8423 3.43393 24.5911 8.18276 24.5911 14.0117C24.5911 17.6432 22.8964 19.3565 20.7548 19.3565C19.5071 19.3565 18.4269 18.7047 17.8124 17.7363C16.844 18.7233 15.5031 19.3378 14.0133 19.3378C11.0709 19.3378 8.6872 16.9541 8.6872 14.0117C8.6872 11.0693 11.0709 8.68558 14.0133 8.68558C16.9557 8.68558 19.3395 11.0693 19.3395 14.0117V15.7436C19.3395 16.6189 19.954 17.2334 20.7548 17.2334C21.7046 17.2334 22.4867 16.4327 22.4867 14.0117C22.4867 9.33739 18.6876 5.53832 14.0133 5.53832C9.339 5.53832 5.53993 9.33739 5.53993 14.0117C5.53993 18.686 9.339 22.4851 14.0133 22.4851C16.7136 22.4851 18.5759 21.1443 18.5759 21.1443L19.9726 22.7458C19.9726 22.7458 17.5889 24.5895 14.0133 24.5895C8.18438 24.5895 3.43555 19.8407 3.43555 14.0117C3.43555 8.18276 8.18438 3.43393 14.0133 3.43393ZM10.7916 14.0117C10.7916 15.7809 12.2255 17.2334 14.0133 17.2334C15.7825 17.2334 17.2351 15.7809 17.2351 14.0117C17.2351 12.2239 15.7825 10.79 14.0133 10.79C12.2255 10.79 10.7916 12.2239 10.7916 14.0117Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1713_218">
|
||||
<rect width="28" height="28" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
5
assets/icons/ok_logo.svg
Normal file
5
assets/icons/ok_logo.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 13.4393C0 7.10718 0 3.92462 1.9623 1.96231C3.9411 1.57261e-07 7.10718 0 13.4393 0H14.5607C20.8928 0 24.0754 1.57261e-07 26.0377 1.96231C28 3.92462 28 7.10718 28 13.4393V14.5607C28 20.8928 28 24.0754 26.0377 26.0377C24.0754 28 20.8928 28 14.5607 28H13.4393C7.10718 28 3.92461 28 1.9623 26.0377C0 24.0754 0 20.9093 0 14.5607V13.4393Z" fill="#FF7700"/>
|
||||
<path d="M13.9997 13.934C15.3684 13.934 16.6051 13.3734 17.4956 12.4829C18.3861 11.5925 18.9467 10.3557 18.9467 8.98704C18.9467 7.61837 18.3861 6.38162 17.4956 5.49116C16.6051 4.6007 15.3684 4.04004 13.9997 4.04004C12.631 4.04004 11.3943 4.6007 10.5039 5.49116C9.61339 6.38162 9.05273 7.61837 9.05273 8.98704C9.05273 10.3557 9.61339 11.5925 10.5039 12.4829C11.3943 13.3734 12.6475 13.934 13.9997 13.934ZM12.4167 7.38751C12.8289 6.97526 13.3896 6.72791 13.9997 6.72791C14.6263 6.72791 15.187 6.97526 15.5828 7.38751C15.995 7.79976 16.2424 8.36042 16.2424 8.97055C16.2424 9.59716 15.995 10.1578 15.5828 10.5536C15.1705 10.9658 14.6098 11.2132 13.9997 11.2132C13.3731 11.2132 12.8124 10.9658 12.4167 10.5536C12.0044 10.1413 11.7571 9.58067 11.7571 8.97055C11.7571 8.36042 12.0209 7.79976 12.4167 7.38751Z" fill="white"/>
|
||||
<path d="M20.6615 15.9293L19.2104 13.9505C19.1279 13.8351 18.963 13.8186 18.8641 13.9175C17.4954 15.0883 15.8134 15.8469 13.9995 15.8469C12.1856 15.8469 10.5037 15.0883 9.13498 13.9175C9.03604 13.8351 8.87114 13.8516 8.78869 13.9505L7.33758 15.9293C7.27162 16.0283 7.28811 16.1602 7.37056 16.2426C8.59081 17.232 10.009 17.9081 11.4931 18.2544L8.42591 23.6631C8.34346 23.8115 8.45889 24.0094 8.62379 24.0094H11.625C11.7239 24.0094 11.8064 23.9599 11.8393 23.861L13.983 19.0789L16.1267 23.861C16.1597 23.9434 16.2422 24.0094 16.3411 24.0094H19.3423C19.5237 24.0094 19.6226 23.828 19.5402 23.6631L16.473 18.2544C17.9571 17.9081 19.3753 17.2485 20.5955 16.2426C20.7274 16.1602 20.7439 16.0283 20.6615 15.9293Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
16
assets/icons/vk_logo.svg
Normal file
16
assets/icons/vk_logo.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1709_298)">
|
||||
<mask id="mask0_1709_298" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="28" height="28">
|
||||
<path d="M27.8614 0H0.138672V28H27.8614V0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_1709_298)">
|
||||
<path d="M0.138672 13.44C0.138672 7.10433 0.138672 3.93649 2.08743 1.96824C4.03619 0 7.17266 0 13.4456 0H14.5545C20.8275 0 23.9639 0 25.9127 1.96824C27.8614 3.93649 27.8614 7.10433 27.8614 13.44V14.56C27.8614 20.8957 27.8614 24.0635 25.9127 26.0318C23.9639 28 20.8275 28 14.5545 28H13.4456C7.17266 28 4.03619 28 2.08743 26.0318C0.138672 24.0635 0.138672 20.8957 0.138672 14.56V13.44Z" fill="#0077FF"/>
|
||||
<path d="M14.89 20.1718C8.57149 20.1718 4.96756 15.7968 4.81738 8.51675H7.98241C8.08637 13.8601 10.4197 16.1234 12.2678 16.5901V8.51675H15.2481V13.125C17.0732 12.9267 18.9905 10.8268 19.6373 8.51675H22.6176C22.1209 11.3634 20.0417 13.4634 18.5632 14.3267C20.0417 15.0267 22.4098 16.8584 23.3108 20.1718H20.0302C19.3255 17.9551 17.5699 16.2401 15.2481 16.0067V20.1718H14.89Z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1709_298">
|
||||
<rect width="28" height="28" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
16
assets/icons/yandex_logo.svg
Normal file
16
assets/icons/yandex_logo.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1709_319)">
|
||||
<mask id="mask0_1709_319" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="28" height="28">
|
||||
<path d="M27.8614 0H0.138672V28H27.8614V0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_1709_319)">
|
||||
<path d="M0.138672 13.44C0.138672 7.10433 0.138672 3.93649 2.08743 1.96824C4.03619 0 7.17266 0 13.4456 0H14.5545C20.8275 0 23.9639 0 25.9127 1.96824C27.8614 3.93649 27.8614 7.10433 27.8614 13.44V14.56C27.8614 20.8957 27.8614 24.0635 25.9127 26.0318C23.9639 28 20.8275 28 14.5545 28H13.4456C7.17266 28 4.03619 28 2.08743 26.0318C0.138672 24.0635 0.138672 20.8957 0.138672 14.56V13.44Z" fill="#FC3F1D"/>
|
||||
</g>
|
||||
<path d="M17.3371 24H21V4H15.6687C10.3069 4 7.49454 6.614 7.49454 10.4755C7.49454 13.809 9.31785 15.6885 12.5476 17.73L14.51 18.9635L11.9911 16.9515L7 24H10.971L16.3029 16.423L14.4486 15.248C12.2079 13.809 11.1107 12.693 11.1107 10.27C11.1107 8.141 12.6873 6.702 15.6844 6.702H17.3224V24H17.3382H17.3371Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1709_319">
|
||||
<rect width="28" height="28" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -21,6 +21,6 @@
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>12.0</string>
|
||||
<string>16.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
platform :ios, '14.0'
|
||||
platform :ios, '16.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
@@ -39,7 +39,7 @@ post_install do |installer|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64"
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0'
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '16.0'
|
||||
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
|
||||
'$(inherited)',
|
||||
## dart: PermissionGroup.camera
|
||||
|
||||
@@ -213,14 +213,10 @@
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||
@@ -287,14 +283,10 @@
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||
@@ -378,7 +370,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
@@ -396,7 +388,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 68;
|
||||
CURRENT_PROJECT_VERSION = 74;
|
||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
@@ -418,6 +410,7 @@
|
||||
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";
|
||||
@@ -474,7 +467,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -526,7 +519,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
@@ -546,7 +539,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 68;
|
||||
CURRENT_PROJECT_VERSION = 74;
|
||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
@@ -568,6 +561,7 @@
|
||||
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;
|
||||
@@ -584,7 +578,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 68;
|
||||
CURRENT_PROJECT_VERSION = 74;
|
||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
@@ -606,6 +600,7 @@
|
||||
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";
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
buildConfiguration = "Release"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import UIKit
|
||||
import Flutter
|
||||
import UIKit
|
||||
import YandexLoginSDK
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
@@ -10,4 +11,20 @@ import Flutter
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
||||
@available(iOS 8.0, *)
|
||||
func application(
|
||||
_ application: UIApplication, continue userActivity: NSUserActivity,
|
||||
restorationHandler: @escaping ([Any]?) -> Void
|
||||
) -> Bool {
|
||||
YXLSdk.shared.processUserActivity(userActivity)
|
||||
return true
|
||||
}
|
||||
|
||||
override public func application(
|
||||
_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]
|
||||
) -> Bool {
|
||||
return YXLSdk.shared.handleOpen(
|
||||
url, sourceApplication: options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>68</string>
|
||||
<string>74</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
@@ -67,5 +67,24 @@
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>yandexauth</string>
|
||||
<string>yandexauth2</string>
|
||||
<string>yandexauth4</string>
|
||||
</array>
|
||||
<key>YAClientId</key>
|
||||
<string>5399b03636104c6d920b87c1ad329254</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>YandexLoginSDK</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>yx5399b03636104c6d920b87c1ad329254</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<array>
|
||||
<string>applinks:eatvio.ru</string>
|
||||
<string>applinks:www.eatvio.ru</string>
|
||||
<string>applinks:5399b03636104c6d920b87c1ad329254.oauth.yandex.ru</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -44,17 +44,18 @@ platform :ios do
|
||||
|
||||
UI.message "Using auth_key_path: #{auth_key_path}"
|
||||
|
||||
# Configure Flutter for release mode
|
||||
UI.message "Configuring Flutter for release mode..."
|
||||
sh("cd .. && flutter build ios --config-only --release")
|
||||
|
||||
match(type: "appstore", readonly: true)
|
||||
increment_build_number(xcodeproj: "Runner.xcodeproj")
|
||||
|
||||
build_date = Time.now.getlocal("+03:00").strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
build_app(
|
||||
workspace: "Runner.xcworkspace",
|
||||
scheme: "Runner",
|
||||
export_method: "app-store",
|
||||
codesigning_identity: "Apple Distribution",
|
||||
xcargs: "--dart-define=HIDE_PLANS=true --dart-define=BUILD_DATE='#{build_date}'"
|
||||
)
|
||||
|
||||
|
||||
|
||||
37
lib/core/app_config.dart
Normal file
37
lib/core/app_config.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_cubit.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/views/cubit/views_cubit.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart';
|
||||
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
|
||||
/// Конфигурация приложения
|
||||
class AppConfig {
|
||||
/// Список глобальных BlocProvider'ов
|
||||
static List<BlocProvider> get globalBlocProviders => [
|
||||
BlocProvider<NewMessagesCubit>(create: (context) => sl<NewMessagesCubit>()),
|
||||
BlocProvider<NotificationsCubit>(create: (context) => sl<NotificationsCubit>()..getNotifications()),
|
||||
BlocProvider<FeedCubit>(create: (context) => sl<FeedCubit>()..loadFeedBlogPostList()),
|
||||
BlocProvider<ProfileAvatarCubit>(create: (context) => sl<ProfileAvatarCubit>()..getMyProfile()),
|
||||
BlocProvider<ChatsCubit>(create: (context) => sl<ChatsCubit>()..getChatRoomsWithLoading()),
|
||||
BlocProvider<ViewsCubit>(create: (context) => sl<ViewsCubit>()),
|
||||
BlocProvider<InternetConnectionCubit>(lazy: false, create: (context) => sl<InternetConnectionCubit>()),
|
||||
];
|
||||
|
||||
/// Делегаты локализации
|
||||
static const List<LocalizationsDelegate> localizationDelegates = [
|
||||
S.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
];
|
||||
|
||||
/// Поддерживаемые локали
|
||||
static const List<Locale> supportedLocales = [Locale.fromSubtags(languageCode: 'ru')];
|
||||
}
|
||||
68
lib/core/app_initializer.dart
Normal file
68
lib/core/app_initializer.dart
Normal file
@@ -0,0 +1,68 @@
|
||||
import 'dart:io' as io;
|
||||
import 'package:eatvio_app/core/env_config.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart' as di;
|
||||
import 'package:eatvio_app/firebase_options.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:appmetrica_plugin/appmetrica_plugin.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
/// Класс для инициализации приложения
|
||||
class AppInitializer {
|
||||
/// Инициализация всех сервисов приложения
|
||||
static Future<void> initialize() async {
|
||||
await _initializeSentry();
|
||||
await _initializeEnvironment();
|
||||
await _initializeFirebase();
|
||||
await _initializeDependencyInjection();
|
||||
await _initializeDebugMode();
|
||||
await _initializeAnalytics();
|
||||
}
|
||||
|
||||
/// Инициализация Sentry
|
||||
static Future<void> _initializeSentry() async {
|
||||
final widgetsBinding = SentryWidgetsFlutterBinding.ensureInitialized();
|
||||
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
|
||||
}
|
||||
|
||||
/// Загрузка переменных окружения
|
||||
static Future<void> _initializeEnvironment() async {
|
||||
await dotenv.load(fileName: ".env");
|
||||
}
|
||||
|
||||
/// Инициализация Firebase
|
||||
static Future<void> _initializeFirebase() async {
|
||||
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
||||
await FirebaseMessaging.instance.requestPermission();
|
||||
}
|
||||
|
||||
/// Инициализация системы внедрения зависимостей
|
||||
static Future<void> _initializeDependencyInjection() async {
|
||||
await di.init();
|
||||
}
|
||||
|
||||
/// Настройка режима отладки
|
||||
static Future<void> _initializeDebugMode() async {
|
||||
if (kDebugMode) {
|
||||
io.HttpClient.enableTimelineLogging = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Инициализация аналитики
|
||||
static Future<void> _initializeAnalytics() async {
|
||||
AppMetrica.activate(AppMetricaConfig(EnvConfig.appMetricaApiKey));
|
||||
AppMetrica.reportEvent('Application started');
|
||||
}
|
||||
|
||||
/// Запуск приложения с Sentry
|
||||
static Future<void> runAppWithSentry(VoidCallback appRunner) async {
|
||||
await SentryFlutter.init((options) {
|
||||
options.dsn = EnvConfig.sentryDsn;
|
||||
options.tracesSampleRate = 1.0;
|
||||
options.debug = false;
|
||||
}, appRunner: appRunner);
|
||||
}
|
||||
}
|
||||
@@ -1,508 +1,53 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/firebase_api.dart';
|
||||
import 'package:eatvio_app/core/token_storage.dart';
|
||||
import 'package:eatvio_app/core/websockets/ws_manager.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart';
|
||||
import 'package:eatvio_app/features/auth/presentation/pages/forget_password_screen.dart';
|
||||
import 'package:eatvio_app/features/auth/presentation/pages/scan_qr_code_screen.dart';
|
||||
import 'package:eatvio_app/features/blog_post/presentation/pages/blog_post_screen.dart';
|
||||
import 'package:eatvio_app/features/blog_post_create/presentation/pages/blog_post_create_screen.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart';
|
||||
import 'package:eatvio_app/features/calendar/presentation/pages/calendar_screen.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/pages/chat_screen.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/pages/chats_screen.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/pages/empty_chat_screen.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/pages/payment_screen.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart';
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/become_consultant_screen.dart';
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/stepper_become_consultant_screen.dart';
|
||||
import 'package:eatvio_app/features/consultations/presentation/pages/consultations_screen.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart';
|
||||
import 'package:eatvio_app/features/menu/presentation/pages/blocked_users_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';
|
||||
import 'package:eatvio_app/features/create_plan/presentation/create_plan_screen.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/consultant_rating_screen.dart';
|
||||
import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart';
|
||||
import 'package:eatvio_app/features/diets/presentation/pages/about_diet_screen.dart';
|
||||
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
|
||||
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart';
|
||||
import 'package:eatvio_app/features/glossary/presentation/pages/glossary_screen.dart';
|
||||
import 'package:eatvio_app/features/menu/presentation/pages/menu_screen.dart';
|
||||
import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart';
|
||||
import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultations_archive_screen.dart';
|
||||
import 'package:eatvio_app/features/my_finance/presentation/pages/user_finance_screen.dart';
|
||||
import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart';
|
||||
import 'package:eatvio_app/features/notifications/presentation/notifications_screen.dart';
|
||||
import 'package:eatvio_app/features/plans/presentation/pages/about_plan_screen.dart';
|
||||
import 'package:eatvio_app/features/profile/presentation/pages/profile_screen.dart';
|
||||
import 'package:eatvio_app/features/profile/presentation/pages/subs_screen.dart';
|
||||
import 'package:eatvio_app/features/recipe/presentation/pages/about_recipe_screen.dart';
|
||||
import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart';
|
||||
import 'package:eatvio_app/features/recipe/presentation/pages/my_recipes_screen.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/pages/registration_screen.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/pages/verify_email_screen.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/pages/verify_screen.dart';
|
||||
import 'package:eatvio_app/features/settings/entities/settings_entity.dart';
|
||||
import 'package:eatvio_app/features/settings/presentation/pages/change_email_screen.dart';
|
||||
import 'package:eatvio_app/features/settings/presentation/pages/change_password_screen.dart';
|
||||
import 'package:eatvio_app/features/settings/presentation/pages/general_info_screen.dart';
|
||||
import 'package:eatvio_app/features/settings/presentation/pages/other_info_screen.dart';
|
||||
import 'package:eatvio_app/features/settings/presentation/pages/physical_activity_screen.dart';
|
||||
import 'package:eatvio_app/features/settings/presentation/pages/physical_form_screen.dart';
|
||||
import 'package:eatvio_app/features/settings/presentation/pages/pressure_screen.dart';
|
||||
import 'package:eatvio_app/features/settings/presentation/pages/settings_screen.dart';
|
||||
import 'package:eatvio_app/features/support/presentation/pages/support_screen.dart';
|
||||
import 'package:eatvio_app/features/update_screen/presentation/update_screen.dart';
|
||||
import 'package:eatvio_app/features/user_details/presentation/user_details_screen.dart';
|
||||
import 'package:eatvio_app/features/welcome/presentation/pages/welcome_screen.dart';
|
||||
import 'package:eatvio_app/core/routing/auth_routes.dart';
|
||||
import 'package:eatvio_app/core/routing/content_routes.dart';
|
||||
import 'package:eatvio_app/core/routing/main_navigation_routes.dart';
|
||||
import 'package:eatvio_app/core/routing/other_routes.dart';
|
||||
import 'package:eatvio_app/core/routing/route_constants.dart';
|
||||
import 'package:eatvio_app/core/routing/settings_routes.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:version/version.dart';
|
||||
import 'dart:developer';
|
||||
|
||||
/// Главный роутер приложения
|
||||
class AppRouter {
|
||||
static GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
static GoRouter appRouter = GoRouter(
|
||||
initialLocation: '/update-screen',
|
||||
initialLocation: RouteConstants.updateScreen,
|
||||
navigatorKey: navigatorKey,
|
||||
redirect: (context, state) {
|
||||
final segments = state.uri.pathSegments;
|
||||
if (segments.length >= 3) {
|
||||
final type = segments[1];
|
||||
final contentIdentifier = segments[2];
|
||||
|
||||
switch (type) {
|
||||
case 'recipe':
|
||||
FlutterNativeSplash.remove();
|
||||
return '/about-recipes-screen?slug=$contentIdentifier';
|
||||
case 'blog_post':
|
||||
FlutterNativeSplash.remove();
|
||||
return '/blog-post-screen?id=$contentIdentifier';
|
||||
case 'plan':
|
||||
FlutterNativeSplash.remove();
|
||||
return '/about-plan-screen?slug=$contentIdentifier';
|
||||
case 'diet':
|
||||
FlutterNativeSplash.remove();
|
||||
return '/about-diet-screen?slug=$contentIdentifier';
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
redirect: _handleRedirect,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/auth-screen',
|
||||
name: 'authScreen',
|
||||
builder: (context, state) => const AuthScreen(),
|
||||
routes: [
|
||||
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,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'scan-qr-code-screen',
|
||||
name: 'scanQRCodeScreen',
|
||||
builder: (context, state) => const ScanQRCodeScreen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
ShellRoute(
|
||||
builder: (context, state, child) {
|
||||
return BlocBuilder<ProfileAvatarCubit, ProfileAvatarState>(
|
||||
builder: (BuildContext context, ProfileAvatarState state) {
|
||||
if (state is ProfileAvatarStateLoaded) {
|
||||
return BottomNavigationBarWidget(profileModel: state.profile, child: child);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
);
|
||||
},
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/my-profile-screen',
|
||||
name: 'myProfileScreen',
|
||||
pageBuilder: (context, state) {
|
||||
return const NoTransitionPage(child: ProfileScreen());
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/feed-screen',
|
||||
name: 'feedScreen',
|
||||
pageBuilder: (context, state) => const NoTransitionPage(child: FeedScreen()),
|
||||
redirect: (context, state) async {
|
||||
try {
|
||||
await sl<WsManager>().initialize();
|
||||
} catch (e, st) {
|
||||
log(e.toString(), stackTrace: st);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/diary-screen',
|
||||
name: 'diaryScreen',
|
||||
pageBuilder: (context, state) => const NoTransitionPage(
|
||||
child: DiaryScreen(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: ConsultationsScreen.path,
|
||||
name: ConsultationsScreen.name,
|
||||
pageBuilder: (context, state) => const NoTransitionPage(child: ConsultationsScreen())),
|
||||
GoRoute(path: '/chat-select-screen', name: 'chatSelectScreen', pageBuilder: (context, state) => const NoTransitionPage(child: ChatsScreen())),
|
||||
GoRoute(
|
||||
path: '/notifications-screen',
|
||||
name: 'notificationsScreen',
|
||||
pageBuilder: (context, state) => const NoTransitionPage(child: NotificationsScreen())),
|
||||
GoRoute(
|
||||
path: '/menu-screen',
|
||||
name: 'menuScreen',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: BlocBuilder<ProfileAvatarCubit, ProfileAvatarState>(
|
||||
builder: (BuildContext context, ProfileAvatarState state) {
|
||||
if (state is ProfileAvatarStateLoaded) {
|
||||
return MenuScreen(profileModel: state.profile);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
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 {
|
||||
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()) {
|
||||
return '/feed-screen';
|
||||
} else {
|
||||
return '/auth-screen';
|
||||
}
|
||||
} catch (e) {
|
||||
return '/feed-screen';
|
||||
}
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/update-screen',
|
||||
name: 'updateScreen',
|
||||
builder: (context, state) => const UpdateScreen(),
|
||||
redirect: (context, state) async {
|
||||
try {
|
||||
final versions = (await sl<Dio>().get('$baseUrl/api/app_versions')).data;
|
||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||
String version = packageInfo.version;
|
||||
Version currentVersion = Version.parse(version);
|
||||
Version latestVersion = Version.parse(versions['min_version']);
|
||||
if (latestVersion > currentVersion) {
|
||||
return null;
|
||||
} else {
|
||||
return '/welcome-screen';
|
||||
}
|
||||
} catch (e) {
|
||||
return '/welcome-screen';
|
||||
}
|
||||
},
|
||||
),
|
||||
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,
|
||||
builder: (context, state) {
|
||||
final chosenPlanId = state.extra as int?;
|
||||
return CardSettingsScreen(chosenPlanId: chosenPlanId);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/about-plan-screen',
|
||||
name: 'aboutPlanScreen',
|
||||
builder: (context, state) {
|
||||
final slug = state.uri.queryParameters['slug'];
|
||||
if (slug != null) {
|
||||
return AboutPlanScreen(
|
||||
slug: slug,
|
||||
navigateToComments: false,
|
||||
index: null,
|
||||
);
|
||||
}
|
||||
|
||||
final data = state.extra as Map<String, dynamic>;
|
||||
return AboutPlanScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
|
||||
},
|
||||
),
|
||||
GoRoute(path: '/glossary-screen', name: 'glossaryScreen', builder: (context, state) => const GlossaryScreen()),
|
||||
GoRoute(
|
||||
path: '/about-recipes-screen',
|
||||
name: 'aboutRecipesScreen',
|
||||
builder: (context, state) {
|
||||
final slug = state.uri.queryParameters['slug'];
|
||||
if (slug != 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,
|
||||
);
|
||||
},
|
||||
),
|
||||
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)),
|
||||
GoRoute(
|
||||
path: '/subs-screen/:slug',
|
||||
name: 'subsScreen',
|
||||
builder: (context, state) => SubsScreen(slug: state.pathParameters['slug']!, subType: state.extra as SubType),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/about-diet-screen',
|
||||
name: 'aboutDietScreen',
|
||||
builder: (context, state) {
|
||||
final slug = state.uri.queryParameters['slug'];
|
||||
if (slug != null) {
|
||||
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);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/blog-post-create-screen',
|
||||
name: 'blogPostCreateScreen',
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
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 sendInitialMessage = data['sendInitialMessage'] as bool?;
|
||||
return ChatScreen(
|
||||
uuid: uuid,
|
||||
sendInitialMessage: sendInitialMessage,
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/empty-chat-screen',
|
||||
name: 'emptyChatScreen',
|
||||
builder: (context, state) => EmptyChatScreen(author: state.extra as Author),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/settings-screen',
|
||||
name: 'settingsScreen',
|
||||
builder: (context, state) => const SettingsScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'general-info-screen',
|
||||
name: 'generalInfoScreen',
|
||||
builder: (context, state) => GeneralInfoScreen(settingsEntity: state.extra as SettingsEntity),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'other-info-screen',
|
||||
name: 'otherInfoScreen',
|
||||
builder: (context, state) => OtherInfoScreen(settingsEntity: state.extra as SettingsEntity),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'physical-activity-screen',
|
||||
name: 'physicalActivityScreen',
|
||||
builder: (context, state) => PhysicalActivityScreen(settingsEntity: state.extra as SettingsEntity),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'physical-form-screen',
|
||||
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: 'change-password-screen',
|
||||
name: 'changePasswordScreen',
|
||||
builder: (context, state) => ChangePasswordScreen(settingsEntity: state.extra as SettingsEntity),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'change-email-screen',
|
||||
name: 'changeEmailScreen',
|
||||
builder: (context, state) => ChangeEmailScreen(settingsEntity: state.extra as SettingsEntity),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(path: '/become-consultant-screen', name: 'becomeConsultantScreen', builder: (context, state) => const BecomeConsultantScreen()),
|
||||
GoRoute(
|
||||
path: '/stepper-become-consultant-screen',
|
||||
name: 'stepperBecomeConsultantScreen',
|
||||
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()),
|
||||
GoRoute(
|
||||
path: '/photo-view-screen/:index',
|
||||
name: 'photoViewScreen',
|
||||
builder: (context, state) => PhotoViewScreen(imageUrls: state.extra as List<String>, index: int.parse(state.pathParameters['index']!)),
|
||||
),
|
||||
GoRoute(
|
||||
path: CreateDietScreen.path,
|
||||
name: CreateDietScreen.name,
|
||||
builder: (context, state) {
|
||||
final diet = state.extra as Diet?;
|
||||
return CreateDietScreen(diet: diet);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: DietPreviewScreen.path,
|
||||
name: DietPreviewScreen.name,
|
||||
builder: (context, state) {
|
||||
final data = state.extra as Diet;
|
||||
return DietPreviewScreen(diet: data);
|
||||
},
|
||||
),
|
||||
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: DevToolsScreen.path, name: DevToolsScreen.name, builder: (context, state) => const DevToolsScreen()),
|
||||
GoRoute(path: ConsultantRatingScreen.path, name: ConsultantRatingScreen.name, builder: (context, state) => const ConsultantRatingScreen()),
|
||||
GoRoute(
|
||||
path: InfoByOrderScreen.path,
|
||||
name: InfoByOrderScreen.name,
|
||||
builder: (context, state) {
|
||||
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,
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: PaymentScreen.path,
|
||||
name: PaymentScreen.name,
|
||||
builder: (context, state) {
|
||||
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,
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: UserDiaryScreen.path,
|
||||
name: UserDiaryScreen.name,
|
||||
builder: (context, state) {
|
||||
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,
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/verify-screen',
|
||||
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(),
|
||||
),
|
||||
...AuthRoutes.createRoutes(),
|
||||
...MainNavigationRoutes.createRoutes(),
|
||||
...ContentRoutes.createRoutes(),
|
||||
...SettingsRoutes.createRoutes(),
|
||||
...OtherRoutes.createRoutes(),
|
||||
],
|
||||
);
|
||||
|
||||
/// Обработка редиректов для deep links
|
||||
static String? _handleRedirect(BuildContext context, GoRouterState state) {
|
||||
final segments = state.uri.pathSegments;
|
||||
if (segments.length >= 3) {
|
||||
final type = segments[1];
|
||||
final contentIdentifier = segments[2];
|
||||
|
||||
switch (type) {
|
||||
case 'recipe':
|
||||
FlutterNativeSplash.remove();
|
||||
return '${RouteConstants.aboutRecipesScreen}?slug=$contentIdentifier';
|
||||
case 'blog_post':
|
||||
FlutterNativeSplash.remove();
|
||||
return '${RouteConstants.blogPostScreen}?id=$contentIdentifier';
|
||||
case 'plan':
|
||||
FlutterNativeSplash.remove();
|
||||
return '${RouteConstants.aboutPlanScreen}?slug=$contentIdentifier';
|
||||
case 'diet':
|
||||
FlutterNativeSplash.remove();
|
||||
return '${RouteConstants.aboutDietScreen}?slug=$contentIdentifier';
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,25 +18,23 @@ void showCommentsActionsBottomSheet(
|
||||
required bool isMyComment,
|
||||
}) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => Container(
|
||||
margin: EdgeInsets.only(left: 8, right: 8, bottom: MediaQuery.of(context).padding.bottom + 20),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: appTheme.whiteA700,
|
||||
),
|
||||
child: CommentsActionsBottomSheet(
|
||||
isMyComment: isMyComment,
|
||||
reply: reply,
|
||||
copy: copy,
|
||||
edit: edit,
|
||||
delete: delete,
|
||||
complain: complain,
|
||||
commentText: commentText,
|
||||
),
|
||||
));
|
||||
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 {
|
||||
@@ -82,44 +80,33 @@ class _CommentsActionsBottomSheetState extends State<CommentsActionsBottomSheet>
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(children: [
|
||||
Expanded(
|
||||
child: CustomTextFormField(
|
||||
autofocus: true,
|
||||
controller: controller,
|
||||
onChanged: (value) => setState(() {}),
|
||||
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,
|
||||
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 SizedBox(height: 7),
|
||||
const BottomSheetHandlerBar(),
|
||||
if (widget.isMyComment)
|
||||
BottomSheetActionWidget(
|
||||
@@ -131,43 +118,22 @@ class _CommentsActionsBottomSheetState extends State<CommentsActionsBottomSheet>
|
||||
}),
|
||||
)
|
||||
else if (widget.reply != null)
|
||||
BottomSheetActionWidget(
|
||||
text: s.reply,
|
||||
iconPath: 'assets/icons/reply.svg',
|
||||
onTap: widget.reply!,
|
||||
),
|
||||
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,
|
||||
),
|
||||
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!,
|
||||
),
|
||||
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,
|
||||
),
|
||||
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,
|
||||
),
|
||||
]
|
||||
BottomSheetActionWidget(text: s.complain, iconPath: 'assets/icons/complain.svg', onTap: widget.complain),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:eatvio_app/core/env_config.dart';
|
||||
|
||||
String baseUrl = EnvConfig.baseUrl;
|
||||
String baseUrl = EnvConfig.devBaseUrl;
|
||||
bool hidePlans = const bool.fromEnvironment('HIDE_PLANS', defaultValue: false);
|
||||
const buildDate = String.fromEnvironment('BUILD_DATE', defaultValue: '');
|
||||
|
||||
|
||||
72
lib/core/constants/app_constants.dart
Normal file
72
lib/core/constants/app_constants.dart
Normal file
@@ -0,0 +1,72 @@
|
||||
/// Константы приложения
|
||||
class AppConstants {
|
||||
// Размеры
|
||||
static const double defaultPadding = 16.0;
|
||||
static const double smallPadding = 8.0;
|
||||
static const double largePadding = 24.0;
|
||||
static const double extraLargePadding = 32.0;
|
||||
|
||||
// Радиусы
|
||||
static const double defaultBorderRadius = 8.0;
|
||||
static const double smallBorderRadius = 4.0;
|
||||
static const double largeBorderRadius = 16.0;
|
||||
|
||||
// Высоты
|
||||
static const double defaultButtonHeight = 48.0;
|
||||
static const double smallButtonHeight = 36.0;
|
||||
static const double largeButtonHeight = 56.0;
|
||||
|
||||
// Ширины
|
||||
static const double maxContentWidth = 400.0;
|
||||
static const double minContentWidth = 280.0;
|
||||
|
||||
// Анимации
|
||||
static const Duration defaultAnimationDuration = Duration(milliseconds: 300);
|
||||
static const Duration fastAnimationDuration = Duration(milliseconds: 150);
|
||||
static const Duration slowAnimationDuration = Duration(milliseconds: 500);
|
||||
|
||||
// Таймауты
|
||||
static const Duration defaultTimeout = Duration(seconds: 30);
|
||||
static const Duration shortTimeout = Duration(seconds: 10);
|
||||
static const Duration longTimeout = Duration(minutes: 2);
|
||||
|
||||
// Лимиты
|
||||
static const int maxImageSize = 5 * 1024 * 1024; // 5MB
|
||||
static const int maxVideoSize = 100 * 1024 * 1024; // 100MB
|
||||
static const int maxTextLength = 1000;
|
||||
static const int maxTitleLength = 100;
|
||||
static const int maxDescriptionLength = 500;
|
||||
|
||||
// Пагинация
|
||||
static const int defaultPageSize = 20;
|
||||
static const int smallPageSize = 10;
|
||||
static const int largePageSize = 50;
|
||||
|
||||
// Форматы
|
||||
static const List<String> supportedImageFormats = ['jpg', 'jpeg', 'png', 'webp'];
|
||||
static const List<String> supportedVideoFormats = ['mp4', 'mov', 'avi'];
|
||||
|
||||
// Регулярные выражения
|
||||
static const String emailRegex = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$';
|
||||
static const String phoneRegex = r'^\+?[1-9]\d{1,14}$';
|
||||
static const String urlRegex = r'^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$';
|
||||
|
||||
// Сообщения
|
||||
static const String defaultErrorMessage = 'Произошла ошибка. Попробуйте еще раз.';
|
||||
static const String networkErrorMessage = 'Проблема с подключением к интернету.';
|
||||
static const String timeoutErrorMessage = 'Превышено время ожидания.';
|
||||
static const String unknownErrorMessage = 'Неизвестная ошибка.';
|
||||
|
||||
// Ключи для SharedPreferences
|
||||
static const String welcomeScreenShownKey = 'welcomeScreenWasShowed';
|
||||
static const String userTokenKey = 'userToken';
|
||||
static const String userDataKey = 'userData';
|
||||
static const String settingsKey = 'settings';
|
||||
|
||||
// API endpoints
|
||||
static const String apiVersion = '/api';
|
||||
static const String authEndpoint = '/auth';
|
||||
static const String usersEndpoint = '/users';
|
||||
static const String postsEndpoint = '/posts';
|
||||
static const String commentsEndpoint = '/comments';
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
abstract class CredentialsStorage {
|
||||
static Future<bool> haveCredentialsInStorage() async {
|
||||
try {
|
||||
if (await const FlutterSecureStorage().read(key: 'email') == null || await const FlutterSecureStorage().read(key: 'password') == null) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Map<String, String>> getCredentialsFromStorage() async {
|
||||
return {
|
||||
'email': (await const FlutterSecureStorage().read(key: 'email'))!,
|
||||
'password': (await const FlutterSecureStorage().read(key: 'password'))!,
|
||||
};
|
||||
}
|
||||
|
||||
static Future<void> saveCredentials(String email, String password) async {
|
||||
await const FlutterSecureStorage().write(key: 'email', value: email);
|
||||
await const FlutterSecureStorage().write(key: 'password', value: password);
|
||||
}
|
||||
|
||||
static Future<void> removeCredentials() async {
|
||||
await const FlutterSecureStorage().delete(key: 'email');
|
||||
await const FlutterSecureStorage().delete(key: 'password');
|
||||
}
|
||||
}
|
||||
42
lib/core/cubit/base_cubit.dart
Normal file
42
lib/core/cubit/base_cubit.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
import 'dart:developer';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
/// Базовый класс для всех Cubit'ов с унифицированной обработкой ошибок
|
||||
abstract class BaseCubit<State> extends Cubit<State> {
|
||||
BaseCubit(super.initialState);
|
||||
|
||||
/// Унифицированная обработка ошибок
|
||||
void handleError(Object error, [StackTrace? stackTrace]) {
|
||||
log('Error in $runtimeType: $error', stackTrace: stackTrace);
|
||||
// Здесь можно добавить отправку ошибок в Sentry или другую систему мониторинга
|
||||
}
|
||||
|
||||
/// Безопасное выполнение асинхронных операций
|
||||
Future<T?> safeExecute<T>(Future<T> Function() operation) async {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error, stackTrace) {
|
||||
handleError(error, stackTrace);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Безопасное выполнение асинхронных операций с обработкой состояния
|
||||
Future<T?> safeExecuteWithState<T>(
|
||||
Future<T> Function() operation, {
|
||||
required State loadingState,
|
||||
required State Function(T result) successState,
|
||||
required State Function(Object error) errorState,
|
||||
}) async {
|
||||
try {
|
||||
emit(loadingState);
|
||||
final result = await operation();
|
||||
emit(successState(result));
|
||||
return result;
|
||||
} catch (error, stackTrace) {
|
||||
handleError(error, stackTrace);
|
||||
emit(errorState(error));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
28
lib/core/cubit/state_utils.dart
Normal file
28
lib/core/cubit/state_utils.dart
Normal file
@@ -0,0 +1,28 @@
|
||||
/// Утилиты для работы с состояниями Cubit'ов
|
||||
class StateUtils {
|
||||
/// Проверяет, является ли состояние загрузочным
|
||||
static bool isLoading(dynamic state) {
|
||||
return state.toString().contains('loading') || state.toString().contains('Loading');
|
||||
}
|
||||
|
||||
/// Проверяет, является ли состояние загруженным
|
||||
static bool isLoaded(dynamic state) {
|
||||
return state.toString().contains('loaded') || state.toString().contains('Loaded');
|
||||
}
|
||||
|
||||
/// Проверяет, является ли состояние ошибочным
|
||||
static bool isError(dynamic state) {
|
||||
return state.toString().contains('error') || state.toString().contains('Error');
|
||||
}
|
||||
|
||||
/// Получает сообщение об ошибке из состояния
|
||||
static String? getErrorMessage(dynamic state) {
|
||||
if (isError(state)) {
|
||||
// Попытка извлечь сообщение об ошибке из состояния
|
||||
final stateString = state.toString();
|
||||
final errorMatch = RegExp(r'error[:\s]+([^,}]+)').firstMatch(stateString);
|
||||
return errorMatch?.group(1)?.trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
144
lib/core/di/cubit_registrations.dart
Normal file
144
lib/core/di/cubit_registrations.dart
Normal file
@@ -0,0 +1,144 @@
|
||||
import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_cubit.dart';
|
||||
import 'package:eatvio_app/core/views/cubit/views_cubit.dart';
|
||||
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart';
|
||||
import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart';
|
||||
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_cubit.dart';
|
||||
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
||||
import 'package:eatvio_app/features/calendar/presentation/bloc/calendar_cubit.dart';
|
||||
import 'package:eatvio_app/features/cancel_plan_bottom_sheet/cubit/cancel_plan_cubit.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
|
||||
import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_cubit.dart';
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart';
|
||||
import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_cubit.dart';
|
||||
import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_cubit.dart';
|
||||
import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart';
|
||||
import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_cubit.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
|
||||
import 'package:eatvio_app/features/diets/presentation/cubit/about_diet_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';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/profile_cubit/profile_cubit.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/archive/consultations_archive_cubit.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_cubit.dart';
|
||||
import 'package:eatvio_app/features/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/my_diets/presentation/cubit/my_diets_cubit.dart';
|
||||
import 'package:eatvio_app/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart';
|
||||
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart';
|
||||
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
||||
import 'package:eatvio_app/features/order_info/cubit/order_info_cubit.dart';
|
||||
import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_cubit.dart';
|
||||
import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart' as profile_plans;
|
||||
import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart';
|
||||
import 'package:eatvio_app/features/profile/presentation/bloc/sub_cubit/sub_cubit.dart';
|
||||
import 'package:eatvio_app/features/recipe/presentation/cubit/about_recipe_cubit.dart';
|
||||
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart';
|
||||
import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart';
|
||||
import 'package:eatvio_app/features/user_details/presentation/cubit/user_details_cubit.dart';
|
||||
import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
/// Регистрация всех Cubit'ов в системе внедрения зависимостей
|
||||
class CubitRegistrations {
|
||||
static final sl = GetIt.instance;
|
||||
|
||||
/// Регистрация всех Cubit'ов
|
||||
static void registerAll() {
|
||||
_registerSingletonCubits();
|
||||
_registerFactoryCubits();
|
||||
}
|
||||
|
||||
/// Регистрация Cubit'ов как синглтонов
|
||||
static void _registerSingletonCubits() {
|
||||
sl.registerLazySingleton<NewMessagesCubit>(() => NewMessagesCubit());
|
||||
sl.registerLazySingleton<NotificationsCubit>(() => NotificationsCubit(wsManager: sl(), notificationsRepository: sl()));
|
||||
sl.registerLazySingleton<ProfileAvatarCubit>(() => ProfileAvatarCubit(profileRepository: sl()));
|
||||
sl.registerLazySingleton<ChatsCubit>(() => ChatsCubit(chatRepository: sl(), wsManager: sl(), profileRepository: sl()));
|
||||
sl.registerLazySingleton<InternetConnectionCubit>(() => InternetConnectionCubit());
|
||||
}
|
||||
|
||||
/// Регистрация Cubit'ов как фабрик
|
||||
static void _registerFactoryCubits() {
|
||||
// Auth related cubits
|
||||
sl.registerFactory<AuthCubit>(() => AuthCubit(authRepository: sl()));
|
||||
sl.registerFactory<RegistrationCubit>(() => RegistrationCubit(registrationRepository: sl()));
|
||||
sl.registerFactory<ResetPasswordCubit>(() => ResetPasswordCubit(resetPasswordRepository: sl()));
|
||||
sl.registerFactory<VerifyEmailCubit>(() => VerifyEmailCubit(profileRepository: sl()));
|
||||
|
||||
// Feed related cubits
|
||||
sl.registerFactory<FeedCubit>(() => FeedCubit(feedRepository: sl()));
|
||||
sl.registerFactory<RecipesCubit>(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl(), aboutRecipeRepository: sl()));
|
||||
sl.registerFactory<DietsCubit>(() => DietsCubit(feedRepository: sl(), dietsRepository: sl()));
|
||||
sl.registerFactory<PlansCubit>(() => PlansCubit(feedRepository: sl(), planRepository: sl()));
|
||||
sl.registerFactory<ConsultantsCubit>(() => ConsultantsCubit(feedRepository: sl()));
|
||||
sl.registerFactory<CommentsCubit>(() => CommentsCubit(feedRepository: sl(), commentsRepository: sl()));
|
||||
sl.registerFactory<FeedProfileCubit>(() => FeedProfileCubit(profileRepository: sl()));
|
||||
|
||||
// Chat related cubits
|
||||
sl.registerFactory<ChatRoomCubit>(() => ChatRoomCubit(cardsRepository: sl(), chatRepository: sl(), wsManager: sl()));
|
||||
|
||||
// Profile related cubits
|
||||
sl.registerFactory<ProfileCubit>(() => ProfileCubit(profileRepository: sl(), chatRepository: sl(), consultantRepository: sl()));
|
||||
sl.registerFactory<profile_plans.PlansCubit>(() => profile_plans.PlansCubit(plansRepository: sl()));
|
||||
sl.registerFactory<SubCubit>(() => SubCubit(profileRepository: sl()));
|
||||
|
||||
// Content creation cubits
|
||||
sl.registerFactory<CreateDietCubit>(() => CreateDietCubit(profileRepository: sl(), createDietRepository: sl(), glossaryRepository: sl()));
|
||||
sl.registerFactory<CreatePlanCubit>(() => CreatePlanCubit(createPlanRepository: sl()));
|
||||
sl.registerFactory<CreateRecipeCubit>(() => CreateRecipeCubit(recipeRepository: sl(), glossaryRepository: sl()));
|
||||
sl.registerFactory<BlogPostCreateCubit>(() => BlogPostCreateCubit(blogPostRepository: sl()));
|
||||
|
||||
// Content viewing cubits
|
||||
sl.registerFactory<AboutDietCubit>(() => AboutDietCubit(dietsRepository: sl(), profileRepository: sl()));
|
||||
sl.registerFactory<AboutPlanCubit>(() => AboutPlanCubit(planRepository: sl()));
|
||||
sl.registerFactory<AboutRecipeCubit>(() => AboutRecipeCubit(recipeRepository: sl(), profileRepository: sl()));
|
||||
sl.registerFactory<BlogPostCubit>(() => BlogPostCubit(sl()));
|
||||
|
||||
// User content cubits
|
||||
sl.registerFactory<MyDietsCubit>(() => MyDietsCubit(myDietsRepository: sl(), createDietRepository: sl()));
|
||||
sl.registerFactory<MyPlansCubit>(() => MyPlansCubit(myPlansRepository: sl()));
|
||||
sl.registerFactory<MyRecipesCubit>(() => MyRecipesCubit(myRecipesRepository: sl(), aboutRecipeRepository: sl()));
|
||||
|
||||
// Other feature cubits
|
||||
sl.registerFactory<CalendarCubit>(() => CalendarCubit(calendarRepository: sl()));
|
||||
sl.registerFactory<DiaryCubit>(
|
||||
() => DiaryCubit(
|
||||
chatRepository: sl(),
|
||||
diaryRepository: sl(),
|
||||
glossaryRepository: sl(),
|
||||
profileRepository: sl(),
|
||||
dietRepository: sl(),
|
||||
settingsRepository: sl(),
|
||||
),
|
||||
);
|
||||
sl.registerFactory<SettingsCubit>(
|
||||
() => SettingsCubit(profileRepository: sl(), settingsRepository: sl(), consultantRepository: sl(), consultationsRepository: sl()),
|
||||
);
|
||||
sl.registerFactory<RegisterConsultantCubit>(() => RegisterConsultantCubit(consultantRepository: sl()));
|
||||
sl.registerFactory<ComplainingCubit>(() => ComplainingCubit(repository: sl()));
|
||||
sl.registerFactory<UserCardsCubit>(() => UserCardsCubit(cardsRepository: sl()));
|
||||
sl.registerFactory<OrderInfoCubit>(() => OrderInfoCubit(chatRepository: sl()));
|
||||
sl.registerFactory<ConsultationsCubit>(() => ConsultationsCubit(consultationsRepository: sl()));
|
||||
sl.registerFactory<ConsultantCommentsCubit>(() => ConsultantCommentsCubit(diaryRepository: sl()));
|
||||
sl.registerFactory<CancelPlanCubit>(() => CancelPlanCubit(cancelPlanRepository: sl()));
|
||||
sl.registerFactory<ConsultationsArchiveCubit>(() => ConsultationsArchiveCubit(consultationsRepository: sl()));
|
||||
sl.registerFactory<ConsultantFinancesCubit>(() => ConsultantFinancesCubit(consultationsRepository: sl()));
|
||||
sl.registerFactory<UserPurchasesCubit>(() => UserPurchasesCubit(consultationsRepository: sl()));
|
||||
sl.registerFactory<BlockedUsersCubit>(() => BlockedUsersCubit(blockedUsersRepository: sl()));
|
||||
sl.registerFactory<NotificationSettingsCubit>(() => NotificationSettingsCubit(notificationSettingsRepository: sl()));
|
||||
sl.registerFactory<ConsultantRatingCubit>(() => ConsultantRatingCubit(feedRepository: sl(), repository: sl(), profileRepository: sl()));
|
||||
sl.registerFactory<ViewsCubit>(() => ViewsCubit(viewsRepository: sl()));
|
||||
sl.registerFactory<UserDetailsCubit>(() => UserDetailsCubit(profileRepository: sl()));
|
||||
}
|
||||
}
|
||||
33
lib/core/di/external_registrations.dart
Normal file
33
lib/core/di/external_registrations.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/dio/phone_verify_error_interceptor.dart';
|
||||
import 'package:eatvio_app/core/interceptors/token_interceptor.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
/// Регистрация внешних зависимостей в системе внедрения зависимостей
|
||||
class ExternalRegistrations {
|
||||
static final sl = GetIt.instance;
|
||||
|
||||
/// Регистрация всех внешних зависимостей
|
||||
static void registerAll() {
|
||||
_registerDio();
|
||||
}
|
||||
|
||||
/// Регистрация Dio HTTP клиента
|
||||
static void _registerDio() {
|
||||
sl.registerLazySingleton(
|
||||
() => Dio(
|
||||
BaseOptions(
|
||||
baseUrl: baseUrl,
|
||||
headers: {
|
||||
'accept': 'application/json',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
),
|
||||
)..interceptors.addAll([
|
||||
TokenInterceptor(),
|
||||
PhoneVerifyErrorInterceptor(),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
89
lib/core/di/repository_registrations.dart
Normal file
89
lib/core/di/repository_registrations.dart
Normal file
@@ -0,0 +1,89 @@
|
||||
import 'package:eatvio_app/core/comments/repositories/comments_repository.dart';
|
||||
import 'package:eatvio_app/core/views/repository/views_repository.dart';
|
||||
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
|
||||
import 'package:eatvio_app/features/auth/repositories/reset_password_repository.dart';
|
||||
import 'package:eatvio_app/features/blog_post_create/repositories/blog_post_repository.dart';
|
||||
import 'package:eatvio_app/features/calendar/repositories/calendar_repository.dart';
|
||||
import 'package:eatvio_app/features/cancel_plan_bottom_sheet/repository/cancel_plan_repository.dart';
|
||||
import 'package:eatvio_app/features/chat/repositories/chat_repository.dart';
|
||||
import 'package:eatvio_app/features/complaining/repositories/complaining_repository.dart';
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/repositories/consultant_rating_repository.dart';
|
||||
import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart';
|
||||
import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart';
|
||||
import 'package:eatvio_app/features/create_plan/repositories/create_plan_repository.dart';
|
||||
import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart';
|
||||
import 'package:eatvio_app/features/diary/repositories/diary_repository.dart';
|
||||
import 'package:eatvio_app/features/diets/repositories/diet_repository.dart';
|
||||
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
|
||||
import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart';
|
||||
import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart';
|
||||
import 'package:eatvio_app/features/menu/repositories/notification_settings_repository.dart';
|
||||
import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart';
|
||||
import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart';
|
||||
import 'package:eatvio_app/features/my_plans/repositories/my_diets_repository.dart';
|
||||
import 'package:eatvio_app/features/notifications/repository/notifications_repository.dart';
|
||||
import 'package:eatvio_app/features/plans/repositories/plan_repository.dart';
|
||||
import 'package:eatvio_app/features/plans/repositories/plans_repository.dart';
|
||||
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
|
||||
import 'package:eatvio_app/features/recipe/repositories/about_recipe_repository.dart';
|
||||
import 'package:eatvio_app/features/recipe/repositories/my_recipes_repository.dart';
|
||||
import 'package:eatvio_app/features/registration/repositories/registration_repository.dart';
|
||||
import 'package:eatvio_app/features/settings/repositories/service_repository.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
/// Регистрация всех Repository в системе внедрения зависимостей
|
||||
class RepositoryRegistrations {
|
||||
static final sl = GetIt.instance;
|
||||
|
||||
/// Регистрация всех Repository
|
||||
static void registerAll() {
|
||||
_registerSingletonRepositories();
|
||||
_registerFactoryRepositories();
|
||||
}
|
||||
|
||||
/// Регистрация Repository как синглтонов
|
||||
static void _registerSingletonRepositories() {
|
||||
// Core repositories
|
||||
sl.registerLazySingleton<ChatRepository>(() => ChatRepository(chatService: sl()));
|
||||
sl.registerLazySingleton<FeedRepository>(() => FeedRepository(feedService: sl()));
|
||||
sl.registerLazySingleton<ProfileRepository>(() => ProfileRepository(profileService: sl()));
|
||||
sl.registerLazySingleton<CalendarRepository>(() => CalendarRepository(calendarService: sl()));
|
||||
sl.registerLazySingleton<SettingsRepository>(() => SettingsRepository(settingsService: sl()));
|
||||
sl.registerLazySingleton<ConsultantRepository>(() => ConsultantRepository(consultantService: sl()));
|
||||
sl.registerLazySingleton<DiaryRepository>(() => DiaryRepository(diaryService: sl()));
|
||||
sl.registerLazySingleton<GlossaryRepository>(() => GlossaryRepository(glossaryService: sl()));
|
||||
sl.registerLazySingleton<DietRepository>(() => DietRepository(dietsService: sl()));
|
||||
sl.registerLazySingleton<PlansRepository>(() => PlansRepository(plansService: sl()));
|
||||
sl.registerLazySingleton<PlanRepository>(() => PlanRepository(planService: sl()));
|
||||
sl.registerLazySingleton<AboutRecipeRepository>(() => AboutRecipeRepository(recipeService: sl()));
|
||||
sl.registerLazySingleton<RecipeRepository>(() => RecipeRepository(recipeService: sl()));
|
||||
sl.registerLazySingleton<ComplainingRepository>(() => ComplainingRepository(service: sl()));
|
||||
sl.registerLazySingleton<AuthRepository>(() => AuthRepository(authService: sl()));
|
||||
sl.registerLazySingleton<CardsRepository>(() => CardsRepository(cardsService: sl()));
|
||||
sl.registerLazySingleton<ConsultationsRepository>(() => ConsultationsRepository(consultationService: sl()));
|
||||
sl.registerLazySingleton<CancelPlanRepository>(() => CancelPlanRepository(cancelService: sl()));
|
||||
sl.registerLazySingleton<CreatePlanRepository>(() => CreatePlanRepository(createPlanService: sl()));
|
||||
sl.registerLazySingleton<NotificationsRepository>(() => NotificationsRepository(notificationsService: sl()));
|
||||
sl.registerLazySingleton<BlockedUsersRepository>(() => BlockedUsersRepository(blockedUserService: sl()));
|
||||
sl.registerLazySingleton<CommentsRepository>(() => CommentsRepository(service: sl()));
|
||||
sl.registerLazySingleton<NotificationSettingsRepository>(() => NotificationSettingsRepository(sl()));
|
||||
|
||||
// Content repositories
|
||||
sl.registerLazySingleton<BlogPostRepository>(() => BlogPostRepository(blogPostService: sl()));
|
||||
sl.registerLazySingleton<CreateDietRepository>(() => CreateDietRepository(createDietService: sl()));
|
||||
sl.registerLazySingleton<MyDietsRepository>(() => MyDietsRepository(myDietsService: sl()));
|
||||
sl.registerLazySingleton<MyPlansRepository>(() => MyPlansRepository(myPlansService: sl()));
|
||||
|
||||
// Auth repositories
|
||||
sl.registerLazySingleton<RegistrationRepository>(() => RegistrationRepository(registrationService: sl()));
|
||||
}
|
||||
|
||||
/// Регистрация Repository как фабрик
|
||||
static void _registerFactoryRepositories() {
|
||||
sl.registerFactory<MyRecipesRepository>(() => MyRecipesRepository(myRecipesService: sl()));
|
||||
sl.registerFactory<ResetPasswordRepository>(() => ResetPasswordRepository(resetPasswordService: sl()));
|
||||
sl.registerFactory<ConsultantRatingRepository>(() => ConsultantRatingRepository(sl()));
|
||||
sl.registerFactory<ViewsRepository>(() => ViewsRepository(viewsService: sl()));
|
||||
}
|
||||
}
|
||||
88
lib/core/di/service_registrations.dart
Normal file
88
lib/core/di/service_registrations.dart
Normal file
@@ -0,0 +1,88 @@
|
||||
import 'package:eatvio_app/core/comments/services/comments_service.dart';
|
||||
import 'package:eatvio_app/core/firebase_api.dart';
|
||||
import 'package:eatvio_app/core/views/service/views_service.dart';
|
||||
import 'package:eatvio_app/core/websockets/ws_service.dart';
|
||||
import 'package:eatvio_app/features/auth/services/auth_service.dart';
|
||||
import 'package:eatvio_app/features/auth/services/reset_password_service.dart';
|
||||
import 'package:eatvio_app/features/blog_post_create/services/blog_post_service.dart';
|
||||
import 'package:eatvio_app/features/calendar/services/calendar_service.dart';
|
||||
import 'package:eatvio_app/features/cancel_plan_bottom_sheet/service/cancel_plan_service.dart';
|
||||
import 'package:eatvio_app/features/chat/services/chat_service.dart';
|
||||
import 'package:eatvio_app/features/complaining/services/complaining_service.dart';
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/services/consultant_service.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/services/consultant_rating_service.dart';
|
||||
import 'package:eatvio_app/features/consultations/services/consultation_service.dart';
|
||||
import 'package:eatvio_app/features/create_diet/services/create_diet_service.dart';
|
||||
import 'package:eatvio_app/features/create_plan/services/create_plan_service.dart';
|
||||
import 'package:eatvio_app/features/create_recipe/services/recipe_service.dart';
|
||||
import 'package:eatvio_app/features/diary/services/diary_service.dart';
|
||||
import 'package:eatvio_app/features/diets/services/diet_service.dart';
|
||||
import 'package:eatvio_app/features/feed/services/feed_service.dart';
|
||||
import 'package:eatvio_app/features/glossary/services/glossary_service.dart';
|
||||
import 'package:eatvio_app/features/menu/services/blocked_users_service.dart';
|
||||
import 'package:eatvio_app/features/menu/services/notification_settings_service.dart';
|
||||
import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart';
|
||||
import 'package:eatvio_app/features/my_finance/services/cards_service.dart';
|
||||
import 'package:eatvio_app/features/my_plans/services/my_plans_service.dart';
|
||||
import 'package:eatvio_app/features/notifications/service/notifications_service.dart';
|
||||
import 'package:eatvio_app/features/plans/services/plan_service.dart';
|
||||
import 'package:eatvio_app/features/plans/services/plans_service.dart';
|
||||
import 'package:eatvio_app/features/profile/services/profile_service.dart';
|
||||
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:get_it/get_it.dart';
|
||||
|
||||
/// Регистрация всех Service в системе внедрения зависимостей
|
||||
class ServiceRegistrations {
|
||||
static final sl = GetIt.instance;
|
||||
|
||||
/// Регистрация всех Service
|
||||
static void registerAll() {
|
||||
_registerSingletonServices();
|
||||
}
|
||||
|
||||
/// Регистрация Service как синглтонов
|
||||
static void _registerSingletonServices() {
|
||||
// Core services
|
||||
sl.registerLazySingleton<ChatService>(() => ChatService(sl()));
|
||||
sl.registerLazySingleton<FeedService>(() => FeedService(sl()));
|
||||
sl.registerLazySingleton<ProfileService>(() => ProfileService(sl()));
|
||||
sl.registerLazySingleton<CalendarService>(() => CalendarService(sl()));
|
||||
sl.registerLazySingleton<SettingsService>(() => SettingsService(sl()));
|
||||
sl.registerLazySingleton<ConsultantService>(() => ConsultantService(sl()));
|
||||
sl.registerLazySingleton<DiaryService>(() => DiaryService(sl()));
|
||||
sl.registerLazySingleton<GlossaryService>(() => GlossaryService(sl()));
|
||||
sl.registerLazySingleton<DietService>(() => DietService(sl()));
|
||||
sl.registerLazySingleton<PlansService>(() => PlansService(sl()));
|
||||
sl.registerLazySingleton<PlanService>(() => PlanService(sl()));
|
||||
sl.registerLazySingleton<MyDietsService>(() => MyDietsService(sl()));
|
||||
sl.registerLazySingleton<MyPlansService>(() => MyPlansService(sl()));
|
||||
sl.registerLazySingleton<WsService>(() => WsService(sl()));
|
||||
sl.registerLazySingleton<AboutRecipeService>(() => AboutRecipeService(sl()));
|
||||
sl.registerLazySingleton<ComplainingService>(() => ComplainingService(sl()));
|
||||
sl.registerLazySingleton<AuthService>(() => AuthService(sl()));
|
||||
sl.registerLazySingleton<CardsService>(() => CardsService(sl()));
|
||||
sl.registerLazySingleton<ConsultationService>(() => ConsultationService(sl()));
|
||||
sl.registerLazySingleton<CancelPlanService>(() => CancelPlanService(sl()));
|
||||
sl.registerLazySingleton<CreatePlanService>(() => CreatePlanService(sl()));
|
||||
sl.registerLazySingleton<NotificationsService>(() => NotificationsService(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()));
|
||||
|
||||
// Content services
|
||||
sl.registerLazySingleton<CreateRecipeService>(() => CreateRecipeService(sl()));
|
||||
sl.registerLazySingleton<BlogPostService>(() => BlogPostService(sl()));
|
||||
sl.registerLazySingleton<CreateDietService>(() => CreateDietService(sl()));
|
||||
|
||||
// Auth services
|
||||
sl.registerLazySingleton<RegistrationService>(() => RegistrationService(sl()));
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,9 @@ class EnvConfig {
|
||||
// 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'] ?? '';
|
||||
@@ -40,4 +43,12 @@ class EnvConfig {
|
||||
// Connection Check URLs
|
||||
static String get googleCheckUrl => dotenv.env['GOOGLE_CHECK_URL'] ?? '';
|
||||
static String get yandexCheckUrl => dotenv.env['YANDEX_CHECK_URL'] ?? '';
|
||||
|
||||
// VK ID Configuration
|
||||
static String get vkClientId => dotenv.env['VK_CLIENT_ID'] ?? '';
|
||||
|
||||
// Yandex ID Configuration
|
||||
static String get yandexClientId => dotenv.env['YANDEX_CLIENT_ID'] ?? '';
|
||||
static String get yandexClientSecret => dotenv.env['YANDEX_CLIENT_SECRET'] ?? '';
|
||||
static String get yandexRedirectUri => dotenv.env['YANDEX_REDIRECT_URI'] ?? '';
|
||||
}
|
||||
|
||||
@@ -1,28 +1,39 @@
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
import 'package:eatvio_app/core/utils/toasts.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/token_storage.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/services.dart' show MethodChannel, PlatformException;
|
||||
|
||||
class FirebaseApi {
|
||||
final _firebaseMessaging = FirebaseMessaging.instance;
|
||||
|
||||
Future<String?> getToken() async {
|
||||
await _firebaseMessaging.requestPermission();
|
||||
return await _firebaseMessaging.getToken();
|
||||
String? token;
|
||||
|
||||
if (await _isHuaweiDevice()) {
|
||||
token = await _getHuaweiPushTokenNative();
|
||||
} else {
|
||||
await _firebaseMessaging.requestPermission();
|
||||
token = await _firebaseMessaging.getToken();
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
Future<void> registerPushToken() async {
|
||||
if (await TokenStorage.haveTokenInStorage()) {
|
||||
try {
|
||||
final deviceInfo = DeviceInfoPlugin();
|
||||
final isHuawei = Platform.isAndroid ? await _isHuaweiDevice() : false;
|
||||
await sl<Dio>().post(
|
||||
'/api/profile/notification/token/register',
|
||||
data: {
|
||||
'platform': Platform.isIOS ? 'ios' : 'android',
|
||||
'platform': Platform.isIOS ? 'ios' : (isHuawei ? 'huawei' : 'android'),
|
||||
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
|
||||
'token': await getToken(),
|
||||
},
|
||||
@@ -32,4 +43,32 @@ class FirebaseApi {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _isHuaweiDevice() async {
|
||||
if (!Platform.isAndroid) return false;
|
||||
try {
|
||||
final info = await DeviceInfoPlugin().androidInfo;
|
||||
final brand = info.brand.toLowerCase();
|
||||
final manufacturer = info.manufacturer.toLowerCase();
|
||||
return brand.contains('huawei') || brand.contains('honor') || manufacturer.contains('huawei') || manufacturer.contains('honor');
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _getHuaweiPushTokenNative() async {
|
||||
try {
|
||||
const channel = MethodChannel('com.eatvioapp/hms_push');
|
||||
final String? token = await channel.invokeMethod<String>('getHuaweiToken');
|
||||
return token;
|
||||
} on PlatformException catch (e, st) {
|
||||
showErrorToast('$e $st');
|
||||
log(e.toString(), stackTrace: st);
|
||||
return null;
|
||||
} catch (e, st) {
|
||||
showErrorToast('$e $st');
|
||||
log(e.toString(), stackTrace: st);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:eatvio_app/core/env_config.dart';
|
||||
import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_state.dart';
|
||||
import 'package:eatvio_app/core/utils/toasts.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class InternetConnectionCubit extends Cubit<InternetConnectionState> {
|
||||
InternetConnectionCubit() : super(InternetConnectionState()) {
|
||||
checkConnection();
|
||||
_connectivitySubscription = _connectivity.onConnectivityChanged.listen(_updateAccessStatus);
|
||||
_listener = AppLifecycleListener(
|
||||
onResume: () async {
|
||||
_connectivitySubscription.resume();
|
||||
await checkConnection();
|
||||
},
|
||||
onHide: _connectivitySubscription.pause,
|
||||
onPause: _connectivitySubscription.pause,
|
||||
);
|
||||
}
|
||||
|
||||
final Connectivity _connectivity = Connectivity();
|
||||
late StreamSubscription<List<ConnectivityResult>> _connectivitySubscription;
|
||||
late final AppLifecycleListener _listener;
|
||||
|
||||
Future<void> checkConnection() async {
|
||||
late List<ConnectivityResult> result;
|
||||
|
||||
try {
|
||||
result = await _connectivity.checkConnectivity();
|
||||
} on PlatformException catch (e) {
|
||||
log('Couldn\'t check connectivity status', error: e);
|
||||
return;
|
||||
}
|
||||
|
||||
return _updateAccessStatus(result);
|
||||
}
|
||||
|
||||
Future<void> _updateAccessStatus(List<ConnectivityResult> result) async {
|
||||
final hasInternet = await _checkInternetAccess(result);
|
||||
if (!hasInternet) showInternetConnectionToast();
|
||||
emit(state.copyWith(isInternetAvailable: hasInternet));
|
||||
}
|
||||
|
||||
Future<bool> _checkInternetAccess(List<ConnectivityResult> result) async {
|
||||
if (result.contains(ConnectivityResult.none)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final uris = [Uri.parse(EnvConfig.googleCheckUrl), Uri.parse(EnvConfig.yandexCheckUrl), Uri.parse(EnvConfig.devBaseUrl), Uri.parse(EnvConfig.baseUrl)];
|
||||
for (final uri in uris) {
|
||||
try {
|
||||
final response = await http.get(uri).timeout(const Duration(seconds: 3));
|
||||
return response.statusCode >= 100 && response.statusCode < 600;
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_connectivitySubscription.cancel();
|
||||
_listener.dispose();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'internet_connection_state.freezed.dart';
|
||||
|
||||
|
||||
@freezed
|
||||
abstract class InternetConnectionState with _$InternetConnectionState {
|
||||
|
||||
factory InternetConnectionState({
|
||||
@Default(true) bool isInternetAvailable,
|
||||
}) = _InternetConnectionState;
|
||||
|
||||
}
|
||||
@@ -1,256 +1,27 @@
|
||||
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/di/cubit_registrations.dart';
|
||||
import 'package:eatvio_app/core/di/external_registrations.dart';
|
||||
import 'package:eatvio_app/core/di/repository_registrations.dart';
|
||||
import 'package:eatvio_app/core/di/service_registrations.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';
|
||||
import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart';
|
||||
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
|
||||
import 'package:eatvio_app/features/auth/repositories/reset_password_repository.dart';
|
||||
import 'package:eatvio_app/features/auth/services/auth_service.dart';
|
||||
import 'package:eatvio_app/features/auth/services/reset_password_service.dart';
|
||||
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_cubit.dart';
|
||||
import 'package:eatvio_app/features/cancel_plan_bottom_sheet/cubit/cancel_plan_cubit.dart';
|
||||
import 'package:eatvio_app/features/cancel_plan_bottom_sheet/repository/cancel_plan_repository.dart';
|
||||
import 'package:eatvio_app/features/cancel_plan_bottom_sheet/service/cancel_plan_service.dart';
|
||||
import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_cubit.dart';
|
||||
import 'package:eatvio_app/features/complaining/repositories/complaining_repository.dart';
|
||||
import 'package:eatvio_app/features/complaining/services/complaining_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';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/archive/consultations_archive_cubit.dart';
|
||||
import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_cubit.dart';
|
||||
import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart';
|
||||
import 'package:eatvio_app/features/create_diet/services/create_diet_service.dart';
|
||||
import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart';
|
||||
import 'package:eatvio_app/features/create_plan/repositories/create_plan_repository.dart';
|
||||
import 'package:eatvio_app/features/create_plan/services/create_plan_service.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_cubit.dart';
|
||||
import 'package:eatvio_app/features/diets/repositories/diet_repository.dart';
|
||||
import 'package:eatvio_app/features/diets/services/diet_service.dart';
|
||||
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart';
|
||||
import 'package:eatvio_app/features/blog_post_create/repositories/blog_post_repository.dart';
|
||||
import 'package:eatvio_app/features/blog_post_create/services/blog_post_service.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_cubit.dart';
|
||||
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart';
|
||||
import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart';
|
||||
import 'package:eatvio_app/features/menu/services/blocked_users_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';
|
||||
import 'package:eatvio_app/features/calendar/repositories/calendar_repository.dart';
|
||||
import 'package:eatvio_app/features/calendar/services/calendar_service.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
|
||||
import 'package:eatvio_app/features/chat/repositories/chat_repository.dart';
|
||||
import 'package:eatvio_app/features/chat/services/chat_service.dart';
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart';
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart';
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/services/consultant_service.dart';
|
||||
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/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';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/profile_cubit/profile_cubit.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart';
|
||||
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
|
||||
import 'package:eatvio_app/features/feed/services/feed_service.dart';
|
||||
import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart';
|
||||
import 'package:eatvio_app/features/glossary/services/glossary_service.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart';
|
||||
import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart';
|
||||
import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart';
|
||||
import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart';
|
||||
import 'package:eatvio_app/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart';
|
||||
import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart';
|
||||
import 'package:eatvio_app/features/my_finance/services/cards_service.dart';
|
||||
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart';
|
||||
import 'package:eatvio_app/features/my_plans/repositories/my_diets_repository.dart';
|
||||
import 'package:eatvio_app/features/my_plans/services/my_plans_service.dart';
|
||||
import 'package:eatvio_app/features/notifications/repository/notifications_repository.dart';
|
||||
import 'package:eatvio_app/features/notifications/service/notifications_service.dart';
|
||||
import 'package:eatvio_app/features/order_info/cubit/order_info_cubit.dart';
|
||||
import 'package:eatvio_app/features/plans/repositories/plans_repository.dart';
|
||||
import 'package:eatvio_app/features/plans/services/plans_service.dart';
|
||||
import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_cubit.dart';
|
||||
import 'package:eatvio_app/features/plans/repositories/plan_repository.dart';
|
||||
import 'package:eatvio_app/features/plans/services/plan_service.dart';
|
||||
import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart';
|
||||
import 'package:eatvio_app/features/profile/presentation/bloc/sub_cubit/sub_cubit.dart';
|
||||
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
|
||||
import 'package:eatvio_app/features/profile/services/profile_service.dart';
|
||||
import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart';
|
||||
import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart';
|
||||
import 'package:eatvio_app/features/create_recipe/services/recipe_service.dart';
|
||||
import 'package:eatvio_app/features/recipe/presentation/cubit/about_recipe_cubit.dart';
|
||||
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart';
|
||||
import 'package:eatvio_app/features/recipe/repositories/about_recipe_repository.dart';
|
||||
import 'package:eatvio_app/features/recipe/repositories/my_recipes_repository.dart';
|
||||
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/presentation/bloc/registration_cubit/registration_cubit.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart';
|
||||
import 'package:eatvio_app/features/registration/repositories/registration_repository.dart';
|
||||
import 'package:eatvio_app/features/registration/services/registration_service.dart';
|
||||
import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart';
|
||||
import 'package:eatvio_app/features/settings/repositories/service_repository.dart';
|
||||
import 'package:eatvio_app/features/settings/services/settings_service.dart';
|
||||
import 'package:eatvio_app/features/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;
|
||||
|
||||
/// Глобальный экземпляр GetIt для внедрения зависимостей
|
||||
final sl = GetIt.instance;
|
||||
|
||||
/// Инициализация системы внедрения зависимостей
|
||||
Future<void> init() async {
|
||||
// Cubit singletons
|
||||
// Регистрация внешних зависимостей (Dio, etc.)
|
||||
ExternalRegistrations.registerAll();
|
||||
|
||||
// Регистрация сервисов
|
||||
ServiceRegistrations.registerAll();
|
||||
|
||||
// Регистрация репозиториев
|
||||
RepositoryRegistrations.registerAll();
|
||||
|
||||
// Регистрация Cubit'ов
|
||||
CubitRegistrations.registerAll();
|
||||
|
||||
// Регистрация WebSocket менеджера
|
||||
sl.registerLazySingleton<WsManager>(() => WsManager(wsService: sl()));
|
||||
sl.registerLazySingleton<NewMessagesCubit>(() => NewMessagesCubit());
|
||||
sl.registerLazySingleton<NotificationsCubit>(() => NotificationsCubit(wsManager: sl(), notificationsRepository: sl()));
|
||||
sl.registerLazySingleton<ProfileAvatarCubit>(() => ProfileAvatarCubit(profileRepository: sl()));
|
||||
sl.registerLazySingleton<ChatsCubit>(() => ChatsCubit(chatRepository: sl(), wsManager: sl(), profileRepository: sl()));
|
||||
|
||||
// Cubit factories
|
||||
sl.registerFactory<FeedCubit>(() => FeedCubit(feedRepository: sl()));
|
||||
sl.registerFactory<RegistrationCubit>(() => RegistrationCubit(registrationRepository: sl()));
|
||||
sl.registerFactory<ChatRoomCubit>(() => ChatRoomCubit(cardsRepository: sl(), chatRepository: sl(), wsManager: sl()));
|
||||
sl.registerFactory<RecipesCubit>(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl(), aboutRecipeRepository: sl()));
|
||||
sl.registerFactory<CalendarCubit>(() => CalendarCubit(calendarRepository: sl()));
|
||||
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<PlansCubit>(() => PlansCubit(feedRepository: sl(), planRepository: sl()));
|
||||
sl.registerFactory<SettingsCubit>(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl(), consultantRepository: sl()));
|
||||
sl.registerFactory<RegisterConsultantCubit>(() => RegisterConsultantCubit(consultantRepository: sl()));
|
||||
sl.registerFactory<DiaryCubit>(() => DiaryCubit(
|
||||
chatRepository: sl(),
|
||||
diaryRepository: sl(),
|
||||
glossaryRepository: sl(),
|
||||
profileRepository: sl(),
|
||||
dietRepository: sl(),
|
||||
settingsRepository: sl(),
|
||||
));
|
||||
sl.registerFactory<AboutDietCubit>(() => AboutDietCubit(dietsRepository: sl(), profileRepository: sl()));
|
||||
sl.registerFactory<BlogPostCreateCubit>(() => BlogPostCreateCubit(blogPostRepository: sl()));
|
||||
sl.registerFactory<SubCubit>(() => SubCubit(profileRepository: sl()));
|
||||
sl.registerFactory<CreateDietCubit>(() => CreateDietCubit(profileRepository: sl(), createDietRepository: sl(), glossaryRepository: sl()));
|
||||
sl.registerFactory<CreatePlanCubit>(() => CreatePlanCubit(createPlanRepository: sl()));
|
||||
sl.registerFactory<FeedProfileCubit>(() => FeedProfileCubit(profileRepository: sl()));
|
||||
sl.registerFactory<AboutPlanCubit>(() => AboutPlanCubit(planRepository: sl()));
|
||||
sl.registerFactory<CreateRecipeCubit>(() => CreateRecipeCubit(recipeRepository: sl(), glossaryRepository: sl()));
|
||||
sl.registerFactory<MyDietsCubit>(() => MyDietsCubit(myDietsRepository: sl(), createDietRepository: sl()));
|
||||
sl.registerFactory<MyPlansCubit>(() => MyPlansCubit(myPlansRepository: sl()));
|
||||
sl.registerFactory<UserDetailsCubit>(() => UserDetailsCubit(profileRepository: sl()));
|
||||
sl.registerFactory<ConsultantsCubit>(() => ConsultantsCubit(feedRepository: sl()));
|
||||
sl.registerFactory<AboutRecipeCubit>(() => AboutRecipeCubit(recipeRepository: sl(), profileRepository: sl()));
|
||||
sl.registerFactory<AuthCubit>(() => AuthCubit(authRepository: sl()));
|
||||
sl.registerFactory<ComplainingCubit>(() => ComplainingCubit(repository: sl()));
|
||||
sl.registerFactory<UserCardsCubit>(() => UserCardsCubit(cardsRepository: sl()));
|
||||
sl.registerFactory<OrderInfoCubit>(() => OrderInfoCubit(chatRepository: sl()));
|
||||
sl.registerFactory<ConsultationsCubit>(() => ConsultationsCubit(consultationsRepository: sl()));
|
||||
sl.registerFactory<ConsultantCommentsCubit>(() => ConsultantCommentsCubit(diaryRepository: sl()));
|
||||
sl.registerFactory<CancelPlanCubit>(() => CancelPlanCubit(cancelPlanRepository: sl()));
|
||||
sl.registerFactory<VerifyEmailCubit>(() => VerifyEmailCubit(profileRepository: sl()));
|
||||
sl.registerFactory<MyRecipesCubit>(() => MyRecipesCubit(myRecipesRepository: sl(), aboutRecipeRepository: sl()));
|
||||
sl.registerFactory<ConsultationsArchiveCubit>(() => ConsultationsArchiveCubit(consultationsRepository: sl()));
|
||||
sl.registerFactory<ConsultantFinancesCubit>(() => ConsultantFinancesCubit(consultationsRepository: sl()));
|
||||
sl.registerFactory<UserPurchasesCubit>(() => UserPurchasesCubit(consultationsRepository: sl()));
|
||||
sl.registerFactory<ResetPasswordCubit>(() => ResetPasswordCubit(resetPasswordRepository: sl()));
|
||||
sl.registerFactory<BlogPostCubit>(() => BlogPostCubit(sl()));
|
||||
sl.registerFactory<BlockedUsersCubit>(() => BlockedUsersCubit(blockedUsersRepository: sl()));
|
||||
|
||||
// Repository
|
||||
sl.registerLazySingleton<ChatRepository>(() => ChatRepository(chatService: sl()));
|
||||
sl.registerLazySingleton<FeedRepository>(() => FeedRepository(feedService: sl()));
|
||||
sl.registerLazySingleton<RegistrationRepository>(() => RegistrationRepository(registrationService: sl()));
|
||||
sl.registerLazySingleton<CalendarRepository>(() => CalendarRepository(calendarService: sl()));
|
||||
sl.registerLazySingleton<ProfileRepository>(() => ProfileRepository(profileService: sl()));
|
||||
sl.registerLazySingleton<RecipeRepository>(() => RecipeRepository(recipeService: sl()));
|
||||
sl.registerLazySingleton<SettingsRepository>(() => SettingsRepository(settingsService: sl()));
|
||||
sl.registerLazySingleton<ConsultantRepository>(() => ConsultantRepository(consultantService: sl()));
|
||||
sl.registerLazySingleton<DiaryRepository>(() => DiaryRepository(diaryService: sl()));
|
||||
sl.registerLazySingleton<GlossaryRepository>(() => GlossaryRepository(glossaryService: sl()));
|
||||
sl.registerLazySingleton<DietRepository>(() => DietRepository(dietsService: sl()));
|
||||
sl.registerLazySingleton<BlogPostRepository>(() => BlogPostRepository(blogPostService: sl()));
|
||||
sl.registerLazySingleton<CreateDietRepository>(() => CreateDietRepository(createDietService: sl()));
|
||||
sl.registerLazySingleton<PlansRepository>(() => PlansRepository(plansService: sl()));
|
||||
sl.registerLazySingleton<PlanRepository>(() => PlanRepository(planService: sl()));
|
||||
sl.registerLazySingleton<MyDietsRepository>(() => MyDietsRepository(myDietsService: sl()));
|
||||
sl.registerLazySingleton<MyPlansRepository>(() => MyPlansRepository(myPlansService: sl()));
|
||||
sl.registerLazySingleton<AboutRecipeRepository>(() => AboutRecipeRepository(recipeService: sl()));
|
||||
sl.registerLazySingleton<ComplainingRepository>(() => ComplainingRepository(service: sl()));
|
||||
sl.registerLazySingleton<AuthRepository>(() => AuthRepository(authService: sl()));
|
||||
sl.registerLazySingleton<CardsRepository>(() => CardsRepository(cardsService: sl()));
|
||||
sl.registerLazySingleton<ConsultationsRepository>(() => ConsultationsRepository(consultationService: sl()));
|
||||
sl.registerLazySingleton<CancelPlanRepository>(() => CancelPlanRepository(cancelService: sl()));
|
||||
sl.registerLazySingleton<CreatePlanRepository>(() => CreatePlanRepository(createPlanService: sl()));
|
||||
sl.registerLazySingleton<NotificationsRepository>(() => NotificationsRepository(notificationsService: sl()));
|
||||
sl.registerFactory<MyRecipesRepository>(() => MyRecipesRepository(myRecipesService: sl()));
|
||||
sl.registerFactory<ResetPasswordRepository>(() => ResetPasswordRepository(resetPasswordService: sl()));
|
||||
sl.registerLazySingleton<BlockedUsersRepository>(() => BlockedUsersRepository(blockedUserService: sl()));
|
||||
sl.registerLazySingleton<CommentsRepository>(() => CommentsRepository(service: sl()));
|
||||
|
||||
// Services
|
||||
sl.registerLazySingleton<ChatService>(() => ChatService(sl()));
|
||||
sl.registerLazySingleton<FeedService>(() => FeedService(sl()));
|
||||
sl.registerLazySingleton<RegistrationService>(() => RegistrationService(sl()));
|
||||
sl.registerLazySingleton<CalendarService>(() => CalendarService(sl()));
|
||||
sl.registerLazySingleton<ProfileService>(() => ProfileService(sl()));
|
||||
sl.registerLazySingleton<CreateRecipeService>(() => CreateRecipeService(sl()));
|
||||
sl.registerLazySingleton<SettingsService>(() => SettingsService(sl()));
|
||||
sl.registerLazySingleton<ConsultantService>(() => ConsultantService(sl()));
|
||||
sl.registerLazySingleton<DiaryService>(() => DiaryService(sl()));
|
||||
sl.registerLazySingleton<GlossaryService>(() => GlossaryService(sl()));
|
||||
sl.registerLazySingleton<DietService>(() => DietService(sl()));
|
||||
sl.registerLazySingleton<BlogPostService>(() => BlogPostService(sl()));
|
||||
sl.registerLazySingleton<CreateDietService>(() => CreateDietService(sl()));
|
||||
sl.registerLazySingleton<PlansService>(() => PlansService(sl()));
|
||||
sl.registerLazySingleton<PlanService>(() => PlanService(sl()));
|
||||
sl.registerLazySingleton<MyDietsService>(() => MyDietsService(sl()));
|
||||
sl.registerLazySingleton<MyPlansService>(() => MyPlansService(sl()));
|
||||
sl.registerLazySingleton<WsService>(() => WsService(sl()));
|
||||
sl.registerLazySingleton<AboutRecipeService>(() => AboutRecipeService(sl()));
|
||||
sl.registerLazySingleton<ComplainingService>(() => ComplainingService(sl()));
|
||||
sl.registerLazySingleton<AuthService>(() => AuthService(sl()));
|
||||
sl.registerLazySingleton<CardsService>(() => CardsService(sl()));
|
||||
sl.registerLazySingleton<ConsultationService>(() => ConsultationService(sl()));
|
||||
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()));
|
||||
|
||||
// External
|
||||
sl.registerLazySingleton(
|
||||
() => Dio(
|
||||
BaseOptions(
|
||||
baseUrl: baseUrl,
|
||||
headers: {
|
||||
'accept': 'application/json',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
),
|
||||
)..interceptors.addAll(
|
||||
[
|
||||
TokenInterceptor(),
|
||||
PhoneVerifyErrorInterceptor(),
|
||||
],
|
||||
),
|
||||
);
|
||||
sl.registerLazySingleton(() async => const FlutterSecureStorage());
|
||||
}
|
||||
}
|
||||
37
lib/core/routing/auth_routes.dart
Normal file
37
lib/core/routing/auth_routes.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
import 'package:eatvio_app/core/routing/route_constants.dart';
|
||||
import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart';
|
||||
import 'package:eatvio_app/features/auth/presentation/pages/forget_password_screen.dart';
|
||||
import 'package:eatvio_app/features/auth/presentation/pages/scan_qr_code_screen.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/pages/registration_screen.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/pages/verify_screen.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/pages/verify_email_screen.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
/// Маршруты для аутентификации
|
||||
class AuthRoutes {
|
||||
/// Создание маршрутов аутентификации
|
||||
static List<RouteBase> createRoutes() {
|
||||
return [
|
||||
GoRoute(
|
||||
path: RouteConstants.authScreen,
|
||||
name: 'authScreen',
|
||||
builder: (context, state) => const AuthScreen(),
|
||||
routes: [
|
||||
GoRoute(path: RouteConstants.registrationScreen, name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()),
|
||||
GoRoute(
|
||||
path: RouteConstants.forgetPasswordScreen,
|
||||
name: 'forgetPasswordScreen',
|
||||
builder: (context, state) => ForgetPasswordScreen(isAuth: state.extra as bool),
|
||||
),
|
||||
GoRoute(path: RouteConstants.scanQRCodeScreen, name: 'scanQRCodeScreen', builder: (context, state) => const ScanQRCodeScreen()),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: RouteConstants.verifyScreen,
|
||||
name: 'verifyScreen',
|
||||
builder: (context, state) => VerifyScreen(passKey: state.extra as String),
|
||||
),
|
||||
GoRoute(path: RouteConstants.verifyEmailScreen, name: 'verifyEmailScreen', builder: (context, state) => const VerifyEmailScreen()),
|
||||
];
|
||||
}
|
||||
}
|
||||
73
lib/core/routing/content_routes.dart
Normal file
73
lib/core/routing/content_routes.dart
Normal file
@@ -0,0 +1,73 @@
|
||||
import 'package:eatvio_app/core/routing/route_constants.dart';
|
||||
import 'package:eatvio_app/features/blog_post/presentation/pages/blog_post_screen.dart';
|
||||
import 'package:eatvio_app/features/blog_post_create/presentation/pages/blog_post_create_screen.dart';
|
||||
import 'package:eatvio_app/features/diets/presentation/pages/about_diet_screen.dart';
|
||||
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
|
||||
import 'package:eatvio_app/features/plans/presentation/pages/about_plan_screen.dart';
|
||||
import 'package:eatvio_app/features/recipe/presentation/pages/about_recipe_screen.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
/// Маршруты для контента
|
||||
class ContentRoutes {
|
||||
/// Создание маршрутов контента
|
||||
static List<RouteBase> createRoutes() {
|
||||
return [
|
||||
GoRoute(
|
||||
path: RouteConstants.aboutPlanScreen,
|
||||
name: 'aboutPlanScreen',
|
||||
builder: (context, state) {
|
||||
final slug = state.uri.queryParameters['slug'];
|
||||
if (slug != null) {
|
||||
return AboutPlanScreen(slug: slug, navigateToComments: false, index: null);
|
||||
}
|
||||
|
||||
final data = state.extra as Map<String, dynamic>;
|
||||
return AboutPlanScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: RouteConstants.aboutRecipesScreen,
|
||||
name: 'aboutRecipesScreen',
|
||||
builder: (context, state) {
|
||||
final slug = state.uri.queryParameters['slug'];
|
||||
if (slug != 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);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: RouteConstants.aboutDietScreen,
|
||||
name: 'aboutDietScreen',
|
||||
builder: (context, state) {
|
||||
final slug = state.uri.queryParameters['slug'];
|
||||
if (slug != null) {
|
||||
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);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: RouteConstants.blogPostCreateScreen,
|
||||
name: 'blogPostCreateScreen',
|
||||
builder: (context, state) => BlogPostCreateScreen(feedBlogPostModel: state.extra != null ? state.extra as FeedBlogPostModel : null),
|
||||
),
|
||||
GoRoute(
|
||||
path: RouteConstants.blogPostScreen,
|
||||
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);
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
94
lib/core/routing/main_navigation_routes.dart
Normal file
94
lib/core/routing/main_navigation_routes.dart
Normal file
@@ -0,0 +1,94 @@
|
||||
import 'package:eatvio_app/core/routing/route_constants.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/pages/chats_screen.dart';
|
||||
import 'package:eatvio_app/features/consultations/presentation/pages/consultations_screen.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart';
|
||||
import 'package:eatvio_app/features/menu/presentation/pages/menu_screen.dart';
|
||||
import 'package:eatvio_app/features/notifications/presentation/notifications_screen.dart';
|
||||
import 'package:eatvio_app/features/profile/presentation/pages/profile_screen.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/websockets/ws_manager.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'dart:developer';
|
||||
|
||||
/// Маршруты для основной навигации
|
||||
class MainNavigationRoutes {
|
||||
/// Создание маршрутов основной навигации
|
||||
static List<RouteBase> createRoutes() {
|
||||
return [
|
||||
ShellRoute(
|
||||
builder: (context, state, child) {
|
||||
return BlocBuilder<ProfileAvatarCubit, ProfileAvatarState>(
|
||||
builder: (BuildContext context, ProfileAvatarState state) {
|
||||
if (state is ProfileAvatarStateLoaded) {
|
||||
return BottomNavigationBarWidget(profileModel: state.profile, child: child);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
);
|
||||
},
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: RouteConstants.myProfileScreen,
|
||||
name: 'myProfileScreen',
|
||||
pageBuilder: (context, state) {
|
||||
return const NoTransitionPage(child: ProfileScreen());
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: RouteConstants.feedScreen,
|
||||
name: 'feedScreen',
|
||||
pageBuilder: (context, state) => const NoTransitionPage(child: FeedScreen()),
|
||||
redirect: (context, state) async {
|
||||
try {
|
||||
await sl<WsManager>().initialize();
|
||||
} catch (e, st) {
|
||||
log(e.toString(), stackTrace: st);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: RouteConstants.diaryScreen,
|
||||
name: 'diaryScreen',
|
||||
pageBuilder: (context, state) => const NoTransitionPage(child: DiaryScreen()),
|
||||
),
|
||||
GoRoute(
|
||||
path: ConsultationsScreen.path,
|
||||
name: ConsultationsScreen.name,
|
||||
pageBuilder: (context, state) => const NoTransitionPage(child: ConsultationsScreen()),
|
||||
),
|
||||
GoRoute(
|
||||
path: RouteConstants.chatSelectScreen,
|
||||
name: 'chatSelectScreen',
|
||||
pageBuilder: (context, state) => const NoTransitionPage(child: ChatsScreen()),
|
||||
),
|
||||
GoRoute(
|
||||
path: RouteConstants.notificationsScreen,
|
||||
name: 'notificationsScreen',
|
||||
pageBuilder: (context, state) => const NoTransitionPage(child: NotificationsScreen()),
|
||||
),
|
||||
GoRoute(
|
||||
path: RouteConstants.menuScreen,
|
||||
name: 'menuScreen',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: BlocBuilder<ProfileAvatarCubit, ProfileAvatarState>(
|
||||
builder: (BuildContext context, ProfileAvatarState state) {
|
||||
if (state is ProfileAvatarStateLoaded) {
|
||||
return MenuScreen(profileModel: state.profile);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
229
lib/core/routing/other_routes.dart
Normal file
229
lib/core/routing/other_routes.dart
Normal file
@@ -0,0 +1,229 @@
|
||||
import 'package:dio/dio.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/routing/route_constants.dart';
|
||||
import 'package:eatvio_app/core/token_storage.dart';
|
||||
import 'package:eatvio_app/features/calendar/presentation/pages/calendar_screen.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/pages/chat_screen.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/pages/empty_chat_screen.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/pages/payment_screen.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart';
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/become_consultant_screen.dart';
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/stepper_become_consultant_screen.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/consultant_rating_screen.dart';
|
||||
import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart';
|
||||
import 'package:eatvio_app/features/create_diet/presentation/diet_preview_screen.dart';
|
||||
import 'package:eatvio_app/features/create_plan/presentation/create_plan_screen.dart';
|
||||
import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart';
|
||||
import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart';
|
||||
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
|
||||
import 'package:eatvio_app/features/glossary/presentation/pages/glossary_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/my_diets/presentation/my_diets_screen.dart';
|
||||
import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart';
|
||||
import 'package:eatvio_app/features/my_finance/presentation/pages/user_finance_screen.dart';
|
||||
import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart';
|
||||
import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart';
|
||||
import 'package:eatvio_app/features/profile/presentation/pages/profile_screen.dart';
|
||||
import 'package:eatvio_app/features/profile/presentation/pages/subs_screen.dart';
|
||||
import 'package:eatvio_app/features/recipe/presentation/pages/my_recipes_screen.dart';
|
||||
import 'package:eatvio_app/features/support/presentation/pages/support_screen.dart';
|
||||
import 'package:eatvio_app/features/update_screen/presentation/update_screen.dart';
|
||||
import 'package:eatvio_app/features/user_details/presentation/user_details_screen.dart';
|
||||
import 'package:eatvio_app/features/welcome/presentation/pages/welcome_screen.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultations_archive_screen.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart';
|
||||
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:version/version.dart';
|
||||
|
||||
/// Остальные маршруты приложения
|
||||
class OtherRoutes {
|
||||
/// Создание остальных маршрутов
|
||||
static List<RouteBase> createRoutes() {
|
||||
return [
|
||||
// Welcome and update screens
|
||||
GoRoute(
|
||||
path: RouteConstants.welcomeScreen,
|
||||
name: 'welcomeScreen',
|
||||
builder: (context, state) => const WelcomeScreen(),
|
||||
redirect: (context, state) async {
|
||||
try {
|
||||
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()) {
|
||||
return RouteConstants.feedScreen;
|
||||
} else {
|
||||
return RouteConstants.authScreen;
|
||||
}
|
||||
} catch (e) {
|
||||
return RouteConstants.feedScreen;
|
||||
}
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: RouteConstants.updateScreen,
|
||||
name: 'updateScreen',
|
||||
builder: (context, state) => const UpdateScreen(),
|
||||
redirect: (context, state) async {
|
||||
try {
|
||||
final versions = (await sl<Dio>().get('$baseUrl/api/app_versions')).data;
|
||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||
String version = packageInfo.version;
|
||||
Version currentVersion = Version.parse(version);
|
||||
Version latestVersion = Version.parse(versions['min_version']);
|
||||
if (latestVersion > currentVersion) {
|
||||
return null;
|
||||
} else {
|
||||
return RouteConstants.welcomeScreen;
|
||||
}
|
||||
} catch (e) {
|
||||
return RouteConstants.welcomeScreen;
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
// Archive routes
|
||||
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()),
|
||||
|
||||
// Finance routes
|
||||
GoRoute(path: RouteConstants.userFinanceScreen, name: 'userFinanceScreen', builder: (context, state) => const UserFinanceScreen()),
|
||||
GoRoute(
|
||||
path: CardSettingsScreen.path,
|
||||
name: CardSettingsScreen.name,
|
||||
builder: (context, state) {
|
||||
final chosenPlanId = state.extra as int?;
|
||||
return CardSettingsScreen(chosenPlanId: chosenPlanId);
|
||||
},
|
||||
),
|
||||
|
||||
// Content creation routes
|
||||
GoRoute(
|
||||
path: RecipeCreateScreen.path,
|
||||
name: RecipeCreateScreen.name,
|
||||
builder: (context, state) => RecipeCreateScreen(recipe: state.extra != null ? state.extra as Recipe : null),
|
||||
),
|
||||
GoRoute(
|
||||
path: CreateDietScreen.path,
|
||||
name: CreateDietScreen.name,
|
||||
builder: (context, state) {
|
||||
final diet = state.extra as Diet?;
|
||||
return CreateDietScreen(diet: diet);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: DietPreviewScreen.path,
|
||||
name: DietPreviewScreen.name,
|
||||
builder: (context, state) {
|
||||
final data = state.extra as Diet;
|
||||
return DietPreviewScreen(diet: data);
|
||||
},
|
||||
),
|
||||
GoRoute(path: RouteConstants.createPlanScreen, name: 'createPlanScreen', 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: MyRecipesScreen.path, name: MyRecipesScreen.name, builder: (context, state) => const MyRecipesScreen()),
|
||||
|
||||
// Profile routes
|
||||
GoRoute(
|
||||
path: RouteConstants.profileScreen,
|
||||
name: 'profileScreen',
|
||||
builder: (context, state) => ProfileScreen(author: state.extra as Author),
|
||||
),
|
||||
GoRoute(
|
||||
path: RouteConstants.subsScreen,
|
||||
name: 'subsScreen',
|
||||
builder: (context, state) => SubsScreen(slug: state.pathParameters['slug']!, subType: state.extra as SubType),
|
||||
),
|
||||
|
||||
// Chat routes
|
||||
GoRoute(
|
||||
path: RouteConstants.chatScreen,
|
||||
name: 'chatScreen',
|
||||
builder: (context, state) {
|
||||
final data = state.extra as Map<String, dynamic>;
|
||||
final uuid = data['uuid'] as String;
|
||||
final sendInitialMessage = data['sendInitialMessage'] as bool?;
|
||||
return ChatScreen(uuid: uuid, sendInitialMessage: sendInitialMessage);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: RouteConstants.emptyChatScreen,
|
||||
name: 'emptyChatScreen',
|
||||
builder: (context, state) => EmptyChatScreen(author: state.extra as Author),
|
||||
),
|
||||
|
||||
// Consultant routes
|
||||
GoRoute(path: RouteConstants.becomeConsultantScreen, name: 'becomeConsultantScreen', builder: (context, state) => const BecomeConsultantScreen()),
|
||||
GoRoute(
|
||||
path: RouteConstants.stepperBecomeConsultantScreen,
|
||||
name: 'stepperBecomeConsultantScreen',
|
||||
builder: (context, state) => StepperBecomeConsultantScreen(slug: state.extra == null ? null : state.extra as String),
|
||||
),
|
||||
GoRoute(path: RouteConstants.calendarScreen, name: 'calendarScreen', builder: (context, state) => const CalendarScreen()),
|
||||
GoRoute(path: RouteConstants.supportScreen, name: 'supportScreen', builder: (context, state) => const SupportScreen()),
|
||||
GoRoute(
|
||||
path: RouteConstants.photoViewScreen,
|
||||
name: 'photoViewScreen',
|
||||
builder: (context, state) => PhotoViewScreen(imageUrls: state.extra as List<String>, index: int.parse(state.pathParameters['index']!)),
|
||||
),
|
||||
|
||||
// Other routes
|
||||
GoRoute(path: RouteConstants.glossaryScreen, name: 'glossaryScreen', builder: (context, state) => const GlossaryScreen()),
|
||||
GoRoute(path: RouteConstants.blockedUsersScreen, name: 'blockedUsersScreen', builder: (context, state) => const BlockedUsersScreen()),
|
||||
GoRoute(
|
||||
path: RouteConstants.notificationsSettingsScreen,
|
||||
name: 'notificationsSettingsScreen',
|
||||
builder: (context, state) => const NotificationsSettingsScreen(),
|
||||
),
|
||||
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(
|
||||
path: InfoByOrderScreen.path,
|
||||
name: InfoByOrderScreen.name,
|
||||
builder: (context, state) {
|
||||
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);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: PaymentScreen.path,
|
||||
name: PaymentScreen.name,
|
||||
builder: (context, state) {
|
||||
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);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: UserDiaryScreen.path,
|
||||
name: UserDiaryScreen.name,
|
||||
builder: (context, state) {
|
||||
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);
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
84
lib/core/routing/route_constants.dart
Normal file
84
lib/core/routing/route_constants.dart
Normal file
@@ -0,0 +1,84 @@
|
||||
/// Константы маршрутов приложения
|
||||
class RouteConstants {
|
||||
// Auth routes
|
||||
static const String authScreen = '/auth-screen';
|
||||
static const String registrationScreen = 'registration-screen';
|
||||
static const String forgetPasswordScreen = 'forget-password-screen';
|
||||
static const String scanQRCodeScreen = 'scan-qr-code-screen';
|
||||
static const String verifyScreen = '/verify-screen';
|
||||
static const String verifyEmailScreen = '/verify-email-screen';
|
||||
|
||||
// Main navigation routes
|
||||
static const String myProfileScreen = '/my-profile-screen';
|
||||
static const String feedScreen = '/feed-screen';
|
||||
static const String diaryScreen = '/diary-screen';
|
||||
static const String consultationsScreen = '/consultations-screen';
|
||||
static const String chatSelectScreen = '/chat-select-screen';
|
||||
static const String notificationsScreen = '/notifications-screen';
|
||||
static const String menuScreen = '/menu-screen';
|
||||
|
||||
// Content routes
|
||||
static const String aboutPlanScreen = '/about-plan-screen';
|
||||
static const String aboutRecipesScreen = '/about-recipes-screen';
|
||||
static const String aboutDietScreen = '/about-diet-screen';
|
||||
static const String blogPostScreen = '/blog-post-screen';
|
||||
static const String blogPostCreateScreen = '/blog-post-create-screen';
|
||||
|
||||
// Profile routes
|
||||
static const String profileScreen = '/profile-screen';
|
||||
static const String subsScreen = '/subs-screen';
|
||||
|
||||
// Settings routes
|
||||
static const String settingsScreen = '/settings-screen';
|
||||
static const String generalInfoScreen = 'general-info-screen';
|
||||
static const String otherInfoScreen = 'other-info-screen';
|
||||
static const String physicalActivityScreen = 'physical-activity-screen';
|
||||
static const String physicalFormScreen = 'physical-form-screen';
|
||||
static const String pressureScreen = 'pressure-screen';
|
||||
static const String changePasswordScreen = 'change-password-screen';
|
||||
static const String changeEmailScreen = 'change-email-screen';
|
||||
|
||||
// Consultant routes
|
||||
static const String becomeConsultantScreen = '/become-consultant-screen';
|
||||
static const String stepperBecomeConsultantScreen = '/stepper-become-consultant-screen';
|
||||
static const String consultantRatingScreen = '/consultant-rating-screen';
|
||||
|
||||
// Other routes
|
||||
static const String welcomeScreen = '/welcome-screen';
|
||||
static const String updateScreen = '/update-screen';
|
||||
static const String calendarScreen = '/calendar-screen';
|
||||
static const String supportScreen = '/support-screen';
|
||||
static const String photoViewScreen = '/photo-view-screen';
|
||||
static const String chatScreen = '/chat-screen';
|
||||
static const String emptyChatScreen = '/empty-chat-screen';
|
||||
static const String glossaryScreen = '/glossary-screen';
|
||||
static const String blockedUsersScreen = '/blocked-users-screen';
|
||||
static const String notificationsSettingsScreen = '/notifications-settings-screen';
|
||||
|
||||
// Finance routes
|
||||
static const String userFinanceScreen = '/user-finance-screen';
|
||||
static const String cardSettingsScreen = '/card-settings-screen';
|
||||
static const String orderInfoScreen = '/order-info-screen';
|
||||
static const String paymentScreen = '/payment-screen';
|
||||
|
||||
// Content creation routes
|
||||
static const String recipeCreateScreen = '/recipe-create-screen';
|
||||
static const String createDietScreen = '/create-diet-screen';
|
||||
static const String dietPreviewScreen = '/diet-preview-screen';
|
||||
static const String createPlanScreen = '/create-plan-screen';
|
||||
static const String myDietsScreen = '/my-diets-screen';
|
||||
static const String myPlansScreen = '/my-plans-screen';
|
||||
static const String myRecipesScreen = '/my-recipes-screen';
|
||||
|
||||
// User details
|
||||
static const String userDetailsScreen = '/user-details-screen';
|
||||
static const String userDiaryScreen = '/user-diary-screen';
|
||||
|
||||
// Dev tools
|
||||
static const String devToolsScreen = '/dev-tools-screen';
|
||||
|
||||
// Archive routes
|
||||
static const String consultationsArchiveScreen = '/consultations-archive-screen';
|
||||
static const String consultantFinancesScreen = '/consultant-finances-screen';
|
||||
static const String userPurchasesScreen = '/user-purchases-screen';
|
||||
}
|
||||
62
lib/core/routing/settings_routes.dart
Normal file
62
lib/core/routing/settings_routes.dart
Normal file
@@ -0,0 +1,62 @@
|
||||
import 'package:eatvio_app/core/routing/route_constants.dart';
|
||||
import 'package:eatvio_app/features/settings/entities/settings_entity.dart';
|
||||
import 'package:eatvio_app/features/settings/presentation/pages/change_email_screen.dart';
|
||||
import 'package:eatvio_app/features/settings/presentation/pages/change_password_screen.dart';
|
||||
import 'package:eatvio_app/features/settings/presentation/pages/general_info_screen.dart';
|
||||
import 'package:eatvio_app/features/settings/presentation/pages/other_info_screen.dart';
|
||||
import 'package:eatvio_app/features/settings/presentation/pages/physical_activity_screen.dart';
|
||||
import 'package:eatvio_app/features/settings/presentation/pages/physical_form_screen.dart';
|
||||
import 'package:eatvio_app/features/settings/presentation/pages/pressure_screen.dart';
|
||||
import 'package:eatvio_app/features/settings/presentation/pages/settings_screen.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
/// Маршруты для настроек
|
||||
class SettingsRoutes {
|
||||
/// Создание маршрутов настроек
|
||||
static List<RouteBase> createRoutes() {
|
||||
return [
|
||||
GoRoute(
|
||||
path: RouteConstants.settingsScreen,
|
||||
name: 'settingsScreen',
|
||||
builder: (context, state) => const SettingsScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: RouteConstants.generalInfoScreen,
|
||||
name: 'generalInfoScreen',
|
||||
builder: (context, state) => GeneralInfoScreen(settingsEntity: state.extra as SettingsEntity),
|
||||
),
|
||||
GoRoute(
|
||||
path: RouteConstants.otherInfoScreen,
|
||||
name: 'otherInfoScreen',
|
||||
builder: (context, state) => OtherInfoScreen(settingsEntity: state.extra as SettingsEntity),
|
||||
),
|
||||
GoRoute(
|
||||
path: RouteConstants.physicalActivityScreen,
|
||||
name: 'physicalActivityScreen',
|
||||
builder: (context, state) => PhysicalActivityScreen(settingsEntity: state.extra as SettingsEntity),
|
||||
),
|
||||
GoRoute(
|
||||
path: RouteConstants.physicalFormScreen,
|
||||
name: 'physicalFormScreen',
|
||||
builder: (context, state) => PhysicalFormScreen(settingsEntity: state.extra as SettingsEntity),
|
||||
),
|
||||
GoRoute(
|
||||
path: RouteConstants.pressureScreen,
|
||||
name: 'pressureScreen',
|
||||
builder: (context, state) => PressureScreen(settingsEntity: state.extra as SettingsEntity),
|
||||
),
|
||||
GoRoute(
|
||||
path: RouteConstants.changePasswordScreen,
|
||||
name: 'changePasswordScreen',
|
||||
builder: (context, state) => ChangePasswordScreen(settingsEntity: state.extra as SettingsEntity),
|
||||
),
|
||||
GoRoute(
|
||||
path: RouteConstants.changeEmailScreen,
|
||||
name: 'changeEmailScreen',
|
||||
builder: (context, state) => ChangeEmailScreen(settingsEntity: state.extra as SettingsEntity),
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -202,6 +202,10 @@ class PrimaryColors {
|
||||
Color get darkGray => const Color(0XFFBBBCC0);
|
||||
|
||||
Color get commentBg => const Color(0XFFFFFDD8);
|
||||
|
||||
Color get cardNotificationsBG => const Color(0xFFA4A4A4);
|
||||
|
||||
Color get grey7B => const Color(0xFF7B7B7B);
|
||||
}
|
||||
|
||||
PrimaryColors get appTheme => ThemeHelper().themeColor();
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
abstract class TokenStorage {
|
||||
static Future<bool> haveTokenInStorage() async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
try {
|
||||
if (await const FlutterSecureStorage().read(key: 'token') == null) {
|
||||
if (sharedPreferences.getString('token') == null) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
@@ -13,19 +14,27 @@ abstract class TokenStorage {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<String?> getTokenFromStorage() async => await const FlutterSecureStorage().read(key: 'token');
|
||||
|
||||
static Future<void> saveTokenInStorage(String token) async {
|
||||
await const FlutterSecureStorage().write(key: 'token', value: token);
|
||||
static Future<String?> getTokenFromStorage() async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
return sharedPreferences.getString('token');
|
||||
}
|
||||
|
||||
static Future<void> removeTokenInStorage() async => (await const FlutterSecureStorage().delete(key: 'token'));
|
||||
static Future<void> saveTokenInStorage(String token) async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
await sharedPreferences.setString('token', token);
|
||||
}
|
||||
|
||||
static Future<void> removeTokenInStorage() async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
await sharedPreferences.remove('token');
|
||||
}
|
||||
}
|
||||
|
||||
abstract class SocketIdStorage {
|
||||
static Future<bool> haveSocketIdInStorage() async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
try {
|
||||
if (await const FlutterSecureStorage().read(key: 'socketId') == null) {
|
||||
if (sharedPreferences.getString('socketId') == null) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
@@ -35,11 +44,18 @@ abstract class SocketIdStorage {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<String?> getSocketIdFromStorage() async => await const FlutterSecureStorage().read(key: 'socketId');
|
||||
|
||||
static Future<void> saveSocketIdInStorage(String socketId) async {
|
||||
await const FlutterSecureStorage().write(key: 'socketId', value: socketId);
|
||||
static Future<String?> getSocketIdFromStorage() async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
return sharedPreferences.getString('socketId');
|
||||
}
|
||||
|
||||
static Future<void> removeSocketIdInStorage() async => (await const FlutterSecureStorage().delete(key: 'socketId'));
|
||||
static Future<void> saveSocketIdInStorage(String socketId) async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
await sharedPreferences.setString('socketId', socketId);
|
||||
}
|
||||
|
||||
static Future<void> removeSocketIdInStorage() async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
await sharedPreferences.remove('socketId');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,30 @@ import 'package:intl/intl.dart';
|
||||
|
||||
const String dateTimeFormatPattern = 'dd/MM/yyyy';
|
||||
|
||||
/// Преобразует дату из формата гггг-мм-дд чч:мм:сс в дд.мм.гггг
|
||||
String formatBuildDate(String dateString) {
|
||||
if (dateString.isEmpty) return '';
|
||||
|
||||
try {
|
||||
final dateTimeParts = dateString.split(' ');
|
||||
if (dateTimeParts.isNotEmpty) {
|
||||
final datePart = dateTimeParts[0];
|
||||
final dateParts = datePart.split('-');
|
||||
if (dateParts.length == 3) {
|
||||
final year = dateParts[0];
|
||||
final month = dateParts[1];
|
||||
final day = dateParts[2];
|
||||
return '$day.$month.$year';
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
return dateString;
|
||||
}
|
||||
return dateString;
|
||||
}
|
||||
|
||||
extension DateTimeExtension on DateTime {
|
||||
String format([
|
||||
String pattern = dateTimeFormatPattern,
|
||||
String? locale,
|
||||
]) {
|
||||
String format([String pattern = dateTimeFormatPattern, String? locale]) {
|
||||
final localDate = isUtc ? toLocal() : this;
|
||||
if (locale != null && locale.isNotEmpty) {
|
||||
initializeDateFormatting(locale);
|
||||
@@ -19,17 +38,13 @@ extension DateTimeExtension on DateTime {
|
||||
final localDate = isUtc ? toLocal() : this;
|
||||
final now = DateTime.now();
|
||||
final yesterday = DateTime(now.year, now.month, now.day).subtract(const Duration(days: 1));
|
||||
return localDate.year == yesterday.year &&
|
||||
localDate.month == yesterday.month &&
|
||||
localDate.day == yesterday.day;
|
||||
return localDate.year == yesterday.year && localDate.month == yesterday.month && localDate.day == yesterday.day;
|
||||
}
|
||||
|
||||
bool isToday() {
|
||||
final localDate = isUtc ? toLocal() : this;
|
||||
final now = DateTime.now();
|
||||
return now.year == localDate.year &&
|
||||
now.month == localDate.month &&
|
||||
now.day == localDate.day;
|
||||
return now.year == localDate.year && now.month == localDate.month && now.day == localDate.day;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,4 +38,16 @@ void showSuccessToast(String message) {
|
||||
textColor: appTheme.whiteA700,
|
||||
fontSize: 16.0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void showInternetConnectionToast() {
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Проверьте подключение к интернету',
|
||||
toastLength: Toast.LENGTH_LONG,
|
||||
gravity: ToastGravity.TOP,
|
||||
timeInSecForIosWeb: 3,
|
||||
backgroundColor: appTheme.red,
|
||||
textColor: Colors.white,
|
||||
fontSize: 16.0,
|
||||
);
|
||||
}
|
||||
|
||||
87
lib/core/utils/validation_utils.dart
Normal file
87
lib/core/utils/validation_utils.dart
Normal file
@@ -0,0 +1,87 @@
|
||||
import 'package:eatvio_app/core/constants/app_constants.dart';
|
||||
|
||||
/// Утилиты для валидации
|
||||
class ValidationUtils {
|
||||
/// Валидация email
|
||||
static bool isValidEmail(String email) {
|
||||
if (email.isEmpty) return false;
|
||||
final regex = RegExp(AppConstants.emailRegex);
|
||||
return regex.hasMatch(email);
|
||||
}
|
||||
|
||||
/// Валидация телефона
|
||||
static bool isValidPhone(String phone) {
|
||||
if (phone.isEmpty) return false;
|
||||
final regex = RegExp(AppConstants.phoneRegex);
|
||||
return regex.hasMatch(phone);
|
||||
}
|
||||
|
||||
/// Валидация URL
|
||||
static bool isValidUrl(String url) {
|
||||
if (url.isEmpty) return false;
|
||||
final regex = RegExp(AppConstants.urlRegex);
|
||||
return regex.hasMatch(url);
|
||||
}
|
||||
|
||||
/// Валидация длины текста
|
||||
static bool isValidTextLength(String text, {int? maxLength}) {
|
||||
final length = maxLength ?? AppConstants.maxTextLength;
|
||||
return text.length <= length;
|
||||
}
|
||||
|
||||
/// Валидация длины заголовка
|
||||
static bool isValidTitleLength(String title) {
|
||||
return title.isNotEmpty && title.length <= AppConstants.maxTitleLength;
|
||||
}
|
||||
|
||||
/// Валидация длины описания
|
||||
static bool isValidDescriptionLength(String description) {
|
||||
return description.length <= AppConstants.maxDescriptionLength;
|
||||
}
|
||||
|
||||
/// Валидация пароля
|
||||
static bool isValidPassword(String password) {
|
||||
// Минимум 8 символов, хотя бы одна буква и одна цифра
|
||||
if (password.length < 8) return false;
|
||||
final hasLetter = RegExp(r'[a-zA-Z]').hasMatch(password);
|
||||
final hasDigit = RegExp(r'[0-9]').hasMatch(password);
|
||||
return hasLetter && hasDigit;
|
||||
}
|
||||
|
||||
/// Валидация имени пользователя
|
||||
static bool isValidUsername(String username) {
|
||||
// От 3 до 20 символов, только буквы, цифры и подчеркивания
|
||||
if (username.length < 3 || username.length > 20) return false;
|
||||
final regex = RegExp(r'^[a-zA-Z0-9_]+$');
|
||||
return regex.hasMatch(username);
|
||||
}
|
||||
|
||||
/// Получение сообщения об ошибке валидации
|
||||
static String? getValidationErrorMessage(String? value, ValidationType type) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Поле не может быть пустым';
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case ValidationType.email:
|
||||
return isValidEmail(value) ? null : 'Введите корректный email';
|
||||
case ValidationType.phone:
|
||||
return isValidPhone(value) ? null : 'Введите корректный номер телефона';
|
||||
case ValidationType.url:
|
||||
return isValidUrl(value) ? null : 'Введите корректный URL';
|
||||
case ValidationType.password:
|
||||
return isValidPassword(value) ? null : 'Пароль должен содержать минимум 8 символов, включая буквы и цифры';
|
||||
case ValidationType.username:
|
||||
return isValidUsername(value) ? null : 'Имя пользователя должно содержать от 3 до 20 символов (буквы, цифры, подчеркивания)';
|
||||
case ValidationType.title:
|
||||
return isValidTitleLength(value) ? null : 'Заголовок не может быть пустым и должен содержать не более ${AppConstants.maxTitleLength} символов';
|
||||
case ValidationType.description:
|
||||
return isValidDescriptionLength(value) ? null : 'Описание не может содержать более ${AppConstants.maxDescriptionLength} символов';
|
||||
case ValidationType.text:
|
||||
return isValidTextLength(value) ? null : 'Текст не может содержать более ${AppConstants.maxTextLength} символов';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Типы валидации
|
||||
enum ValidationType { email, phone, url, password, username, title, description, text }
|
||||
128
lib/core/views/cubit/views_cubit.dart
Normal file
128
lib/core/views/cubit/views_cubit.dart
Normal file
@@ -0,0 +1,128 @@
|
||||
import 'dart:async';
|
||||
import 'package:eatvio_app/core/views/cubit/views_state.dart';
|
||||
import 'package:eatvio_app/core/views/repository/views_repository.dart';
|
||||
import 'package:eatvio_app/features/feed/models/view_model.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class ViewsCubit extends Cubit<ViewsCubitState> {
|
||||
final ViewsRepository viewsRepository;
|
||||
ViewsCubit({required this.viewsRepository}) : super(ViewsCubitState()) {
|
||||
_listener = AppLifecycleListener(onPause: _sendViews, onInactive: _sendViews);
|
||||
}
|
||||
|
||||
late final AppLifecycleListener _listener;
|
||||
|
||||
final Set<int> postsIds = <int>{};
|
||||
final Set<int> plansIds = <int>{};
|
||||
final Set<int> dietsIds = <int>{};
|
||||
final Set<int> recipesIds = <int>{};
|
||||
|
||||
final Set<int> _sentPosts = <int>{};
|
||||
final Set<int> _sentPlans = <int>{};
|
||||
final Set<int> _sentDiets = <int>{};
|
||||
final Set<int> _sentRecipes = <int>{};
|
||||
|
||||
final Map<ViewableContentType, Map<int, Timer>> _visibilityTimers = {
|
||||
ViewableContentType.posts: <int, Timer>{},
|
||||
ViewableContentType.plans: <int, Timer>{},
|
||||
ViewableContentType.diets: <int, Timer>{},
|
||||
ViewableContentType.recipes: <int, Timer>{},
|
||||
};
|
||||
|
||||
Timer? _debounceTimer;
|
||||
|
||||
Set<int> _idsForType(ViewableContentType type) {
|
||||
switch (type) {
|
||||
case ViewableContentType.posts:
|
||||
return postsIds;
|
||||
case ViewableContentType.plans:
|
||||
return plansIds;
|
||||
case ViewableContentType.diets:
|
||||
return dietsIds;
|
||||
case ViewableContentType.recipes:
|
||||
return recipesIds;
|
||||
}
|
||||
}
|
||||
|
||||
Set<int> _sentIdsForType(ViewableContentType type) {
|
||||
switch (type) {
|
||||
case ViewableContentType.posts:
|
||||
return _sentPosts;
|
||||
case ViewableContentType.plans:
|
||||
return _sentPlans;
|
||||
case ViewableContentType.diets:
|
||||
return _sentDiets;
|
||||
case ViewableContentType.recipes:
|
||||
return _sentRecipes;
|
||||
}
|
||||
}
|
||||
|
||||
void handleVisibility({required double visibleFraction, required int id, required ViewableContentType type}) {
|
||||
final timers = _visibilityTimers[type]!;
|
||||
final ids = _idsForType(type);
|
||||
final sent = _sentIdsForType(type);
|
||||
|
||||
if (sent.contains(id)) {
|
||||
timers[id]?.cancel();
|
||||
timers.remove(id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ids.contains(id)) return;
|
||||
|
||||
if (visibleFraction >= 0.5) {
|
||||
if (timers.containsKey(id)) return;
|
||||
timers[id] = Timer(const Duration(seconds: 1), () {
|
||||
ids.add(id);
|
||||
timers.remove(id);
|
||||
_scheduleDebouncedSend();
|
||||
});
|
||||
} else {
|
||||
timers[id]?.cancel();
|
||||
timers.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _viewContent({required ViewableContentType type, required List<int> ids}) async {
|
||||
await viewsRepository.viewContent(
|
||||
viewModel: ViewModel(type: type.getStringifiedVersion(), ids: ids),
|
||||
);
|
||||
}
|
||||
|
||||
void _scheduleDebouncedSend() {
|
||||
_debounceTimer?.cancel();
|
||||
_debounceTimer = Timer(const Duration(seconds: 4), _sendViews);
|
||||
}
|
||||
|
||||
void _send(Set<int> ids, Set<int> sent, ViewableContentType type) {
|
||||
final newIds = ids.difference(sent);
|
||||
if (newIds.isNotEmpty) {
|
||||
_viewContent(type: type, ids: newIds.toList());
|
||||
sent.addAll(newIds);
|
||||
ids.removeAll(newIds);
|
||||
}
|
||||
}
|
||||
|
||||
void _sendViews() {
|
||||
_send(postsIds, _sentPosts, ViewableContentType.posts);
|
||||
_send(plansIds, _sentPlans, ViewableContentType.plans);
|
||||
_send(dietsIds, _sentDiets, ViewableContentType.diets);
|
||||
_send(recipesIds, _sentRecipes, ViewableContentType.recipes);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
for (final map in _visibilityTimers.values) {
|
||||
for (final t in map.values) {
|
||||
t.cancel();
|
||||
}
|
||||
map.clear();
|
||||
}
|
||||
_debounceTimer?.cancel();
|
||||
_listener.dispose();
|
||||
_sendViews();
|
||||
await super.close();
|
||||
}
|
||||
}
|
||||
3
lib/core/views/cubit/views_state.dart
Normal file
3
lib/core/views/cubit/views_state.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
class ViewsCubitState {
|
||||
ViewsCubitState();
|
||||
}
|
||||
16
lib/core/views/repository/views_repository.dart
Normal file
16
lib/core/views/repository/views_repository.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'package:eatvio_app/core/views/service/views_service.dart';
|
||||
import 'package:eatvio_app/features/feed/models/view_model.dart';
|
||||
|
||||
class ViewsRepository {
|
||||
final ViewsService viewsService;
|
||||
|
||||
ViewsRepository({required this.viewsService});
|
||||
|
||||
Future<void> viewContent({required ViewModel viewModel}) async {
|
||||
try {
|
||||
await viewsService.viewContent(viewModel: viewModel);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
15
lib/core/views/service/views_service.dart
Normal file
15
lib/core/views/service/views_service.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/features/feed/models/view_model.dart';
|
||||
import 'package:retrofit/retrofit.dart';
|
||||
|
||||
part 'views_service.g.dart';
|
||||
|
||||
@RestApi()
|
||||
abstract class ViewsService {
|
||||
factory ViewsService(Dio dio, {String baseUrl}) = _ViewsService;
|
||||
|
||||
@POST('/api/views')
|
||||
Future<void> viewContent({
|
||||
@Body() required ViewModel viewModel,
|
||||
});
|
||||
}
|
||||
@@ -2,7 +2,8 @@ import 'package:eatvio_app/core/data/selectionPopupModel/selection_popup_model.d
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CustomDropDown extends StatelessWidget {
|
||||
const CustomDropDown({super.key,
|
||||
const CustomDropDown({
|
||||
super.key,
|
||||
this.alignment,
|
||||
this.width,
|
||||
this.focusNode,
|
||||
@@ -48,12 +49,7 @@ class CustomDropDown extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return alignment != null
|
||||
? Align(
|
||||
alignment: alignment ?? Alignment.center,
|
||||
child: _buildDropDownWidget(context),
|
||||
)
|
||||
: _buildDropDownWidget(context);
|
||||
return alignment != null ? Align(alignment: alignment ?? Alignment.center, child: _buildDropDownWidget(context)) : _buildDropDownWidget(context);
|
||||
}
|
||||
|
||||
DropdownButtonFormField<SelectionPopupModel> _buildDropDownWidget(BuildContext context) {
|
||||
@@ -61,7 +57,7 @@ class CustomDropDown extends StatelessWidget {
|
||||
focusNode: focusNode ?? FocusNode(),
|
||||
icon: const Icon(Icons.keyboard_arrow_down_rounded),
|
||||
autofocus: autofocus!,
|
||||
value: value,
|
||||
initialValue: value,
|
||||
isDense: true,
|
||||
isExpanded: true,
|
||||
items: items?.map((SelectionPopupModel item) {
|
||||
@@ -69,12 +65,7 @@ class CustomDropDown extends StatelessWidget {
|
||||
value: item,
|
||||
child: Text(
|
||||
item.title,
|
||||
style: hintStyle ??
|
||||
const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.black,
|
||||
),
|
||||
style: hintStyle ?? const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
@@ -90,19 +81,9 @@ class CustomDropDown extends StatelessWidget {
|
||||
|
||||
InputDecoration _buildInputDecoration(BuildContext context) {
|
||||
return InputDecoration(
|
||||
labelStyle: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
labelStyle: const TextStyle(fontSize: 16, color: Colors.black, fontWeight: FontWeight.w400),
|
||||
hintText: hintText ?? "",
|
||||
hintStyle: hintStyle ??
|
||||
TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.grey.shade400,
|
||||
height: 1,
|
||||
),
|
||||
hintStyle: hintStyle ?? TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.grey.shade400, height: 1),
|
||||
prefixIcon: prefix,
|
||||
prefixIconConstraints: prefixConstraints,
|
||||
suffixIcon: suffix,
|
||||
@@ -111,18 +92,9 @@ class CustomDropDown extends StatelessWidget {
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
|
||||
fillColor: const Color(0xFFF6F6F6),
|
||||
filled: filled,
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(20)),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
enabledBorder: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(20)),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(20)),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
border: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(20)), borderSide: BorderSide.none),
|
||||
enabledBorder: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(20)), borderSide: BorderSide.none),
|
||||
focusedBorder: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(20)), borderSide: BorderSide.none),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
class CustomDropDownButton<T> extends StatelessWidget {
|
||||
const CustomDropDownButton({
|
||||
super.key,
|
||||
required this.items,
|
||||
required this.onChanged,
|
||||
this.value,
|
||||
this.hint,
|
||||
this.hasError = false,
|
||||
});
|
||||
const CustomDropDownButton({super.key, required this.items, required this.onChanged, this.value, this.hint, this.hasError = false});
|
||||
final T? value;
|
||||
final List<DropdownMenuItem<T>> items;
|
||||
final void Function(T?)? onChanged;
|
||||
@@ -20,11 +13,7 @@ class CustomDropDownButton<T> extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return DropdownButtonFormField(
|
||||
hint: hint,
|
||||
icon: SvgPicture.asset(
|
||||
'assets/icons/arrow_down2.svg',
|
||||
width: 12,
|
||||
height: 8,
|
||||
),
|
||||
icon: SvgPicture.asset('assets/icons/arrow_down2.svg', width: 12, height: 8),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
dropdownColor: appTheme.lightGray0,
|
||||
decoration: InputDecoration(
|
||||
@@ -43,7 +32,7 @@ class CustomDropDownButton<T> extends StatelessWidget {
|
||||
fillColor: appTheme.lightGray0,
|
||||
filled: true,
|
||||
),
|
||||
value: value,
|
||||
initialValue: value,
|
||||
items: items,
|
||||
onChanged: onChanged,
|
||||
);
|
||||
|
||||
51
lib/features/auth/entities/auth_result_entity.dart
Normal file
51
lib/features/auth/entities/auth_result_entity.dart
Normal file
@@ -0,0 +1,51 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:vk_id/vk_id.dart';
|
||||
|
||||
part 'auth_result_entity.freezed.dart';
|
||||
|
||||
@freezed
|
||||
abstract class AuthResultEntity with _$AuthResultEntity {
|
||||
const factory AuthResultEntity({
|
||||
required String accessToken,
|
||||
required String userId,
|
||||
String? refreshToken,
|
||||
String? firstName,
|
||||
String? lastName,
|
||||
String? email,
|
||||
String? phone,
|
||||
GenderEnum? gender,
|
||||
String? birthDate,
|
||||
}) = _AuthResultEntity;
|
||||
}
|
||||
|
||||
enum GenderEnum {
|
||||
male,
|
||||
female,
|
||||
notSet;
|
||||
|
||||
static GenderEnum? fromVkGender(VkSex? vkGender) {
|
||||
switch (vkGender) {
|
||||
case VkSex.male:
|
||||
return GenderEnum.male;
|
||||
case VkSex.female:
|
||||
return GenderEnum.female;
|
||||
case VkSex.notStated:
|
||||
return GenderEnum.notSet;
|
||||
case null:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static GenderEnum? fromString(String? gender) {
|
||||
switch (gender) {
|
||||
case 'male':
|
||||
return GenderEnum.male;
|
||||
case 'female':
|
||||
return GenderEnum.female;
|
||||
case 'notSet':
|
||||
return GenderEnum.notSet;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
12
lib/features/auth/models/social_exchange_request.dart
Normal file
12
lib/features/auth/models/social_exchange_request.dart
Normal file
@@ -0,0 +1,12 @@
|
||||
class SocialExchangeRequest {
|
||||
final String provider;
|
||||
final String accessToken;
|
||||
|
||||
SocialExchangeRequest({required this.provider, required this.accessToken});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'provider': provider, 'access_token': accessToken};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
30
lib/features/auth/models/yandex_profile_model.dart
Normal file
30
lib/features/auth/models/yandex_profile_model.dart
Normal file
@@ -0,0 +1,30 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'yandex_profile_model.freezed.dart';
|
||||
part 'yandex_profile_model.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class YandexProfileModel with _$YandexProfileModel {
|
||||
factory YandexProfileModel({
|
||||
required String id,
|
||||
required String login,
|
||||
required String birthday,
|
||||
@JsonKey(name: 'client_id') required String clientId,
|
||||
@JsonKey(name: 'display_name') required String displayName,
|
||||
@JsonKey(name: 'real_name') required String realName,
|
||||
@JsonKey(name: 'first_name') required String firstName,
|
||||
@JsonKey(name: 'last_name') required String lastName,
|
||||
@JsonKey(name: 'sex') required String sex,
|
||||
@JsonKey(name: 'default_email') required String defaultEmail,
|
||||
@JsonKey(name: 'default_phone') required YandexPhoneData defaultPhone,
|
||||
}) = _YandexProfileModel;
|
||||
|
||||
factory YandexProfileModel.fromJson(Map<String, dynamic> json) => _$YandexProfileModelFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class YandexPhoneData with _$YandexPhoneData {
|
||||
factory YandexPhoneData({required int id, required String number}) = _YandexPhoneData;
|
||||
|
||||
factory YandexPhoneData.fromJson(Map<String, dynamic> json) => _$YandexPhoneDataFromJson(json);
|
||||
}
|
||||
@@ -6,28 +6,17 @@ import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state
|
||||
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
|
||||
class AuthCubit extends Cubit<AuthState> {
|
||||
final AuthRepository authRepository;
|
||||
|
||||
AuthCubit({
|
||||
required this.authRepository,
|
||||
}) : super(const AuthState());
|
||||
AuthCubit({required this.authRepository}) : super(const AuthState());
|
||||
|
||||
Future<bool> getTokenByCredentials(String email, String password) async {
|
||||
emit(const AuthState.loading());
|
||||
try {
|
||||
final token = (await authRepository.getTokenByCredentials(
|
||||
email,
|
||||
password,
|
||||
))
|
||||
.token;
|
||||
emit(
|
||||
AuthState.loaded(
|
||||
token: token,
|
||||
),
|
||||
);
|
||||
await TokenStorage.saveTokenInStorage(token);
|
||||
final token = (await authRepository.getTokenByCredentials(email, password)).token;
|
||||
emit(AuthState.loaded(token: token));
|
||||
await TokenStorage.saveTokenInStorage(token);
|
||||
return true;
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
@@ -36,4 +25,19 @@ class AuthCubit extends Cubit<AuthState> {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> getTokenBySocial({required String provider, required String accessToken}) async {
|
||||
emit(const AuthState.loading());
|
||||
try {
|
||||
final token = (await authRepository.getTokenBySocial(provider: provider, accessToken: accessToken)).token;
|
||||
emit(AuthState.loaded(token: token));
|
||||
await TokenStorage.saveTokenInStorage(token);
|
||||
return true;
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
showErrorToast('Не удалось войти через $provider');
|
||||
emit(AuthState.error(errorMessage: '$e'));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
// ignore_for_file: unused_element, duplicate_ignore
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/firebase_api.dart';
|
||||
@@ -7,6 +10,7 @@ import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/utils/image_constant.dart';
|
||||
import 'package:eatvio_app/core/utils/toasts.dart';
|
||||
import 'package:eatvio_app/core/websockets/ws_manager.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/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart';
|
||||
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state.dart';
|
||||
@@ -24,6 +28,7 @@ import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:eatvio_app/features/auth/sdk/social_auth_facade.dart';
|
||||
|
||||
class AuthScreen extends StatefulWidget {
|
||||
const AuthScreen({super.key});
|
||||
@@ -47,12 +52,20 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
super.initState();
|
||||
var keyboardVisibilityController = KeyboardVisibilityController();
|
||||
keyboardSubscription = keyboardVisibilityController.onChange.listen((bool visible) {
|
||||
setState(() {
|
||||
isFocused = visible;
|
||||
});
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isFocused = visible;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
keyboardSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _reconnectWebSocket(BuildContext context) async {
|
||||
try {
|
||||
await sl<WsManager>().clear();
|
||||
@@ -108,6 +121,42 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Social auth buttons at the top (outlined)
|
||||
const Text('Авторизация через:', style: TextStyle(fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_SocialButton(
|
||||
onTap: () => _onVkPressed(),
|
||||
backgroundColor: const Color(0xFF2787F5),
|
||||
tooltip: 'VK ID',
|
||||
child: SvgPicture.asset('assets/icons/vk_logo.svg', height: 24, width: 24),
|
||||
),
|
||||
_SocialButton(
|
||||
onTap: _onOkPressed,
|
||||
backgroundColor: const Color(0xFFF58220),
|
||||
tooltip: 'Одноклассники',
|
||||
child: SvgPicture.asset('assets/icons/ok_logo.svg', height: 24, width: 24),
|
||||
),
|
||||
_SocialButton(
|
||||
onTap: _onMailRuPressed,
|
||||
backgroundColor: const Color(0xFF005FF9),
|
||||
tooltip: 'Mail.ru',
|
||||
child: SvgPicture.asset('assets/icons/mail_logo.svg', height: 24, width: 24),
|
||||
),
|
||||
_SocialButton(
|
||||
onTap: _onYandexPressed,
|
||||
backgroundColor: const Color(0xFFFFCC00),
|
||||
tooltip: 'Яндекс ID',
|
||||
child: SvgPicture.asset('assets/icons/yandex_logo.svg', height: 24, width: 24),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const CustomDivider(),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: 16),
|
||||
Text(s.email),
|
||||
const SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
@@ -309,3 +358,88 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SocialButton extends StatelessWidget {
|
||||
const _SocialButton({required this.onTap, required this.child, required this.backgroundColor, required this.tooltip});
|
||||
|
||||
final VoidCallback onTap;
|
||||
final Widget child;
|
||||
final Color backgroundColor;
|
||||
final String tooltip;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Tooltip(
|
||||
message: tooltip,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
onTap: onTap,
|
||||
child: Ink(
|
||||
height: 48,
|
||||
width: (MediaQuery.of(context).size.width - 16 * 2 - 12 * 3) / 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: const Color(0xFFDBDBDB), width: 1),
|
||||
),
|
||||
child: Center(child: child),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onVkPressed() async {
|
||||
try {
|
||||
final accessToken = await SocialAuthFacade.signInWithVk();
|
||||
if (accessToken == null) {
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
void _onOkPressed() async {
|
||||
try {
|
||||
final accessToken = await SocialAuthFacade.signInWithOk();
|
||||
if (accessToken == null) {
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
void _onMailRuPressed() async {
|
||||
try {
|
||||
final accessToken = await SocialAuthFacade.signInWithMailRu();
|
||||
if (accessToken == null) {
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
void _onYandexPressed() async {
|
||||
final accessToken = await SocialAuthFacade.signInWithYandex();
|
||||
if (accessToken == null) return;
|
||||
}
|
||||
|
||||
Future<void> _reconnectAfterAuth() async {
|
||||
final context = AppRouter.navigatorKey.currentContext;
|
||||
if (context == null) return;
|
||||
try {
|
||||
await sl<WsManager>().clear();
|
||||
await sl<WsManager>().initialize();
|
||||
await sl<ProfileAvatarCubit>().getMyProfile();
|
||||
await sl<ChatsCubit>().getChatRoomsWithLoading();
|
||||
await sl<ChatsCubit>().connectToWebSocket();
|
||||
await sl<NotificationsCubit>().getNotifications();
|
||||
await sl<NotificationsCubit>().connectToWebSocket();
|
||||
await sl<FirebaseApi>().registerPushToken();
|
||||
showSuccessToast('Успешный вход');
|
||||
} catch (_) {}
|
||||
AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:io';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/firebase_api.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/token_storage.dart';
|
||||
import 'package:eatvio_app/core/utils/toasts.dart';
|
||||
@@ -92,6 +93,7 @@ class _ScanQRCodeScreenState extends State<ScanQRCodeScreen> {
|
||||
await sl<ChatsCubit>().connectToWebSocket();
|
||||
await sl<NotificationsCubit>().getNotifications();
|
||||
await sl<NotificationsCubit>().connectToWebSocket();
|
||||
await sl<FirebaseApi>().registerPushToken();
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:eatvio_app/features/auth/models/credentials_model.dart';
|
||||
import 'package:eatvio_app/features/auth/models/token_model.dart';
|
||||
import 'package:eatvio_app/features/auth/services/auth_service.dart';
|
||||
import 'package:eatvio_app/features/auth/models/social_exchange_request.dart';
|
||||
|
||||
class AuthRepository {
|
||||
final AuthService authService;
|
||||
@@ -22,4 +23,17 @@ class AuthRepository {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<TokenModel> getTokenBySocial({
|
||||
required String provider,
|
||||
required String accessToken,
|
||||
}) async {
|
||||
try {
|
||||
return await authService.getTokenBySocial(
|
||||
body: SocialExchangeRequest(provider: provider, accessToken: accessToken),
|
||||
);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
283
lib/features/auth/sdk/social_auth_facade.dart
Normal file
283
lib/features/auth/sdk/social_auth_facade.dart
Normal file
@@ -0,0 +1,283 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart';
|
||||
import 'package:eatvio_app/features/auth/models/yandex_profile_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:eatvio_app/core/env_config.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:vk_id/vk_id.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
class SocialAuthFacade {
|
||||
static Future<String?> signInWithVk() async {
|
||||
final context = AppRouter.navigatorKey.currentContext;
|
||||
if (context == null) return null;
|
||||
final result = await signInWithVkAndFetchProfile(context: context);
|
||||
return result?.accessToken;
|
||||
}
|
||||
|
||||
static Future<AuthResultEntity?> signInWithVkAndFetchProfile({required BuildContext context}) async {
|
||||
final clientIdStr = EnvConfig.vkClientId.trim();
|
||||
if (clientIdStr.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final controller = VkIDController(clID: int.parse(clientIdStr));
|
||||
final authUri = controller.generateAuthorizeLinkWithCodeVerifier();
|
||||
|
||||
final authUrl = authUri.value.toString();
|
||||
final codeVerifier = authUri.key;
|
||||
final redirectScheme = 'vk$clientIdStr://';
|
||||
|
||||
final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUrl, redirectSchemePrefix: redirectScheme);
|
||||
if (redirectParams == null) {
|
||||
return null;
|
||||
}
|
||||
return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<AuthResultEntity?> _parseVKAuthResult({
|
||||
required Map<String, String> redirectParams,
|
||||
required VkIDController controller,
|
||||
required String codeVerifier,
|
||||
}) async {
|
||||
final code = redirectParams['code'];
|
||||
final deviceId = redirectParams['device_id'];
|
||||
final state = redirectParams['state'];
|
||||
if (code == null || deviceId == null || state == null) {
|
||||
return null;
|
||||
}
|
||||
final authRes = await controller.retrieveOAuthToken(authorizationCode: code, deviceId: deviceId, codeVerifier: codeVerifier, state: state);
|
||||
final auth = authRes.result;
|
||||
if (auth == null) {
|
||||
var msg = "Retrieve OAuth token error";
|
||||
final err = authRes.error?.vkErr;
|
||||
if (err != null) {
|
||||
msg += ": ${err.error} - ${err.description}";
|
||||
}
|
||||
log(msg);
|
||||
return null;
|
||||
}
|
||||
final profileRes = await controller.getProfileInfo();
|
||||
final profile = profileRes.result;
|
||||
if (profile == null) {
|
||||
var msg = "Profile info load error";
|
||||
final err = profileRes.error?.vkErr;
|
||||
if (err != null) {
|
||||
msg += ": ${err.error} - ${err.description}";
|
||||
}
|
||||
log(msg);
|
||||
return null;
|
||||
}
|
||||
|
||||
return AuthResultEntity(
|
||||
accessToken: auth.accessToken,
|
||||
refreshToken: auth.refreshToken,
|
||||
userId: profile.userId.toString(),
|
||||
firstName: profile.firstName,
|
||||
lastName: profile.lastName,
|
||||
email: profile.email,
|
||||
phone: profile.phone,
|
||||
gender: GenderEnum.fromVkGender(profile.parsedSex),
|
||||
birthDate: profile.bDayDt?.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> exchangeCodeForToken(String code, String clientId, String clientSecret) async {
|
||||
final response = await http.post(
|
||||
Uri.parse('https://oauth.yandex.ru/token'),
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: {'grant_type': 'authorization_code', 'code': code, 'client_id': clientId, 'client_secret': clientSecret},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return jsonDecode(response.body) as Map<String, dynamic>;
|
||||
} else {
|
||||
log('Error exchanging code: ${response.body}');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Future<String?> signInWithOk() async {
|
||||
final context = AppRouter.navigatorKey.currentContext;
|
||||
if (context == null) return null;
|
||||
final vkAuthRes = await _signInWithVkIdProvider(context: context, provider: VkProvider.okRu);
|
||||
return vkAuthRes?.accessToken;
|
||||
}
|
||||
|
||||
static Future<String?> signInWithMailRu() async {
|
||||
final context = AppRouter.navigatorKey.currentContext;
|
||||
if (context == null) return null;
|
||||
final vkAuthRes = await _signInWithVkIdProvider(context: context, provider: VkProvider.mailRu);
|
||||
return vkAuthRes?.accessToken;
|
||||
}
|
||||
|
||||
static Future<AuthResultEntity?> signInWithYandex() async {
|
||||
final clientId = EnvConfig.yandexClientId.trim();
|
||||
final redirectUri = EnvConfig.yandexRedirectUri.trim();
|
||||
|
||||
if (clientId.isEmpty || redirectUri.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final context = AppRouter.navigatorKey.currentContext;
|
||||
if (context == null) return null;
|
||||
|
||||
final authorizeUrl = Uri.https('oauth.yandex.ru', '/authorize', {
|
||||
'response_type': 'code',
|
||||
'client_id': clientId,
|
||||
'redirect_uri': redirectUri,
|
||||
'force_confirm': 'yes',
|
||||
}).toString();
|
||||
|
||||
final redirectSchemePrefix = redirectUri;
|
||||
|
||||
final params = await _presentOAuthWebView(context: context, initialUrl: authorizeUrl, redirectSchemePrefix: redirectSchemePrefix);
|
||||
|
||||
if (params == null) return null;
|
||||
|
||||
final code = params['code'];
|
||||
if (code == null || code.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final tokens = await _getTokensFomYandex(code: code, clientId: clientId, clientSecret: EnvConfig.yandexClientSecret);
|
||||
if (tokens == null) {
|
||||
return null;
|
||||
}
|
||||
final response = await http.get(Uri.parse('https://login.yandex.ru/info?format=json'), headers: {'Authorization': 'OAuth ${tokens.accessToken}'});
|
||||
|
||||
final Map<String, dynamic> json = jsonDecode(response.body);
|
||||
final yandexProfileData = YandexProfileModel.fromJson(json);
|
||||
return AuthResultEntity(
|
||||
accessToken: tokens.accessToken,
|
||||
refreshToken: tokens.refreshToken,
|
||||
userId: yandexProfileData.id,
|
||||
firstName: yandexProfileData.firstName,
|
||||
lastName: yandexProfileData.lastName,
|
||||
email: yandexProfileData.defaultEmail,
|
||||
phone: yandexProfileData.defaultPhone.number,
|
||||
gender: GenderEnum.fromString(yandexProfileData.sex),
|
||||
birthDate: yandexProfileData.birthday,
|
||||
);
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Future<({String accessToken, String refreshToken})?> _getTokensFomYandex({
|
||||
required String code,
|
||||
required String clientId,
|
||||
required String clientSecret,
|
||||
}) async {
|
||||
final response = await http.post(
|
||||
Uri.parse('https://oauth.yandex.ru/token'),
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: {'grant_type': 'authorization_code', 'code': code, 'client_id': clientId, 'client_secret': clientSecret},
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
log('Yandex token exchange failed: ${response.body}');
|
||||
return null;
|
||||
}
|
||||
|
||||
final tokens = jsonDecode(response.body);
|
||||
final accessToken = tokens['access_token'] as String;
|
||||
final refreshToken = tokens['refresh_token'] as String;
|
||||
return (accessToken: accessToken, refreshToken: refreshToken);
|
||||
}
|
||||
|
||||
static Future<AuthResultEntity?> _signInWithVkIdProvider({required BuildContext context, required VkProvider provider}) async {
|
||||
final clientIdStr = EnvConfig.vkClientId.trim();
|
||||
if (clientIdStr.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
final controller = VkIDController(clID: int.parse(clientIdStr));
|
||||
|
||||
final authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider);
|
||||
final codeVerifier = authUri.key;
|
||||
final authUrl = authUri.value.toString();
|
||||
final redirectScheme = 'vk$clientIdStr://';
|
||||
final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUrl, redirectSchemePrefix: redirectScheme);
|
||||
if (redirectParams == null) return null;
|
||||
|
||||
final code = redirectParams['code'];
|
||||
final deviceId = redirectParams['device_id'];
|
||||
if (code == null || deviceId == null) return null;
|
||||
|
||||
return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Map<String, String>?> _presentOAuthWebView({
|
||||
required BuildContext context,
|
||||
required String initialUrl,
|
||||
required String redirectSchemePrefix,
|
||||
}) async {
|
||||
final controller = WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setNavigationDelegate(
|
||||
NavigationDelegate(
|
||||
onNavigationRequest: (request) {
|
||||
final url = request.url;
|
||||
if (url.startsWith(redirectSchemePrefix)) {
|
||||
final uri = Uri.parse(url);
|
||||
final params = <String, String>{}..addAll(uri.queryParameters);
|
||||
if (uri.fragment.isNotEmpty) {
|
||||
final fragPairs = uri.fragment.split('&');
|
||||
for (final pair in fragPairs) {
|
||||
final eq = pair.indexOf('=');
|
||||
if (eq > 0 && eq < pair.length - 1) {
|
||||
final key = Uri.decodeComponent(pair.substring(0, eq));
|
||||
final value = Uri.decodeComponent(pair.substring(eq + 1));
|
||||
params[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
Navigator.of(context, rootNavigator: true).pop(params);
|
||||
return NavigationDecision.prevent;
|
||||
}
|
||||
return NavigationDecision.navigate;
|
||||
},
|
||||
onPageStarted: (url) {},
|
||||
onPageFinished: (url) {},
|
||||
onWebResourceError: (error) {},
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
await controller.loadRequest(Uri.parse(initialUrl));
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
if (!context.mounted) return null;
|
||||
return showDialog<Map<String, String>>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) {
|
||||
const borderRadius = BorderRadius.all(Radius.circular(16));
|
||||
return Dialog(
|
||||
insetPadding: const EdgeInsets.all(16),
|
||||
shape: const RoundedRectangleBorder(borderRadius: borderRadius),
|
||||
elevation: 0,
|
||||
child: ClipRRect(
|
||||
borderRadius: borderRadius,
|
||||
child: SizedBox(height: 560, child: WebViewWidget(controller: controller)),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/features/auth/models/credentials_model.dart';
|
||||
import 'package:eatvio_app/features/auth/models/token_model.dart';
|
||||
import 'package:eatvio_app/features/auth/models/social_exchange_request.dart';
|
||||
import 'package:retrofit/retrofit.dart';
|
||||
|
||||
part 'auth_service.g.dart';
|
||||
@@ -13,4 +14,9 @@ abstract class AuthService {
|
||||
Future<TokenModel> getTokenByCredentials({
|
||||
@Body() required CredentialsModel credentials,
|
||||
});
|
||||
|
||||
@POST('/api/social/login')
|
||||
Future<TokenModel> getTokenBySocial({
|
||||
@Body() required SocialExchangeRequest body,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,4 +18,26 @@ class BlogPostCubit extends Cubit<BlogPostState> {
|
||||
emit(state.copyWith(isLoading: false));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> toggleLike() async {
|
||||
final current = state.blogPostModel;
|
||||
if (current == null) return;
|
||||
|
||||
// optimistic update
|
||||
final wasLiked = current.wasLikedByMe;
|
||||
final updated = current.copyWith(wasLikedByMe: !wasLiked, totalLikes: wasLiked ? current.totalLikes - 1 : current.totalLikes + 1);
|
||||
emit(state.copyWith(blogPostModel: updated));
|
||||
|
||||
try {
|
||||
if (wasLiked) {
|
||||
await feedRepository.unlikedBlogPost(id: current.id);
|
||||
} else {
|
||||
await feedRepository.likeBlogPost(id: current.id);
|
||||
}
|
||||
} catch (e, st) {
|
||||
// rollback on error
|
||||
log(e.toString(), stackTrace: st);
|
||||
emit(state.copyWith(blogPostModel: current));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +32,6 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
|
||||
final CommentsCubit commentsCubit = sl<CommentsCubit>();
|
||||
final textEditingController = TextEditingController();
|
||||
int? replyId;
|
||||
bool isLiked = false;
|
||||
int totalLikes = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -60,26 +58,30 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
|
||||
builder: (BuildContext context, CommentsState state) {
|
||||
if (blogPostState.isLoading) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(
|
||||
onPressed: () {
|
||||
if (context.canPop()) {
|
||||
GoRouter.of(context).pop();
|
||||
return;
|
||||
}
|
||||
GoRouter.of(context).go('/feed-screen');
|
||||
},
|
||||
),
|
||||
appBar: AppBar(
|
||||
leading: BackButton(
|
||||
onPressed: () {
|
||||
if (context.canPop()) {
|
||||
GoRouter.of(context).pop();
|
||||
return;
|
||||
}
|
||||
GoRouter.of(context).go('/feed-screen');
|
||||
},
|
||||
),
|
||||
body: const Loader());
|
||||
),
|
||||
body: const Loader(),
|
||||
);
|
||||
}
|
||||
isLiked = blogPostState.blogPostModel?.wasLikedByMe ?? false;
|
||||
totalLikes = blogPostState.blogPostModel?.totalLikes ?? 0;
|
||||
final bool isLiked = blogPostState.blogPostModel?.wasLikedByMe ?? false;
|
||||
final int totalLikes = blogPostState.blogPostModel?.totalLikes ?? 0;
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: true,
|
||||
bottomSheet: Container(
|
||||
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)))),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
@@ -114,24 +116,14 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
|
||||
maxLines: 5,
|
||||
minLines: 1,
|
||||
keyboardType: TextInputType.multiline,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.black,
|
||||
height: 1.2,
|
||||
),
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1.2),
|
||||
decoration: InputDecoration(
|
||||
isDense: false,
|
||||
// убираем true, чтобы padding учитывался корректно
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
// вертикальный паддинг увеличен
|
||||
hintText: 'Комментарий',
|
||||
hintStyle: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1.2,
|
||||
color: Color(0xFFAEAEB2),
|
||||
),
|
||||
hintStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, height: 1.2, color: Color(0xFFAEAEB2)),
|
||||
filled: true,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
@@ -196,7 +188,7 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
|
||||
id: widget.id,
|
||||
feedBlogPostModel: blogPostState.blogPostModel,
|
||||
userSlug: blogPostState.blogPostModel?.author.authorSlug ?? '',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
],
|
||||
),
|
||||
@@ -215,9 +207,7 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 16,
|
||||
foregroundImage: NetworkImage(
|
||||
baseUrl + (blogPostState.blogPostModel?.author.authorProfileIcon ?? ''),
|
||||
),
|
||||
foregroundImage: NetworkImage(baseUrl + (blogPostState.blogPostModel?.author.authorProfileIcon ?? '')),
|
||||
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
@@ -242,30 +232,32 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
|
||||
if (blogPostState.blogPostModel?.description.isNotEmpty ?? false)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Html(data: blogPostState.blogPostModel?.description.replaceAll('\n', '<br>'), style: {"*": Style(color: Colors.black87)}),
|
||||
child: Html(
|
||||
data: blogPostState.blogPostModel?.description.replaceAll('\n', '<br>'),
|
||||
style: {"*": Style(color: Colors.black87)},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (blogPostState.blogPostModel?.image.isNotEmpty ?? false)
|
||||
CachedNetworkImage(
|
||||
imageUrl: '$baseUrl/storage/${blogPostState.blogPostModel?.image ?? ''}',
|
||||
),
|
||||
CachedNetworkImage(imageUrl: '$baseUrl/storage/${blogPostState.blogPostModel?.image ?? ''}'),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
_iconButton(
|
||||
icon: Icons.favorite_border_rounded,
|
||||
icon: isLiked ? Icons.favorite_rounded : Icons.favorite_border_rounded,
|
||||
text: totalLikes.toString(),
|
||||
iconColor: isLiked ? Colors.red.withValues(alpha: 0.8) : null,
|
||||
backgroundColor: isLiked ? Colors.red.withValues(alpha: 0.1) : null,
|
||||
textColor: isLiked ? Colors.red.withValues(alpha: 0.8) : null,
|
||||
onTap: () async {
|
||||
setState(() {
|
||||
isLiked = !isLiked;
|
||||
isLiked ? totalLikes++ : totalLikes--;
|
||||
});
|
||||
isLiked ? await context.read<FeedCubit>().likePost(widget.id) : await context.read<FeedCubit>().unlikedPost(widget.id);
|
||||
sl<FeedCubit>().updateFeedBlogPostList(blogPostState.blogPostModel, isLiked);
|
||||
await context.read<BlogPostCubit>().toggleLike();
|
||||
// ignore: use_build_context_synchronously
|
||||
final updated = context.read<BlogPostCubit>().state.blogPostModel;
|
||||
if (updated != null) {
|
||||
// ignore: use_build_context_synchronously
|
||||
context.read<FeedCubit>().updateFeedBlogPostList(updated, updated.wasLikedByMe);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
@@ -284,8 +276,10 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
|
||||
const Spacer(),
|
||||
Icon(Icons.visibility_rounded, size: 16, color: appTheme.gray500),
|
||||
const SizedBox(width: 4),
|
||||
Text(blogPostState.blogPostModel?.totalViews.toString() ?? '',
|
||||
style: TextStyle(fontSize: 12, color: appTheme.gray500, fontWeight: FontWeight.w400)),
|
||||
Text(
|
||||
blogPostState.blogPostModel?.totalViews.toString() ?? '',
|
||||
style: TextStyle(fontSize: 12, color: appTheme.gray500, fontWeight: FontWeight.w400),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -332,7 +326,11 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
|
||||
children: [
|
||||
Icon(icon, size: 20, color: iconColor ?? appTheme.gray600),
|
||||
if (text != null) const SizedBox(width: 8),
|
||||
if (text != null) Text(text, style: TextStyle(color: textColor ?? appTheme.gray600, fontWeight: FontWeight.w500)),
|
||||
if (text != null)
|
||||
Text(
|
||||
text,
|
||||
style: TextStyle(color: textColor ?? appTheme.gray600, fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,61 +1,34 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:eatvio_app/core/cubit/base_cubit.dart';
|
||||
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_state.dart';
|
||||
import 'package:eatvio_app/features/blog_post_create/repositories/blog_post_repository.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||
|
||||
class BlogPostCreateCubit extends Cubit<BlogPostCreateState> {
|
||||
class BlogPostCreateCubit extends BaseCubit<BlogPostCreateState> {
|
||||
final BlogPostRepository blogPostRepository;
|
||||
|
||||
BlogPostCreateCubit({
|
||||
required this.blogPostRepository,
|
||||
}) : super(const BlogPostCreateState());
|
||||
BlogPostCreateCubit({required this.blogPostRepository}) : super(const BlogPostCreateState());
|
||||
|
||||
Future<bool> createBlogPost({
|
||||
String? title,
|
||||
String? videoUrl,
|
||||
String? description,
|
||||
XFile? image,
|
||||
}) async {
|
||||
Future<bool> createBlogPost({String? title, String? videoUrl, String? description, XFile? image}) async {
|
||||
try {
|
||||
emit(const BlogPostCreateState.loading());
|
||||
await blogPostRepository.createBlogPost(
|
||||
title,
|
||||
videoUrl,
|
||||
description,
|
||||
image,
|
||||
);
|
||||
await blogPostRepository.createBlogPost(title, videoUrl, description, image);
|
||||
emit(const BlogPostCreateState.loaded());
|
||||
return true;
|
||||
} catch (e, st) {
|
||||
log(e.toString(), stackTrace: st);
|
||||
} catch (error, stackTrace) {
|
||||
handleError(error, stackTrace);
|
||||
emit(const BlogPostCreateState.loaded());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> updateBlogPost({
|
||||
required int id,
|
||||
String? title,
|
||||
String? videoUrl,
|
||||
String? description,
|
||||
XFile? image,
|
||||
String? oldImageUrl,
|
||||
}) async {
|
||||
Future<bool> updateBlogPost({required int id, String? title, String? videoUrl, String? description, XFile? image, String? oldImageUrl}) async {
|
||||
try {
|
||||
emit(const BlogPostCreateState.loading());
|
||||
await blogPostRepository.updateBlogPost(
|
||||
id,
|
||||
title,
|
||||
videoUrl,
|
||||
description,
|
||||
image,
|
||||
oldImageUrl,
|
||||
);
|
||||
await blogPostRepository.updateBlogPost(id, title, videoUrl, description, image, oldImageUrl);
|
||||
emit(const BlogPostCreateState.loaded());
|
||||
return true;
|
||||
} catch (e, st) {
|
||||
log(e.toString(), stackTrace: st);
|
||||
} catch (error, stackTrace) {
|
||||
handleError(error, stackTrace);
|
||||
emit(const BlogPostCreateState.loaded());
|
||||
return false;
|
||||
}
|
||||
@@ -65,8 +38,8 @@ class BlogPostCreateCubit extends Cubit<BlogPostCreateState> {
|
||||
try {
|
||||
await blogPostRepository.deleteBlogPost(id);
|
||||
return true;
|
||||
} catch (e, st) {
|
||||
log(e.toString(), stackTrace: st);
|
||||
} catch (error, stackTrace) {
|
||||
handleError(error, stackTrace);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,55 @@
|
||||
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());
|
||||
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;
|
||||
}
|
||||
|
||||
Future<void> getMyProfile() async {
|
||||
emit(const ProfileAvatarState.loading());
|
||||
final profile = await profileRepository.getMyProfile();
|
||||
final profileDetails = await profileRepository.getMyProfileDetails();
|
||||
try {
|
||||
emit(
|
||||
ProfileAvatarState.loaded(
|
||||
profile: await profileRepository.getMyProfile(),
|
||||
),
|
||||
);
|
||||
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);
|
||||
} catch (e) {
|
||||
AppRouter.navigatorKey.currentContext?.go('/auth-screen');
|
||||
emit(ProfileAvatarState.error(errorMessage: e.toString()));
|
||||
|
||||
@@ -23,81 +23,105 @@ class _RegistrationAsScreenState extends State<RegistrationAsScreen> {
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
'Регистрация в качестве',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1.1,
|
||||
),
|
||||
),
|
||||
child: Text('Регистрация в качестве', style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, height: 1.1)),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
RadioListTile<RegistrationAsType>(
|
||||
value: RegistrationAsType.physical,
|
||||
groupValue: registrationAsType,
|
||||
title: const Text(
|
||||
'Физического лица',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
height: 1,
|
||||
Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Radio<RegistrationAsType>(
|
||||
value: RegistrationAsType.physical,
|
||||
// ignore: deprecated_member_use
|
||||
groupValue: registrationAsType,
|
||||
activeColor: appTheme.blueA400,
|
||||
// ignore: deprecated_member_use
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
registrationAsType = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text('Физического лица', style: TextStyle(fontSize: 18, height: 1)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
activeColor: appTheme.blueA400,
|
||||
onChanged: (value) {},
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
RadioListTile<RegistrationAsType>(
|
||||
value: RegistrationAsType.selfBusy,
|
||||
groupValue: registrationAsType,
|
||||
title: const Text(
|
||||
'Самозанятого',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
height: 1,
|
||||
Row(
|
||||
children: [
|
||||
Radio<RegistrationAsType>(
|
||||
value: RegistrationAsType.selfBusy,
|
||||
// ignore: deprecated_member_use
|
||||
groupValue: registrationAsType,
|
||||
activeColor: appTheme.blueA400,
|
||||
// ignore: deprecated_member_use
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
registrationAsType = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text('Самозанятого', style: TextStyle(fontSize: 18, height: 1)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onChanged: null,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
RadioListTile<RegistrationAsType>(
|
||||
value: RegistrationAsType.iP,
|
||||
groupValue: registrationAsType,
|
||||
title: const Text(
|
||||
'Индивидуального предпринимателя',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
height: 1,
|
||||
Row(
|
||||
children: [
|
||||
Radio<RegistrationAsType>(
|
||||
value: RegistrationAsType.iP,
|
||||
// ignore: deprecated_member_use
|
||||
groupValue: registrationAsType,
|
||||
activeColor: appTheme.blueA400,
|
||||
// ignore: deprecated_member_use
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
registrationAsType = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text('Индивидуального предпринимателя', style: TextStyle(fontSize: 18, height: 1)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onChanged: null,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
RadioListTile<RegistrationAsType>(
|
||||
value: RegistrationAsType.ooo,
|
||||
groupValue: registrationAsType,
|
||||
title: const Text(
|
||||
'Общества с ограниченной ответственностью',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
height: 1,
|
||||
Row(
|
||||
children: [
|
||||
Radio<RegistrationAsType>(
|
||||
value: RegistrationAsType.ooo,
|
||||
// ignore: deprecated_member_use
|
||||
groupValue: registrationAsType,
|
||||
activeColor: appTheme.blueA400,
|
||||
// ignore: deprecated_member_use
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
registrationAsType = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text('Общества с ограниченной ответственностью', style: TextStyle(fontSize: 18, height: 1)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onChanged: null,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 16,
|
||||
),
|
||||
child: _divider(context),
|
||||
],
|
||||
),
|
||||
Padding(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), child: _divider(context)),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
'* На данный момент регистрация доступна только в качестве физического лица. Остальные варианты скоро появятся',
|
||||
),
|
||||
child: Text('* На данный момент регистрация доступна только в качестве физического лица. Остальные варианты скоро появятся'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -108,10 +132,7 @@ class _RegistrationAsScreenState extends State<RegistrationAsScreen> {
|
||||
return Container(
|
||||
height: 0.4,
|
||||
width: MediaQuery.of(context).size.width - 32,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black26,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
decoration: BoxDecoration(color: Colors.black26, borderRadius: BorderRadius.circular(16)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,42 @@
|
||||
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'rating_model.freezed.dart';
|
||||
part 'rating_model.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class RatingModel with _$RatingModel {
|
||||
abstract class RatingPaginationModel with _$RatingPaginationModel {
|
||||
factory RatingPaginationModel({
|
||||
required List<RatingModel> data,
|
||||
}) = _RatingPaginationModel;
|
||||
|
||||
factory RatingPaginationModel.fromJson(Map<String, dynamic> json) => _$RatingPaginationModelFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class RatingModel with _$RatingModel {
|
||||
factory RatingModel({
|
||||
required DateTime date,
|
||||
required String title,
|
||||
required int amount,
|
||||
@JsonKey(name: 'created_at', fromJson: parseDate) required DateTime date,
|
||||
@JsonKey(name: 'log_data') required RatingEnum type,
|
||||
@JsonKey(name: 'log_scores') required int amount,
|
||||
}) = _RatingModel;
|
||||
|
||||
factory RatingModel.fromJson(Map<String, dynamic> json) => _$RatingModelFromJson(json);
|
||||
}
|
||||
}
|
||||
|
||||
enum RatingEnum {
|
||||
@JsonValue('ADDED_RECIPE')
|
||||
addedRecipe,
|
||||
@JsonValue('ADDED_PUBLIC_DIET')
|
||||
addedPublicDiet,
|
||||
@JsonValue('ADDED_BLOG_POST')
|
||||
addedBlogPost,
|
||||
@JsonValue('INVITED_A_NEW_USER')
|
||||
invitedUser,
|
||||
@JsonValue('HAS_MEDICAL_DEGREE')
|
||||
hasMedicalDegree,
|
||||
@JsonValue('CONSULTED')
|
||||
consulted,
|
||||
@JsonValue('PRODUCT_NUTRITINAL_VALUE_APPROVED')
|
||||
addedGlossary;
|
||||
}
|
||||
|
||||
20
lib/features/consultant_rating/models/statistics_model.dart
Normal file
20
lib/features/consultant_rating/models/statistics_model.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
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,
|
||||
@JsonKey(name: 'referral_link') String? refLink,
|
||||
}) = _StatisticsModel;
|
||||
|
||||
factory StatisticsModel.fromJson(Map<String, dynamic> json) => _$StatisticsModelFromJson(json);
|
||||
}
|
||||
@@ -1,12 +1,18 @@
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/utils/toasts.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart';
|
||||
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_state.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/rating_history_widget.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/rating_widget.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/statistic_widget.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class ConsultantRatingScreen extends StatelessWidget {
|
||||
static const name = 'consultantRatingScreen';
|
||||
@@ -15,7 +21,10 @@ class ConsultantRatingScreen extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const ConsultantRatingPageContent();
|
||||
return BlocProvider<ConsultantRatingCubit>(
|
||||
create: (context) => sl<ConsultantRatingCubit>()..getConsultantStatistics()..getScoreLogs(),
|
||||
child: const ConsultantRatingPageContent(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,52 +34,59 @@ class ConsultantRatingPageContent extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return Scaffold(
|
||||
backgroundColor: appTheme.gray100,
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
s.rating_and_statistics,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
const RatingWidget(
|
||||
rating: 1058,
|
||||
return BlocBuilder<ConsultantRatingCubit, ConsultantRatingState>(
|
||||
builder: (context, state) {
|
||||
return Scaffold(
|
||||
backgroundColor: appTheme.gray100,
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
s.rating_and_statistics,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: CustomOutlinedButton(
|
||||
text: s.copy_referral_link,
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||
buttonStyle: ButtonStyle(
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
|
||||
),
|
||||
body: state.error != null
|
||||
? DataErrorWidget(onReload: () async {
|
||||
await context.read<ConsultantRatingCubit>().getConsultantStatistics();
|
||||
if (!context.mounted) return;
|
||||
await context.read<ConsultantRatingCubit>().getScoreLogs();
|
||||
})
|
||||
: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
RatingWidget(
|
||||
rating: state.absoluteRating!,
|
||||
),
|
||||
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: () {
|
||||
Clipboard.setData(ClipboardData(text: state.statistics?.refLink ?? ''));
|
||||
showInfoToast(s.link_was_copied);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
StatisticWidget(isLoading: state.isStatisticsLoading, statistics: state.statistics,),
|
||||
const SizedBox(height: 16),
|
||||
RatingHistoryWidget(
|
||||
isLoading: state.isRatingLoading,
|
||||
ratingHistory: state.ratings,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onPressed: () {},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const StatisticWidget(),
|
||||
const SizedBox(height: 16),
|
||||
RatingHistoryWidget(
|
||||
ratingHistory: [
|
||||
RatingModel(date: DateTime.now(), title: ' djkasjdasjkdkjasjkdjkaskjdjklasjkld', amount: 201300),
|
||||
RatingModel(date: DateTime.now(), title: 'djkasjdasjkdkjasjkdjkaskjdjklasjkld', amount: 89323),
|
||||
RatingModel(date: DateTime.now(), title: 'djkasjdasjkdkjasjkdjkaskjdjklasjkld', amount: 20),
|
||||
RatingModel(date: DateTime.now(), title: 'djkasjdasjkdkjasjkdjkaskjdjklasjkld', amount: 1212),
|
||||
RatingModel(date: DateTime.now(), title: 'qweqweeq', amount: 323),
|
||||
RatingModel(date: DateTime.now(), title: 'ee', amount: 1),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
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:eatvio_app/features/feed/repositories/feed_repository.dart';
|
||||
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class ConsultantRatingCubit extends Cubit<ConsultantRatingState> {
|
||||
final ConsultantRatingRepository repository;
|
||||
final FeedRepository feedRepository;
|
||||
final ProfileRepository profileRepository;
|
||||
|
||||
ConsultantRatingCubit({required this.repository, required this.feedRepository, required this.profileRepository}) : super(ConsultantRatingState());
|
||||
|
||||
Future<void> getConsultantStatistics({String? date}) async {
|
||||
try {
|
||||
emit(state.copyWith(isStatisticsLoading: true));
|
||||
final statistics = await repository.getConsultantStatistics(date: date);
|
||||
final absoluteRating = await feedRepository.getConsultants((await profileRepository.getMyProfile()).name);
|
||||
emit(state.copyWith(isStatisticsLoading: false, statistics: statistics, error: null, absoluteRating: absoluteRating.first.totalScores));
|
||||
} catch (e, st) {
|
||||
emit(state.copyWith(isStatisticsLoading: true, error: e.toString()));
|
||||
log(e.toString(), stackTrace: st);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> getScoreLogs({String? date}) async {
|
||||
try {
|
||||
emit(state.copyWith(isRatingLoading: true));
|
||||
final scoreLogs = await repository.getScoreLogs(date: date);
|
||||
emit(state.copyWith(isRatingLoading: false, ratings: scoreLogs, error: null));
|
||||
} catch (e, st) {
|
||||
emit(state.copyWith(isRatingLoading: true, error: e.toString()));
|
||||
log(e.toString(), stackTrace: st);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
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,
|
||||
@Default(0) int? absoluteRating,
|
||||
}) = _ConsultantRatingState;
|
||||
}
|
||||
@@ -1,42 +1,29 @@
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/utils/date_time_utils.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_drop_down_button.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class PeriodFilterWidget extends StatefulWidget {
|
||||
const PeriodFilterWidget({super.key});
|
||||
|
||||
const PeriodFilterWidget({super.key, required this.isStatistic});
|
||||
final bool isStatistic;
|
||||
@override
|
||||
State<PeriodFilterWidget> createState() => _PeriodFilterWidgetState();
|
||||
}
|
||||
|
||||
class _PeriodFilterWidgetState extends State<PeriodFilterWidget> {
|
||||
final List<String> periodDropDownTitles = [];
|
||||
int periodDropDownIndex = 0;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final s = S.of(context);
|
||||
if (periodDropDownTitles.isEmpty) {
|
||||
periodDropDownTitles.addAll([
|
||||
s.for_whole_period_of_time,
|
||||
s.for_a_year,
|
||||
s.for_a_month,
|
||||
s.for_a_week,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
PeriodFilterStatus _currentState = PeriodFilterStatus.all;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CustomDropDownButton(
|
||||
CustomDropDownButton<PeriodFilterStatus>(
|
||||
hint: Text(
|
||||
s.choose_diet_duration,
|
||||
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
|
||||
@@ -44,24 +31,59 @@ class _PeriodFilterWidgetState extends State<PeriodFilterWidget> {
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
periodDropDownIndex = periodDropDownTitles.indexOf(value);
|
||||
_currentState = value;
|
||||
});
|
||||
if (widget.isStatistic) {
|
||||
if (value == PeriodFilterStatus.all) {
|
||||
context.read<ConsultantRatingCubit>().getConsultantStatistics();
|
||||
return;
|
||||
}
|
||||
context.read<ConsultantRatingCubit>().getConsultantStatistics(date: value.value);
|
||||
return;
|
||||
}
|
||||
if (value == PeriodFilterStatus.all) {
|
||||
context.read<ConsultantRatingCubit>().getScoreLogs();
|
||||
return;
|
||||
}
|
||||
context.read<ConsultantRatingCubit>().getScoreLogs(date: value.value);
|
||||
return;
|
||||
}
|
||||
},
|
||||
items: periodDropDownTitles
|
||||
items: PeriodFilterStatus.values
|
||||
.mapIndexed(
|
||||
(index, value) => DropdownMenuItem(
|
||||
value: periodDropDownTitles[index],
|
||||
value: PeriodFilterStatus.values[index],
|
||||
child: Text(
|
||||
periodDropDownTitles[index],
|
||||
PeriodFilterStatus.values[index].name,
|
||||
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
value: periodDropDownTitles[periodDropDownIndex],
|
||||
value: _currentState,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum PeriodFilterStatus {
|
||||
all,
|
||||
month,
|
||||
week,
|
||||
year;
|
||||
|
||||
String get name => switch (this) {
|
||||
PeriodFilterStatus.all => 'Все периоды',
|
||||
PeriodFilterStatus.month => 'За месяц',
|
||||
PeriodFilterStatus.week => 'За неделю',
|
||||
PeriodFilterStatus.year => 'За год',
|
||||
};
|
||||
|
||||
String? get value => switch (this) {
|
||||
PeriodFilterStatus.all => null,
|
||||
PeriodFilterStatus.month => DateTime.now().subtract(const Duration(days: 30)).format('yyyy-MM-dd'),
|
||||
PeriodFilterStatus.week => DateTime.now().subtract(const Duration(days: 7)).format('yyyy-MM-dd'),
|
||||
PeriodFilterStatus.year => DateTime.now().subtract(const Duration(days: 365)).format('yyyy-MM-dd'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ class RatingHistoryItem extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 80,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.gray100,
|
||||
@@ -29,7 +28,7 @@ class RatingHistoryItem extends StatelessWidget {
|
||||
date.format('dd.MM.yyyy'),
|
||||
style: TextStyles.textXSSemibold.copyWith(color: appTheme.gray),
|
||||
),
|
||||
const Spacer(),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/widgets/loader.dart';
|
||||
import 'package:eatvio_app/core/widgets/main_background_bouble.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/period_filter_widget.dart';
|
||||
@@ -9,7 +10,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
class RatingHistoryWidget extends StatelessWidget {
|
||||
const RatingHistoryWidget({super.key, required this.ratingHistory});
|
||||
const RatingHistoryWidget({super.key, required this.ratingHistory, required this.isLoading});
|
||||
final bool isLoading;
|
||||
final List<RatingModel> ratingHistory;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -20,50 +22,60 @@ class RatingHistoryWidget extends StatelessWidget {
|
||||
right: 16,
|
||||
top: 16,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
s.rating_history,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
const Spacer(),
|
||||
SvgPicture.asset(
|
||||
'assets/icons/question_mark.svg',
|
||||
height: 22,
|
||||
width: 22,
|
||||
colorFilter: ColorFilter.mode(
|
||||
appTheme.blueA400,
|
||||
BlendMode.srcIn,
|
||||
child: isLoading
|
||||
? const Center(child: Loader())
|
||||
: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
s.rating_history,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
const Spacer(),
|
||||
SvgPicture.asset(
|
||||
'assets/icons/question_mark.svg',
|
||||
height: 22,
|
||||
width: 22,
|
||||
colorFilter: ColorFilter.mode(
|
||||
appTheme.blueA400,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
const PeriodFilterWidget(),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
ListView.builder(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: index == ratingHistory.length - 1 ? EdgeInsets.zero : const EdgeInsets.only(bottom: 12),
|
||||
child: RatingHistoryItem(
|
||||
title: ratingHistory[index].title,
|
||||
amount: ratingHistory[index].amount,
|
||||
date: ratingHistory[index].date,
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: ratingHistory.length,
|
||||
shrinkWrap: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
const PeriodFilterWidget(isStatistic: false),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
ListView.builder(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: index == ratingHistory.length - 1 ? EdgeInsets.zero : const EdgeInsets.only(bottom: 12),
|
||||
child: RatingHistoryItem(
|
||||
title: switch (ratingHistory[index].type) {
|
||||
RatingEnum.addedRecipe => 'Добавление рецепта',
|
||||
RatingEnum.addedPublicDiet => 'Добавление публичной диеты',
|
||||
RatingEnum.addedBlogPost => 'Добавление поста',
|
||||
RatingEnum.invitedUser => 'Регистрация по реф. ссылке',
|
||||
RatingEnum.hasMedicalDegree => 'Получение мед. степени',
|
||||
RatingEnum.consulted => 'Консультация',
|
||||
RatingEnum.addedGlossary => 'Добавление продукта в глоссарий',
|
||||
},
|
||||
amount: ratingHistory[index].amount,
|
||||
date: ratingHistory[index].date,
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: ratingHistory.length,
|
||||
shrinkWrap: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/widgets/loader.dart';
|
||||
import 'package:eatvio_app/core/widgets/main_background_bouble.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/models/statistics_model.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/period_filter_widget.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/statistic_bouble.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
@@ -8,14 +10,15 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
class StatisticWidget extends StatelessWidget {
|
||||
const StatisticWidget({super.key});
|
||||
|
||||
const StatisticWidget({super.key, required this.isLoading, required this.statistics});
|
||||
final bool isLoading;
|
||||
final StatisticsModel? statistics;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return MainBackgroundBouble(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
child: isLoading ? const Center(child: Loader()) : Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
@@ -38,15 +41,17 @@ class StatisticWidget extends StatelessWidget {
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
const PeriodFilterWidget(),
|
||||
const PeriodFilterWidget(
|
||||
isStatistic: true,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: StatisticBouble(number: 12, title: s.invited_people_count)),
|
||||
Expanded(child: StatisticBouble(number: statistics?.countReferal ?? 0, title: s.invited_people_count)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: StatisticBouble(number: 12, title: s.published_posts_count)),
|
||||
Expanded(child: StatisticBouble(number: statistics?.blogPostsCount ?? 0, title: s.published_posts_count)),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
@@ -54,9 +59,9 @@ class StatisticWidget extends StatelessWidget {
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: StatisticBouble(number: 12, title: s.published_recipes_count)),
|
||||
Expanded(child: StatisticBouble(number: statistics?.recipesCount ?? 0, title: s.published_recipes_count)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: StatisticBouble(number: 12, title: s.published_diets_count)),
|
||||
Expanded(child: StatisticBouble(number: statistics?.dietsCount ?? 0, title: s.published_diets_count)),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
@@ -64,9 +69,9 @@ class StatisticWidget extends StatelessWidget {
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: StatisticBouble(number: 12, title: s.consultations_count)),
|
||||
Expanded(child: StatisticBouble(number: statistics?.consultationsCount ?? 0, title: s.consultations_count)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: StatisticBouble(number: 12, title: s.active_consultations_count)),
|
||||
Expanded(child: StatisticBouble(number: statistics?.qwe ?? 0, title: s.added_products_in_glossary)),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/models/statistics_model.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/services/consultant_rating_service.dart';
|
||||
|
||||
class ConsultantRatingRepository {
|
||||
final ConsultantRatingService service;
|
||||
|
||||
ConsultantRatingRepository(this.service);
|
||||
|
||||
Future<StatisticsModel> getConsultantStatistics({String? date}) async {
|
||||
try {
|
||||
return await service.getConsultantStatistics(date: date);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<RatingModel>> getScoreLogs({String? date}) async {
|
||||
try {
|
||||
final data = (await service.getScoreLogs(date: date)).data;
|
||||
return data;
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/models/statistics_model.dart';
|
||||
|
||||
import 'package:retrofit/retrofit.dart';
|
||||
|
||||
part 'consultant_rating_service.g.dart';
|
||||
|
||||
@RestApi()
|
||||
abstract class ConsultantRatingService {
|
||||
factory ConsultantRatingService(Dio dio, {String baseUrl}) = _ConsultantRatingService;
|
||||
|
||||
@GET('/api/consultants/rating/statistics')
|
||||
Future<StatisticsModel> getConsultantStatistics({@Query('filters[duration][\$gte]') String? date});
|
||||
|
||||
@GET('/api/consultants/score/logs')
|
||||
Future<RatingPaginationModel> getScoreLogs({@Query('filters[duration][\$gte]') String? date});
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:eatvio_app/core/const.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_divider.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
|
||||
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
|
||||
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart';
|
||||
import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart';
|
||||
import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
|
||||
import 'package:eatvio_app/features/user_details/presentation/user_details_screen.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -63,7 +65,7 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
CircleAvatar(radius: 30, backgroundImage: NetworkImage(baseUrl + widget.consultationsModel.userProfilePhotoUrl)),
|
||||
CircleAvatar(radius: 30, backgroundImage: NetworkImage(widget.consultationsModel.userProfilePhotoUrl)),
|
||||
const SizedBox(width: 12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -284,6 +286,19 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
|
||||
),
|
||||
],
|
||||
),
|
||||
// Кнопка получения выплаты
|
||||
if (widget.consultationsModel.chosenPlanState == ChosenPlanStateEnum.stateDone) ...[
|
||||
const SizedBox(height: 16),
|
||||
CustomOutlinedButton(
|
||||
text: s.collectCharge,
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.green)),
|
||||
onPressed: () async {
|
||||
GoRouter.of(context).push(CardSettingsScreen.path, extra: widget.consultationsModel.id);
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -26,10 +26,7 @@ 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);
|
||||
}
|
||||
|
||||
@@ -65,11 +62,7 @@ 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),
|
||||
@@ -79,34 +72,16 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
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),
|
||||
),
|
||||
),
|
||||
child: BookmarkButton(
|
||||
isFavorite: widget.diet.isFavorite,
|
||||
onTap: () {},
|
||||
width: 52,
|
||||
height: 52,
|
||||
size: 24,
|
||||
shape: BoxShape.circle,
|
||||
color: appTheme.gray50,
|
||||
),
|
||||
),
|
||||
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),
|
||||
@@ -133,24 +108,15 @@ 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -160,17 +126,10 @@ 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -212,12 +171,7 @@ 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,
|
||||
// ),
|
||||
@@ -233,10 +187,7 @@ 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),
|
||||
@@ -252,10 +203,7 @@ 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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -265,10 +213,7 @@ 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)),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -279,23 +224,15 @@ 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),
|
||||
],
|
||||
@@ -308,9 +245,7 @@ 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -320,7 +255,7 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) {
|
||||
return _dayOfPlan(widget.diet.dailySchedule[index] ?? {}, index, s);
|
||||
return _dayOfPlan(widget.diet.dailySchedule[index + 1] ?? {}, index, s);
|
||||
},
|
||||
itemCount: widget.diet.dailySchedule.length,
|
||||
),
|
||||
@@ -331,12 +266,7 @@ 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),
|
||||
],
|
||||
@@ -355,14 +285,10 @@ 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) {
|
||||
@@ -376,9 +302,7 @@ 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(
|
||||
@@ -386,31 +310,12 @@ 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(
|
||||
@@ -443,20 +348,10 @@ 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)),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -46,21 +46,14 @@ 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(
|
||||
@@ -73,21 +66,17 @@ class _AddMealWidgetState extends State<AddMealWidget> {
|
||||
autofocus: true,
|
||||
hintText: 'Введите название продукта/блюда',
|
||||
onChanged: (query) {
|
||||
EasyDebounce.debounce(
|
||||
'glossary-search',
|
||||
const Duration(milliseconds: 250),
|
||||
() async {
|
||||
EasyDebounce.debounce('glossary-search', const Duration(milliseconds: 250), () async {
|
||||
selectedGlossary = null;
|
||||
if (query.isEmpty) {
|
||||
glossariesList = [];
|
||||
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(() {});
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -100,22 +89,20 @@ 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,
|
||||
@@ -130,28 +117,12 @@ 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),
|
||||
);
|
||||
},
|
||||
@@ -166,25 +137,15 @@ 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),
|
||||
@@ -194,18 +155,11 @@ 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -222,8 +176,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);
|
||||
@@ -235,19 +189,11 @@ 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>(
|
||||
@@ -255,13 +201,8 @@ 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) {
|
||||
@@ -275,18 +216,9 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,12 +22,7 @@ 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);
|
||||
}
|
||||
@@ -71,21 +66,13 @@ 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
|
||||
@@ -115,11 +102,30 @@ 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,
|
||||
|
||||
@@ -14,12 +14,7 @@ class AddProductModal extends StatefulWidget {
|
||||
final DiaryStateLoaded state;
|
||||
final DiaryCubit diaryCubit;
|
||||
|
||||
const AddProductModal({
|
||||
super.key,
|
||||
this.portion,
|
||||
required this.state,
|
||||
required this.diaryCubit,
|
||||
});
|
||||
const AddProductModal({super.key, this.portion, required this.state, required this.diaryCubit});
|
||||
|
||||
@override
|
||||
State<AddProductModal> createState() => _AddProductModalState();
|
||||
@@ -58,75 +53,62 @@ class _AddProductModalState extends State<AddProductModal> {
|
||||
return StatefulBuilder(
|
||||
builder: (BuildContext context, void Function(void Function()) setState) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16) +
|
||||
EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
padding: const EdgeInsets.all(16) + EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Добавление блюда',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF323F4B),
|
||||
),
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.8 - 32,
|
||||
width: MediaQuery.of(context).size.width * 0.76 - 32,
|
||||
child: CustomTextFormField(
|
||||
controller: productNameController,
|
||||
autofocus: true,
|
||||
hintText: 'Введите название продукта/блюда',
|
||||
onChanged: (query) {
|
||||
EasyDebounce.debounce(
|
||||
'glossary-search',
|
||||
const Duration(milliseconds: 250),
|
||||
() async {
|
||||
selectedGlossary = null;
|
||||
if (query.isEmpty) {
|
||||
glossariesList = [];
|
||||
setState(() {});
|
||||
return;
|
||||
}
|
||||
glossariesList = await widget.diaryCubit.getGlossaryListByQuery(query);
|
||||
EasyDebounce.debounce('glossary-search', const Duration(milliseconds: 250), () async {
|
||||
selectedGlossary = null;
|
||||
if (query.isEmpty) {
|
||||
glossariesList = [];
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
glossariesList = await widget.diaryCubit.getGlossaryListByQuery(query);
|
||||
setState(() {});
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.2 - 4,
|
||||
width: MediaQuery.of(context).size.width * 0.24 - 4,
|
||||
child: CustomTextFormField(
|
||||
controller: quantityController,
|
||||
keyboardType: TextInputType.number,
|
||||
hintText: selectedGlossary == null
|
||||
? '100'
|
||||
: selectedGlossary!.grInPortion == 0
|
||||
? '100'
|
||||
: '1',
|
||||
? '100'
|
||||
: '1',
|
||||
suffixText: selectedGlossary == null
|
||||
? 'гр'
|
||||
: selectedGlossary?.grInPortion == 0
|
||||
? 'гр'
|
||||
: 'шт',
|
||||
? 'гр'
|
||||
: 'шт',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
glossariesList.isEmpty ? const SizedBox(height: 16) : const SizedBox.shrink(),
|
||||
Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 256,
|
||||
),
|
||||
constraints: const BoxConstraints(maxHeight: 256),
|
||||
child: ListView.separated(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
@@ -141,28 +123,12 @@ class _AddProductModalState extends State<AddProductModal> {
|
||||
setState(() {});
|
||||
FocusManager.instance.primaryFocus?.nextFocus();
|
||||
},
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
title: Text(
|
||||
glossariesList[i].name,
|
||||
style: const TextStyle(
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
title: Text(glossariesList[i].name, style: const TextStyle(height: 1)),
|
||||
subtitle: glossariesList[i].grInPortion == 0
|
||||
? null
|
||||
: Text(
|
||||
'${glossariesList[i].grInPortion} г',
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.add_rounded,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
: Text('${glossariesList[i].grInPortion} г', style: TextStyle(height: 1, color: Colors.grey.shade600)),
|
||||
trailing: Icon(Icons.add_rounded, color: Colors.grey.shade400),
|
||||
contentPadding: const EdgeInsets.only(left: 12),
|
||||
);
|
||||
},
|
||||
@@ -188,14 +154,8 @@ class _AddProductModalState extends State<AddProductModal> {
|
||||
child: Ink(
|
||||
width: 52,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: const Color(0xFFF6F6F6),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.delete_outline_rounded,
|
||||
color: Colors.red,
|
||||
),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFF6F6F6)),
|
||||
child: const Icon(Icons.delete_outline_rounded, color: Colors.red),
|
||||
),
|
||||
),
|
||||
if (widget.portion != null) const SizedBox(width: 8),
|
||||
@@ -209,8 +169,8 @@ class _AddProductModalState extends State<AddProductModal> {
|
||||
portion: selectedGlossary == null
|
||||
? 'gr'
|
||||
: selectedGlossary!.grInPortion == 0
|
||||
? 'gr'
|
||||
: 'pcs',
|
||||
? 'gr'
|
||||
: 'pcs',
|
||||
quantity: int.parse(quantityController.text),
|
||||
productNutritionalValueId: selectedGlossary?.id,
|
||||
);
|
||||
@@ -223,8 +183,8 @@ class _AddProductModalState extends State<AddProductModal> {
|
||||
portion: selectedGlossary == null
|
||||
? 'gr'
|
||||
: selectedGlossary!.grInPortion == 0
|
||||
? 'gr'
|
||||
: 'pcs',
|
||||
? 'gr'
|
||||
: 'pcs',
|
||||
quantity: int.parse(quantityController.text),
|
||||
productNutritionalValueId: selectedGlossary?.id,
|
||||
portionState: widget.portion!.portionState,
|
||||
@@ -237,13 +197,8 @@ class _AddProductModalState extends State<AddProductModal> {
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
builder: (context) => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
child: GlossaryModerationRequestModalWidget(
|
||||
productName: productNameController.text,
|
||||
diaryCubit: widget.diaryCubit,
|
||||
),
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: GlossaryModerationRequestModalWidget(productName: productNameController.text, diaryCubit: widget.diaryCubit),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -252,18 +207,11 @@ class _AddProductModalState extends State<AddProductModal> {
|
||||
child: Ink(
|
||||
width: MediaQuery.of(context).size.width - (widget.portion == null ? 32 : 92),
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: appTheme.blueA400,
|
||||
),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
|
||||
child: Center(
|
||||
child: Text(
|
||||
widget.portion == null ? 'Добавить' : 'Сохранить',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
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();
|
||||
@@ -22,9 +21,7 @@ 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) {
|
||||
@@ -34,55 +31,84 @@ class _MealButtonWidgetState extends State<MealButtonWidget> {
|
||||
} else {
|
||||
bgColor = const Color(0xFFF1F2F6);
|
||||
}
|
||||
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(
|
||||
|
||||
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);
|
||||
},
|
||||
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,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
calories > 0 ? '$calories ккал' : '— ккал',
|
||||
style: const TextStyle(color: Color(0xFF6D6F71), fontSize: 12, fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
if (state is DiaryStateLoadedTomorrowWithDiet) {
|
||||
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(
|
||||
|
||||
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);
|
||||
},
|
||||
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,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
calories > 0 ? '$calories ккал' : '— ккал',
|
||||
style: const TextStyle(color: Color(0xFF6D6F71), fontSize: 12, fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
|
||||
117
lib/features/diary/presentation/widgets/meal_summary_widget.dart
Normal file
117
lib/features/diary/presentation/widgets/meal_summary_widget.dart
Normal file
@@ -0,0 +1,117 @@
|
||||
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MealSummaryWidget extends StatelessWidget {
|
||||
final List<PortionModel> portions;
|
||||
final int mealNumber;
|
||||
|
||||
const MealSummaryWidget({super.key, required this.portions, required this.mealNumber});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final totals = _calculateTotals();
|
||||
|
||||
if (totals['calories'] == 0 && totals['proteins'] == 0 && totals['fats'] == 0 && totals['carbohydrates'] == 0) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0xFF2F80ED), width: 2),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Прием $mealNumber',
|
||||
style: const TextStyle(color: Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
_buildNutritionChip('К', totals['calories']!.round()),
|
||||
const SizedBox(width: 4),
|
||||
_buildNutritionChip('Б', totals['proteins']!.round()),
|
||||
const SizedBox(width: 4),
|
||||
_buildNutritionChip('Ж', totals['fats']!.round()),
|
||||
const SizedBox(width: 4),
|
||||
_buildNutritionChip('У', totals['carbohydrates']!.round()),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNutritionChip(String label, int value) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(color: const Color(0xFFF6FAFF), borderRadius: BorderRadius.circular(12)),
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '$label ',
|
||||
style: const TextStyle(color: Color(0xFF2F80ED), fontSize: 12, fontWeight: FontWeight.w600),
|
||||
),
|
||||
TextSpan(
|
||||
text: '$value',
|
||||
style: const TextStyle(color: Color(0xFF6D6F71), fontSize: 12, fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, double> _calculateTotals() {
|
||||
return calculateMealTotals(portions);
|
||||
}
|
||||
|
||||
static Map<String, double> calculateMealTotals(List<PortionModel> portions) {
|
||||
double totalCalories = 0;
|
||||
double totalProteins = 0;
|
||||
double totalFats = 0;
|
||||
double totalCarbohydrates = 0;
|
||||
|
||||
for (final portion in portions) {
|
||||
if (portion.portionState == PortionState.eatenDeleted) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Для продуктов из диеты учитываем только съеденные (с галочкой)
|
||||
if (portion.fromDiet && portion.portionState != PortionState.eatenFromDiet) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final caloriesInfo = portion.productCaloriesInfo;
|
||||
if (caloriesInfo == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
double calories, proteins, fats, carbohydrates;
|
||||
|
||||
if (portion.portion == 'gr') {
|
||||
calories = (caloriesInfo.calories * portion.quantity) / 100;
|
||||
proteins = (caloriesInfo.proteins * portion.quantity) / 100;
|
||||
fats = (caloriesInfo.fats * portion.quantity) / 100;
|
||||
carbohydrates = (caloriesInfo.carbohydrates * portion.quantity) / 100;
|
||||
} else {
|
||||
calories = caloriesInfo.energyOnPortion;
|
||||
proteins = caloriesInfo.proteinsOnPortion;
|
||||
fats = caloriesInfo.fatsOnPortion;
|
||||
carbohydrates = caloriesInfo.carbohydratesOnPortion;
|
||||
}
|
||||
|
||||
totalCalories += calories;
|
||||
totalProteins += proteins;
|
||||
totalFats += fats;
|
||||
totalCarbohydrates += carbohydrates;
|
||||
}
|
||||
|
||||
return {'calories': totalCalories, 'proteins': totalProteins, 'fats': totalFats, 'carbohydrates': totalCarbohydrates};
|
||||
}
|
||||
}
|
||||
@@ -40,11 +40,7 @@ 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(
|
||||
@@ -57,23 +53,17 @@ class _MealWidgetState extends State<MealWidget> {
|
||||
MealButtonWidget(mealNumber: 5),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: 16),
|
||||
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) {
|
||||
@@ -91,10 +81,7 @@ 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),
|
||||
],
|
||||
@@ -103,7 +90,7 @@ class _MealWidgetState extends State<MealWidget> {
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -23,9 +23,12 @@ class _MoodRatingWidgetState extends State<MoodRatingWidget> {
|
||||
builder: (BuildContext context, DiaryState state) {
|
||||
if (state is DiaryStateLoaded) {
|
||||
final selectedIndex = state.selectedMeal - 1;
|
||||
if (isFirstFetch[selectedIndex]) {
|
||||
currentSelectedRating[selectedIndex] = state.diary.meals[selectedIndex].mood.toDouble() + 1;
|
||||
isFirstFetch[selectedIndex] = false;
|
||||
final meals = state.diary.meals;
|
||||
if (selectedIndex >= 0 && selectedIndex < meals.length) {
|
||||
if (isFirstFetch[selectedIndex]) {
|
||||
currentSelectedRating[selectedIndex] = meals[selectedIndex].mood.toDouble() + 1;
|
||||
isFirstFetch[selectedIndex] = false;
|
||||
}
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 24),
|
||||
@@ -35,21 +38,17 @@ class _MoodRatingWidgetState extends State<MoodRatingWidget> {
|
||||
Text(
|
||||
state.isConsultantView ? 'Настроение пользователя после приема пищи' : 'Ваше настроение после приёма пищи',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
),
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
'assets/icons/mood_1.svg',
|
||||
height: 32,
|
||||
width: 32,
|
||||
),
|
||||
SvgPicture.asset('assets/icons/mood_1.svg', height: 32, width: 32),
|
||||
RatingBar.builder(
|
||||
ignoreGestures: state.isConsultantView || state.isDiaryFinished || state.isDiaryCommented,
|
||||
initialRating: state.diary.meals[selectedIndex].mood.toDouble() + 1,
|
||||
initialRating: (selectedIndex >= 0 && selectedIndex < state.diary.meals.length)
|
||||
? state.diary.meals[selectedIndex].mood.toDouble() + 1
|
||||
: 3,
|
||||
itemCount: 6,
|
||||
itemSize: MediaQuery.of(context).size.width / 6 - (124 / 6),
|
||||
updateOnDrag: true,
|
||||
@@ -63,20 +62,11 @@ class _MoodRatingWidgetState extends State<MoodRatingWidget> {
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFC82222),
|
||||
border: currentSelectedRating[selectedIndex] == 1
|
||||
? Border.all(
|
||||
width: 2,
|
||||
color: const Color(0xFFAA3333),
|
||||
strokeAlign: BorderSide.strokeAlignOutside,
|
||||
)
|
||||
? Border.all(width: 2, color: const Color(0xFFAA3333), strokeAlign: BorderSide.strokeAlignOutside)
|
||||
: null,
|
||||
borderRadius: currentSelectedRating[selectedIndex] == 1
|
||||
? const BorderRadius.horizontal(
|
||||
left: Radius.circular(8),
|
||||
right: Radius.circular(2),
|
||||
)
|
||||
: const BorderRadius.horizontal(
|
||||
left: Radius.circular(8),
|
||||
),
|
||||
? const BorderRadius.horizontal(left: Radius.circular(8), right: Radius.circular(2))
|
||||
: const BorderRadius.horizontal(left: Radius.circular(8)),
|
||||
),
|
||||
);
|
||||
case 1:
|
||||
@@ -87,11 +77,7 @@ class _MoodRatingWidgetState extends State<MoodRatingWidget> {
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFF3E3E),
|
||||
border: currentSelectedRating[selectedIndex] == 2
|
||||
? Border.all(
|
||||
width: 2,
|
||||
color: const Color(0xFF550000),
|
||||
strokeAlign: BorderSide.strokeAlignOutside,
|
||||
)
|
||||
? Border.all(width: 2, color: const Color(0xFF550000), strokeAlign: BorderSide.strokeAlignOutside)
|
||||
: null,
|
||||
borderRadius: currentSelectedRating[selectedIndex] == 2 ? BorderRadius.circular(2) : null,
|
||||
),
|
||||
@@ -104,11 +90,7 @@ class _MoodRatingWidgetState extends State<MoodRatingWidget> {
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8C926),
|
||||
border: currentSelectedRating[selectedIndex] == 3
|
||||
? Border.all(
|
||||
width: 2,
|
||||
color: const Color(0xFF8C5D00),
|
||||
strokeAlign: BorderSide.strokeAlignOutside,
|
||||
)
|
||||
? Border.all(width: 2, color: const Color(0xFF8C5D00), strokeAlign: BorderSide.strokeAlignOutside)
|
||||
: null,
|
||||
borderRadius: currentSelectedRating[selectedIndex] == 3 ? BorderRadius.circular(2) : null,
|
||||
),
|
||||
@@ -121,11 +103,7 @@ class _MoodRatingWidgetState extends State<MoodRatingWidget> {
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFF3BF),
|
||||
border: currentSelectedRating[selectedIndex] == 4
|
||||
? Border.all(
|
||||
width: 2,
|
||||
color: const Color(0xFFB5911B),
|
||||
strokeAlign: BorderSide.strokeAlignOutside,
|
||||
)
|
||||
? Border.all(width: 2, color: const Color(0xFFB5911B), strokeAlign: BorderSide.strokeAlignOutside)
|
||||
: null,
|
||||
borderRadius: currentSelectedRating[selectedIndex] == 4 ? BorderRadius.circular(2) : null,
|
||||
),
|
||||
@@ -138,11 +116,7 @@ class _MoodRatingWidgetState extends State<MoodRatingWidget> {
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF62E833),
|
||||
border: currentSelectedRating[selectedIndex] == 5
|
||||
? Border.all(
|
||||
width: 2,
|
||||
color: const Color(0xFF276918),
|
||||
strokeAlign: BorderSide.strokeAlignOutside,
|
||||
)
|
||||
? Border.all(width: 2, color: const Color(0xFF276918), strokeAlign: BorderSide.strokeAlignOutside)
|
||||
: null,
|
||||
borderRadius: currentSelectedRating[selectedIndex] == 5 ? BorderRadius.circular(2) : null,
|
||||
),
|
||||
@@ -155,20 +129,11 @@ class _MoodRatingWidgetState extends State<MoodRatingWidget> {
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF4DCE20),
|
||||
border: currentSelectedRating[selectedIndex] == 6
|
||||
? Border.all(
|
||||
width: 2,
|
||||
color: const Color(0xFF184D12),
|
||||
strokeAlign: BorderSide.strokeAlignOutside,
|
||||
)
|
||||
? Border.all(width: 2, color: const Color(0xFF184D12), strokeAlign: BorderSide.strokeAlignOutside)
|
||||
: null,
|
||||
borderRadius: currentSelectedRating[selectedIndex] == 6
|
||||
? const BorderRadius.horizontal(
|
||||
left: Radius.circular(2),
|
||||
right: Radius.circular(8),
|
||||
)
|
||||
: const BorderRadius.horizontal(
|
||||
right: Radius.circular(8),
|
||||
),
|
||||
? const BorderRadius.horizontal(left: Radius.circular(2), right: Radius.circular(8))
|
||||
: const BorderRadius.horizontal(right: Radius.circular(8)),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -177,20 +142,12 @@ class _MoodRatingWidgetState extends State<MoodRatingWidget> {
|
||||
setState(() {
|
||||
currentSelectedRating[selectedIndex] = value;
|
||||
});
|
||||
EasyDebounce.debounce(
|
||||
'change-satiety',
|
||||
const Duration(milliseconds: 250),
|
||||
() {
|
||||
context.read<DiaryCubit>().fillMood(diaryId: state.diary.id, mood: currentSelectedRating[selectedIndex].toInt() - 1);
|
||||
},
|
||||
);
|
||||
EasyDebounce.debounce('change-satiety', const Duration(milliseconds: 250), () {
|
||||
context.read<DiaryCubit>().fillMood(diaryId: state.diary.id, mood: currentSelectedRating[selectedIndex].toInt() - 1);
|
||||
});
|
||||
},
|
||||
),
|
||||
SvgPicture.asset(
|
||||
'assets/icons/mood_2.svg',
|
||||
height: 32,
|
||||
width: 32,
|
||||
),
|
||||
SvgPicture.asset('assets/icons/mood_2.svg', height: 32, width: 32),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:dotted_border/dotted_border.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
@@ -15,10 +16,7 @@ import 'package:flutter_svg/flutter_svg.dart';
|
||||
class ProductWidget extends StatefulWidget {
|
||||
final PortionModel portion;
|
||||
|
||||
const ProductWidget({
|
||||
super.key,
|
||||
required this.portion,
|
||||
});
|
||||
const ProductWidget({super.key, required this.portion});
|
||||
|
||||
@override
|
||||
State<ProductWidget> createState() => _ProductWidgetState();
|
||||
@@ -37,6 +35,60 @@ 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);
|
||||
@@ -48,15 +100,9 @@ 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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,15 +115,21 @@ 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: Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'),
|
||||
const SizedBox(height: 4),
|
||||
_buildNutritionalInfo(),
|
||||
],
|
||||
),
|
||||
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 =
|
||||
@@ -99,21 +151,20 @@ 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: Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'),
|
||||
const SizedBox(height: 4),
|
||||
_buildNutritionalInfo(),
|
||||
],
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
// ignore: use_full_hex_values_for_flutter_colors
|
||||
tileColor: const Color(0xfff6faff03).withValues(alpha: 0.01),
|
||||
),
|
||||
@@ -122,12 +173,13 @@ 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);
|
||||
}
|
||||
@@ -147,47 +199,36 @@ 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: Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'),
|
||||
const SizedBox(height: 4),
|
||||
_buildNutritionalInfo(),
|
||||
],
|
||||
),
|
||||
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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/widgets/meal_summary_widget.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/widgets/product_loader_widget.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/widgets/product_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -16,33 +17,33 @@ class _ProductsListWidgetState extends State<ProductsListWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<DiaryCubit, DiaryState>(
|
||||
builder: (BuildContext context, DiaryState state) {
|
||||
builder: (BuildContext context, DiaryState state) {
|
||||
if (state is DiaryStateLoaded) {
|
||||
int mealIndex = state.selectedMeal - 1;
|
||||
final portions = state.diary.meals[mealIndex].portions;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (int i = 0; i < state.diary.meals[mealIndex].portions.length; i++) ...[
|
||||
ProductWidget(portion: state.diary.meals[mealIndex].portions[i]),
|
||||
i < state.diary.meals[mealIndex].portions.length - 1 ? const SizedBox(height: 8) : const SizedBox.shrink(),
|
||||
for (int i = 0; i < portions.length; i++) ...[
|
||||
ProductWidget(portion: portions[i]),
|
||||
i < portions.length - 1 ? const SizedBox(height: 8) : const SizedBox.shrink(),
|
||||
],
|
||||
if (state.isLoadingAddingProductInMeal)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 8),
|
||||
child: ProductLoaderWidget(),
|
||||
),
|
||||
if (portions.isNotEmpty) MealSummaryWidget(portions: portions, mealNumber: state.selectedMeal),
|
||||
if (state.isLoadingAddingProductInMeal) const Padding(padding: EdgeInsets.only(top: 8), child: ProductLoaderWidget()),
|
||||
],
|
||||
);
|
||||
}
|
||||
if (state is DiaryStateLoadedTomorrowWithDiet) {
|
||||
int mealIndex = state.selectedMeal - 1;
|
||||
final portions = state.diary.meals[mealIndex].portions;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (int i = 0; i < state.diary.meals[mealIndex].portions.length; i++) ...[
|
||||
ProductWidget(portion: state.diary.meals[mealIndex].portions[i]),
|
||||
i < state.diary.meals[mealIndex].portions.length - 1 ? const SizedBox(height: 8) : const SizedBox.shrink(),
|
||||
for (int i = 0; i < portions.length; i++) ...[
|
||||
ProductWidget(portion: portions[i]),
|
||||
i < portions.length - 1 ? const SizedBox(height: 8) : const SizedBox.shrink(),
|
||||
],
|
||||
if (portions.isNotEmpty) MealSummaryWidget(portions: portions, mealNumber: state.selectedMeal),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,9 +23,12 @@ class _SatietyRatingWidgetState extends State<SatietyRatingWidget> {
|
||||
builder: (BuildContext context, DiaryState state) {
|
||||
if (state is DiaryStateLoaded) {
|
||||
final selectedIndex = state.selectedMeal - 1;
|
||||
if (isFirstFetch[selectedIndex]) {
|
||||
currentSelectedRating[selectedIndex] = state.diary.meals[selectedIndex].satiety.toDouble() + 1;
|
||||
isFirstFetch[selectedIndex] = false;
|
||||
final meals = state.diary.meals;
|
||||
if (selectedIndex >= 0 && selectedIndex < meals.length) {
|
||||
if (isFirstFetch[selectedIndex]) {
|
||||
currentSelectedRating[selectedIndex] = meals[selectedIndex].satiety.toDouble() + 1;
|
||||
isFirstFetch[selectedIndex] = false;
|
||||
}
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 24),
|
||||
@@ -35,21 +38,17 @@ class _SatietyRatingWidgetState extends State<SatietyRatingWidget> {
|
||||
Text(
|
||||
state.isConsultantView ? 'Сытость пользователя после приема пищи' : 'Ваша сытость после приёма пищи',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
),
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
'assets/icons/satiety_emoji_left.svg',
|
||||
height: 32,
|
||||
width: 32,
|
||||
),
|
||||
SvgPicture.asset('assets/icons/satiety_emoji_left.svg', height: 32, width: 32),
|
||||
RatingBar.builder(
|
||||
ignoreGestures: state.isConsultantView || state.isDiaryFinished || state.isDiaryCommented,
|
||||
initialRating: state.diary.meals[selectedIndex].satiety.toDouble() + 1,
|
||||
initialRating: (selectedIndex >= 0 && selectedIndex < state.diary.meals.length)
|
||||
? state.diary.meals[selectedIndex].satiety.toDouble() + 1
|
||||
: 3,
|
||||
itemCount: 6,
|
||||
itemSize: MediaQuery.of(context).size.width / 6 - (124 / 6),
|
||||
updateOnDrag: true,
|
||||
@@ -63,20 +62,11 @@ class _SatietyRatingWidgetState extends State<SatietyRatingWidget> {
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFC82222),
|
||||
border: currentSelectedRating[selectedIndex] == 1
|
||||
? Border.all(
|
||||
width: 2,
|
||||
color: const Color(0xFFAA3333),
|
||||
strokeAlign: BorderSide.strokeAlignOutside,
|
||||
)
|
||||
? Border.all(width: 2, color: const Color(0xFFAA3333), strokeAlign: BorderSide.strokeAlignOutside)
|
||||
: null,
|
||||
borderRadius: currentSelectedRating[selectedIndex] == 1
|
||||
? const BorderRadius.horizontal(
|
||||
left: Radius.circular(8),
|
||||
right: Radius.circular(2),
|
||||
)
|
||||
: const BorderRadius.horizontal(
|
||||
left: Radius.circular(8),
|
||||
),
|
||||
? const BorderRadius.horizontal(left: Radius.circular(8), right: Radius.circular(2))
|
||||
: const BorderRadius.horizontal(left: Radius.circular(8)),
|
||||
),
|
||||
);
|
||||
case 1:
|
||||
@@ -87,11 +77,7 @@ class _SatietyRatingWidgetState extends State<SatietyRatingWidget> {
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFF3E3E),
|
||||
border: currentSelectedRating[selectedIndex] == 2
|
||||
? Border.all(
|
||||
width: 2,
|
||||
color: const Color(0xFF550000),
|
||||
strokeAlign: BorderSide.strokeAlignOutside,
|
||||
)
|
||||
? Border.all(width: 2, color: const Color(0xFF550000), strokeAlign: BorderSide.strokeAlignOutside)
|
||||
: null,
|
||||
borderRadius: currentSelectedRating[selectedIndex] == 2 ? BorderRadius.circular(2) : null,
|
||||
),
|
||||
@@ -104,11 +90,7 @@ class _SatietyRatingWidgetState extends State<SatietyRatingWidget> {
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8C926),
|
||||
border: currentSelectedRating[selectedIndex] == 3
|
||||
? Border.all(
|
||||
width: 2,
|
||||
color: const Color(0xFF8C5D00),
|
||||
strokeAlign: BorderSide.strokeAlignOutside,
|
||||
)
|
||||
? Border.all(width: 2, color: const Color(0xFF8C5D00), strokeAlign: BorderSide.strokeAlignOutside)
|
||||
: null,
|
||||
borderRadius: currentSelectedRating[selectedIndex] == 3 ? BorderRadius.circular(2) : null,
|
||||
),
|
||||
@@ -121,11 +103,7 @@ class _SatietyRatingWidgetState extends State<SatietyRatingWidget> {
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFF3BF),
|
||||
border: currentSelectedRating[selectedIndex] == 4
|
||||
? Border.all(
|
||||
width: 2,
|
||||
color: const Color(0xFFB5911B),
|
||||
strokeAlign: BorderSide.strokeAlignOutside,
|
||||
)
|
||||
? Border.all(width: 2, color: const Color(0xFFB5911B), strokeAlign: BorderSide.strokeAlignOutside)
|
||||
: null,
|
||||
borderRadius: currentSelectedRating[selectedIndex] == 4 ? BorderRadius.circular(2) : null,
|
||||
),
|
||||
@@ -138,11 +116,7 @@ class _SatietyRatingWidgetState extends State<SatietyRatingWidget> {
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF62E833),
|
||||
border: currentSelectedRating[selectedIndex] == 5
|
||||
? Border.all(
|
||||
width: 2,
|
||||
color: const Color(0xFF276918),
|
||||
strokeAlign: BorderSide.strokeAlignOutside,
|
||||
)
|
||||
? Border.all(width: 2, color: const Color(0xFF276918), strokeAlign: BorderSide.strokeAlignOutside)
|
||||
: null,
|
||||
borderRadius: currentSelectedRating[selectedIndex] == 5 ? BorderRadius.circular(2) : null,
|
||||
),
|
||||
@@ -155,20 +129,11 @@ class _SatietyRatingWidgetState extends State<SatietyRatingWidget> {
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF4DCE20),
|
||||
border: currentSelectedRating[selectedIndex] == 6
|
||||
? Border.all(
|
||||
width: 2,
|
||||
color: const Color(0xFF184D12),
|
||||
strokeAlign: BorderSide.strokeAlignOutside,
|
||||
)
|
||||
? Border.all(width: 2, color: const Color(0xFF184D12), strokeAlign: BorderSide.strokeAlignOutside)
|
||||
: null,
|
||||
borderRadius: currentSelectedRating[selectedIndex] == 6
|
||||
? const BorderRadius.horizontal(
|
||||
left: Radius.circular(2),
|
||||
right: Radius.circular(8),
|
||||
)
|
||||
: const BorderRadius.horizontal(
|
||||
right: Radius.circular(8),
|
||||
),
|
||||
? const BorderRadius.horizontal(left: Radius.circular(2), right: Radius.circular(8))
|
||||
: const BorderRadius.horizontal(right: Radius.circular(8)),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -178,20 +143,12 @@ class _SatietyRatingWidgetState extends State<SatietyRatingWidget> {
|
||||
setState(() {
|
||||
currentSelectedRating[selectedIndex] = value;
|
||||
});
|
||||
EasyDebounce.debounce(
|
||||
'change-satiety',
|
||||
const Duration(milliseconds: 250),
|
||||
() {
|
||||
context.read<DiaryCubit>().fillSatiety(diaryId: state.diary.id, satiety: currentSelectedRating[selectedIndex].toInt() - 1);
|
||||
},
|
||||
);
|
||||
EasyDebounce.debounce('change-satiety', const Duration(milliseconds: 250), () {
|
||||
context.read<DiaryCubit>().fillSatiety(diaryId: state.diary.id, satiety: currentSelectedRating[selectedIndex].toInt() - 1);
|
||||
});
|
||||
},
|
||||
),
|
||||
SvgPicture.asset(
|
||||
'assets/icons/satiety_emoji_right.svg',
|
||||
height: 32,
|
||||
width: 32,
|
||||
),
|
||||
SvgPicture.asset('assets/icons/satiety_emoji_right.svg', height: 32, width: 32),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user