Snippets Collections
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

Mon Mar 31 2025 10:48:24 GMT+0000 (Coordinated Universal Time)

#dart #flutter #localnotification #firebase #firestore
star

Thu Mar 13 2025 21:53:56 GMT+0000 (Coordinated Universal Time)

#dart #flutter #localnotification

Save snippets that work with our extensions

Available in the Chrome Web Store Get Firefox Add-on Get VS Code extension