import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:geolocator/geolocator.dart'; import 'package:get/get.dart'; import 'package:timezone/data/latest.dart' as tz; import 'package:timezone/timezone.dart' as tz; import 'package:android_intent_plus/android_intent.dart'; import '../../main.dart'; import '../models/calendar_event_model.dart'; import '../models/task_model.dart'; /// **Сервис для работы с локальными уведомлениями** /// Позволяет отправлять мгновенные и запланированные уведомления. /// Также обрабатывает разрешения и настройку часового пояса. class NotificationService { /// **Экземпляр плагина уведомлений** final FlutterLocalNotificationsPlugin _plugin = FlutterLocalNotificationsPlugin(); /// **ID канала для уведомлений (используется в Android)** static const String _channelId = 'calendar_events_channel'; static const String _channelName = 'Calendar Events'; /// **Инициализация сервиса уведомлений** /// Должна быть вызвана **один раз** в `main.dart` перед использованием. Future<void> init() async { // Инициализация часовых зон для работы с запланированными уведомлениями tz.initializeTimeZones(); // Запрос разрешений на геолокацию (используется для определения часового пояса) await _requestLocationPermission(); // Автоматически устанавливаем часовой пояс устройства await _setTimeZoneAutomatically(); // Запрашиваем разрешения на уведомления await _requestNotificationPermissions(); // Настройки инициализации для Android const AndroidInitializationSettings androidInitSettings = AndroidInitializationSettings('@mipmap/ic_launcher'); // Настройки инициализации для iOS const DarwinInitializationSettings iosInitSettings = DarwinInitializationSettings(); // Общие настройки для всех платформ const InitializationSettings initSettings = InitializationSettings( android: androidInitSettings, iOS: iosInitSettings, ); // Инициализируем плагин await _plugin.initialize( initSettings, onDidReceiveNotificationResponse: _onSelectNotification, ); } /// **Запрос разрешений на уведомления (Android 13+ и iOS)** Future<void> _requestNotificationPermissions() async { final androidSettings = _plugin.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>(); final iosSettings = _plugin.resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>(); if (androidSettings != null) { final bool? granted = await androidSettings.requestNotificationsPermission(); debugPrint("📢 Android notification permission: ${granted == true ? "Granted" : "Denied"}"); } if (iosSettings != null) { final bool? granted = await iosSettings.requestPermissions( alert: true, badge: true, sound: true, ); debugPrint("📢 iOS notification permission: ${granted == true ? "Granted" : "Denied"}"); } } /// **Запрос разрешений на доступ к геолокации** /// Нужно для определения точного часового пояса. Future<void> _requestLocationPermission() async { LocationPermission permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); } if (permission == LocationPermission.deniedForever) { debugPrint("🚫 Location permission permanently denied. Timezone detection may not work."); } else if (permission == LocationPermission.always || permission == LocationPermission.whileInUse) { debugPrint("✅ Location permission granted."); } } /// **Автоматически определяет часовой пояс устройства** Future<void> _setTimeZoneAutomatically() async { final Duration offset = _getSystemTimeZoneOffset(); final String timeZoneName = _offsetToTimeZoneName(offset); debugPrint("📍 Auto-detected system timezone: $timeZoneName"); if (tz.timeZoneDatabase.locations.containsKey(timeZoneName)) { tz.setLocalLocation(tz.getLocation(timeZoneName)); debugPrint("✅ Timezone set to: $timeZoneName"); } else { debugPrint("⚠ Timezone not found, using UTC."); tz.setLocalLocation(tz.getLocation('UTC')); } } /// **Возвращает смещение текущего часового пояса от UTC** Duration _getSystemTimeZoneOffset() { return DateTime.now().timeZoneOffset; } /// **Конвертирует смещение UTC в название часового пояса** /// Используется, если `timezone` сам не определяет корректный часовой пояс. String _offsetToTimeZoneName(Duration offset) { final int hours = offset.inHours; final int minutes = offset.inMinutes.remainder(60); final String sign = hours >= 0 ? '+' : '-'; final String formatted = '${sign}${hours.abs().toString().padLeft(2, '0')}:${minutes.abs().toString().padLeft(2, '0')}'; // 🕒 Сопоставление часовых поясов по смещению UTC return { '-12:00': 'Etc/GMT+12', '-11:00': 'Pacific/Midway', '-10:00': 'Pacific/Honolulu', '-09:30': 'Pacific/Marquesas', '-09:00': 'America/Anchorage', '-08:00': 'America/Los_Angeles', '-07:00': 'America/Denver', '-06:00': 'America/Chicago', '-05:00': 'America/New_York', '-04:00': 'America/Caracas', '-03:30': 'America/St_Johns', '-03:00': 'America/Argentina/Buenos_Aires', '-02:00': 'Atlantic/South_Georgia', '-01:00': 'Atlantic/Azores', '+00:00': 'UTC', '+01:00': 'Europe/London', '+02:00': 'Europe/Berlin', '+03:00': 'Europe/Moscow', '+03:30': 'Asia/Tehran', '+04:00': 'Asia/Dubai', '+04:30': 'Asia/Kabul', '+05:00': 'Asia/Tashkent', '+05:30': 'Asia/Kolkata', '+05:45': 'Asia/Kathmandu', '+06:00': 'Asia/Dhaka', '+06:30': 'Asia/Yangon', '+07:00': 'Asia/Bangkok', '+08:00': 'Asia/Shanghai', '+09:00': 'Asia/Tokyo', '+09:30': 'Australia/Darwin', '+10:00': 'Australia/Sydney', '+10:30': 'Australia/Lord_Howe', '+11:00': 'Pacific/Noumea', '+12:00': 'Pacific/Fiji', '+12:45': 'Pacific/Chatham', '+13:00': 'Pacific/Tongatapu', '+14:00': 'Pacific/Kiritimati', }[formatted] ?? 'UTC'; } /// **Показать мгновенное уведомление** Future<void> showInstantNotification(String title, String body) async { const NotificationDetails platformChannelSpecifics = NotificationDetails( android: AndroidNotificationDetails(_channelId, _channelName, importance: Importance.max, priority: Priority.high), iOS: DarwinNotificationDetails(), ); await _plugin.show(0, title, body, platformChannelSpecifics); } /// **Запланировать уведомление на определённое время** Future<void> scheduleNotification(CalendarEventModel event) async { await _setTimeZoneAutomatically(); final notificationId = event.hashCode; final now = tz.TZDateTime.now(tz.local); final scheduledDate = tz.TZDateTime.from(event.date, tz.local); if (scheduledDate.isBefore(now)) { debugPrint("⚠ Ошибка: Время уведомления в прошлом. Пропускаем."); return; } const platformChannelSpecifics = NotificationDetails( android: AndroidNotificationDetails(_channelId, _channelName, importance: Importance.max, priority: Priority.high), iOS: DarwinNotificationDetails(), ); await _plugin.zonedSchedule( notificationId, event.getTitleForCalendar().isNotEmpty ? event.getTitleForCalendar() : "Напоминание", event.jsonData.toString().isNotEmpty ? event.jsonData.toString() : "У вас запланировано событие", scheduledDate, platformChannelSpecifics, uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime, androidScheduleMode: AndroidScheduleMode.alarmClock, payload: event.jsonData.toString(), ); debugPrint("✅ Уведомление запланировано: ID=$notificationId, Время=$scheduledDate"); } /// **Отменить запланированное уведомление** Future<void> cancelNotification(CalendarEventModel event) async { await _plugin.cancel(event.hashCode); } /// **Обработчик нажатия на уведомление** void _onSelectNotification(NotificationResponse details) { debugPrint('📩 Уведомление нажато, payload: ${details.payload}'); } /// **Тестирование уведомлений** (запланировать уведомление через 10 секунд) Future<void> testScheduleNotification() async { final event = CalendarEventModel( id: "test_id", date: DateTime.now().add(const Duration(seconds: 10)), jsonData: TaskModel( reminderTime: DateTime.now().add(const Duration(seconds: 20)), title: "TEST TASK", id: uuid.v4(), subTasks: [], ).toJson(), eventType: EventType.task, ); debugPrint("🚀 Тестовое уведомление запланировано на ${event.date}"); await scheduleNotification(event); } /// **Отключение оптимизации батареи (чтобы уведомления работали в фоне)** Future<void> requestIgnoreBatteryOptimizations() async { try { const intent = AndroidIntent(action: 'android.settings.IGNORE_BATTERY_OPTIMIZATION_SETTINGS'); await intent.launch(); } catch (e) { debugPrint("⚠ Ошибка при запуске настроек батареи: $e"); } } }
Preview:
downloadDownload PNG
downloadDownload JPEG
downloadDownload SVG
Tip: You can change the style, width & colours of the snippet with the inspect tool before clicking Download!
Click to optimize width for Twitter