import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../entities/api_errors.dart'; import '../services/storage_service.dart'; enum FirebaseApiFilterType { isEqualTo, isNotEqualTo, isGreaterThan, isGreaterThanOrEqualTo, isLessThan, isLessThanOrEqualTo, arrayContains, arrayContainsAny, whereIn, whereNotIn, isNull, } class FirebaseFilterEntity { final String field; final FirebaseApiFilterType operator; final dynamic value; FirebaseFilterEntity({ required this.field, required this.operator, required this.value, }); } abstract class FirebaseApiClient { Future<dynamic> get(String collection,{int limit = 20 ,List<FirebaseFilterEntity>? filters}); Future<dynamic> getById(String collection, String id); Future<dynamic> postWithId(String collection, {required Map<String, dynamic> params}); Future<void> post(String collection, {required String id, required Map<String, dynamic> params}); Future<void> putWithId(String collection, {required String id, required Map<String, dynamic> params}); Future<void> put(String collection, {required Map<String, dynamic> params}); Future<void> deleteDoc(String collection, String id); } class FirebaseApiClientImpl extends FirebaseApiClient { final FirebaseFirestore _client = FirebaseFirestore.instance; // ================ CACHE ================ // static const Duration firebaseCacheDuration = Duration(minutes: 10); // или сколько нужно String _buildCacheKey(String collection, [String? id]) => id != null ? 'firebase_cache_$collection\_$id' : 'firebase_cache_$collection'; String _buildTimestampKey(String collection, [String? id]) => id != null ? 'firebase_cache_time_$collection\_$id' : 'firebase_cache_time_$collection'; final _storageService = StorageService(); bool _isCacheValid(DateTime? cachedTime) { if (cachedTime == null) return false; final now = DateTime.now(); return now.difference(cachedTime) < firebaseCacheDuration; } // ================ METHODS ================ // @override Future<dynamic> get(String collection,{int limit = 20 ,List<FirebaseFilterEntity>? filters}) async { try { final cacheKey = _buildCacheKey(collection); final timeKey = _buildTimestampKey(collection); // Получение из кэша final cachedTimeRaw = await _storageService.get(key: timeKey); final cachedData = await _storageService.get(key: cacheKey); final cachedTime = cachedTimeRaw is String ? DateTime.tryParse(cachedTimeRaw) : null; if (_isCacheValid(cachedTime) && cachedData != null) { debugPrint("⚡️ Firebase cache hit: $collection"); return cachedData; } debugPrint("🔥 Firebase fetch: $collection"); Query query = _client.collection(collection); if (filters != null) { for (var filter in filters) { switch (filter.operator) { case FirebaseApiFilterType.isEqualTo: query = query.where(filter.field, isEqualTo: filter.value); break; case FirebaseApiFilterType.isNotEqualTo: query = query.where(filter.field, isNotEqualTo: filter.value); break; case FirebaseApiFilterType.isGreaterThan: query = query.where(filter.field, isGreaterThan: filter.value); break; case FirebaseApiFilterType.isGreaterThanOrEqualTo: query = query.where(filter.field, isGreaterThanOrEqualTo: filter.value); break; case FirebaseApiFilterType.isLessThan: query = query.where(filter.field, isLessThan: filter.value); break; case FirebaseApiFilterType.isLessThanOrEqualTo: query = query.where(filter.field, isLessThanOrEqualTo: filter.value); break; case FirebaseApiFilterType.arrayContains: query = query.where(filter.field, arrayContains: filter.value); break; case FirebaseApiFilterType.arrayContainsAny: query = query.where(filter.field, arrayContainsAny: filter.value); break; case FirebaseApiFilterType.whereIn: query = query.where(filter.field, whereIn: filter.value); break; case FirebaseApiFilterType.whereNotIn: query = query.where(filter.field, whereNotIn: filter.value); break; case FirebaseApiFilterType.isNull: query = query.where(filter.field, isNull: filter.value); break; default: throw ArgumentError('Unsupported operator: ${filter.operator}'); } } } final response = await query.limit(limit).get(); final result = response.docs.map((doc) => doc.data()).toList(); // Сохраняем в кэш await _storageService.save(key: cacheKey, value: result); await _storageService.save(key: timeKey, value: DateTime.now().toIso8601String()); return result; } catch (e) { throw _handleError(e, "errorGettingDataCollection".tr); } } @override Future<dynamic> getById(String collection, String id) async { try { final response = await _client.collection(collection).doc(id).get(); if (!response.exists) { throw Exception('Document with ID $id not found in collection $collection'); } return response.data(); } catch (e) { throw _handleError(e, "errorGettingDocumentById".tr); } } @override Future<void> put(String collection, {required Map<String, dynamic> params}) async { if (!params.containsKey('id')) { throw ArgumentError('documentIdIsRequiredToUpdate'.tr); } try { await _client.collection(collection).doc(params['id']).update(params); } catch (e) { throw _handleError(e, 'errorUpdatingDocument'.tr); } } @override Future<void> deleteDoc(String collection, String id) async { try { await _client.collection(collection).doc(id).delete(); } catch (e) { throw _handleError(e, 'errorDeletingDocument'.tr); } } Exception _handleError(dynamic error, String defaultMessage) { if (error is FirebaseException) { switch (error.code) { case 'permission-denied': return UnauthorisedException(); case 'not-found': return Exception(defaultMessage); default: return ExceptionWithMessage('$defaultMessage: ${error.message}'); } } return Exception(defaultMessage); } @override Future<void> post(String collection, {required String id, required Map<String, dynamic> params}) async { try { await _client.collection(collection).doc(id).set(params); } catch (e) { throw _handleError(e, 'errorPostingDataCollection'.tr); } } @override Future<dynamic> postWithId(String collection, {required Map<String, dynamic> params}) async { try { debugPrint("Post data $collection\n$params"); await _client.collection(collection).doc(params['id']).set(params); return params; } catch (e) { throw _handleError(e, 'errorPostingDataCollection'.tr); } } @override Future<void> putWithId(String collection, {required String id, required Map<String, dynamic> params}) async { try { await _client.collection(collection).doc(id).update(params); } catch (e) { throw _handleError(e, 'errorUpdatingDocument'.tr); } } }
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"); } } }
star
photo_camera
Mon Mar 31 2025 10:48:24 GMT+0000 (Coordinated Universal Time)
#dart #flutter #localnotification #firebase #firestore