Merge remote-tracking branch 'upstream/main' into personalDiets

This commit is contained in:
misha
2025-10-02 12:25:40 +03:00
76 changed files with 2058 additions and 1732 deletions

5
.gitignore vendored
View File

@@ -16,4 +16,7 @@
*.mocks.dart
android/build/
ios/XCBuildData/
env/.env
env/.env
.env
android/key.properties
app/upload-keystore.jks

View 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"}}]}

View File

@@ -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
@@ -33,6 +36,10 @@ android {
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
View 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 * { *; }

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,6 +12,7 @@ pluginManagement {
repositories {
google()
mavenCentral()
maven { url 'https://developer.huawei.com/repo/' }
gradlePluginPortal()
}
}

View File

@@ -388,7 +388,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 71;
CURRENT_PROJECT_VERSION = 74;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -410,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";
@@ -538,7 +539,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 71;
CURRENT_PROJECT_VERSION = 74;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -560,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;
@@ -576,7 +578,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 71;
CURRENT_PROJECT_VERSION = 74;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -598,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";

View File

@@ -21,7 +21,7 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>71</string>
<string>74</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>

View File

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

View File

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

View File

@@ -5,6 +5,9 @@ import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/dio/phone_verify_error_interceptor.dart';
import 'package:eatvio_app/core/firebase_api.dart';
import 'package:eatvio_app/core/interceptors/token_interceptor.dart';
import 'package:eatvio_app/core/views/cubit/views_cubit.dart';
import 'package:eatvio_app/core/views/repository/views_repository.dart';
import 'package:eatvio_app/core/views/service/views_service.dart';
import 'package:eatvio_app/core/websockets/ws_manager.dart';
import 'package:eatvio_app/core/websockets/ws_service.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart';
@@ -140,7 +143,7 @@ Future<void> init() async {
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<SettingsCubit>(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl(), consultantRepository: sl(), consultationsRepository: sl()));
sl.registerFactory<RegisterConsultantCubit>(() => RegisterConsultantCubit(consultantRepository: sl()));
sl.registerFactory<DiaryCubit>(
() => DiaryCubit(
@@ -183,6 +186,7 @@ Future<void> init() async {
sl.registerFactory<NotificationSettingsCubit>(() => NotificationSettingsCubit(notificationSettingsRepository: sl()));
sl.registerFactory<ConsultantRatingCubit>(() => ConsultantRatingCubit(sl()));
sl.registerFactory<PersonalDietSuggestionCubit>(() => PersonalDietSuggestionCubit(dietRepository: sl(), personalDietSuggestionService: sl()));
sl.registerFactory<ViewsCubit>(() => ViewsCubit(viewsRepository: sl()));
// Repository
sl.registerLazySingleton<ChatRepository>(() => ChatRepository(chatService: sl()));
@@ -217,6 +221,7 @@ Future<void> init() async {
sl.registerLazySingleton<NotificationSettingsRepository>(() => NotificationSettingsRepository(sl()));
sl.registerFactory<ConsultantRatingRepository>(() => ConsultantRatingRepository(sl()));
sl.registerLazySingleton<PersonalDietSuggestionRepository>(() => PersonalDietSuggestionRepository(personalDietSuggestionService: sl()));
sl.registerFactory<ViewsRepository>(() => ViewsRepository(viewsService: sl()));
// Services
sl.registerLazySingleton<ChatService>(() => ChatService(sl()));
@@ -253,6 +258,7 @@ Future<void> init() async {
sl.registerLazySingleton<ConsultantRatingService>(() => ConsultantRatingService(sl()));
sl.registerLazySingleton<NotificationSettingsService>(() => NotificationSettingsService(sl()));
sl.registerLazySingleton<PersonalDietSuggestionService>(() => PersonalDietSuggestionService(sl()));
sl.registerLazySingleton<ViewsService>(() => ViewsService(sl()));
// External
sl.registerLazySingleton(

View File

@@ -204,6 +204,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();

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

View File

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

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

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

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

View File

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

View File

@@ -1,3 +1,5 @@
// ignore_for_file: unused_element, duplicate_ignore
import 'dart:async';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/const.dart';
@@ -24,6 +26,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});
@@ -165,6 +168,52 @@ class _AuthScreenState extends State<AuthScreen> {
),
],
),
// const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_SocialButton(
onTap: () => _onVkPressed(context),
backgroundColor: const Color(0xFF2787F5),
tooltip: 'VK ID',
child: SvgPicture.asset(
'assets/icons/vk.svg',
height: 24,
width: 24,
colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn),
),
),
_SocialButton(
onTap: _onOkPressed,
backgroundColor: const Color(0xFFF58220),
tooltip: 'Одноклассники',
child: SvgPicture.asset(
'assets/icons/ok.svg',
height: 24,
width: 24,
colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn),
),
),
const _SocialButton(
onTap: _onMailRuPressed,
backgroundColor: Color(0xFF005FF9),
tooltip: 'Mail.ru',
child: Text(
'Mail',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700),
),
),
const _SocialButton(
onTap: _onYandexPressed,
backgroundColor: Color(0xFFFFCC00),
tooltip: 'Яндекс ID',
child: Text(
'Yandex',
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w700),
),
),
],
),
const Spacer(),
Center(
child: RichText(
@@ -317,3 +366,88 @@ class _AuthScreenState extends State<AuthScreen> {
);
}
}
// ignore: unused_element
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(12),
onTap: onTap,
child: Ink(
height: 48,
width: (MediaQuery.of(context).size.width - 16 * 2 - 12 * 3) / 4,
decoration: BoxDecoration(color: backgroundColor, borderRadius: BorderRadius.circular(12)),
child: Center(child: child),
),
),
);
}
}
// ignore: unused_element
void _onVkPressed(BuildContext context) async {
final accessToken = await SocialAuthFacade.signInWithVk(context: context);
if (accessToken == null) return;
final isAuthenticated = await sl<AuthCubit>().getTokenBySocial(provider: 'vk', accessToken: accessToken);
if (isAuthenticated) {
if (AppRouter.navigatorKey.currentContext != null) {
// Reuse success flow
// ignore: use_build_context_synchronously
await _reconnectAfterAuth();
}
}
}
void _onOkPressed() async {
final accessToken = await SocialAuthFacade.signInWithOk();
if (accessToken == null) return;
final isAuthenticated = await sl<AuthCubit>().getTokenBySocial(provider: 'ok', accessToken: accessToken);
if (isAuthenticated) {
await _reconnectAfterAuth();
}
}
void _onMailRuPressed() async {
final accessToken = await SocialAuthFacade.signInWithMailRu();
if (accessToken == null) return;
final isAuthenticated = await sl<AuthCubit>().getTokenBySocial(provider: 'mailru', accessToken: accessToken);
if (isAuthenticated) {
await _reconnectAfterAuth();
}
}
void _onYandexPressed() async {
final accessToken = await SocialAuthFacade.signInWithYandex();
if (accessToken == null) return;
final isAuthenticated = await sl<AuthCubit>().getTokenBySocial(provider: 'yandex', accessToken: accessToken);
if (isAuthenticated) {
await _reconnectAfterAuth();
}
}
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});
}

View File

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

View File

@@ -0,0 +1,157 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_login_yandex/flutter_login_yandex.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:vk_id/vk_id.dart';
import 'package:webview_flutter/webview_flutter.dart';
class VkAuthResult {
VkAuthResult({required this.accessToken, this.refreshToken, this.userId, this.firstName, this.lastName, this.email, this.phone, this.gender, this.birthDate});
final String accessToken;
final String? refreshToken;
final String? userId;
final String? firstName;
final String? lastName;
final String? email;
final String? phone;
final String? gender;
final String? birthDate;
}
class SocialAuthFacade {
static Future<String?> signInWithVk({required BuildContext context}) async {
final result = await signInWithVkAndFetchProfile(context: context);
return result?.accessToken;
}
static Future<VkAuthResult?> signInWithVkAndFetchProfile({required BuildContext context}) async {
final clientIdStr = dotenv.maybeGet('VK_CLIENT_ID')?.trim() ?? Platform.environment['VK_CLIENT_ID'];
if (clientIdStr == null || clientIdStr.isEmpty) {
return null;
}
try {
dynamic controller = VkIDController(clID: int.parse(clientIdStr));
final authUri = controller.generateAuthorizeLinkWithCodeVerifier();
final redirectScheme = 'vk$clientIdStr://';
final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUri.toString(), redirectSchemePrefix: redirectScheme);
if (redirectParams == null) return null;
final code = redirectParams['code'];
final deviceId = redirectParams['device_id'];
if (code == null || deviceId == null) return null;
final tokenResponse = await controller.exchangeAuthorizationCodeForToken(code: code, deviceId: deviceId);
final accessToken = _extractAccessToken(tokenResponse);
if (accessToken == null) return null;
return VkAuthResult(accessToken: accessToken);
} catch (_) {
return null;
}
}
static Future<String?> signInWithOk() async {
final context = AppRouter.navigatorKey.currentContext;
if (context == null) return null;
return _signInWithVkIdProvider(context: context, provider: 'ok');
}
static Future<String?> signInWithMailRu() async {
final context = AppRouter.navigatorKey.currentContext;
if (context == null) return null;
return _signInWithVkIdProvider(context: context, provider: 'mail');
}
static Future<String?> signInWithYandex() async {
final clientId = dotenv.maybeGet('YANDEX_CLIENT_ID')?.trim() ?? Platform.environment['YANDEX_CLIENT_ID'];
final redirectUri = dotenv.maybeGet('YANDEX_REDIRECT_URI')?.trim() ?? Platform.environment['YANDEX_REDIRECT_URI'];
if (clientId == null || clientId.isEmpty || redirectUri == null || redirectUri.isEmpty) {
return null;
}
try {
final plugin = FlutterLoginYandex();
final response = await plugin.signIn();
final token = (response is Map) ? response['token'] as String? : null;
return (token != null && token.isNotEmpty) ? token : null;
} catch (_) {
return null;
}
}
static Future<String?> _signInWithVkIdProvider({required BuildContext context, required String provider}) async {
final clientIdStr = dotenv.maybeGet('VK_CLIENT_ID')?.trim() ?? Platform.environment['VK_CLIENT_ID'];
if (clientIdStr == null || clientIdStr.isEmpty) {
return null;
}
try {
dynamic controller = VkIDController(clID: int.parse(clientIdStr));
final authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider);
final redirectScheme = 'vk$clientIdStr://';
final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUri.toString(), redirectSchemePrefix: redirectScheme);
if (redirectParams == null) return null;
final code = redirectParams['code'];
final deviceId = redirectParams['device_id'];
if (code == null || deviceId == null) return null;
final tokenResponse = await controller.exchangeAuthorizationCodeForToken(code: code, deviceId: deviceId, provider: provider);
final accessToken = _extractAccessToken(tokenResponse);
return accessToken;
} catch (_) {
return null;
}
}
static String? _extractAccessToken(dynamic tokenResponse) {
if (tokenResponse == null) return null;
if (tokenResponse is Map) {
return (tokenResponse['access_token'] as String?) ?? (tokenResponse['token'] as String?);
}
try {
final token = (tokenResponse as dynamic).accessToken as String?;
if (token != null && token.isNotEmpty) return token;
} catch (_) {}
try {
final token = (tokenResponse as dynamic).token as String?;
if (token != null && token.isNotEmpty) return token;
} catch (_) {}
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);
Navigator.of(context, rootNavigator: true).pop(params);
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
),
)
..loadRequest(Uri.parse(initialUrl));
return showDialog<Map<String, String>>(
context: context,
barrierDismissible: false,
builder: (_) {
return Dialog(
insetPadding: const EdgeInsets.all(16),
child: SizedBox(height: 560, child: WebViewWidget(controller: controller)),
);
},
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,13 +15,55 @@ class DiaryRepository {
Stream<String> get forceFetchStream => _diaryForceFetchController.stream;
DiaryRepository({
required this.diaryService,
});
DiaryRepository({required this.diaryService});
Future<DiaryDayModel> getDiaryByDate(String date) async {
try {
return (await getDiaryIndexByDate(date)).last;
Set<int> ids = {};
final DiaryDayModel diaryDay = (await getDiaryIndexByDate(date)).last;
for (var meal in diaryDay.meals) {
for (var portion in meal.portions) {
if (portion.productNutritionalValueId != null) {
ids.add(portion.productNutritionalValueId!);
}
}
}
if (ids.isNotEmpty) {
final glossary = await getGlossariesIndex(ids: ids.toList());
final List<MealModel> updatedMeals = List.from(diaryDay.meals);
for (var mealIndex = 0; mealIndex < updatedMeals.length; mealIndex++) {
final meal = updatedMeals[mealIndex];
final updatedPortions = meal.portions.map((portion) {
if (portion.productNutritionalValueId != null) {
try {
final glossaryItem = glossary.glossariesList.firstWhere((g) => g.id == portion.productNutritionalValueId);
return portion.copyWith(
productCaloriesInfo: ProductCaloriesInfoModel(
id: glossaryItem.id,
productName: glossaryItem.name,
calories: glossaryItem.energyOn100Gr,
proteins: glossaryItem.proteinsOn100Gr,
fats: glossaryItem.fatsOn100Gr,
carbohydrates: glossaryItem.carbohydratesOn100Gr,
energyOnPortion: glossaryItem.energyOnPortion,
proteinsOnPortion: glossaryItem.proteinsOnPortion,
fatsOnPortion: glossaryItem.fatsOnPortion,
carbohydratesOnPortion: glossaryItem.carbohydratesOnPortion,
),
);
} catch (e) {
return portion;
}
}
return portion;
}).toList();
updatedMeals[mealIndex] = meal.copyWith(portions: updatedPortions);
}
final updatedDiaryDay = diaryDay.copyWith(meals: updatedMeals);
return updatedDiaryDay;
}
return diaryDay;
} catch (e) {
rethrow;
}
@@ -40,7 +82,53 @@ class DiaryRepository {
}
Future<DiaryDayModel> getDiaryDay(int diaryId) async {
return (await diaryService.getDiaryDay(diary: diaryId)).diary;
// Align with getDiaryByDate: enrich portions with glossary nutritional info
final diaryWrapper = await diaryService.getDiaryDay(diary: diaryId);
final DiaryDayModel diaryDay = diaryWrapper.diary;
Set<int> ids = {};
for (var meal in diaryDay.meals) {
for (var portion in meal.portions) {
if (portion.productNutritionalValueId != null) {
ids.add(portion.productNutritionalValueId!);
}
}
}
if (ids.isNotEmpty) {
final glossary = await getGlossariesIndex(ids: ids.toList());
final List<MealModel> updatedMeals = List.from(diaryDay.meals);
for (var mealIndex = 0; mealIndex < updatedMeals.length; mealIndex++) {
final meal = updatedMeals[mealIndex];
final updatedPortions = meal.portions.map((portion) {
if (portion.productNutritionalValueId != null) {
try {
final glossaryItem = glossary.glossariesList.firstWhere((g) => g.id == portion.productNutritionalValueId);
return portion.copyWith(
productCaloriesInfo: ProductCaloriesInfoModel(
id: glossaryItem.id,
productName: glossaryItem.name,
calories: glossaryItem.energyOn100Gr,
proteins: glossaryItem.proteinsOn100Gr,
fats: glossaryItem.fatsOn100Gr,
carbohydrates: glossaryItem.carbohydratesOn100Gr,
energyOnPortion: glossaryItem.energyOnPortion,
proteinsOnPortion: glossaryItem.proteinsOnPortion,
fatsOnPortion: glossaryItem.fatsOnPortion,
carbohydratesOnPortion: glossaryItem.carbohydratesOnPortion,
),
);
} catch (e) {
return portion;
}
}
return portion;
}).toList();
updatedMeals[mealIndex] = meal.copyWith(portions: updatedPortions);
}
final updatedDiaryDay = diaryDay.copyWith(meals: updatedMeals);
return updatedDiaryDay;
}
return diaryDay;
}
Future<NutritionDataModel> getNutritionData(int index) async {
@@ -75,109 +163,47 @@ class DiaryRepository {
int? productNutritionalValueId,
required PortionState portionState,
}) async {
Map<String, Object?> data = ProductInMeal(
productName: productName,
portion: portion,
quantity: quantity,
).toJson();
Map<String, Object?> data = ProductInMeal(productName: productName, portion: portion, quantity: quantity).toJson();
data['product_nutritional_value_id'] = productNutritionalValueId;
data['state'] = portionState.name;
return await diaryService.updateProductInMeal(
diary: diaryId,
typeNo: mealId,
portionModel: portionId,
productInMeal: data,
);
return await diaryService.updateProductInMeal(diary: diaryId, typeNo: mealId, portionModel: portionId, productInMeal: data);
}
Future<void> markEaten({
required int diaryId,
required int mealId,
required int portionId,
}) async {
return await diaryService.markEaten(
diary: diaryId,
typeNo: mealId,
portionModel: portionId,
);
Future<void> markEaten({required int diaryId, required int mealId, required int portionId}) async {
return await diaryService.markEaten(diary: diaryId, typeNo: mealId, portionModel: portionId);
}
Future<void> unmarkEaten({
required int diaryId,
required int mealId,
required int portionId,
}) async {
return await diaryService.unmarkEaten(
diary: diaryId,
typeNo: mealId,
portionModel: portionId,
);
Future<void> unmarkEaten({required int diaryId, required int mealId, required int portionId}) async {
return await diaryService.unmarkEaten(diary: diaryId, typeNo: mealId, portionModel: portionId);
}
Future<void> restore({
required int diaryId,
required int mealId,
required int portionId,
}) async {
return await diaryService.restore(
diary: diaryId,
typeNo: mealId,
portionModel: portionId,
);
Future<void> restore({required int diaryId, required int mealId, required int portionId}) async {
return await diaryService.restore(diary: diaryId, typeNo: mealId, portionModel: portionId);
}
Future<void> deleteProductFromMeal({
required int diaryId,
required int mealId,
required int portionId,
}) async {
return await diaryService.deleteProductFromMeal(
diary: diaryId,
typeNo: mealId,
portionModel: portionId,
);
Future<void> deleteProductFromMeal({required int diaryId, required int mealId, required int portionId}) async {
return await diaryService.deleteProductFromMeal(diary: diaryId, typeNo: mealId, portionModel: portionId);
}
Future<void> uploadPhotos({
required int diaryId,
required int mealId,
required List<File> files,
}) async {
Future<void> uploadPhotos({required int diaryId, required int mealId, required List<File> files}) async {
try {
for (int i = 0; i < files.length; i++) {
await diaryService.uploadPhotoOfMeal(
diary: diaryId,
typeNo: mealId,
photo: files[i],
);
await diaryService.uploadPhotoOfMeal(diary: diaryId, typeNo: mealId, photo: files[i]);
}
} catch (e) {
rethrow;
}
}
Future<void> deletePhoto({
required int diaryId,
required int mealId,
required DiaryPhotoModel photo,
}) async {
Future<void> deletePhoto({required int diaryId, required int mealId, required DiaryPhotoModel photo}) async {
try {
await diaryService.deletePhotoOfMeal(
diaryId: diaryId,
typeNo: mealId,
userAttachment: photo.id,
);
await diaryService.deletePhotoOfMeal(diaryId: diaryId, typeNo: mealId, userAttachment: photo.id);
} catch (e) {
rethrow;
}
}
Future<void> finishDiaryDay({
required int diaryId,
required String wakeUp,
required String retire,
String? comment,
}) async {
Future<void> finishDiaryDay({required int diaryId, required String wakeUp, required String retire, String? comment}) async {
return await diaryService.finishDiaryDay(
diary: diaryId,
finishDiaryDayModel: FinishDiaryDayModel(wakeUp: wakeUp, retire: retire, comment: comment),
@@ -192,87 +218,45 @@ class DiaryRepository {
required double carbohydrates,
}) async {
return await diaryService.sendProductToModeration(
data: ProductModerationModel(
name: name,
energy: energy,
proteins: proteins,
fats: fats,
carbohydrates: carbohydrates,
).toJson(),
data: ProductModerationModel(name: name, energy: energy, proteins: proteins, fats: fats, carbohydrates: carbohydrates).toJson(),
);
}
Future<void> fillSatiety({
required int diaryId,
required int mealId,
required int satiety,
}) async {
Future<void> fillSatiety({required int diaryId, required int mealId, required int satiety}) async {
try {
return await diaryService.fillSatiety(
diary: diaryId,
typeNo: mealId,
satiety: satiety,
);
return await diaryService.fillSatiety(diary: diaryId, typeNo: mealId, satiety: satiety);
} catch (e) {
rethrow;
}
}
Future<void> fillMood({
required int diaryId,
required int mealId,
required int mood,
}) async {
Future<void> fillMood({required int diaryId, required int mealId, required int mood}) async {
try {
return await diaryService.fillMood(
diary: diaryId,
typeNo: mealId,
mood: mood,
);
return await diaryService.fillMood(diary: diaryId, typeNo: mealId, mood: mood);
} catch (e) {
rethrow;
}
}
Future<void> fillTime({
required int diaryId,
required int mealId,
required String time,
}) async {
Future<void> fillTime({required int diaryId, required int mealId, required String time}) async {
try {
return await diaryService.fillTime(
diary: diaryId,
typeNo: mealId,
time: time,
);
return await diaryService.fillTime(diary: diaryId, typeNo: mealId, time: time);
} catch (e) {
rethrow;
}
}
Future<void> fillWakeup({
required int diaryId,
required String time,
}) async {
Future<void> fillWakeup({required int diaryId, required String time}) async {
try {
return await diaryService.fillWakeup(
diary: diaryId,
wakeUp: time,
);
return await diaryService.fillWakeup(diary: diaryId, wakeUp: time);
} catch (e) {
rethrow;
}
}
Future<void> fillRetire({
required int diaryId,
required String time,
}) async {
Future<void> fillRetire({required int diaryId, required String time}) async {
try {
return await diaryService.fillRetire(
diary: diaryId,
retire: time,
);
return await diaryService.fillRetire(diary: diaryId, retire: time);
} catch (e) {
rethrow;
}
@@ -287,10 +271,7 @@ class DiaryRepository {
}
}
Future<ConsultantReviewModel> getDiaryComments({
required int planId,
required int diaryId,
}) async {
Future<ConsultantReviewModel> getDiaryComments({required int planId, required int diaryId}) async {
try {
return await diaryService.getDiaryComments(planId: planId, diaryId: diaryId);
} catch (e) {
@@ -305,4 +286,18 @@ class DiaryRepository {
rethrow;
}
}
Future<GlossaryPaginationModel> getGlossariesIndex({required List<int> ids}) async {
Map<String, dynamic> filters = {};
if (ids.isNotEmpty) {
for (int i = 0; i < ids.length; i++) {
filters['filters[id][\$in][$i]'] = ids[i];
}
}
try {
return await diaryService.getGlossariesIndex(filters: filters);
} catch (e) {
rethrow;
}
}
}

View File

@@ -15,32 +15,20 @@ abstract class DiaryService {
factory DiaryService(Dio dio, {String baseUrl}) = _DiaryService;
@GET('/api/diary/index/{date}')
Future<List<DiaryDayModel>> getDiaryIndexByDate({
@Path('date') required String date,
});
Future<List<DiaryDayModel>> getDiaryIndexByDate({@Path('date') required String date});
@GET('/api/diary/index/{date}')
Future<List<DiaryFutureDietDayModel>> getDiaryIndexByDateForDietOnly({
@Path('date') required String date,
});
Future<List<DiaryFutureDietDayModel>> getDiaryIndexByDateForDietOnly({@Path('date') required String date});
@GET('/api/diary/{diary}/show')
Future<DiaryDayWrapperModel> getDiaryDay({
@Path('diary') required int diary,
});
Future<DiaryDayWrapperModel> getDiaryDay({@Path('diary') required int diary});
@GET('/api/diary/{diary}/dailyNutritionData')
Future<NutritionDataModel> getNutrition({
@Path('diary') required int diary,
});
Future<NutritionDataModel> getNutrition({@Path('diary') required int diary});
@POST('/api/diary/{diary}/meals/{type_no}/photo/upload')
@MultiPart()
Future<void> uploadPhotoOfMeal({
@Path('diary') required int diary,
@Path('type_no') required int typeNo,
@Part(name: 'photo') required File photo,
});
Future<void> uploadPhotoOfMeal({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Part(name: 'photo') required File photo});
@DELETE('/api/diary/{diary}/meals/{type_no}/photo/{user_attachment}/delete')
Future<void> deletePhotoOfMeal({
@@ -68,25 +56,13 @@ abstract class DiaryService {
});
@POST('/api/diary/{diary}/meals/{type_no}/{portionModel}/mark_eaten')
Future<void> markEaten({
@Path('diary') required int diary,
@Path('type_no') required int typeNo,
@Path('portionModel') required int portionModel,
});
Future<void> markEaten({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Path('portionModel') required int portionModel});
@POST('/api/diary/{diary}/meals/{type_no}/{portionModel}/unmark_eaten')
Future<void> unmarkEaten({
@Path('diary') required int diary,
@Path('type_no') required int typeNo,
@Path('portionModel') required int portionModel,
});
Future<void> unmarkEaten({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Path('portionModel') required int portionModel});
@POST('/api/diary/{diary}/meals/{type_no}/portions/{portionModel}/restore')
Future<void> restore({
@Path('diary') required int diary,
@Path('type_no') required int typeNo,
@Path('portionModel') required int portionModel,
});
Future<void> restore({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Path('portionModel') required int portionModel});
@DELETE('/api/diary/{diary}/meals/{type_no}/portions/{portionModel}/delete')
Future<void> deleteProductFromMeal({
@@ -96,63 +72,35 @@ abstract class DiaryService {
});
@POST('/api/diary/{diary}/finishDay')
Future<void> finishDiaryDay({
@Path('diary') required int diary,
@Body(nullToAbsent: true) required FinishDiaryDayModel finishDiaryDayModel,
});
Future<void> finishDiaryDay({@Path('diary') required int diary, @Body(nullToAbsent: true) required FinishDiaryDayModel finishDiaryDayModel});
@POST('/api/diary/{diary}/meals/{type_no}/satiety/fill')
Future<void> fillSatiety({
@Path('diary') required int diary,
@Path('type_no') required int typeNo,
@Field() required int satiety,
});
Future<void> fillSatiety({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Field() required int satiety});
@POST('/api/diary/{diary}/meals/{type_no}/mood/fill')
Future<void> fillMood({
@Path('diary') required int diary,
@Path('type_no') required int typeNo,
@Field() required int mood,
});
Future<void> fillMood({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Field() required int mood});
@POST('/api/diary/{diary}/meals/{type_no}/time/fill')
Future<void> fillTime({
@Path('diary') required int diary,
@Path('type_no') required int typeNo,
@Field() required String time,
});
Future<void> fillTime({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Field() required String time});
@POST('/api/diary/{diary}/wakeUp/fill')
Future<void> fillWakeup({
@Path('diary') required int diary,
@Field() required String wakeUp,
});
Future<void> fillWakeup({@Path('diary') required int diary, @Field() required String wakeUp});
@POST('/api/diary/{diary}/retire/fill')
Future<void> fillRetire({
@Path('diary') required int diary,
@Field() required String retire,
});
Future<void> fillRetire({@Path('diary') required int diary, @Field() required String retire});
@POST('/api/diary/product_nutritional_values/sendToModeration')
Future<GlossaryModerationModel> sendProductToModeration({
@Body() required Map<String, dynamic> data,
});
Future<GlossaryModerationModel> sendProductToModeration({@Body() required Map<String, dynamic> data});
@POST('/api/diary/{id}/consultant/comment')
Future<void> commentUserDay({
@Path('id') required int id,
@Body() required ConsultantCommentModel body,
});
Future<void> commentUserDay({@Path('id') required int id, @Body() required ConsultantCommentModel body});
@GET('/api/chosen_plans/{planId}/{diaryId}/comments/show')
Future<ConsultantReviewModel> getDiaryComments({
@Path() required int planId,
@Path() required int diaryId,
});
Future<ConsultantReviewModel> getDiaryComments({@Path() required int planId, @Path() required int diaryId});
@POST('/api/diary/{diaryId}/consultant/comment/view')
Future<void> viewCommentOfDiaryDayByConsultant({
@Path() required int diaryId,
});
Future<void> viewCommentOfDiaryDayByConsultant({@Path() required int diaryId});
@GET('/api/glossaries/index')
Future<GlossaryPaginationModel> getGlossariesIndex({@Queries() Map<String, dynamic>? filters});
}

View File

@@ -0,0 +1,16 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'view_model.freezed.dart';
part 'view_model.g.dart';
@freezed
abstract class ViewModel with _$ViewModel {
factory ViewModel({
required String type,
List<int>? ids,
List<String>? slugs,
}) = _ViewModel;
factory ViewModel.fromJson(Map<String, dynamic> json) => _$ViewModelFromJson(json);
}

View File

@@ -12,70 +12,56 @@ class FeedCubit extends Cubit<FeedState> {
bool isLoading = false;
String? _query;
FeedCubit({
required this.feedRepository,
}) : super(const FeedState.loading());
FeedCubit({required this.feedRepository}) : super(const FeedState.loading());
void updateFeedBlogPostList(FeedBlogPostModel? feedBlogPostModel, bool wasLikedByMe) {
if (feedBlogPostModel == null) return;
List<FeedBlogPostModel> feedBlogPostList = [...(state as FeedStateLoaded).feedBlogPostList];
final int index = feedBlogPostList.indexOf(feedBlogPostModel);
feedBlogPostList[index] = feedBlogPostModel.copyWith(
wasLikedByMe: wasLikedByMe,
totalLikes: wasLikedByMe == false ? feedBlogPostList[index].totalLikes - 1 : feedBlogPostList[index].totalLikes + 1,
);
emit(
FeedState.loaded(
feedBlogPostList: feedBlogPostList,
isLoading: isLoading,
),
);
if (feedBlogPostModel == null || isClosed || state is! FeedStateLoaded) return;
final List<FeedBlogPostModel> feedBlogPostList = [...(state as FeedStateLoaded).feedBlogPostList];
final int index = feedBlogPostList.indexWhere((e) => e.id == feedBlogPostModel.id);
if (index == -1) return;
final current = feedBlogPostList[index];
feedBlogPostList[index] = current.copyWith(wasLikedByMe: wasLikedByMe, totalLikes: wasLikedByMe ? current.totalLikes + 1 : current.totalLikes - 1);
if (!isClosed) {
emit(FeedState.loaded(feedBlogPostList: feedBlogPostList, isLoading: isLoading));
}
}
Future<void> loadFeedBlogPostList({
String? query,
}) async {
Future<void> loadFeedBlogPostList({String? query}) async {
try {
_query = query;
final feedBlogPosts = await feedRepository.getFeedBlogPosts(query: query);
lastPage = feedBlogPosts.meta.lastPage;
emit(
FeedState.loaded(
feedBlogPostList: feedBlogPosts.feedBlogPostList,
isLoading: false,
),
);
if (!isClosed) {
emit(FeedState.loaded(feedBlogPostList: feedBlogPosts.feedBlogPostList, isLoading: false));
}
} catch (e) {
emit(FeedState.error(errorMessage: e.toString()));
if (!isClosed) {
emit(FeedState.error(errorMessage: e.toString()));
}
}
}
Future<void> loadNextPage() async {
if (currentPage < lastPage && !isLoading) {
if (currentPage < lastPage && !isLoading && !isClosed) {
isLoading = true;
emit((state as FeedStateLoaded).copyWith(isLoading: isLoading));
if (!isClosed) {
emit((state as FeedStateLoaded).copyWith(isLoading: isLoading));
}
try {
if (currentPage < lastPage) {
if (currentPage < lastPage && !isClosed) {
currentPage++;
final feedBlogPosts = await feedRepository.getFeedBlogPosts(
page: currentPage,
query: _query,
);
final feedBlogPosts = await feedRepository.getFeedBlogPosts(page: currentPage, query: _query);
isLoading = false;
emit(
FeedState.loaded(
feedBlogPostList: [
...(state as FeedStateLoaded).feedBlogPostList,
...feedBlogPosts.feedBlogPostList,
],
isLoading: isLoading,
),
);
if (!isClosed) {
emit(FeedState.loaded(feedBlogPostList: [...(state as FeedStateLoaded).feedBlogPostList, ...feedBlogPosts.feedBlogPostList], isLoading: isLoading));
}
}
} catch (e) {
emit(FeedState.error(errorMessage: e.toString()));
if (!isClosed) {
emit(FeedState.error(errorMessage: e.toString()));
}
}
}
}

View File

@@ -1,5 +1,6 @@
import 'package:easy_debounce/easy_debounce.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/views/cubit/views_cubit.dart';
import 'package:eatvio_app/core/widgets/add_item_button.dart';
import 'package:eatvio_app/core/widgets/custom_search_bar.dart';
import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart';
@@ -35,7 +36,6 @@ class FeedScreen extends StatelessWidget {
providers: [
BlocProvider<RecipesCubit>(create: (context) => sl<RecipesCubit>()..loadRecipesList()),
BlocProvider<FeedProfileCubit>(create: (context) => sl<FeedProfileCubit>()..getProfile()),
BlocProvider<FeedCubit>(create: (context) => sl<FeedCubit>()..loadFeedBlogPostList()),
BlocProvider<DietsCubit>(create: (context) => sl<DietsCubit>()..loadDiets()),
if (!hidePlans) BlocProvider<PlansCubit>(create: (context) => sl<PlansCubit>()..loadPlans()),
BlocProvider<ConsultantsCubit>(create: (context) => sl<ConsultantsCubit>()..getConsultants()),
@@ -66,12 +66,9 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
final dietsScrollController = ScrollController();
final plansScrollController = ScrollController();
final consultantsScrollController = ScrollController();
@override
void initState() {
// Initialize chipsLabelList based on hidePlans setting
chipsLabelList = hidePlans ? ['Все', 'Рецепты', 'Диеты', 'Консультанты'] : ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты'];
consultantsScrollController.addListener(_onScrollConsultants);
super.initState();
}
@@ -228,10 +225,10 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
];
},
body: switch (_selectedChipIndex) {
0 => const BlogPosts(),
1 => const Recipes(),
2 => const Diets(),
3 when !hidePlans => const Plans(),
0 => BlogPosts(handleVisibility: context.read<ViewsCubit>().handleVisibility),
1 => Recipes(handleVisibility: context.read<ViewsCubit>().handleVisibility),
2 => Diets(handleVisibility: context.read<ViewsCubit>().handleVisibility),
3 when !hidePlans => Plans(handleVisibility: context.read<ViewsCubit>().handleVisibility),
3 when hidePlans => const Consultants(),
4 when !hidePlans => const Consultants(),
_ => Center(child: Text(chipsLabelList[_selectedChipIndex])),
@@ -240,3 +237,20 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
);
}
}
enum ViewableContentType {
plans,
posts,
diets,
recipes;
String getStringifiedVersion() => switch (this) {
ViewableContentType.plans => 'plans',
ViewableContentType.posts => 'blog_posts',
ViewableContentType.diets => 'diets',
ViewableContentType.recipes => 'recipes',
};
}

View File

@@ -21,10 +21,7 @@ import 'package:share_plus/share_plus.dart';
class BlogPostWidget extends StatefulWidget {
final int index;
const BlogPostWidget({
super.key,
required this.index,
});
const BlogPostWidget({super.key, required this.index});
@override
State<BlogPostWidget> createState() => _BlogPostWidgetState();
@@ -44,10 +41,7 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
totalLikes = state.feedBlogPostList[widget.index].totalLikes;
return InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () => GoRouter.of(context).pushNamed(
'blogPostScreen',
extra: state.feedBlogPostList[widget.index].id,
),
onTap: () => GoRouter.of(context).pushNamed('blogPostScreen', extra: state.feedBlogPostList[widget.index].id),
child: Material(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
@@ -95,21 +89,12 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
Text(
state.feedBlogPostList[widget.index].author.authorName,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
height: 1,
color: Colors.black87,
fontWeight: FontWeight.w500,
fontSize: 16,
),
style: const TextStyle(height: 1, color: Colors.black87, fontWeight: FontWeight.w500, fontSize: 16),
),
const SizedBox(height: 4),
Text(
'${calculateDifference(state.feedBlogPostList[widget.index].createdAt)} в ${state.feedBlogPostList[widget.index].createdAt.hour.toString().padLeft(2, '0')}:${state.feedBlogPostList[widget.index].createdAt.minute.toString().padLeft(2, '0')}',
style: const TextStyle(
height: 1,
color: Colors.black45,
fontSize: 14,
),
style: const TextStyle(height: 1, color: Colors.black45, fontSize: 14),
),
],
),
@@ -119,9 +104,7 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
!state.feedBlogPostList[widget.index].author.authorIsMe)
Padding(
padding: const EdgeInsets.only(right: 8),
child: SubscribeButtonWidget(
slug: state.feedBlogPostList[widget.index].author.authorSlug,
),
child: SubscribeButtonWidget(slug: state.feedBlogPostList[widget.index].author.authorSlug),
),
// Кнопка меню
CustomPopup(
@@ -141,11 +124,7 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Text(
state.feedBlogPostList[widget.index].title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.black87),
),
),
],
@@ -156,15 +135,12 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Html(
data: (state.feedBlogPostList[widget.index].description.length > 200
? state.feedBlogPostList[widget.index].description.substring(0, 200)
: state.feedBlogPostList[widget.index].description).replaceAll('\n', '<br>'),
style: {
"*": Style(
fontSize: FontSize(16),
color: Colors.black87,
),
},
data:
(state.feedBlogPostList[widget.index].description.length > 200
? state.feedBlogPostList[widget.index].description.substring(0, 200)
: state.feedBlogPostList[widget.index].description)
.replaceAll('\n', '<br>'),
style: {"*": Style(fontSize: FontSize(16), color: Colors.black87)},
),
),
if (state.feedBlogPostList[widget.index].description.length > 200)
@@ -172,12 +148,7 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
padding: EdgeInsets.symmetric(horizontal: 12),
child: Text(
'Показать ещё',
style: TextStyle(
height: 1,
color: Color(0xFF8B8B8B),
fontSize: 14,
fontWeight: FontWeight.w600,
),
style: TextStyle(height: 1, color: Color(0xFF8B8B8B), fontSize: 14, fontWeight: FontWeight.w600),
),
),
if (state.feedBlogPostList[widget.index].title.isNotEmpty || state.feedBlogPostList[widget.index].description.isNotEmpty)
@@ -186,10 +157,7 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
onTap: () {
context.pushTransparentRoute(
rootNavigator: true,
PhotoViewScreen(
imageUrls: ['$baseUrl/storage/${state.feedBlogPostList[widget.index].image}'],
index: 0,
),
PhotoViewScreen(imageUrls: ['$baseUrl/storage/${state.feedBlogPostList[widget.index].image}'], index: 0),
);
},
child: Hero(
@@ -218,11 +186,14 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
isLiked = !isLiked;
isLiked ? totalLikes++ : totalLikes--;
});
isLiked
? await context.read<FeedCubit>().likePost(state.feedBlogPostList[widget.index].id)
: await context.read<FeedCubit>().unlikedPost(state.feedBlogPostList[widget.index].id);
if (!context.mounted) return;
await context.read<FeedCubit>().loadFeedBlogPostList();
// Update feed list immediately by ID (no reload)
context.read<FeedCubit>().updateFeedBlogPostList(state.feedBlogPostList[widget.index], isLiked);
// Fire network call without introducing UI delay
if (isLiked) {
await context.read<FeedCubit>().likePost(state.feedBlogPostList[widget.index].id);
} else {
await context.read<FeedCubit>().unlikedPost(state.feedBlogPostList[widget.index].id);
}
},
),
const SizedBox(width: 8),
@@ -231,16 +202,10 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
'assets/icons/comment.svg',
height: 20,
width: 20,
colorFilter: ColorFilter.mode(
appTheme.gray600,
BlendMode.srcIn,
),
colorFilter: ColorFilter.mode(appTheme.gray600, BlendMode.srcIn),
),
text: state.feedBlogPostList[widget.index].totalComments.toString(),
onTap: () => GoRouter.of(context).pushNamed(
'blogPostScreen',
extra: state.feedBlogPostList[widget.index].id,
),
onTap: () => GoRouter.of(context).pushNamed('blogPostScreen', extra: state.feedBlogPostList[widget.index].id),
),
const SizedBox(width: 8),
FeedIconButton(
@@ -254,28 +219,18 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
final box = context.findRenderObject() as RenderBox?;
SharePlus.instance.share(
ShareParams(
uri: Uri.parse(
'$baseUrl/ru/blog_post/${state.feedBlogPostList[widget.index].id}',
),
uri: Uri.parse('$baseUrl/ru/blog_post/${state.feedBlogPostList[widget.index].id}'),
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
),
);
},
),
const Spacer(),
Icon(
Icons.visibility_rounded,
size: 16,
color: appTheme.gray500,
),
Icon(Icons.visibility_rounded, size: 16, color: appTheme.gray500),
const SizedBox(width: 4),
Text(
state.feedBlogPostList[widget.index].totalViews.toString(),
style: TextStyle(
fontSize: 12,
color: appTheme.gray500,
fontWeight: FontWeight.w400,
),
style: TextStyle(fontSize: 12, color: appTheme.gray500, fontWeight: FontWeight.w400),
),
],
),

View File

@@ -3,13 +3,16 @@ import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_state.dart';
import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/blog_post_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:visibility_detector/visibility_detector.dart';
class BlogPosts extends StatelessWidget {
const BlogPosts({super.key});
const BlogPosts({super.key, required this.handleVisibility});
final Function({required double visibleFraction,required int id, required ViewableContentType type}) handleVisibility;
@override
Widget build(BuildContext context) {
return RefreshIndicator(
@@ -29,25 +32,26 @@ class BlogPosts extends StatelessWidget {
if (index == state.feedBlogPostList.length) {
return const Loader();
}
return BlogPostWidget(index: index);
return VisibilityDetector(
onVisibilityChanged: (info) {
final id = state.feedBlogPostList[index].id;
handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.posts);
},
key: ValueKey(index),
child: BlogPostWidget(index: index),
);
},
separatorBuilder: (context, index) => const SizedBox(height: 8),
itemCount: state.isLoading ? state.feedBlogPostList.length + 1 : state.feedBlogPostList.length,
),
);
} else if (state is FeedStateError) {
return DataErrorWidget(
onReload: () => context.read<FeedCubit>().loadFeedBlogPostList(),
);
return DataErrorWidget(onReload: () => context.read<FeedCubit>().loadFeedBlogPostList());
} else {
return Scaffold(
backgroundColor: Colors.grey.shade200,
body: const Loader(),
);
return Scaffold(backgroundColor: Colors.grey.shade200, body: const Loader());
}
},
),
);
}
}

View File

@@ -25,13 +25,7 @@ class CommentsWidget extends StatefulWidget {
final CommentModel comment;
final int? replyId;
const CommentsWidget({
super.key,
required this.id,
required this.commentSourceType,
required this.comment,
this.replyId,
});
const CommentsWidget({super.key, required this.id, required this.commentSourceType, required this.comment, this.replyId});
@override
State<CommentsWidget> createState() => _CommentsWidgetState();
@@ -47,10 +41,7 @@ class _CommentsWidgetState extends State<CommentsWidget> {
_comment(s, widget.comment.data[i]),
if (widget.comment.data[i].childrenComments.isNotEmpty) const SizedBox(height: 8),
for (int j = 0; j < widget.comment.data[i].childrenComments.length; j++)
Padding(
padding: const EdgeInsets.only(left: 40),
child: _comment(s, widget.comment.data[i].childrenComments[j], 16, true),
),
Padding(padding: const EdgeInsets.only(left: 40), child: _comment(s, widget.comment.data[i].childrenComments[j], 16, true)),
const SizedBox(height: 8),
],
const SizedBox(height: 48),
@@ -72,7 +63,7 @@ class _CommentsWidgetState extends State<CommentsWidget> {
: null,
copy: () {
Clipboard.setData(ClipboardData(text: comment.body));
showInfoToast(s.link_was_copied);
showInfoToast(s.text_was_copied);
},
edit: (String text) {
context.read<CommentsCubit>().updateComment(id: comment.id, body: text);
@@ -84,10 +75,7 @@ class _CommentsWidgetState extends State<CommentsWidget> {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => ComplainingBottomSheet(
complainedContentType: ComplainedContent.comment,
id: comment.id,
),
builder: (context) => ComplainingBottomSheet(complainedContentType: ComplainedContent.comment, id: comment.id),
);
},
isMyComment: isMe,
@@ -117,9 +105,7 @@ class _CommentsWidgetState extends State<CommentsWidget> {
);
},
child: CircleAvatar(
foregroundImage: NetworkImage(
comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : baseUrl + comment.authorProfileIcon,
),
foregroundImage: NetworkImage(comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : baseUrl + comment.authorProfileIcon),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
radius: radius,
),
@@ -131,24 +117,14 @@ class _CommentsWidgetState extends State<CommentsWidget> {
children: [
Text(
comment.authorName,
style: TextStyle(
height: 1,
color: appTheme.gray,
fontSize: 14,
fontWeight: FontWeight.w500,
),
style: TextStyle(height: 1, color: appTheme.gray, fontSize: 14, fontWeight: FontWeight.w500),
),
const SizedBox(height: 4),
SizedBox(
width: isChild ? MediaQuery.of(context).size.width * 0.72 : MediaQuery.of(context).size.width * 0.8,
child: Text(
comment.body,
style: TextStyle(
height: 1,
color: appTheme.black,
fontSize: 16,
fontWeight: FontWeight.w500,
),
style: TextStyle(height: 1, color: appTheme.black, fontSize: 16, fontWeight: FontWeight.w500),
),
),
const SizedBox(height: 4),
@@ -156,12 +132,7 @@ class _CommentsWidgetState extends State<CommentsWidget> {
children: [
Text(
comment.createdAt.format('dd MMMM', 'ru'),
style: TextStyle(
height: 1,
color: appTheme.gray,
fontSize: 14,
fontWeight: FontWeight.w500,
),
style: TextStyle(height: 1, color: appTheme.gray, fontSize: 14, fontWeight: FontWeight.w500),
),
const SizedBox(width: 16),
if (!isChild)
@@ -171,12 +142,7 @@ class _CommentsWidgetState extends State<CommentsWidget> {
},
child: Text(
s.answer,
style: TextStyle(
height: 1,
color: appTheme.gray,
fontSize: 14,
fontWeight: FontWeight.w600,
),
style: TextStyle(height: 1, color: appTheme.gray, fontSize: 14, fontWeight: FontWeight.w600),
),
),
],

View File

@@ -3,15 +3,17 @@ import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_state.dart';
import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/diet_filter_bottom_sheet.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/diets_widget.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/filter_badge.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:visibility_detector/visibility_detector.dart';
class Diets extends StatelessWidget {
const Diets({super.key});
const Diets({super.key, required this.handleVisibility});
final Function({required double visibleFraction, required int id, required ViewableContentType type}) handleVisibility;
@override
Widget build(BuildContext context) {
return RefreshIndicator(
@@ -19,10 +21,7 @@ class Diets extends StatelessWidget {
child: BlocBuilder<DietsCubit, DietsState>(
builder: (BuildContext context, DietsState state) {
if (state.isLoading) {
return Scaffold(
backgroundColor: Colors.grey.shade200,
body: const Loader(),
);
return Scaffold(backgroundColor: Colors.grey.shade200, body: const Loader());
} else if (state.errorMessage != null) {
return DataErrorWidget(onReload: () => context.read<DietsCubit>().loadDiets());
} else {
@@ -39,7 +38,14 @@ class Diets extends StatelessWidget {
shrinkWrap: true,
physics: const BouncingScrollPhysics(),
itemBuilder: (context, index) {
return DietsWidget(diet: state.dietsList[index], index: index);
return VisibilityDetector(
key: ValueKey(index),
onVisibilityChanged: (info) {
final id = state.dietsList[index].id;
handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.diets);
},
child: DietsWidget(diet: state.dietsList[index], index: index),
);
},
separatorBuilder: (context, index) {
return const SizedBox(height: 8);
@@ -50,11 +56,7 @@ class Diets extends StatelessWidget {
Positioned(
right: 0,
bottom: 74,
child: FilterBadge(
filterBottomSheet: DietFilterBottomSheet(
dietsCubit: context.read<DietsCubit>(),
),
),
child: FilterBadge(filterBottomSheet: DietFilterBottomSheet(dietsCubit: context.read<DietsCubit>())),
),
],
);

View File

@@ -3,15 +3,17 @@ import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_state.dart';
import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/filter_badge.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/plan_widget.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/plans_filter_bottom_sheet.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:visibility_detector/visibility_detector.dart';
class Plans extends StatelessWidget {
const Plans({super.key});
const Plans({super.key, required this.handleVisibility});
final Function({required double visibleFraction, required int id, required ViewableContentType type}) handleVisibility;
@override
Widget build(BuildContext context) {
return RefreshIndicator(
@@ -19,10 +21,7 @@ class Plans extends StatelessWidget {
child: BlocBuilder<PlansCubit, PlansState>(
builder: (BuildContext context, PlansState state) {
if (state.isLoading) {
return Scaffold(
backgroundColor: Colors.grey.shade200,
body: const Loader(),
);
return Scaffold(backgroundColor: Colors.grey.shade200, body: const Loader());
} else if (state.errorMessage != null) {
return DataErrorWidget(onReload: () => context.read<PlansCubit>().loadPlans());
}
@@ -39,9 +38,13 @@ class Plans extends StatelessWidget {
shrinkWrap: true,
physics: const BouncingScrollPhysics(),
itemBuilder: (context, index) {
return PlanWidget(
plan: state.plans[index],
index: index,
return VisibilityDetector(
key: ValueKey(index),
onVisibilityChanged: (info) {
final id = state.plans[index].id;
handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.plans);
},
child: PlanWidget(plan: state.plans[index], index: index),
);
},
separatorBuilder: (context, index) {
@@ -53,11 +56,7 @@ class Plans extends StatelessWidget {
Positioned(
right: 0,
bottom: 74,
child: FilterBadge(
filterBottomSheet: PlansFilterBottomSheet(
plansCubit: context.read<PlansCubit>(),
),
),
child: FilterBadge(filterBottomSheet: PlansFilterBottomSheet(plansCubit: context.read<PlansCubit>())),
),
],
);

View File

@@ -3,13 +3,15 @@ import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_state.dart';
import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/recipes_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:visibility_detector/visibility_detector.dart';
class Recipes extends StatelessWidget {
const Recipes({super.key});
const Recipes({super.key, required this.handleVisibility});
final Function({required double visibleFraction,required int id, required ViewableContentType type}) handleVisibility;
@override
Widget build(BuildContext context) {
return RefreshIndicator(
@@ -40,9 +42,16 @@ class Recipes extends StatelessWidget {
shrinkWrap: true,
physics: const BouncingScrollPhysics(),
itemBuilder: (context, index) {
return RecipesWidget(
recipe: state.recipesList[index],
index: index,
return VisibilityDetector(
key: ValueKey(index),
onVisibilityChanged: (info) {
final id = state.recipesList[index].id;
handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.recipes);
},
child: RecipesWidget(
recipe: state.recipesList[index],
index: index,
),
);
},
separatorBuilder: (context, index) {

View File

@@ -5,103 +5,55 @@ import 'package:eatvio_app/features/feed/models/feed_blog_post_settings_model.da
import 'package:eatvio_app/features/feed/models/feed_consultant_model.dart';
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
import 'package:eatvio_app/features/feed/models/view_model.dart';
import 'package:eatvio_app/features/feed/services/feed_service.dart';
class FeedRepository {
final FeedService feedService;
FeedRepository({
required this.feedService,
});
FeedRepository({required this.feedService});
Future<FeedBlogPostSettingsModel> getFeedBlogPosts({
String? query,
int page = 1,
}) async {
Future<FeedBlogPostSettingsModel> getFeedBlogPosts({String? query, int page = 1}) async {
try {
return (await feedService.getBlogPostList(
page: page,
query: query,
));
return (await feedService.getBlogPostList(page: page, query: query));
} catch (e) {
rethrow;
}
}
Future<FeedBlogPostModel> getBlogPost({
required int id
}) async {
Future<FeedBlogPostModel> getBlogPost({required int id}) async {
try {
return await feedService.getBlogPost(
postId: id
);
return await feedService.getBlogPost(postId: id);
} catch (e) {
rethrow;
}
}
Future<List<Recipe>> getRecipes({
String? query,
List<int>? tags,
int page = 1,
}) async {
Future<List<Recipe>> getRecipes({String? query, List<int>? tags, int page = 1}) async {
try {
return (await feedService.getRecipes(
query: query,
tags: tags,
page: page,
))
.recipesList;
return (await feedService.getRecipes(query: query, tags: tags, page: page)).recipesList;
} catch (e) {
rethrow;
}
}
Future<DietsPagination> getDiets({
int page = 1,
String? query,
String? types,
int? duration,
String? sorting,
}) async {
Future<DietsPagination> getDiets({int page = 1, String? query, String? types, int? duration, String? sorting}) async {
try {
return (await feedService.getDiets(
page: page,
query: query,
types: types,
duration: duration,
sorting: sorting,
));
return (await feedService.getDiets(page: page, query: query, types: types, duration: duration, sorting: sorting));
} catch (e) {
rethrow;
}
}
Future<PlanPaginationModel> getPlans({
int page = 1,
String? query,
String? types,
int? duration,
int? minPrice,
int? maxPrice,
}) async {
Future<PlanPaginationModel> getPlans({int page = 1, String? query, String? types, int? duration, int? minPrice, int? maxPrice}) async {
try {
return (await feedService.getPlans(
page: page,
query: query,
types: types,
duration: duration,
minPrice: minPrice,
maxPrice: maxPrice,
));
return (await feedService.getPlans(page: page, query: query, types: types, duration: duration, minPrice: minPrice, maxPrice: maxPrice));
} catch (e) {
rethrow;
}
}
Future<CommentModel> getBlogPostComments({
required String id,
}) async {
Future<CommentModel> getBlogPostComments({required String id}) async {
try {
return (await feedService.getBlogPostComments(id: id));
} catch (e) {
@@ -109,25 +61,15 @@ class FeedRepository {
}
}
Future<void> sendBlogPostComment({
required String id,
required String commentText,
int? parentCommentId,
}) async {
Future<void> sendBlogPostComment({required String id, required String commentText, int? parentCommentId}) async {
try {
await feedService.sendBlogPostComment(
id: id,
body: commentText,
parentCommentId: parentCommentId,
);
await feedService.sendBlogPostComment(id: id, body: commentText, parentCommentId: parentCommentId);
} catch (e) {
rethrow;
}
}
Future<CommentModel> getDietsComments({
required String id,
}) async {
Future<CommentModel> getDietsComments({required String id}) async {
try {
return (await feedService.getDietsComments(id: id));
} catch (e) {
@@ -135,25 +77,15 @@ class FeedRepository {
}
}
Future<void> sendDietsComment({
required String id,
required String commentText,
int? parentCommentId,
}) async {
Future<void> sendDietsComment({required String id, required String commentText, int? parentCommentId}) async {
try {
await feedService.sendDietsComment(
id: id,
body: commentText,
parentCommentId: parentCommentId,
);
await feedService.sendDietsComment(id: id, body: commentText, parentCommentId: parentCommentId);
} catch (e) {
rethrow;
}
}
Future<CommentModel> getRecipesComments({
required String id,
}) async {
Future<CommentModel> getRecipesComments({required String id}) async {
try {
return (await feedService.getRecipesComments(id: id));
} catch (e) {
@@ -161,25 +93,15 @@ class FeedRepository {
}
}
Future<void> sendRecipesComment({
required String id,
required String commentText,
int? parentCommentId,
}) async {
Future<void> sendRecipesComment({required String id, required String commentText, int? parentCommentId}) async {
try {
await feedService.sendRecipesComment(
id: id,
body: commentText,
parentCommentId: parentCommentId,
);
await feedService.sendRecipesComment(id: id, body: commentText, parentCommentId: parentCommentId);
} catch (e) {
rethrow;
}
}
Future<CommentModel> getPlansComments({
required String slug,
}) async {
Future<CommentModel> getPlansComments({required String slug}) async {
try {
return (await feedService.getPlansComments(slug: slug));
} catch (e) {
@@ -187,25 +109,15 @@ class FeedRepository {
}
}
Future<void> sendPlansComment({
required String slug,
required String commentText,
int? parentCommentId,
}) async {
Future<void> sendPlansComment({required String slug, required String commentText, int? parentCommentId}) async {
try {
await feedService.sendPlansComment(
slug: slug,
body: commentText,
parentCommentId: parentCommentId,
);
await feedService.sendPlansComment(slug: slug, body: commentText, parentCommentId: parentCommentId);
} catch (e) {
rethrow;
}
}
Future<void> likeBlogPost({
required int id,
}) async {
Future<void> likeBlogPost({required int id}) async {
try {
await feedService.likeBlogPost(id: id);
} catch (e) {
@@ -213,9 +125,7 @@ class FeedRepository {
}
}
Future<void> unlikedBlogPost({
required int id,
}) async {
Future<void> unlikedBlogPost({required int id}) async {
try {
await feedService.unlikedBlogPost(id: id);
} catch (e) {
@@ -262,4 +172,12 @@ class FeedRepository {
rethrow;
}
}
Future<void> viewContent({required ViewModel viewModel}) async {
try {
await feedService.viewContent(viewModel: viewModel);
} catch (e) {
rethrow;
}
}
}

View File

@@ -6,6 +6,7 @@ import 'package:eatvio_app/features/feed/models/feed_blog_post_settings_model.da
import 'package:eatvio_app/features/feed/models/feed_consultant_model.dart';
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
import 'package:eatvio_app/features/feed/models/recipes_pagination_model.dart';
import 'package:eatvio_app/features/feed/models/view_model.dart';
import 'package:retrofit/retrofit.dart';
part 'feed_service.g.dart';
@@ -131,4 +132,9 @@ abstract class FeedService {
Future<void> viewPlan({
@Path() required String slug,
});
@POST('/api/views')
Future<void> viewContent({
@Body() required ViewModel viewModel,
});
}

View File

@@ -33,21 +33,12 @@ class _UserPurchasesScreen extends State<UserPurchasesScreen> {
return Scaffold(
backgroundColor: const Color(0xFFF0F2F5),
appBar: AppBar(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadiusDirectional.vertical(
bottom: Radius.circular(12),
),
),
title: Text(
'Финансы',
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
shape: const RoundedRectangleBorder(borderRadius: BorderRadiusDirectional.vertical(bottom: Radius.circular(12))),
title: Text('Финансы', style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
centerTitle: true,
),
body: state.isLoading
? const Center(
child: Loader(),
)
? const Center(child: Loader())
: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
@@ -67,10 +58,7 @@ class _UserPurchasesScreen extends State<UserPurchasesScreen> {
.map(
(value) => DropdownMenuItem(
value: value,
child: Text(
value.name,
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
),
child: Text(value.name, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
),
)
.toList(),
@@ -105,60 +93,41 @@ class _UserPurchasesScreen extends State<UserPurchasesScreen> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Builder(builder: (context) {
final idLen = state.purchasesModels[index].bankId.length;
return Text(
'${state.purchasesModels[index].bankId.substring(0, 4)}...${state.purchasesModels[index].bankId.substring(idLen - 4, idLen)} ',
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
);
}),
Builder(
builder: (context) {
final idLen = state.purchasesModels[index].bankId.length;
return Text(
'${state.purchasesModels[index].bankId.substring(0, 4)}...${state.purchasesModels[index].bankId.substring(idLen - 4, idLen)} ',
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
);
},
),
Text(
state.purchasesModels[index].startDate.format('yyyy.MM.dd'),
style: const TextStyle(
color: Color(0xFF6D6F71),
height: 1,
fontWeight: FontWeight.w500,
),
state.purchasesModels[index].startDate.format('dd.MM.yyyy'),
style: const TextStyle(color: Color(0xFF6D6F71), height: 1, fontWeight: FontWeight.w500),
),
],
),
const SizedBox(height: 8),
Text(
state.purchasesModels[index].title,
style: TextStyle(
color: appTheme.blueA400,
height: 1,
fontSize: 18,
fontWeight: FontWeight.w600,
),
style: TextStyle(color: appTheme.blueA400, height: 1, fontSize: 18, fontWeight: FontWeight.w600),
),
const SizedBox(height: 8),
Text(
'Продавец: ${state.purchasesModels[index].consultantName}',
style: const TextStyle(
color: Color(0xFF323F4B),
height: 1,
fontSize: 14,
fontWeight: FontWeight.w500,
),
style: const TextStyle(color: Color(0xFF323F4B), height: 1, fontSize: 14, fontWeight: FontWeight.w500),
),
const SizedBox(height: 8),
Text(
'${state.purchasesModels[index].duration} ${getDayForm(state.purchasesModels[index].duration, s)}',
style: const TextStyle(
color: Color(0xFF323F4B),
height: 1,
fontSize: 14,
fontWeight: FontWeight.w500,
),
style: const TextStyle(color: Color(0xFF323F4B), height: 1, fontSize: 14, fontWeight: FontWeight.w500),
),
const SizedBox(height: 8),
Container(
height: 1,
width: double.infinity,
decoration: const BoxDecoration(
color: Color(0xFFDEDEDF),
),
decoration: const BoxDecoration(color: Color(0xFFDEDEDF)),
),
const SizedBox(height: 8),
Row(
@@ -166,16 +135,9 @@ class _UserPurchasesScreen extends State<UserPurchasesScreen> {
children: [
Text(
'${state.purchasesModels[index].paymentPrice} ₽',
style: const TextStyle(
color: Color(0xFF323F4B),
height: 1,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
StatusWidget(
status: purchaseStatus,
style: const TextStyle(color: Color(0xFF323F4B), height: 1, fontSize: 18, fontWeight: FontWeight.w600),
),
StatusWidget(status: purchaseStatus),
],
),
],
@@ -189,10 +151,7 @@ class _UserPurchasesScreen extends State<UserPurchasesScreen> {
else
const Padding(
padding: EdgeInsets.symmetric(vertical: 40.0),
child: Text(
'У вас нет таких консультаций',
style: TextStyles.textMMedium,
),
child: Text('У вас нет таких консультаций', style: TextStyles.textMMedium),
),
],
),
@@ -200,7 +159,8 @@ class _UserPurchasesScreen extends State<UserPurchasesScreen> {
),
),
],
)),
),
),
);
},
),

View File

@@ -3,9 +3,11 @@ import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/menu/models/notification_settings_model.dart';
import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart';
import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_state.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
class NotificationsSettingsScreen extends StatelessWidget {
const NotificationsSettingsScreen({super.key});
@@ -21,8 +23,10 @@ class NotificationsSettingsContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
final s = S.of(context);
return Scaffold(
appBar: AppBar(title: const Text('Уведомления')),
appBar: AppBar(title: Text(s.notifications)),
body: BlocBuilder<NotificationSettingsCubit, NotificationSettingsState>(
builder: (context, state) {
if (state is NotificationSettingsStateLoading) {
@@ -58,7 +62,7 @@ class NotificationsSettingsContent extends StatelessWidget {
_tile(
padding: const EdgeInsets.symmetric(horizontal: 16),
onTap: () {},
title: 'Уведомление о новых обновлениях',
title: 'Новые обновления',
subtitle: null,
trailing: CupertinoSwitch(
value: notificationSettings.newUpdatesNotification,
@@ -72,7 +76,7 @@ class NotificationsSettingsContent extends StatelessWidget {
_tile(
padding: const EdgeInsets.symmetric(horizontal: 16),
onTap: () {},
title: 'Уведомления о взвешивании',
title: 'Напоминать о взвешивании',
subtitle: null,
trailing: CupertinoSwitch(
value: notificationSettings.weightMeasurementsNotification,
@@ -111,27 +115,29 @@ class NotificationsSettingsContent extends StatelessWidget {
),
),
_divider(context),
const SizedBox(height: 16),
_buildDiaryRemindersSection(context, notificationSettings.diaryReminderNotifications, cubit),
],
);
}
Widget _buildDiaryRemindersSection(BuildContext context, DiaryReminderNotifications diaryReminderNotifications, NotificationSettingsCubit cubit) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Text(
'Напоминания о дневнике',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: appTheme.blueA400),
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Напоминание о заполнении дневника',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: appTheme.cardNotificationsBG),
),
),
_buildReminderTile(context, 'Первое напоминание', diaryReminderNotifications.firstReminder, 'first_reminder', cubit),
_buildReminderTile(context, 'Второе напоминание', diaryReminderNotifications.secondReminder, 'second_reminder', cubit),
_buildReminderTile(context, 'Третье напоминание', diaryReminderNotifications.thirdReminder, 'third_reminder', cubit),
],
const SizedBox(height: 8),
_buildReminderTile(context, '№1 Напоминание о приеме пищи', diaryReminderNotifications.firstReminder, 'first_reminder', cubit),
const SizedBox(height: 8),
_buildReminderTile(context, '№2 Напоминание о приеме пищи', diaryReminderNotifications.secondReminder, 'second_reminder', cubit),
const SizedBox(height: 8),
_buildReminderTile(context, '№3 Напоминание о приеме пищи', diaryReminderNotifications.thirdReminder, 'third_reminder', cubit),
],
),
);
}
@@ -147,41 +153,58 @@ class NotificationsSettingsContent extends StatelessWidget {
}
Widget _buildReminderTile(BuildContext context, String title, ReminderSettings reminder, String reminderType, NotificationSettingsCubit cubit) {
return _tile(
padding: const EdgeInsets.symmetric(horizontal: 16),
onTap: reminder.enabled
? () {
_showTimePicker(context, reminder, reminderType, cubit);
}
: null,
title: title,
subtitle: reminder.enabled
? Row(
children: [
Icon(Icons.access_time, size: 16, color: appTheme.blueA400),
const SizedBox(width: 8),
Text(
'${reminder.localTimeHours.toString().padLeft(2, '0')}:${reminder.localTimeMins.toString().padLeft(2, '0')}',
style: TextStyle(fontSize: 14, color: appTheme.blueA400, fontWeight: FontWeight.w500),
),
const SizedBox(width: 4),
const Text('(нажмите для изменения)', style: TextStyle(fontSize: 12, color: Colors.grey)),
],
)
: null,
trailing: Row(
mainAxisSize: MainAxisSize.min,
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: appTheme.dividerColor),
child: Column(
children: [
CupertinoSwitch(
value: reminder.enabled,
activeTrackColor: appTheme.blueA400,
onChanged: (newValue) {
_updateReminderSetting(cubit, reminderType, {
'enabled': newValue,
'local_time_hours': reminder.localTimeHours,
'local_time_mins': reminder.localTimeMins,
});
},
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
reminderType == 'third_reminder' ? SvgPicture.asset('assets/icons/retire.svg') : SvgPicture.asset('assets/icons/wakeup.svg'),
const SizedBox(width: 8),
Text(
title,
style: const TextStyle(color: Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w500),
),
],
),
const SizedBox(height: 8),
Row(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: GestureDetector(
onTap: reminder.enabled
? () {
_showTimePicker(context, reminder, reminderType, cubit);
}
: null,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(color: reminder.enabled ? Colors.white : Colors.white.withAlpha((255 * 0.7).toInt()), borderRadius: BorderRadius.circular(48)),
child: Center(
child: Text(
'${reminder.localTimeHours.toString().padLeft(2, '0')}:${reminder.localTimeMins.toString().padLeft(2, '0')}',
style: TextStyle(fontSize: 17, color: appTheme.gray, fontWeight: FontWeight.w500),
),
),
),
),
),
const SizedBox(width: 8),
CupertinoSwitch(
value: reminder.enabled,
activeTrackColor: appTheme.blueA400,
onChanged: (newValue) {
_updateReminderSetting(cubit, reminderType, {
'enabled': newValue,
'local_time_hours': reminder.localTimeHours,
'local_time_mins': reminder.localTimeMins,
});
},
),
],
),
],
),

View File

@@ -12,7 +12,6 @@ import 'package:eatvio_app/features/feed/presentation/widgets/plans_filter_botto
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart';
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.dart';
import 'package:eatvio_app/features/my_plans/presentation/widgets/my_plan_widget.dart';
import 'package:eatvio_app/features/my_plans/presentation/widgets/my_plans_tab.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -64,9 +63,7 @@ class MyPlansScreenContent extends StatelessWidget {
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return [
SliverAppBar(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(bottom: Radius.circular(12)),
),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(bottom: Radius.circular(12))),
backgroundColor: appTheme.gray100,
foregroundColor: appTheme.gray100,
automaticallyImplyLeading: false,
@@ -76,21 +73,12 @@ class MyPlansScreenContent extends StatelessWidget {
snap: true,
scrolledUnderElevation: 0,
bottom: PreferredSize(
preferredSize: Size(
MediaQuery.sizeOf(context).width,
64,
),
preferredSize: Size(MediaQuery.sizeOf(context).width, 32),
child: Padding(
padding: const EdgeInsets.only(bottom: 0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 8),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 4),
child: MyPlansTab(),
),
const SizedBox(height: 8),
Row(
children: [
const SizedBox(width: 4),
@@ -104,20 +92,14 @@ class MyPlansScreenContent extends StatelessWidget {
_ => activePlansSearchController,
},
onChanged: (query) {
EasyDebounce.debounce(
'searchPlans',
const Duration(milliseconds: 500),
() {
context.read<MyPlansCubit>().loadPlans(query: query == '' ? null : query);
},
);
EasyDebounce.debounce('searchPlans', const Duration(milliseconds: 500), () {
context.read<MyPlansCubit>().loadPlans(query: query == '' ? null : query);
});
},
),
),
const SizedBox(width: 12),
AddItemButton(
onTap: () => GoRouter.of(context).pushNamed(CreatePlanScreen.name),
),
AddItemButton(onTap: () => GoRouter.of(context).pushNamed(CreatePlanScreen.name)),
const SizedBox(width: 4),
],
),
@@ -134,10 +116,7 @@ class MyPlansScreenContent extends StatelessWidget {
child: BlocBuilder<MyPlansCubit, MyPlansState>(
builder: (BuildContext context, MyPlansState state) {
if (state.isLoading) {
return Scaffold(
backgroundColor: Colors.grey.shade200,
body: const Loader(),
);
return Scaffold(backgroundColor: Colors.grey.shade200, body: const Loader());
} else if (state.error != null) {
return DataErrorWidget(onReload: () => context.read<MyPlansCubit>().loadPlans());
} else {
@@ -162,11 +141,7 @@ class MyPlansScreenContent extends StatelessWidget {
Positioned(
right: 0,
bottom: 74,
child: FilterBadge(
filterBottomSheet: PlansFilterBottomSheet(
myPlansCubit: context.read<MyPlansCubit>(),
),
),
child: FilterBadge(filterBottomSheet: PlansFilterBottomSheet(myPlansCubit: context.read<MyPlansCubit>())),
),
],
);

View File

@@ -14,16 +14,13 @@ import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_svg/svg.dart';
// Removed SVG icon usage after switching to text buttons
import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
class MyPlanWidget extends StatefulWidget {
final PlanModel plan;
const MyPlanWidget({
super.key,
required this.plan,
});
const MyPlanWidget({super.key, required this.plan});
@override
State<MyPlanWidget> createState() => _MyPlanWidgetState();
@@ -34,10 +31,7 @@ class _MyPlanWidgetState extends State<MyPlanWidget> {
Widget build(BuildContext context) {
final s = S.of(context);
return DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: Colors.white),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
@@ -48,23 +42,14 @@ class _MyPlanWidgetState extends State<MyPlanWidget> {
onTap: () => GoRouter.of(context).pushNamed('aboutPlanScreen', extra: {'slug': widget.plan.slug, 'navigateToComments': false}),
child: ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
child: CachedNetworkImage(
imageUrl: '$baseUrl/storage/${widget.plan.image}',
fit: BoxFit.fitWidth,
width: MediaQuery.of(context).size.width,
),
child: CachedNetworkImage(imageUrl: '$baseUrl/storage/${widget.plan.image}', fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width),
),
),
Positioned(
bottom: 16,
child: SizedBox(
width: MediaQuery.sizeOf(context).width,
child: InfoRow(
plan: widget.plan,
itemsBackgroundColor: appTheme.whiteA700,
disableDescriptionOfTypes: true,
type: ProductType.plan,
),
child: InfoRow(plan: widget.plan, itemsBackgroundColor: appTheme.whiteA700, disableDescriptionOfTypes: true, type: ProductType.plan),
),
),
],
@@ -75,19 +60,16 @@ class _MyPlanWidgetState extends State<MyPlanWidget> {
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
widget.plan.name,
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
),
child: Text(widget.plan.name, style: TextStyles.textMSemibold.copyWith(color: appTheme.black)),
),
if (widget.plan.details.isNotEmpty) ...[
const SizedBox(
height: 12,
),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Html(
data: widget.plan.details.length > 200 ? '${widget.plan.details.substring(0, 200).replaceAll('\n', '<br>')}...' : widget.plan.details.replaceAll('\n', '<br>'),
data: widget.plan.details.length > 200
? '${widget.plan.details.substring(0, 200).replaceAll('\n', '<br>')}...'
: widget.plan.details.replaceAll('\n', '<br>'),
style: {
"*": Style(
margin: Margins.zero,
@@ -116,26 +98,61 @@ class _MyPlanWidgetState extends State<MyPlanWidget> {
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: [
// Buttons aligned with MyRecipeWidget: Delete (gray) and Share (blue)
Row(
children: [
Expanded(
child: CustomOutlinedButton(
text: switch (state.selectedStateIndex) {
1 => s.publish,
_ => s.edit,
onPressed: () async {
final result = await context.read<MyPlansCubit>().deletePlan(widget.plan.slug);
if (result) {
Future.delayed(const Duration(seconds: 3), () {
if (!context.mounted) return;
showBottomSheetWithBlur(
context: context,
child: Column(
children: [
const SizedBox(height: 20),
const BottomSheetHandlerBar(),
const SizedBox(height: 20),
Text(widget.plan.name, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
Text(s.deleted, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: CustomOutlinedButton(
onPressed: () {
context.pop();
},
text: 'Хорошо',
height: 52,
buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
),
),
),
const SizedBox(height: 20),
],
),
);
});
}
},
text: 'Удалить',
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.black),
buttonStyle: ButtonStyle(
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0),
side: const WidgetStatePropertyAll(BorderSide.none),
),
),
),
if (state.selectedStateIndex == 0) ...[
const SizedBox(width: 6),
CustomOutlinedButton(
const SizedBox(width: 8),
Expanded(
child: CustomOutlinedButton(
onPressed: () async {
final box = context.findRenderObject() as RenderBox?;
await SharePlus.instance.share(
@@ -146,109 +163,14 @@ class _MyPlanWidgetState extends State<MyPlanWidget> {
);
},
height: 52,
width: 52,
leftIcon: SvgPicture.asset(
'assets/icons/share2.svg',
width: 20,
height: 20,
colorFilter: ColorFilter.mode(appTheme.whiteA700, BlendMode.srcIn),
),
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.red),
buttonStyle: ButtonStyle(
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
side: const WidgetStatePropertyAll(BorderSide.none),
),
text: '',
text: s.share,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
),
],
],
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: CustomOutlinedButton(
text: switch (state.selectedStateIndex) {
0 => s.removeFromActive,
_ => s.edit,
},
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.black),
buttonStyle: ButtonStyle(
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0),
side: const WidgetStatePropertyAll(BorderSide.none),
),
),
),
const SizedBox(width: 6),
CustomOutlinedButton(
onPressed: () async {
final result = await context.read<MyPlansCubit>().deletePlan(widget.plan.slug);
if (result) {
Future.delayed(const Duration(seconds: 3), () {
if (!context.mounted) return;
showBottomSheetWithBlur(
context: context,
child: Column(
children: [
const SizedBox(
height: 20,
),
const BottomSheetHandlerBar(),
const SizedBox(
height: 20,
),
Text(
widget.plan.name,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
Text(
s.deleted,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
const SizedBox(
height: 20,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: CustomOutlinedButton(
onPressed: () {
context.pop();
},
text: 'Хорошо',
height: 52,
buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
),
),
),
const SizedBox(
height: 20,
),
],
),
);
});
}
},
height: 52,
width: 52,
leftIcon: SvgPicture.asset(
'assets/icons/trash_can.svg',
width: 20,
height: 20,
),
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.red),
buttonStyle: ButtonStyle(
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0),
side: const WidgetStatePropertyAll(BorderSide.none),
),
text: '',
),
],
),

View File

@@ -1,71 +1,3 @@
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/animated_option_button.dart';
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart';
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// File intentionally left minimal after removing tabs for My Plans
class MyPlansTab extends StatefulWidget {
const MyPlansTab({super.key, this.additionalOnTap});
final void Function()? additionalOnTap;
@override
State<MyPlansTab> createState() => _MyPlansTabState();
}
class _MyPlansTabState extends State<MyPlansTab> {
final List<String> tabTitles = [];
@override
void didChangeDependencies() {
final s = S.of(context);
if (tabTitles.isEmpty) {
tabTitles.add(s.published);
tabTitles.add(s.drafted);
}
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<MyPlansCubit, MyPlansState>(
builder: (context, state) {
return Align(
alignment: Alignment.centerLeft,
child: Row(
children: [
Expanded(
child: Container(
height: 48,
decoration: BoxDecoration(
color: appTheme.whiteA700,
borderRadius: BorderRadius.circular(100),
),
padding: const EdgeInsets.all(4),
child: Row(
children: [
...List.generate(
2,
(index) => Expanded(
child: AnimatedOptionButton(
textStyle: TextStyles.textSMedium,
isSelected: state.selectedStateIndex == index,
text: tabTitles[index],
onTap: () {
widget.additionalOnTap?.call();
context.read<MyPlansCubit>().changeSelectedStateIndex(index);
},
),
),
),
],
),
),
),
],
),
);
},
);
}
}
// Drafts/published tab UI removed per requirements

View File

@@ -307,7 +307,7 @@ class _ChosenPlanFinishedToConsultantWidget extends StatelessWidget {
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.green)),
onPressed: () {
GoRouter.of(context).pushNamed(CardSettingsScreen.name, extra: notification.planId);
GoRouter.of(context).push(CardSettingsScreen.path, extra: notification.planId);
},
),
],

View File

@@ -19,6 +19,7 @@ abstract class ProfileModel with _$ProfileModel {
@JsonKey(name: "updated_at") DateTime? updatedAt,
@JsonKey(name: "is_public_profile", fromJson: parseBool) required bool isPublicProfile,
@JsonKey(name: "is_blocked_by_me", fromJson: parseBool) required bool isBlockedByMe,
@JsonKey(name: "is_blocked_by_author", fromJson: parseBool) required bool isBlockedByAuthor,
@JsonKey(name: "is_blocked_by_user", fromJson: parseBool, defaultValue: false) required bool isBlockedByUser,
@JsonKey(name: 'is_entity_subscribed_by_me') required bool isEntitySubscribedByMe,
@JsonKey(name: "slug") required String slug,

View File

@@ -18,11 +18,7 @@ class ProfileCubit extends Cubit<ProfileState> {
final ConsultantRepository consultantRepository;
String _currentPeriod = '1';
ProfileCubit({
required this.profileRepository,
required this.chatRepository,
required this.consultantRepository,
}) : super(const ProfileState.loading());
ProfileCubit({required this.profileRepository, required this.chatRepository, required this.consultantRepository}) : super(const ProfileState.loading());
Future<void> getProfileBySlug(String slug) async {
emit(const ProfileState.loading());
@@ -42,9 +38,7 @@ class ProfileCubit extends Cubit<ProfileState> {
}
}
Future<void> getMyProfile({
String period = '1',
}) async {
Future<void> getMyProfile({String period = '1'}) async {
emit(const ProfileState.loading());
_currentPeriod = period;
try {
@@ -77,15 +71,14 @@ class ProfileCubit extends Cubit<ProfileState> {
profile: (state as ProfileStateLoaded).profile,
profileDetails: (state as ProfileStateLoaded).profileDetails,
blogPosts: feedBlogPostList,
weightPoints: (state as ProfileStateLoaded).weightPoints,
circumferencePoints: (state as ProfileStateLoaded).circumferencePoints,
consultantInfo: (state as ProfileStateLoaded).consultantInfo,
),
);
}
Future<void> addCircumference({
required int chestCircumference,
required int waistCircumference,
required int hipCircumference,
}) async {
Future<void> addCircumference({required int chestCircumference, required int waistCircumference, required int hipCircumference}) async {
try {
await profileRepository.addCircumference(
chestCircumference: chestCircumference,
@@ -98,9 +91,7 @@ class ProfileCubit extends Cubit<ProfileState> {
}
}
Future<void> addWeight({
required double weight,
}) async {
Future<void> addWeight({required double weight}) async {
try {
await profileRepository.addWeight(weight: weight);
await getMyProfile(period: _currentPeriod);
@@ -109,25 +100,15 @@ class ProfileCubit extends Cubit<ProfileState> {
}
}
Future<String?> getChatRoomIdBySlug({
required Author author,
}) async {
Future<String?> getChatRoomIdBySlug({required Author author}) async {
try {
final chatIdOrError = await chatRepository.getChatByUserSlug(author.authorSlug);
if (chatIdOrError == '404') {
AppRouter.navigatorKey.currentContext?.pushNamed(
'emptyChatScreen',
extra: author,
);
AppRouter.navigatorKey.currentContext?.pushNamed('emptyChatScreen', extra: author);
} else if (chatIdOrError == 'error') {
showErrorToast('Ошибка создания чата');
} else {
AppRouter.navigatorKey.currentContext?.pushNamed(
'chatScreen',
extra: {
'uuid': chatIdOrError,
},
);
AppRouter.navigatorKey.currentContext?.pushNamed('chatScreen', extra: {'uuid': chatIdOrError});
return chatIdOrError;
}
} catch (e, st) {

View File

@@ -1,11 +1,11 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:dismissible_page/dismissible_page.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/enums/complained_content.dart';
import 'package:eatvio_app/core/permission_handler/permission_handler.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
@@ -93,39 +93,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
}
isFirstFetch = false;
}
// Проверяем, заблокировал ли пользователь меня
if (state.profile.isBlockedByUser) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.block,
size: 64,
color: Colors.grey[400],
),
const SizedBox(height: 16),
Text(
'Пользователь ограничил доступ к своей странице',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
color: Colors.grey[600],
),
),
],
),
),
),
);
}
return widget.author == null || widget.author!.authorIsMe ? _myProfileScreen(state) : _profileScreen(state);
} else if (state is ProfileStateError) {
return Scaffold(
@@ -207,11 +175,42 @@ class _ProfileScreenState extends State<ProfileScreen> {
),
_subscriptionsAndSubscribers(state),
if (state.profile.badges.isNotEmpty) Padding(padding: const EdgeInsets.only(bottom: 16), child: _badges(state)),
if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe) _socialLinks(state),
if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe) _education(state),
if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe) _diplomasAndCertificates(state),
if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !hidePlans && !state.profile.isBlockedByMe) _activePlans(state),
_blogPosts(state),
if (state.profile.isBlockedByAuthor)
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.block, size: 48, color: appTheme.grey7B),
const SizedBox(height: 16),
Center(
child: Text(
'Пользователь ограничил доступ\nк своей странице',
textAlign: TextAlign.center,
style: TextStyles.textMSemibold.copyWith(color: appTheme.grey7B),
),
),
],
),
if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe && !state.profile.isBlockedByAuthor)
_socialLinks(state),
if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe && !state.profile.isBlockedByAuthor)
_education(state),
if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe && !state.profile.isBlockedByAuthor)
_diplomasAndCertificates(state),
if (state.profile.roles.contains('consultant') &&
state.consultantInfo != null &&
!hidePlans &&
!state.profile.isBlockedByMe &&
!state.profile.isBlockedByAuthor)
_activePlans(state),
if (state.blogPosts.isNotEmpty && !state.profile.isBlockedByAuthor && !state.profile.isBlockedByMe)
const Padding(
padding: EdgeInsets.only(left: 8, top: 16, bottom: 8),
child: Text(
'Блог',
style: TextStyle(color: Color(0xFF323F4B), fontSize: 24, fontWeight: FontWeight.w600),
),
),
if (state.blogPosts.isNotEmpty && !state.profile.isBlockedByAuthor && !state.profile.isBlockedByMe) _blogPosts(state),
],
),
),
@@ -277,9 +276,9 @@ class _ProfileScreenState extends State<ProfileScreen> {
_subscriptionsAndSubscribers(state),
if (state.profile.badges.isNotEmpty) Padding(padding: const EdgeInsets.only(bottom: 16), child: _badges(state)),
if (state.profile.roles.contains('consultant')) _socialLinks(state),
Padding(padding: const EdgeInsets.only(bottom: 16), child: _charts()),
if (state.profile.roles.contains('consultant')) _education(state),
if (state.profile.roles.contains('consultant')) _diplomasAndCertificates(state),
Padding(padding: const EdgeInsets.only(bottom: 16), child: _charts()),
if (state.profile.roles.contains('consultant') && !hidePlans) _activePlans(state),
_newBlogPostButton(),
_blogPosts(state),
@@ -512,6 +511,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
children: [
ListView.separated(
shrinkWrap: true,
padding: const EdgeInsets.all(0),
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _educationBlock(
state.consultantInfo!.educationModel.completedCourses![index].values.first.title,
@@ -766,7 +766,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
ListView _blogPosts(ProfileStateLoaded state) {
return ListView.separated(
padding: const EdgeInsets.symmetric(vertical: 16),
padding: const EdgeInsets.only(bottom: 16),
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => ProfileBlogPostWidget(index: index, profileCubit: profileCubit),

View File

@@ -40,6 +40,12 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
final pageController = PageController();
final RegistrationCubit registrationCubit = sl<RegistrationCubit>();
@override
void initState() {
super.initState();
RegistrationAnalyticsService.reportRegistrationStep(step);
}
@override
Widget build(BuildContext context) {
return BlocProvider<RegistrationCubit>(

View File

@@ -1,5 +1,7 @@
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart';
import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart';
import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:eatvio_app/features/settings/models/profile_settings_model.dart';
@@ -11,16 +13,20 @@ class SettingsCubit extends Cubit<SettingsState> {
final ProfileRepository profileRepository;
final SettingsRepository settingsRepository;
final ConsultantRepository consultantRepository;
final ConsultationsRepository consultationsRepository;
SettingsCubit({
required this.profileRepository,
required this.settingsRepository,
required this.profileRepository,
required this.settingsRepository,
required this.consultantRepository,
required this.consultationsRepository,
}) : super(const SettingsState.loading());
Future<void> init() async {
final profile = await profileRepository.getMyProfile();
late final ConsultationRequestModel? consultantInfo;
late final bool hasActiveConsultation;
try {
if (profile.phoneConfirmationState.contains('confirmed')) {
consultantInfo = await consultantRepository.getMyRequestStatus(profile.slug);
@@ -30,6 +36,13 @@ class SettingsCubit extends Cubit<SettingsState> {
} catch (e) {
consultantInfo = null;
}
try {
hasActiveConsultation = await _checkActiveConsultation();
} catch (e) {
hasActiveConsultation = false;
}
try {
emit(
SettingsState.loaded(
@@ -37,6 +50,7 @@ class SettingsCubit extends Cubit<SettingsState> {
profileDetails: await profileRepository.getMyProfileDetails(),
profile: profile,
consultationRequestModel: consultantInfo,
hasActiveConsultation: hasActiveConsultation,
),
);
} catch (e) {
@@ -44,13 +58,9 @@ class SettingsCubit extends Cubit<SettingsState> {
}
}
Future<void> updateProfileDetails({
required ProfileDetailsModel profileDetailsModel,
}) async {
Future<void> updateProfileDetails({required ProfileDetailsModel profileDetailsModel}) async {
try {
await settingsRepository.updateProfileDetails(
profileDetailsModel: profileDetailsModel,
);
await settingsRepository.updateProfileDetails(profileDetailsModel: profileDetailsModel);
} catch (e) {
rethrow;
} finally {
@@ -58,13 +68,9 @@ class SettingsCubit extends Cubit<SettingsState> {
}
}
Future<void> updateProfileSettings({
required ProfileSettingsModel profileSettingsModel,
}) async {
Future<void> updateProfileSettings({required ProfileSettingsModel profileSettingsModel}) async {
try {
await settingsRepository.updateProfileSettings(
profileSettingsModel: profileSettingsModel,
);
await settingsRepository.updateProfileSettings(profileSettingsModel: profileSettingsModel);
} catch (e) {
rethrow;
} finally {
@@ -80,4 +86,17 @@ class SettingsCubit extends Cubit<SettingsState> {
return false;
}
}
Future<bool> _checkActiveConsultation() async {
try {
final userPurchases = await consultationsRepository.getUserPurchases(null, null);
final activeStates = ChosenPlanStateEnum.getActiveStates();
return userPurchases.any((purchase) =>
activeStates.contains(purchase.chosenPlanState)
);
} catch (e) {
return false;
}
}
}

View File

@@ -16,6 +16,7 @@ abstract class SettingsState with _$SettingsState {
required ProfileDetailsModel profileDetails,
required ProfileModel profile,
ConsultationRequestModel? consultationRequestModel,
required bool hasActiveConsultation,
}) = SettingsStateLoaded;
const factory SettingsState.error({

View File

@@ -32,6 +32,24 @@ class _SettingsScreenState extends State<SettingsScreen> {
late bool bedTimeSwitchState;
late bool moodSwitchState;
void _showConsultationWarning() {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Предупреждение'),
content: const Text('Во время консультации этот параметр нельзя отключить'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Понятно'),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return BlocProvider<SettingsCubit>(
@@ -58,8 +76,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
children: [
_additionalInfo(state),
const SizedBox(height: 12),
// _notifications(state),
// const SizedBox(height: 12),
_notifications(state),
const SizedBox(height: 12),
_becomeConsultantButton(state),
_deleteAccountButton(state, settingsCubit),
],
@@ -147,76 +165,35 @@ class _SettingsScreenState extends State<SettingsScreen> {
);
}
// ignore: unused_element
Column _notifications(SettingsStateLoaded state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text('Уведомления', style: TextStyle(fontSize: 16, height: 1, color: Colors.grey)),
child: Text('При использовании дневника', style: TextStyle(fontSize: 16, height: 1, color: Colors.grey)),
),
_tile(
padding: const EdgeInsets.symmetric(horizontal: 16),
onTap: () {
setState(() {
updatesNotificationsSwitchState = !updatesNotificationsSwitchState;
});
settingsCubit.updateProfileSettings(profileSettingsModel: state.profileSettings.copyWith(newUpdatesNotifications: updatesNotificationsSwitchState));
},
title: 'Уведомление о новых обновлениях',
trailing: CupertinoSwitch(
value: updatesNotificationsSwitchState,
activeTrackColor: appTheme.blueA400,
onChanged: (newValue) {
setState(() {
updatesNotificationsSwitchState = newValue;
});
settingsCubit.updateProfileSettings(
profileSettingsModel: state.profileSettings.copyWith(newUpdatesNotifications: updatesNotificationsSwitchState),
);
},
),
),
_divider(context),
_tile(
padding: const EdgeInsets.symmetric(horizontal: 16),
onTap: () {
setState(() {
fillDiaryNotificationsSwitchState = !fillDiaryNotificationsSwitchState;
});
settingsCubit.updateProfileSettings(
profileSettingsModel: state.profileSettings.copyWith(diaryReminderNotifications: fillDiaryNotificationsSwitchState),
);
},
title: 'Напоминание о заполнении дневника',
trailing: CupertinoSwitch(
value: fillDiaryNotificationsSwitchState,
activeTrackColor: appTheme.blueA400,
onChanged: (newValue) {
setState(() {
fillDiaryNotificationsSwitchState = newValue;
});
settingsCubit.updateProfileSettings(
profileSettingsModel: state.profileSettings.copyWith(diaryReminderNotifications: fillDiaryNotificationsSwitchState),
);
},
),
),
_divider(context),
_tile(
padding: const EdgeInsets.symmetric(horizontal: 16),
onTap: () {
if (state.hasActiveConsultation) {
_showConsultationWarning();
return;
}
setState(() {
moodSwitchState = !moodSwitchState;
});
settingsCubit.updateProfileSettings(profileSettingsModel: state.profileSettings.copyWith(isMoodFillable: moodSwitchState));
},
title: 'Указывать “настроение” в заполнение дневника',
title: 'Указывать "настроение" в заполнение дневника',
trailing: CupertinoSwitch(
value: moodSwitchState,
value: state.hasActiveConsultation ? true : moodSwitchState,
activeTrackColor: appTheme.blueA400,
onChanged: (newValue) {
if (state.hasActiveConsultation) {
_showConsultationWarning();
return;
}
setState(() {
moodSwitchState = newValue;
});
@@ -228,16 +205,24 @@ class _SettingsScreenState extends State<SettingsScreen> {
_tile(
padding: const EdgeInsets.symmetric(horizontal: 16),
onTap: () {
if (state.hasActiveConsultation) {
_showConsultationWarning();
return;
}
setState(() {
bedTimeSwitchState = !bedTimeSwitchState;
});
settingsCubit.updateProfileSettings(profileSettingsModel: state.profileSettings.copyWith(isBedtimeFillable: bedTimeSwitchState));
},
title: 'Указывать “отход ко сну” в заполнение дневника',
title: 'Указывать "отход ко сну" в заполнение дневника',
trailing: CupertinoSwitch(
value: bedTimeSwitchState,
value: state.hasActiveConsultation ? true : bedTimeSwitchState,
activeTrackColor: appTheme.blueA400,
onChanged: (newValue) {
if (state.hasActiveConsultation) {
_showConsultationWarning();
return;
}
setState(() {
bedTimeSwitchState = newValue;
});

View File

@@ -231,7 +231,7 @@ class _UserDetailsContentState extends State<UserDetailsContent> {
children: [
Text(s.has_hypo, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const SizedBox(height: 4),
CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.hasHypo.toString())),
CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.hasHypo ? 'да' : 'нет')),
],
),
),
@@ -242,7 +242,7 @@ class _UserDetailsContentState extends State<UserDetailsContent> {
children: [
Text(s.has_hyper, style: TextStyles.textSMedium.copyWith(color: appTheme.black)),
const SizedBox(height: 4),
CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.hasHyper.toString())),
CustomTextFormField(readOnly: true, controller: TextEditingController(text: profileDetails.hasHyper ? 'да' : 'нет')),
],
),
),

View File

@@ -526,6 +526,7 @@ class MessageLookup extends MessageLookupByLibrary {
"not_liked_groceries": MessageLookupByLibrary.simpleMessage(
"Не любимые продукты",
),
"notifications": MessageLookupByLibrary.simpleMessage("Уведомления"),
"ok": MessageLookupByLibrary.simpleMessage("Хорошо"),
"one_option": MessageLookupByLibrary.simpleMessage("1 опция"),
"option": MessageLookupByLibrary.simpleMessage("Опция"),
@@ -712,6 +713,9 @@ class MessageLookup extends MessageLookupByLibrary {
"tell_your_schedule": MessageLookupByLibrary.simpleMessage(
"Расскажите в свободной форме о том как просыпаетесь, когда едите, когда ложитесь спать:",
),
"text_was_copied": MessageLookupByLibrary.simpleMessage(
"Text copied successfully",
),
"there_is_a_video": MessageLookupByLibrary.simpleMessage("Есть видео"),
"three_options": MessageLookupByLibrary.simpleMessage("3 опции"),
"time": MessageLookupByLibrary.simpleMessage("Время прохождения"),

View File

@@ -241,6 +241,7 @@ class MessageLookup extends MessageLookupByLibrary {
),
"no": MessageLookupByLibrary.simpleMessage("Нет"),
"no_account": MessageLookupByLibrary.simpleMessage("У меня нет аккаунта"),
"notifications": MessageLookupByLibrary.simpleMessage("Уведомления"),
"one_option": MessageLookupByLibrary.simpleMessage("1 опция"),
"option": MessageLookupByLibrary.simpleMessage("Опция"),
"option_content": MessageLookupByLibrary.simpleMessage("Содержание опции"),
@@ -325,6 +326,9 @@ class MessageLookup extends MessageLookupByLibrary {
"tell_your_schedule": MessageLookupByLibrary.simpleMessage(
"Расскажите в свободной форме о том как просыпаетесь, когда едите, когда ложитесь спать:",
),
"text_was_copied": MessageLookupByLibrary.simpleMessage(
"Текст успешно скопирован",
),
"three_options": MessageLookupByLibrary.simpleMessage("3 опции"),
"toOffer": MessageLookupByLibrary.simpleMessage("Предложить"),
"to_draft": MessageLookupByLibrary.simpleMessage("В черновик"),

View File

@@ -28,10 +28,9 @@ class S {
static const AppLocalizationDelegate delegate = AppLocalizationDelegate();
static Future<S> load(Locale locale) {
final name =
(locale.countryCode?.isEmpty ?? false)
? locale.languageCode
: locale.toString();
final name = (locale.countryCode?.isEmpty ?? false)
? locale.languageCode
: locale.toString();
final localeName = Intl.canonicalizedLocale(name);
return initializeMessages(localeName).then((_) {
Intl.defaultLocale = localeName;
@@ -1055,6 +1054,16 @@ class S {
);
}
/// `Text copied successfully`
String get text_was_copied {
return Intl.message(
'Text copied successfully',
name: 'text_was_copied',
desc: '',
args: [],
);
}
/// `Почта успешно скопирована`
String get mail_was_copied {
return Intl.message(
@@ -3549,6 +3558,16 @@ class S {
args: [],
);
}
/// `Уведомления`
String get notifications {
return Intl.message(
'Уведомления',
name: 'notifications',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<S> {

View File

@@ -129,6 +129,7 @@
"carbohydrates": "Углеводы",
"answer": "Ответить",
"link_was_copied": "Ссылка успешно скопирована",
"text_was_copied": "Text copied successfully",
"mail_was_copied": "Почта успешно скопирована",
"choose_start_diet_date": "Выберите дату старты диеты. Она отобразиться у Вас в дневнике",
"continue_": "Продолжить",
@@ -420,6 +421,7 @@
"itCantBeRecovered": "Его нельзя будет восстановить",
"delete": "Удалить",
"cannotDeleteAccount": "Не получилось удалить ваш аккаунт",
"resend": "Отправить повторно"
"resend": "Отправить повторно",
"notifications": "Уведомления"
}

View File

@@ -129,6 +129,7 @@
"carbohydrates": "Углеводы",
"answer": "Ответить",
"link_was_copied": "Ссылка успешно скопирована",
"text_was_copied": "Текст успешно скопирован",
"mail_was_copied": "Почта успешно скопирована",
"choose_start_diet_date": "Выберите дату старты диеты. Она отобразиться у Вас в дневнике",
"continue_": "Продолжить",
@@ -213,5 +214,6 @@
"my_plans": "Мои планы",
"from": "от",
"user_data": "Данные пользователя",
"infoAboutOrder": "Информация о заказе"
"infoAboutOrder": "Информация о заказе",
"notifications": "Уведомления"
}

View File

@@ -3,6 +3,7 @@ import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/locator_service.dart' as di;
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.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';
@@ -67,7 +68,10 @@ class MyApp extends StatelessWidget {
child: MaterialApp.router(
theme: ThemeHelper().themeData(),
routerConfig: AppRouter.appRouter,
builder: (context, child) => InternetConnectionCheckerScreen(child: child!),
builder: (context, child) => BlocProvider(
create: (context) => sl<ViewsCubit>(),
child: InternetConnectionCheckerScreen(child: child!),
),
debugShowCheckedModeBanner: false,
localizationsDelegates: const [
S.delegate,

View File

@@ -1,6 +1,6 @@
name: eatvio_app
description: A new Flutter project.
version: 1.2.11+71
version: 1.2.14+74
publish_to: 'none'
environment:
@@ -74,6 +74,8 @@ dependencies:
flutter_dotenv: ^5.2.1
uuid: ^4.5.1
appmetrica_plugin: ^3.3.0
vk_id: ^1.0.3
flutter_login_yandex: ^0.0.5
dev_dependencies:
build_runner: ^2.7.0

View File

@@ -30,6 +30,7 @@ void main() {
badges: [],
subscriptions: [],
subscribers: [],
isBlockedByAuthor: false,
),
),
);
@@ -72,23 +73,17 @@ void main() {
});
test('getBlogPostByAuthorSlug', () async {
when(mockProfileService.getBlogPostByAuthorSlug(slug: '')).thenAnswer(
(_) async => [],
);
when(mockProfileService.getBlogPostByAuthorSlug(slug: '')).thenAnswer((_) async => []);
expect(await mockProfileService.getBlogPostByAuthorSlug(slug: 'slug'), isA<List<FeedBlogPostModel>>());
});
test('getWeight', () async {
when(mockProfileService.getWeight(period: '')).thenAnswer(
(_) async => [],
);
when(mockProfileService.getWeight(period: '')).thenAnswer((_) async => []);
expect(await mockProfileService.getWeight(period: ''), isA<List<WeightModel>>());
});
test('getCircumferences', () async {
when(mockProfileService.getCircumferences(period: '')).thenAnswer(
(_) async => [],
);
when(mockProfileService.getCircumferences(period: '')).thenAnswer((_) async => []);
expect(await mockProfileService.getCircumferences(period: ''), isA<List<CircumferenceModel>>());
});
});