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

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