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

