Merge pull request 'build 73' (#694) from main into test-flight

Reviewed-on: eatvio/eatvio_app#694
This commit is contained in:
mso7nueh
2025-09-10 13:46:09 +00:00
38 changed files with 1196 additions and 809 deletions

View File

@@ -21,20 +21,19 @@ jobs:
- name: Copy .env file
run: |
mkdir -p env
cp env_repo/.env env/.env
cp env_repo/.env .env
- name: Load environment variables from .env
run: |
set -a
source env/.env
source .env
set +a
# Export all variables to GITHUB_ENV
while IFS= read -r line; do
if [[ $line =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then
echo "$line" >> $GITHUB_ENV
fi
done < env/.env
done < .env
- name: Unlock login keychain
run: |

View File

@@ -26,20 +26,19 @@ jobs:
- name: Copy .env file
run: |
mkdir -p env
cp env_repo/.env env/.env
cp env_repo/.env .env
- name: Load environment variables from .env
run: |
set -a
source env/.env
source .env
set +a
# Export all variables to GITHUB_ENV
while IFS= read -r line; do
if [[ $line =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then
echo "$line" >> $GITHUB_ENV
fi
done < env/.env
done < .env
- name: Create key.properties file
run: |

0
env/.env vendored
View File

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

View File

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

View File

@@ -24,6 +24,7 @@ import 'package:eatvio_app/features/consultations/presentation/pages/consultatio
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart';
import 'package:eatvio_app/features/menu/presentation/pages/blocked_users_screen.dart';
import 'package:eatvio_app/features/menu/presentation/pages/notifications_settings_screen.dart';
import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart';
import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart';
import 'package:eatvio_app/features/create_diet/presentation/diet_preview_screen.dart';
@@ -111,23 +112,13 @@ class AppRouter {
name: 'authScreen',
builder: (context, state) => const AuthScreen(),
routes: [
GoRoute(
path: 'registration-screen',
name: 'registrationScreen',
builder: (context, state) => const RegistrationScreen(),
),
GoRoute(path: 'registration-screen', name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()),
GoRoute(
path: 'forget-password-screen',
name: 'forgetPasswordScreen',
builder: (context, state) => ForgetPasswordScreen(
isAuth: state.extra as bool,
),
),
GoRoute(
path: 'scan-qr-code-screen',
name: 'scanQRCodeScreen',
builder: (context, state) => const ScanQRCodeScreen(),
builder: (context, state) => ForgetPasswordScreen(isAuth: state.extra as bool),
),
GoRoute(path: 'scan-qr-code-screen', name: 'scanQRCodeScreen', builder: (context, state) => const ScanQRCodeScreen()),
],
),
ShellRoute(
@@ -165,19 +156,23 @@ class AppRouter {
GoRoute(
path: '/diary-screen',
name: 'diaryScreen',
pageBuilder: (context, state) => const NoTransitionPage(
child: DiaryScreen(),
),
pageBuilder: (context, state) => const NoTransitionPage(child: DiaryScreen()),
),
GoRoute(
path: ConsultationsScreen.path,
name: ConsultationsScreen.name,
pageBuilder: (context, state) => const NoTransitionPage(child: ConsultationsScreen())),
GoRoute(path: '/chat-select-screen', name: 'chatSelectScreen', pageBuilder: (context, state) => const NoTransitionPage(child: ChatsScreen())),
path: ConsultationsScreen.path,
name: ConsultationsScreen.name,
pageBuilder: (context, state) => const NoTransitionPage(child: ConsultationsScreen()),
),
GoRoute(
path: '/notifications-screen',
name: 'notificationsScreen',
pageBuilder: (context, state) => const NoTransitionPage(child: NotificationsScreen())),
path: '/chat-select-screen',
name: 'chatSelectScreen',
pageBuilder: (context, state) => const NoTransitionPage(child: ChatsScreen()),
),
GoRoute(
path: '/notifications-screen',
name: 'notificationsScreen',
pageBuilder: (context, state) => const NoTransitionPage(child: NotificationsScreen()),
),
GoRoute(
path: '/menu-screen',
name: 'menuScreen',
@@ -194,11 +189,7 @@ class AppRouter {
),
],
),
GoRoute(
name: 'blockedUsersScreen',
path: '/blocked-users-screen',
builder: (context, state) => const BlockedUsersScreen(),
),
GoRoute(name: 'blockedUsersScreen', path: '/blocked-users-screen', builder: (context, state) => const BlockedUsersScreen()),
GoRoute(
path: '/welcome-screen',
name: 'welcomeScreen',
@@ -241,26 +232,10 @@ class AppRouter {
}
},
),
GoRoute(
path: ConsultationsArchiveScreen.path,
name: ConsultationsArchiveScreen.name,
builder: (context, state) => const ConsultationsArchiveScreen(),
),
GoRoute(
path: ConsultantFinancesScreen.path,
name: ConsultantFinancesScreen.name,
builder: (context, state) => const ConsultantFinancesScreen(),
),
GoRoute(
path: UserPurchasesScreen.path,
name: UserPurchasesScreen.name,
builder: (context, state) => const UserPurchasesScreen(),
),
GoRoute(
path: '/user-finance-screen',
name: 'userFinanceScreen',
builder: (context, state) => const UserFinanceScreen(),
),
GoRoute(path: ConsultationsArchiveScreen.path, name: ConsultationsArchiveScreen.name, builder: (context, state) => const ConsultationsArchiveScreen()),
GoRoute(path: ConsultantFinancesScreen.path, name: ConsultantFinancesScreen.name, builder: (context, state) => const ConsultantFinancesScreen()),
GoRoute(path: UserPurchasesScreen.path, name: UserPurchasesScreen.name, builder: (context, state) => const UserPurchasesScreen()),
GoRoute(path: '/user-finance-screen', name: 'userFinanceScreen', builder: (context, state) => const UserFinanceScreen()),
GoRoute(
path: CardSettingsScreen.path,
name: CardSettingsScreen.name,
@@ -275,11 +250,7 @@ class AppRouter {
builder: (context, state) {
final slug = state.uri.queryParameters['slug'];
if (slug != null) {
return AboutPlanScreen(
slug: slug,
navigateToComments: false,
index: null,
);
return AboutPlanScreen(slug: slug, navigateToComments: false, index: null);
}
final data = state.extra as Map<String, dynamic>;
@@ -293,29 +264,23 @@ class AppRouter {
builder: (context, state) {
final slug = state.uri.queryParameters['slug'];
if (slug != null) {
return AboutRecipeScreen(
recipeSlug: slug,
navigateToComments: false,
index: null,
);
return AboutRecipeScreen(recipeSlug: slug, navigateToComments: false, index: null);
}
final data = state.extra as Map<String, dynamic>;
return AboutRecipeScreen(
recipeSlug: data['recipe'] as String,
index: data['index'] as int?,
navigateToComments: data['navigateToComments'] as bool,
);
return AboutRecipeScreen(recipeSlug: data['recipe'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
},
),
GoRoute(
path: RecipeCreateScreen.path,
name: RecipeCreateScreen.name,
builder: (context, state) => RecipeCreateScreen(
recipe: state.extra != null ? state.extra as Recipe : null,
),
builder: (context, state) => RecipeCreateScreen(recipe: state.extra != null ? state.extra as Recipe : null),
),
GoRoute(
path: '/profile-screen',
name: 'profileScreen',
builder: (context, state) => ProfileScreen(author: state.extra as Author),
),
GoRoute(path: '/profile-screen', name: 'profileScreen', builder: (context, state) => ProfileScreen(author: state.extra as Author)),
GoRoute(
path: '/subs-screen/:slug',
name: 'subsScreen',
@@ -327,11 +292,7 @@ class AppRouter {
builder: (context, state) {
final slug = state.uri.queryParameters['slug'];
if (slug != null) {
return AboutDietScreen(
slug: slug,
index: null,
navigateToComments: false,
);
return AboutDietScreen(slug: slug, index: null, navigateToComments: false);
}
final data = state.extra as Map<String, dynamic>;
return AboutDietScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
@@ -343,19 +304,18 @@ class AppRouter {
builder: (context, state) => BlogPostCreateScreen(feedBlogPostModel: state.extra != null ? state.extra as FeedBlogPostModel : null),
),
GoRoute(
path: '/blog-post-screen',
name: 'blogPostScreen',
builder: (context, state) {
final id = state.uri.queryParameters['id'];
final parsedId = int.tryParse(id ?? '');
if (parsedId != null) {
return BlogPostScreen(
id: parsedId,
);
}
path: '/blog-post-screen',
name: 'blogPostScreen',
builder: (context, state) {
final id = state.uri.queryParameters['id'];
final parsedId = int.tryParse(id ?? '');
if (parsedId != null) {
return BlogPostScreen(id: parsedId);
}
return BlogPostScreen(id: state.extra as int);
}),
return BlogPostScreen(id: state.extra as int);
},
),
GoRoute(
path: '/chat-screen',
name: 'chatScreen',
@@ -363,10 +323,7 @@ class AppRouter {
final data = state.extra as Map<String, dynamic>;
final uuid = data['uuid'] as String;
final sendInitialMessage = data['sendInitialMessage'] as bool?;
return ChatScreen(
uuid: uuid,
sendInitialMessage: sendInitialMessage,
);
return ChatScreen(uuid: uuid, sendInitialMessage: sendInitialMessage);
},
),
GoRoute(
@@ -399,7 +356,11 @@ class AppRouter {
name: 'physicalFormScreen',
builder: (context, state) => PhysicalFormScreen(settingsEntity: state.extra as SettingsEntity),
),
GoRoute(path: 'pressure-screen', name: 'pressureScreen', builder: (context, state) => PressureScreen(settingsEntity: state.extra as SettingsEntity)),
GoRoute(
path: 'pressure-screen',
name: 'pressureScreen',
builder: (context, state) => PressureScreen(settingsEntity: state.extra as SettingsEntity),
),
GoRoute(
path: 'change-password-screen',
name: 'changePasswordScreen',
@@ -416,9 +377,7 @@ class AppRouter {
GoRoute(
path: '/stepper-become-consultant-screen',
name: 'stepperBecomeConsultantScreen',
builder: (context, state) => StepperBecomeConsultantScreen(
slug: state.extra == null ? null : state.extra as String,
),
builder: (context, state) => StepperBecomeConsultantScreen(slug: state.extra == null ? null : state.extra as String),
),
GoRoute(path: '/calendar-screen', name: 'calendarScreen', builder: (context, state) => const CalendarScreen()),
GoRoute(path: '/support-screen', name: 'supportScreen', builder: (context, state) => const SupportScreen()),
@@ -446,7 +405,11 @@ class AppRouter {
GoRoute(path: CreatePlanScreen.path, name: CreatePlanScreen.name, builder: (context, state) => const CreatePlanScreen()),
GoRoute(path: MyDietsScreen.path, name: MyDietsScreen.name, builder: (context, state) => const MyDietsScreen()),
GoRoute(path: MyPlansScreen.path, name: MyPlansScreen.name, builder: (context, state) => const MyPlansScreen()),
GoRoute(path: UserDetailsScreen.path, name: UserDetailsScreen.name, builder: (context, state) => UserDetailsScreen(slug: state.extra as String)),
GoRoute(
path: UserDetailsScreen.path,
name: UserDetailsScreen.name,
builder: (context, state) => UserDetailsScreen(slug: state.extra as String),
),
GoRoute(path: DevToolsScreen.path, name: DevToolsScreen.name, builder: (context, state) => const DevToolsScreen()),
GoRoute(path: ConsultantRatingScreen.path, name: ConsultantRatingScreen.name, builder: (context, state) => const ConsultantRatingScreen()),
GoRoute(
@@ -456,10 +419,7 @@ class AppRouter {
final data = state.extra as Map<String, dynamic>;
final planId = data['planId'] as int;
final successfulPayment = data['successfulPayment'] as bool?;
return InfoByOrderScreen(
planId: planId,
successfulPayment: successfulPayment,
);
return InfoByOrderScreen(planId: planId, successfulPayment: successfulPayment);
},
),
GoRoute(
@@ -469,10 +429,7 @@ class AppRouter {
final data = state.extra as Map<String, dynamic>;
final planId = data['planId'] as int;
final url = data['url'] as String;
return PaymentScreen(
url: url,
planId: planId,
);
return PaymentScreen(url: url, planId: planId);
},
),
GoRoute(
@@ -482,10 +439,7 @@ class AppRouter {
final data = state.extra as Map<String, int>;
final diaryId = data['diaryId'] as int;
final currentPlanDay = data['currentDiaryDay'] as int;
return UserDiaryScreen(
diaryId: diaryId,
currentPlanDay: currentPlanDay,
);
return UserDiaryScreen(diaryId: diaryId, currentPlanDay: currentPlanDay);
},
),
GoRoute(
@@ -493,16 +447,9 @@ class AppRouter {
name: 'verifyScreen',
builder: (context, state) => VerifyScreen(passKey: state.extra as String),
),
GoRoute(
path: '/verify-email-screen',
name: 'verifyEmailScreen',
builder: (context, state) => const VerifyEmailScreen(),
),
GoRoute(
path: MyRecipesScreen.path,
name: MyRecipesScreen.name,
builder: (context, state) => const MyRecipesScreen(),
),
GoRoute(path: '/verify-email-screen', name: 'verifyEmailScreen', builder: (context, state) => const VerifyEmailScreen()),
GoRoute(path: MyRecipesScreen.path, name: MyRecipesScreen.name, builder: (context, state) => const MyRecipesScreen()),
GoRoute(path: '/notifications-settings-screen', name: 'notificationsSettingsScreen', builder: (context, state) => const NotificationsSettingsScreen()),
],
);
}

View File

@@ -43,8 +43,11 @@ import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_cubit.dart';
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart';
import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart';
import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart';
import 'package:eatvio_app/features/menu/repositories/notification_settings_repository.dart';
import 'package:eatvio_app/features/menu/services/blocked_users_service.dart';
import 'package:eatvio_app/features/menu/services/notification_settings_service.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
import 'package:eatvio_app/features/calendar/presentation/bloc/calendar_cubit.dart';
@@ -136,14 +139,16 @@ Future<void> init() async {
sl.registerFactory<PlansCubit>(() => PlansCubit(feedRepository: sl(), planRepository: sl()));
sl.registerFactory<SettingsCubit>(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl(), consultantRepository: sl()));
sl.registerFactory<RegisterConsultantCubit>(() => RegisterConsultantCubit(consultantRepository: sl()));
sl.registerFactory<DiaryCubit>(() => DiaryCubit(
chatRepository: sl(),
diaryRepository: sl(),
glossaryRepository: sl(),
profileRepository: sl(),
dietRepository: sl(),
settingsRepository: sl(),
));
sl.registerFactory<DiaryCubit>(
() => DiaryCubit(
chatRepository: sl(),
diaryRepository: sl(),
glossaryRepository: sl(),
profileRepository: sl(),
dietRepository: sl(),
settingsRepository: sl(),
),
);
sl.registerFactory<AboutDietCubit>(() => AboutDietCubit(dietsRepository: sl(), profileRepository: sl()));
sl.registerFactory<BlogPostCreateCubit>(() => BlogPostCreateCubit(blogPostRepository: sl()));
sl.registerFactory<SubCubit>(() => SubCubit(profileRepository: sl()));
@@ -172,6 +177,7 @@ Future<void> init() async {
sl.registerFactory<ResetPasswordCubit>(() => ResetPasswordCubit(resetPasswordRepository: sl()));
sl.registerFactory<BlogPostCubit>(() => BlogPostCubit(sl()));
sl.registerFactory<BlockedUsersCubit>(() => BlockedUsersCubit(blockedUsersRepository: sl()));
sl.registerFactory<NotificationSettingsCubit>(() => NotificationSettingsCubit(notificationSettingsRepository: sl()));
sl.registerFactory<ConsultantRatingCubit>(() => ConsultantRatingCubit(sl()));
// Repository
@@ -204,6 +210,7 @@ Future<void> init() async {
sl.registerFactory<ResetPasswordRepository>(() => ResetPasswordRepository(resetPasswordService: sl()));
sl.registerLazySingleton<BlockedUsersRepository>(() => BlockedUsersRepository(blockedUserService: sl()));
sl.registerLazySingleton<CommentsRepository>(() => CommentsRepository(service: sl()));
sl.registerLazySingleton<NotificationSettingsRepository>(() => NotificationSettingsRepository(sl()));
sl.registerFactory<ConsultantRatingRepository>(() => ConsultantRatingRepository(sl()));
// Services
@@ -239,22 +246,12 @@ Future<void> init() async {
sl.registerLazySingleton<CommentsService>(() => CommentsService(sl()));
sl.registerLazySingleton<MyRecipesService>(() => MyRecipesService(sl()));
sl.registerLazySingleton<ConsultantRatingService>(() => ConsultantRatingService(sl()));
sl.registerLazySingleton<NotificationSettingsService>(() => NotificationSettingsService(sl()));
// External
sl.registerLazySingleton(
() => Dio(
BaseOptions(
baseUrl: baseUrl,
headers: {
'accept': 'application/json',
'content-type': 'application/json',
},
),
)..interceptors.addAll(
[
TokenInterceptor(),
PhoneVerifyErrorInterceptor(),
],
),
() =>
Dio(BaseOptions(baseUrl: baseUrl, headers: {'accept': 'application/json', 'content-type': 'application/json'}))
..interceptors.addAll([TokenInterceptor(), PhoneVerifyErrorInterceptor()]),
);
}

View File

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

View File

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

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

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

@@ -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;
}
@@ -75,109 +117,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 +172,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 +225,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 +240,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

@@ -12,70 +12,57 @@ 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;
if (feedBlogPostModel == null || isClosed) 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 (!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

@@ -0,0 +1,39 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'notification_settings_model.g.dart';
part 'notification_settings_model.freezed.dart';
@freezed
abstract class NotificationSettingsModel with _$NotificationSettingsModel {
const factory NotificationSettingsModel({
@JsonKey(name: 'new_updates_notification') required bool newUpdatesNotification,
@JsonKey(name: 'weight_measurements_notification') required bool weightMeasurementsNotification,
@JsonKey(name: 'is_email_notifications_enabled') required bool isEmailNotificationsEnabled,
@JsonKey(name: 'is_push_notifications_enabled') required bool isPushNotificationsEnabled,
@JsonKey(name: 'diary_reminder_notifications') required DiaryReminderNotifications diaryReminderNotifications,
}) = _NotificationSettingsModel;
factory NotificationSettingsModel.fromJson(Map<String, Object?> json) => _$NotificationSettingsModelFromJson(json);
}
@freezed
abstract class DiaryReminderNotifications with _$DiaryReminderNotifications {
const factory DiaryReminderNotifications({
@JsonKey(name: 'first_reminder') required ReminderSettings firstReminder,
@JsonKey(name: 'second_reminder') required ReminderSettings secondReminder,
@JsonKey(name: 'third_reminder') required ReminderSettings thirdReminder,
}) = _DiaryReminderNotifications;
factory DiaryReminderNotifications.fromJson(Map<String, Object?> json) => _$DiaryReminderNotificationsFromJson(json);
}
@freezed
abstract class ReminderSettings with _$ReminderSettings {
const factory ReminderSettings({
required bool enabled,
@JsonKey(name: 'local_time_hours') required int localTimeHours,
@JsonKey(name: 'local_time_mins') required int localTimeMins,
}) = _ReminderSettings;
factory ReminderSettings.fromJson(Map<String, Object?> json) => _$ReminderSettingsFromJson(json);
}

View File

@@ -0,0 +1,33 @@
import 'dart:developer';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_state.dart';
import 'package:eatvio_app/features/menu/repositories/notification_settings_repository.dart';
class NotificationSettingsCubit extends Cubit<NotificationSettingsState> {
final NotificationSettingsRepository _notificationSettingsRepository;
NotificationSettingsCubit({required NotificationSettingsRepository notificationSettingsRepository})
: _notificationSettingsRepository = notificationSettingsRepository,
super(const NotificationSettingsStateLoading());
Future<void> getNotificationSettings() async {
emit(const NotificationSettingsStateLoading());
try {
final notificationSettings = await _notificationSettingsRepository.getNotificationSettings();
emit(NotificationSettingsStateLoaded(notificationSettings: notificationSettings));
} catch (e, stackTrace) {
log('Error getting notification settings: $e', stackTrace: stackTrace);
emit(NotificationSettingsStateError(errorMessage: e.toString()));
}
}
Future<void> updateNotificationSettings(Map<String, dynamic> data) async {
try {
final notificationSettings = await _notificationSettingsRepository.updateNotificationSettings(data);
emit(NotificationSettingsStateLoaded(notificationSettings: notificationSettings));
} catch (e, stackTrace) {
log('Error updating notification settings: $e', stackTrace: stackTrace);
emit(NotificationSettingsStateError(errorMessage: e.toString()));
}
}
}

View File

@@ -0,0 +1,31 @@
import 'package:equatable/equatable.dart';
import 'package:eatvio_app/features/menu/models/notification_settings_model.dart';
abstract class NotificationSettingsState extends Equatable {
const NotificationSettingsState();
@override
List<Object?> get props => [];
}
class NotificationSettingsStateLoading extends NotificationSettingsState {
const NotificationSettingsStateLoading();
}
class NotificationSettingsStateLoaded extends NotificationSettingsState {
final NotificationSettingsModel notificationSettings;
const NotificationSettingsStateLoaded({required this.notificationSettings});
@override
List<Object?> get props => [notificationSettings];
}
class NotificationSettingsStateError extends NotificationSettingsState {
final String errorMessage;
const NotificationSettingsStateError({required this.errorMessage});
@override
List<Object?> get props => [errorMessage];
}

View File

@@ -67,6 +67,8 @@ class MenuScreen extends StatelessWidget {
_divider(context),
_tile(leading: ImageConstant.icnGlossary, title: 'Глоссарий', onTap: () => GoRouter.of(context).push('/glossary-screen')),
_divider(context),
_tile(leading: 'assets/icons/notifications.svg', title: 'Уведомления', onTap: () => GoRouter.of(context).pushNamed('notificationsSettingsScreen')),
_divider(context),
_tile(leading: ImageConstant.icnSupport, title: 'Поддержка', onTap: () => GoRouter.of(context).push('/support-screen')),
_divider(context),
_tile(leading: 'assets/icons/blocked_users.svg', title: 'Чёрный список', onTap: () => GoRouter.of(context).pushNamed('blockedUsersScreen')),

View File

@@ -0,0 +1,285 @@
import 'package:eatvio_app/core/locator_service.dart';
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});
@override
Widget build(BuildContext context) {
return BlocProvider(create: (context) => sl<NotificationSettingsCubit>()..getNotificationSettings(), child: const NotificationsSettingsContent());
}
}
class NotificationsSettingsContent extends StatelessWidget {
const NotificationsSettingsContent({super.key});
@override
Widget build(BuildContext context) {
final s = S.of(context);
return Scaffold(
appBar: AppBar(title: Text(s.notifications)),
body: BlocBuilder<NotificationSettingsCubit, NotificationSettingsState>(
builder: (context, state) {
if (state is NotificationSettingsStateLoading) {
return const Center(child: CircularProgressIndicator());
} else if (state is NotificationSettingsStateError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Ошибка: ${state.errorMessage}'),
ElevatedButton(
onPressed: () {
context.read<NotificationSettingsCubit>().getNotificationSettings();
},
child: const Text('Повторить'),
),
],
),
);
} else if (state is NotificationSettingsStateLoaded) {
return _buildSettingsContent(context, state.notificationSettings);
}
return const Center(child: Text('Нет данных'));
},
),
);
}
Widget _buildSettingsContent(BuildContext context, NotificationSettingsModel notificationSettings) {
final cubit = context.read<NotificationSettingsCubit>();
return Column(
children: [
_tile(
padding: const EdgeInsets.symmetric(horizontal: 16),
onTap: () {},
title: 'Новые обновления',
subtitle: null,
trailing: CupertinoSwitch(
value: notificationSettings.newUpdatesNotification,
activeTrackColor: appTheme.blueA400,
onChanged: (newValue) {
_updateNotificationSetting(cubit, {'new_updates_notification': newValue});
},
),
),
_divider(context),
_tile(
padding: const EdgeInsets.symmetric(horizontal: 16),
onTap: () {},
title: 'Напоминать о взвешивании',
subtitle: null,
trailing: CupertinoSwitch(
value: notificationSettings.weightMeasurementsNotification,
activeTrackColor: appTheme.blueA400,
onChanged: (newValue) {
_updateNotificationSetting(cubit, {'weight_measurements_notification': newValue});
},
),
),
_divider(context),
_tile(
padding: const EdgeInsets.symmetric(horizontal: 16),
onTap: () {},
title: 'Email уведомления',
subtitle: null,
trailing: CupertinoSwitch(
value: notificationSettings.isEmailNotificationsEnabled,
activeTrackColor: appTheme.blueA400,
onChanged: (newValue) {
_updateNotificationSetting(cubit, {'is_email_notifications_enabled': newValue});
},
),
),
_divider(context),
_tile(
padding: const EdgeInsets.symmetric(horizontal: 16),
onTap: () {},
title: 'Push уведомления',
subtitle: null,
trailing: CupertinoSwitch(
value: notificationSettings.isPushNotificationsEnabled,
activeTrackColor: appTheme.blueA400,
onChanged: (newValue) {
_updateNotificationSetting(cubit, {'is_push_notifications_enabled': newValue});
},
),
),
_divider(context),
_buildDiaryRemindersSection(context, notificationSettings.diaryReminderNotifications, cubit),
],
);
}
Widget _buildDiaryRemindersSection(BuildContext context, DiaryReminderNotifications diaryReminderNotifications, NotificationSettingsCubit cubit) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Напоминание о заполнении дневника',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: appTheme.cardNotificationsBG),
),
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),
],
),
);
}
void _updateNotificationSetting(NotificationSettingsCubit cubit, Map<String, dynamic> data) {
cubit.updateNotificationSettings(data);
}
void _updateReminderSetting(NotificationSettingsCubit cubit, String reminderType, Map<String, dynamic> data) {
final updateData = {
'diary_reminder_notifications': {reminderType: data},
};
cubit.updateNotificationSettings(updateData);
}
Widget _buildReminderTile(BuildContext context, String title, ReminderSettings reminder, String reminderType, NotificationSettingsCubit cubit) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: appTheme.dividerColor),
child: Column(
children: [
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,
});
},
),
],
),
],
),
);
}
void _showTimePicker(BuildContext context, ReminderSettings reminder, String reminderType, NotificationSettingsCubit cubit) {
int selectedHour = reminder.localTimeHours;
int selectedMinute = reminder.localTimeMins;
showCupertinoModalPopup<void>(
context: context,
builder: (BuildContext context) {
return Container(
height: 300,
padding: const EdgeInsets.only(top: 6.0),
margin: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
color: CupertinoColors.systemBackground.resolveFrom(context),
child: SafeArea(
top: false,
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CupertinoButton(child: const Text('Отмена'), onPressed: () => Navigator.of(context).pop()),
CupertinoButton(
child: const Text('Готово'),
onPressed: () {
_updateReminderSetting(cubit, reminderType, {
'enabled': reminder.enabled,
'local_time_hours': selectedHour,
'local_time_mins': selectedMinute,
});
Navigator.of(context).pop();
},
),
],
),
Expanded(
child: CupertinoDatePicker(
mode: CupertinoDatePickerMode.time,
use24hFormat: true,
initialDateTime: DateTime(2024, 1, 1, selectedHour, selectedMinute),
onDateTimeChanged: (DateTime newDateTime) {
selectedHour = newDateTime.hour;
selectedMinute = newDateTime.minute;
},
),
),
],
),
),
);
},
);
}
ListTile _tile({required String title, required Function()? onTap, Widget? trailing, EdgeInsets? padding, Widget? subtitle}) {
return ListTile(
contentPadding: padding,
onTap: onTap,
title: Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500, height: 1)),
subtitle: subtitle,
trailing: trailing ?? const Icon(Icons.arrow_forward_ios_rounded, size: 12, color: Colors.black38),
);
}
Center _divider(BuildContext context) {
return Center(
child: Container(
height: 0.4,
width: MediaQuery.of(context).size.width - 32,
decoration: BoxDecoration(color: Colors.black26, borderRadius: BorderRadius.circular(16)),
),
);
}
}

View File

@@ -0,0 +1,16 @@
import 'package:eatvio_app/features/menu/models/notification_settings_model.dart';
import 'package:eatvio_app/features/menu/services/notification_settings_service.dart';
class NotificationSettingsRepository {
final NotificationSettingsService _notificationSettingsService;
NotificationSettingsRepository(this._notificationSettingsService);
Future<NotificationSettingsModel> getNotificationSettings() async {
return await _notificationSettingsService.getNotificationSettings();
}
Future<NotificationSettingsModel> updateNotificationSettings(Map<String, dynamic> data) async {
return await _notificationSettingsService.updateNotificationSettings(data: data);
}
}

View File

@@ -0,0 +1,16 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/menu/models/notification_settings_model.dart';
import 'package:retrofit/retrofit.dart';
part 'notification_settings_service.g.dart';
@RestApi()
abstract class NotificationSettingsService {
factory NotificationSettingsService(Dio dio, {String baseUrl}) = _NotificationSettingsService;
@GET('/api/profile/notification_settings')
Future<NotificationSettingsModel> getNotificationSettings();
@POST('/api/profile/update_notification_settings')
Future<NotificationSettingsModel> updateNotificationSettings({@Body() required Map<String, dynamic> data});
}

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

@@ -8,6 +8,7 @@ import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart';
import 'package:eatvio_app/features/registration/presentation/widgets/change_email_dialog.dart';
import 'package:eatvio_app/features/registration/presentation/widgets/code_not_received_dialog.dart';
import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -113,7 +114,21 @@ class _VerifyEmailScreenState extends State<VerifyEmailScreen> {
Text("Код отправлен на почту\n$email", textAlign: TextAlign.center),
const SizedBox(height: 20),
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: List.generate(4, (index) => _buildCodeField(context, index))),
const SizedBox(height: 40),
const SizedBox(height: 16),
GestureDetector(
onTap: () {
final verifyEmailCubit = context.read<VerifyEmailCubit>();
showDialog(context: context, builder: (context) => CodeNotReceivedDialog(
currentEmail: email,
verifyEmailCubit: verifyEmailCubit,
));
},
child: Text(
'Не приходит код?',
style: TextStyles.textSMedium.copyWith(color: appTheme.blueA400, decoration: TextDecoration.underline),
),
),
const SizedBox(height: 24),
CustomOutlinedButton(
text: 'Запросить код повторно',
height: 52,

View File

@@ -0,0 +1,71 @@
import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/registration/presentation/widgets/change_email_dialog.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart';
class CodeNotReceivedDialog extends StatelessWidget {
final String currentEmail;
final VerifyEmailCubit verifyEmailCubit;
const CodeNotReceivedDialog({super.key, required this.currentEmail, required this.verifyEmailCubit});
void _showChangeEmailDialog(BuildContext context) {
Navigator.of(context).pop(); // Закрываем текущий диалог
showDialog(
context: context,
builder: (context) => ChangeEmailDialog(currentEmail: currentEmail, verifyEmailCubit: verifyEmailCubit),
);
}
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
child: Container(
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16.0)),
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Почему может не приходить код?', style: TextStyles.textLSemibold.copyWith(color: appTheme.black)),
const SizedBox(height: 16),
RichText(
text: TextSpan(
style: TextStyles.textMMedium.copyWith(color: appTheme.gray600),
children: [
const TextSpan(text: 'Опечатка в email: Если адрес введён неправильно, код не придёт. ', style: TextStyles.textFS13FW400),
TextSpan(
text: 'Ввести email повторно.',
style: TextStyles.textMMedium.copyWith(color: appTheme.blueA400, decoration: TextDecoration.underline),
recognizer: TapGestureRecognizer()..onTap = () => _showChangeEmailDialog(context),
),
const TextSpan(
style: TextStyles.textFS13FW400,
text:
'\n\nПроверьте спам/промо/мусорную папку: Код может попасть туда из-за фильтров почтового провайдера\n\nПодождите 1-2 минуты: Иногда email задерживается из-за сетевых проблем.\n\nУбедитесь в стабильном соединении: Проверьте интернет и уведомления на устройстве.',
),
],
),
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
style: ElevatedButton.styleFrom(
backgroundColor: appTheme.blueA400,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: Text('Понятно', style: TextStyles.textMSemibold.copyWith(color: Colors.white)),
),
),
],
),
),
);
}
}

View File

@@ -58,8 +58,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,6 +147,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
);
}
// ignore: unused_element
Column _notifications(SettingsStateLoaded state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,

View File

@@ -511,6 +511,7 @@ class MessageLookup extends MessageLookupByLibrary {
"not_liked_groceries": MessageLookupByLibrary.simpleMessage(
"Не любимые продукты",
),
"notifications": MessageLookupByLibrary.simpleMessage("Уведомления"),
"ok": MessageLookupByLibrary.simpleMessage("Хорошо"),
"one_option": MessageLookupByLibrary.simpleMessage("1 опция"),
"option": 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("Содержание опции"),

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;
@@ -3469,6 +3468,16 @@ class S {
args: [],
);
}
/// `Уведомления`
String get notifications {
return Intl.message(
'Уведомления',
name: 'notifications',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<S> {

View File

@@ -411,6 +411,7 @@
"itCantBeRecovered": "Его нельзя будет восстановить",
"delete": "Удалить",
"cannotDeleteAccount": "Не получилось удалить ваш аккаунт",
"resend": "Отправить повторно"
"resend": "Отправить повторно",
"notifications": "Уведомления"
}

View File

@@ -213,5 +213,6 @@
"my_plans": "Мои планы",
"from": "от",
"user_data": "Данные пользователя",
"infoAboutOrder": "Информация о заказе"
"infoAboutOrder": "Информация о заказе",
"notifications": "Уведомления"
}

View File

@@ -29,7 +29,7 @@ Future<void> main() async {
WidgetsBinding widgetsBinding = SentryWidgetsFlutterBinding.ensureInitialized();
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
await dotenv.load(fileName: "env/.env");
await dotenv.load(fileName: ".env");
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
await FirebaseMessaging.instance.requestPermission();

View File

@@ -1,6 +1,6 @@
name: eatvio_app
description: A new Flutter project.
version: 1.2.11+71
version: 1.2.12+72
publish_to: 'none'
environment:
@@ -90,7 +90,7 @@ dev_dependencies:
flutter:
uses-material-design: true
assets:
- env/.env
- .env
- assets/
- assets/images/
- assets/audio/