Polish navigation, Quran flows, and sharing UX
This commit is contained in:
@@ -1,7 +1,43 @@
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:timezone/data/latest.dart' as tz_data;
|
||||
import 'package:timezone/timezone.dart' as tz;
|
||||
|
||||
/// Notification service for Adhan and Iqamah notifications.
|
||||
import '../local/models/app_settings.dart';
|
||||
import 'notification_analytics_service.dart';
|
||||
import 'notification_runtime_service.dart';
|
||||
|
||||
class NotificationPermissionStatus {
|
||||
const NotificationPermissionStatus({
|
||||
required this.notificationsAllowed,
|
||||
required this.exactAlarmAllowed,
|
||||
});
|
||||
|
||||
final bool notificationsAllowed;
|
||||
final bool exactAlarmAllowed;
|
||||
}
|
||||
|
||||
class NotificationPendingAlert {
|
||||
const NotificationPendingAlert({
|
||||
required this.id,
|
||||
required this.type,
|
||||
required this.title,
|
||||
required this.body,
|
||||
required this.scheduledAt,
|
||||
});
|
||||
|
||||
final int id;
|
||||
final String type;
|
||||
final String title;
|
||||
final String body;
|
||||
final DateTime? scheduledAt;
|
||||
}
|
||||
|
||||
/// Notification service for Adzan and Iqamah reminders.
|
||||
///
|
||||
/// This service owns the local notifications setup, permission requests,
|
||||
/// timezone setup, and scheduling lifecycle for prayer notifications.
|
||||
class NotificationService {
|
||||
NotificationService._();
|
||||
static final NotificationService instance = NotificationService._();
|
||||
@@ -10,16 +46,100 @@ class NotificationService {
|
||||
FlutterLocalNotificationsPlugin();
|
||||
|
||||
bool _initialized = false;
|
||||
String? _lastSyncSignature;
|
||||
static const int _checklistReminderId = 920001;
|
||||
|
||||
/// Initialize notification channels.
|
||||
static const _adhanDetails = NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
'adhan_channel',
|
||||
'Adzan Notifications',
|
||||
channelDescription: 'Pengingat waktu adzan',
|
||||
importance: Importance.max,
|
||||
priority: Priority.high,
|
||||
playSound: true,
|
||||
),
|
||||
iOS: DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
),
|
||||
macOS: DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
),
|
||||
);
|
||||
|
||||
static const _iqamahDetails = NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
'iqamah_channel',
|
||||
'Iqamah Reminders',
|
||||
channelDescription: 'Pengingat waktu iqamah',
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
playSound: true,
|
||||
),
|
||||
iOS: DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentSound: true,
|
||||
),
|
||||
macOS: DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentSound: true,
|
||||
),
|
||||
);
|
||||
|
||||
static const _habitDetails = NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
'habit_channel',
|
||||
'Pengingat Ibadah Harian',
|
||||
channelDescription: 'Pengingat checklist, streak, dan kebiasaan ibadah',
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
playSound: true,
|
||||
),
|
||||
iOS: DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentSound: true,
|
||||
),
|
||||
macOS: DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentSound: true,
|
||||
),
|
||||
);
|
||||
|
||||
static const _systemDetails = NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
'system_channel',
|
||||
'Peringatan Sistem',
|
||||
channelDescription: 'Peringatan status izin dan sinkronisasi jadwal',
|
||||
importance: Importance.defaultImportance,
|
||||
priority: Priority.defaultPriority,
|
||||
playSound: false,
|
||||
),
|
||||
iOS: DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentSound: false,
|
||||
),
|
||||
macOS: DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentSound: false,
|
||||
),
|
||||
);
|
||||
|
||||
/// Initialize plugin, permissions, and timezone once.
|
||||
Future<void> init() async {
|
||||
if (_initialized) return;
|
||||
|
||||
const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||
tz_data.initializeTimeZones();
|
||||
_configureLocalTimeZone();
|
||||
|
||||
const androidSettings =
|
||||
AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||
const darwinSettings = DarwinInitializationSettings(
|
||||
requestAlertPermission: true,
|
||||
requestBadgePermission: true,
|
||||
requestSoundPermission: true,
|
||||
requestAlertPermission: false,
|
||||
requestBadgePermission: false,
|
||||
requestSoundPermission: false,
|
||||
);
|
||||
|
||||
const settings = InitializationSettings(
|
||||
@@ -28,71 +148,509 @@ class NotificationService {
|
||||
macOS: darwinSettings,
|
||||
);
|
||||
|
||||
await _plugin.initialize(settings);
|
||||
await _plugin.initialize(settings: settings);
|
||||
await _requestPermissions();
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
/// Schedule an Adhan notification at a specific time.
|
||||
Future<void> scheduleAdhan({
|
||||
void _configureLocalTimeZone() {
|
||||
final tzId = _resolveTimeZoneIdByOffset(DateTime.now().timeZoneOffset);
|
||||
try {
|
||||
tz.setLocalLocation(tz.getLocation(tzId));
|
||||
} catch (_) {
|
||||
tz.setLocalLocation(tz.UTC);
|
||||
}
|
||||
}
|
||||
|
||||
// We prioritize Indonesian zones for better prayer scheduling defaults.
|
||||
String _resolveTimeZoneIdByOffset(Duration offset) {
|
||||
switch (offset.inMinutes) {
|
||||
case 420:
|
||||
return 'Asia/Jakarta';
|
||||
case 480:
|
||||
return 'Asia/Makassar';
|
||||
case 540:
|
||||
return 'Asia/Jayapura';
|
||||
default:
|
||||
if (offset.inMinutes % 60 == 0) {
|
||||
final etcHours = -(offset.inMinutes ~/ 60);
|
||||
final sign = etcHours >= 0 ? '+' : '';
|
||||
return 'Etc/GMT$sign$etcHours';
|
||||
}
|
||||
return 'UTC';
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _requestPermissions() async {
|
||||
if (Platform.isAndroid) {
|
||||
final androidPlugin = _plugin.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin>();
|
||||
await androidPlugin?.requestNotificationsPermission();
|
||||
await androidPlugin?.requestExactAlarmsPermission();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Platform.isIOS) {
|
||||
await _plugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
IOSFlutterLocalNotificationsPlugin>()
|
||||
?.requestPermissions(alert: true, badge: true, sound: true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Platform.isMacOS) {
|
||||
await _plugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
MacOSFlutterLocalNotificationsPlugin>()
|
||||
?.requestPermissions(alert: true, badge: true, sound: true);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> syncPrayerNotifications({
|
||||
required String cityId,
|
||||
required Map<String, bool> adhanEnabled,
|
||||
required Map<String, int> iqamahOffset,
|
||||
required Map<String, Map<String, String>> schedulesByDate,
|
||||
}) async {
|
||||
await init();
|
||||
|
||||
final hasAnyEnabled = adhanEnabled.values.any((v) => v);
|
||||
if (!hasAnyEnabled) {
|
||||
await cancelAllPending();
|
||||
_lastSyncSignature = null;
|
||||
return;
|
||||
}
|
||||
|
||||
final signature = _buildSyncSignature(
|
||||
cityId, adhanEnabled, iqamahOffset, schedulesByDate);
|
||||
if (_lastSyncSignature == signature) return;
|
||||
|
||||
await cancelAllPending();
|
||||
|
||||
final now = DateTime.now();
|
||||
final dateEntries = schedulesByDate.entries.toList()
|
||||
..sort((a, b) => a.key.compareTo(b.key));
|
||||
|
||||
for (final dateEntry in dateEntries) {
|
||||
final date = DateTime.tryParse(dateEntry.key);
|
||||
if (date == null) continue;
|
||||
|
||||
for (final prayerKey in const [
|
||||
'subuh',
|
||||
'dzuhur',
|
||||
'ashar',
|
||||
'maghrib',
|
||||
'isya',
|
||||
]) {
|
||||
final canonicalPrayer = _canonicalPrayerKey(prayerKey);
|
||||
if (canonicalPrayer == null) continue;
|
||||
if (!(adhanEnabled[canonicalPrayer] ?? false)) continue;
|
||||
|
||||
final rawTime = (dateEntry.value[prayerKey] ?? '').trim();
|
||||
final prayerTime = _parseScheduleDateTime(date, rawTime);
|
||||
if (prayerTime == null || !prayerTime.isAfter(now)) continue;
|
||||
|
||||
await _scheduleAdhan(
|
||||
id: _notificationId(
|
||||
cityId: cityId,
|
||||
dateKey: dateEntry.key,
|
||||
prayerKey: canonicalPrayer,
|
||||
isIqamah: false,
|
||||
),
|
||||
prayerName: _localizedPrayerName(canonicalPrayer),
|
||||
time: prayerTime,
|
||||
);
|
||||
|
||||
final offsetMinutes = iqamahOffset[canonicalPrayer] ?? 0;
|
||||
if (offsetMinutes <= 0) continue;
|
||||
|
||||
final iqamahTime = prayerTime.add(Duration(minutes: offsetMinutes));
|
||||
if (!iqamahTime.isAfter(now)) continue;
|
||||
|
||||
await _scheduleIqamah(
|
||||
id: _notificationId(
|
||||
cityId: cityId,
|
||||
dateKey: dateEntry.key,
|
||||
prayerKey: canonicalPrayer,
|
||||
isIqamah: true,
|
||||
),
|
||||
prayerName: _localizedPrayerName(canonicalPrayer),
|
||||
iqamahTime: iqamahTime,
|
||||
offsetMinutes: offsetMinutes,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_lastSyncSignature = signature;
|
||||
}
|
||||
|
||||
Future<void> _scheduleAdhan({
|
||||
required int id,
|
||||
required String prayerName,
|
||||
required DateTime time,
|
||||
}) async {
|
||||
await _plugin.zonedSchedule(
|
||||
id,
|
||||
'Adhan - $prayerName',
|
||||
'It\'s time for $prayerName prayer',
|
||||
tz.TZDateTime.from(time, tz.local),
|
||||
const NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
'adhan_channel',
|
||||
'Adhan Notifications',
|
||||
channelDescription: 'Prayer time adhan notifications',
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
),
|
||||
iOS: DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
),
|
||||
),
|
||||
id: id,
|
||||
title: 'Adzan • $prayerName',
|
||||
body: 'Waktu sholat $prayerName telah masuk.',
|
||||
scheduledDate: tz.TZDateTime.from(time, tz.local),
|
||||
notificationDetails: _adhanDetails,
|
||||
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
|
||||
payload: 'adhan|$prayerName|${time.toIso8601String()}',
|
||||
);
|
||||
await NotificationAnalyticsService.instance.track(
|
||||
'notif_push_scheduled',
|
||||
dimensions: <String, dynamic>{
|
||||
'event_type': 'adhan',
|
||||
'prayer': prayerName,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Schedule an Iqamah reminder notification.
|
||||
Future<void> scheduleIqamah({
|
||||
Future<void> _scheduleIqamah({
|
||||
required int id,
|
||||
required String prayerName,
|
||||
required DateTime adhanTime,
|
||||
required DateTime iqamahTime,
|
||||
required int offsetMinutes,
|
||||
}) async {
|
||||
final iqamahTime = adhanTime.add(Duration(minutes: offsetMinutes));
|
||||
await _plugin.zonedSchedule(
|
||||
id + 100, // Offset IDs for iqamah
|
||||
'Iqamah - $prayerName',
|
||||
'Iqamah for $prayerName in $offsetMinutes minutes',
|
||||
tz.TZDateTime.from(iqamahTime, tz.local),
|
||||
const NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
'iqamah_channel',
|
||||
'Iqamah Reminders',
|
||||
channelDescription: 'Iqamah reminder notifications',
|
||||
importance: Importance.defaultImportance,
|
||||
priority: Priority.defaultPriority,
|
||||
),
|
||||
iOS: DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentSound: true,
|
||||
),
|
||||
),
|
||||
id: id,
|
||||
title: 'Iqamah • $prayerName',
|
||||
body: 'Iqamah $prayerName dalam $offsetMinutes menit.',
|
||||
scheduledDate: tz.TZDateTime.from(iqamahTime, tz.local),
|
||||
notificationDetails: _iqamahDetails,
|
||||
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
|
||||
payload: 'iqamah|$prayerName|${iqamahTime.toIso8601String()}',
|
||||
);
|
||||
await NotificationAnalyticsService.instance.track(
|
||||
'notif_push_scheduled',
|
||||
dimensions: <String, dynamic>{
|
||||
'event_type': 'iqamah',
|
||||
'prayer': prayerName,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Cancel all pending notifications.
|
||||
Future<void> cancelAll() async {
|
||||
await _plugin.cancelAll();
|
||||
DateTime? _parseScheduleDateTime(DateTime date, String hhmm) {
|
||||
final match = RegExp(r'^(\d{1,2}):(\d{2})').firstMatch(hhmm);
|
||||
if (match == null) return null;
|
||||
|
||||
final hour = int.tryParse(match.group(1) ?? '');
|
||||
final minute = int.tryParse(match.group(2) ?? '');
|
||||
if (hour == null || minute == null) return null;
|
||||
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) return null;
|
||||
return DateTime(date.year, date.month, date.day, hour, minute);
|
||||
}
|
||||
|
||||
String? _canonicalPrayerKey(String scheduleKey) {
|
||||
switch (scheduleKey) {
|
||||
case 'subuh':
|
||||
case 'fajr':
|
||||
return 'fajr';
|
||||
case 'dzuhur':
|
||||
case 'dhuhr':
|
||||
return 'dhuhr';
|
||||
case 'ashar':
|
||||
case 'asr':
|
||||
return 'asr';
|
||||
case 'maghrib':
|
||||
return 'maghrib';
|
||||
case 'isya':
|
||||
case 'isha':
|
||||
return 'isha';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String _localizedPrayerName(String canonicalPrayerKey) {
|
||||
switch (canonicalPrayerKey) {
|
||||
case 'fajr':
|
||||
return 'Subuh';
|
||||
case 'dhuhr':
|
||||
return 'Dzuhur';
|
||||
case 'asr':
|
||||
return 'Ashar';
|
||||
case 'maghrib':
|
||||
return 'Maghrib';
|
||||
case 'isha':
|
||||
return 'Isya';
|
||||
default:
|
||||
return canonicalPrayerKey;
|
||||
}
|
||||
}
|
||||
|
||||
int _notificationId({
|
||||
required String cityId,
|
||||
required String dateKey,
|
||||
required String prayerKey,
|
||||
required bool isIqamah,
|
||||
}) {
|
||||
final seed = '$cityId|$dateKey|$prayerKey|${isIqamah ? 'iqamah' : 'adhan'}';
|
||||
var hash = 17;
|
||||
for (final rune in seed.runes) {
|
||||
hash = 37 * hash + rune;
|
||||
}
|
||||
final bounded = hash.abs() % 700000;
|
||||
return isIqamah ? bounded + 800000 : bounded + 100000;
|
||||
}
|
||||
|
||||
String _buildSyncSignature(
|
||||
String cityId,
|
||||
Map<String, bool> adhanEnabled,
|
||||
Map<String, int> iqamahOffset,
|
||||
Map<String, Map<String, String>> schedulesByDate,
|
||||
) {
|
||||
final sortedAdhan = adhanEnabled.entries.toList()
|
||||
..sort((a, b) => a.key.compareTo(b.key));
|
||||
final sortedIqamah = iqamahOffset.entries.toList()
|
||||
..sort((a, b) => a.key.compareTo(b.key));
|
||||
final sortedDates = schedulesByDate.entries.toList()
|
||||
..sort((a, b) => a.key.compareTo(b.key));
|
||||
|
||||
final buffer = StringBuffer(cityId);
|
||||
for (final e in sortedAdhan) {
|
||||
buffer.write('|${e.key}:${e.value ? 1 : 0}');
|
||||
}
|
||||
for (final e in sortedIqamah) {
|
||||
buffer.write('|${e.key}:${e.value}');
|
||||
}
|
||||
for (final dateEntry in sortedDates) {
|
||||
buffer.write('|${dateEntry.key}');
|
||||
final times = dateEntry.value.entries.toList()
|
||||
..sort((a, b) => a.key.compareTo(b.key));
|
||||
for (final t in times) {
|
||||
buffer.write('|${t.key}:${t.value}');
|
||||
}
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
Future<void> cancelAllPending() async {
|
||||
try {
|
||||
await _plugin.cancelAllPendingNotifications();
|
||||
} catch (_) {
|
||||
await _plugin.cancelAll();
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> pendingCount() async {
|
||||
final pending = await _plugin.pendingNotificationRequests();
|
||||
return pending.length;
|
||||
}
|
||||
|
||||
Future<void> syncHabitNotifications({
|
||||
required AppSettings settings,
|
||||
}) async {
|
||||
await init();
|
||||
|
||||
if (!settings.alertsEnabled || !settings.dailyChecklistReminderEnabled) {
|
||||
await cancelChecklistReminder();
|
||||
return;
|
||||
}
|
||||
|
||||
final reminderTime = settings.checklistReminderTime ?? '09:00';
|
||||
final parts = _parseHourMinute(reminderTime);
|
||||
if (parts == null) {
|
||||
await cancelChecklistReminder();
|
||||
return;
|
||||
}
|
||||
|
||||
final now = DateTime.now();
|
||||
var target = DateTime(
|
||||
now.year,
|
||||
now.month,
|
||||
now.day,
|
||||
parts.$1,
|
||||
parts.$2,
|
||||
);
|
||||
if (!target.isAfter(now)) {
|
||||
target = target.add(const Duration(days: 1));
|
||||
}
|
||||
|
||||
await _plugin.zonedSchedule(
|
||||
id: _checklistReminderId,
|
||||
title: 'Checklist Ibadah Harian',
|
||||
body: 'Jangan lupa perbarui progres ibadah hari ini.',
|
||||
scheduledDate: tz.TZDateTime.from(target, tz.local),
|
||||
notificationDetails: _habitDetails,
|
||||
androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle,
|
||||
matchDateTimeComponents: DateTimeComponents.time,
|
||||
payload: 'checklist|daily|${target.toIso8601String()}',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> cancelChecklistReminder() async {
|
||||
await _plugin.cancel(id: _checklistReminderId);
|
||||
}
|
||||
|
||||
int nonPrayerNotificationId(String seed) {
|
||||
var hash = 17;
|
||||
for (final rune in seed.runes) {
|
||||
hash = 41 * hash + rune;
|
||||
}
|
||||
return 900000 + (hash.abs() % 80000);
|
||||
}
|
||||
|
||||
Future<bool> showNonPrayerAlert({
|
||||
required AppSettings settings,
|
||||
required int id,
|
||||
required String title,
|
||||
required String body,
|
||||
String payloadType = 'system',
|
||||
bool silent = false,
|
||||
bool bypassQuietHours = false,
|
||||
bool bypassDailyCap = false,
|
||||
}) async {
|
||||
await init();
|
||||
|
||||
final runtime = NotificationRuntimeService.instance;
|
||||
if (!settings.alertsEnabled) return false;
|
||||
if (!bypassQuietHours && runtime.isWithinQuietHours(settings)) return false;
|
||||
if (!bypassDailyCap &&
|
||||
runtime.nonPrayerPushCountToday() >= settings.maxNonPrayerPushPerDay) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await _plugin.show(
|
||||
id: id,
|
||||
title: title,
|
||||
body: body,
|
||||
notificationDetails: silent ? _systemDetails : _habitDetails,
|
||||
payload: '$payloadType|non_prayer|${DateTime.now().toIso8601String()}',
|
||||
);
|
||||
|
||||
if (!bypassDailyCap) {
|
||||
await runtime.incrementNonPrayerPushCount();
|
||||
}
|
||||
await NotificationAnalyticsService.instance.track(
|
||||
'notif_push_fired',
|
||||
dimensions: <String, dynamic>{
|
||||
'event_type': payloadType,
|
||||
'channel': 'push',
|
||||
},
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<NotificationPermissionStatus> getPermissionStatus() async {
|
||||
await init();
|
||||
|
||||
try {
|
||||
if (Platform.isAndroid) {
|
||||
final androidPlugin = _plugin.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin>();
|
||||
final notificationsAllowed =
|
||||
await androidPlugin?.areNotificationsEnabled() ?? true;
|
||||
final exactAlarmAllowed =
|
||||
await androidPlugin?.canScheduleExactNotifications() ?? true;
|
||||
return NotificationPermissionStatus(
|
||||
notificationsAllowed: notificationsAllowed,
|
||||
exactAlarmAllowed: exactAlarmAllowed,
|
||||
);
|
||||
}
|
||||
|
||||
if (Platform.isIOS) {
|
||||
final iosPlugin = _plugin.resolvePlatformSpecificImplementation<
|
||||
IOSFlutterLocalNotificationsPlugin>();
|
||||
final options = await iosPlugin?.checkPermissions();
|
||||
return NotificationPermissionStatus(
|
||||
notificationsAllowed: options?.isEnabled ?? true,
|
||||
exactAlarmAllowed: true,
|
||||
);
|
||||
}
|
||||
|
||||
if (Platform.isMacOS) {
|
||||
final macPlugin = _plugin.resolvePlatformSpecificImplementation<
|
||||
MacOSFlutterLocalNotificationsPlugin>();
|
||||
final options = await macPlugin?.checkPermissions();
|
||||
return NotificationPermissionStatus(
|
||||
notificationsAllowed: options?.isEnabled ?? true,
|
||||
exactAlarmAllowed: true,
|
||||
);
|
||||
}
|
||||
} catch (_) {
|
||||
// Fallback to non-blocking defaults if platform query fails.
|
||||
}
|
||||
|
||||
return const NotificationPermissionStatus(
|
||||
notificationsAllowed: true,
|
||||
exactAlarmAllowed: true,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<NotificationPendingAlert>> pendingAlerts() async {
|
||||
final pending = await _plugin.pendingNotificationRequests();
|
||||
final alerts = pending.map(_mapPendingRequest).toList()
|
||||
..sort((a, b) {
|
||||
final aTime = a.scheduledAt;
|
||||
final bTime = b.scheduledAt;
|
||||
if (aTime == null && bTime == null) return a.id.compareTo(b.id);
|
||||
if (aTime == null) return 1;
|
||||
if (bTime == null) return -1;
|
||||
return aTime.compareTo(bTime);
|
||||
});
|
||||
return alerts;
|
||||
}
|
||||
|
||||
NotificationPendingAlert _mapPendingRequest(PendingNotificationRequest raw) {
|
||||
final payload = raw.payload ?? '';
|
||||
final parts = payload.split('|');
|
||||
if (parts.length >= 3) {
|
||||
final type = parts[0].trim().toLowerCase();
|
||||
final title = raw.title ?? '${_labelForType(type)} • ${parts[1].trim()}';
|
||||
final body = raw.body ?? '';
|
||||
final scheduledAt = DateTime.tryParse(parts[2].trim());
|
||||
return NotificationPendingAlert(
|
||||
id: raw.id,
|
||||
type: type,
|
||||
title: title,
|
||||
body: body,
|
||||
scheduledAt: scheduledAt,
|
||||
);
|
||||
}
|
||||
|
||||
final fallbackType = _inferTypeFromTitle(raw.title ?? '');
|
||||
return NotificationPendingAlert(
|
||||
id: raw.id,
|
||||
type: fallbackType,
|
||||
title: raw.title ?? 'Pengingat',
|
||||
body: raw.body ?? '',
|
||||
scheduledAt: null,
|
||||
);
|
||||
}
|
||||
|
||||
String _inferTypeFromTitle(String title) {
|
||||
final normalized = title.toLowerCase();
|
||||
if (normalized.contains('iqamah')) return 'iqamah';
|
||||
if (normalized.contains('adzan')) return 'adhan';
|
||||
return 'alert';
|
||||
}
|
||||
|
||||
String _labelForType(String type) {
|
||||
switch (type) {
|
||||
case 'adhan':
|
||||
return 'Adzan';
|
||||
case 'iqamah':
|
||||
return 'Iqamah';
|
||||
case 'checklist':
|
||||
return 'Checklist';
|
||||
case 'streak_risk':
|
||||
return 'Streak';
|
||||
case 'system':
|
||||
return 'Sistem';
|
||||
default:
|
||||
return 'Pengingat';
|
||||
}
|
||||
}
|
||||
|
||||
(int, int)? _parseHourMinute(String hhmm) {
|
||||
final match = RegExp(r'^(\d{1,2}):(\d{2})$').firstMatch(hhmm.trim());
|
||||
if (match == null) return null;
|
||||
final hour = int.tryParse(match.group(1) ?? '');
|
||||
final minute = int.tryParse(match.group(2) ?? '');
|
||||
if (hour == null || minute == null) return null;
|
||||
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) return null;
|
||||
return (hour, minute);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user