feat(notifications): Issue-236

This commit is contained in:
mso7nueh
2025-08-29 14:25:13 +03:00
parent 12bf9f9e5f
commit d13eb78f0e
10 changed files with 483 additions and 139 deletions

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

@@ -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,262 @@
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:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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) {
return Scaffold(
appBar: AppBar(title: const Text('Уведомления')),
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),
const SizedBox(height: 16),
_buildDiaryRemindersSection(context, notificationSettings.diaryReminderNotifications, cubit),
],
);
}
Widget _buildDiaryRemindersSection(BuildContext context, DiaryReminderNotifications diaryReminderNotifications, NotificationSettingsCubit cubit) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Text(
'Напоминания о дневнике',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: appTheme.blueA400),
),
),
_buildReminderTile(context, 'Первое напоминание', diaryReminderNotifications.firstReminder, 'first_reminder', cubit),
_buildReminderTile(context, 'Второе напоминание', diaryReminderNotifications.secondReminder, 'second_reminder', cubit),
_buildReminderTile(context, 'Третье напоминание', 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 _tile(
padding: const EdgeInsets.symmetric(horizontal: 16),
onTap: reminder.enabled
? () {
_showTimePicker(context, reminder, reminderType, cubit);
}
: null,
title: title,
subtitle: reminder.enabled
? Row(
children: [
Icon(Icons.access_time, size: 16, color: appTheme.blueA400),
const SizedBox(width: 8),
Text(
'${reminder.localTimeHours.toString().padLeft(2, '0')}:${reminder.localTimeMins.toString().padLeft(2, '0')}',
style: TextStyle(fontSize: 14, color: appTheme.blueA400, fontWeight: FontWeight.w500),
),
const SizedBox(width: 4),
Text('(нажмите для изменения)', style: TextStyle(fontSize: 12, color: Colors.grey)),
],
)
: null,
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
CupertinoSwitch(
value: reminder.enabled,
activeTrackColor: appTheme.blueA400,
onChanged: (newValue) {
_updateReminderSetting(cubit, reminderType, {
'enabled': newValue,
'local_time_hours': reminder.localTimeHours,
'local_time_mins': reminder.localTimeMins,
});
},
),
],
),
);
}
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

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