Improve notifications, tilawah flow, and dzikir structure
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
|
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
|
||||||
<application
|
<application
|
||||||
|
|||||||
@@ -25,6 +25,16 @@ public final class GeneratedPluginRegistrant {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e(TAG, "Error registering plugin audio_session, com.ryanheise.audio_session.AudioSessionPlugin", e);
|
Log.e(TAG, "Error registering plugin audio_session, com.ryanheise.audio_session.AudioSessionPlugin", e);
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
flutterEngine.getPlugins().add(new io.flutter.plugins.firebase.core.FlutterFirebaseCorePlugin());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error registering plugin firebase_core, io.flutter.plugins.firebase.core.FlutterFirebaseCorePlugin", e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
flutterEngine.getPlugins().add(new io.flutter.plugins.firebase.messaging.FlutterFirebaseMessagingPlugin());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error registering plugin firebase_messaging, io.flutter.plugins.firebase.messaging.FlutterFirebaseMessagingPlugin", e);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
flutterEngine.getPlugins().add(new com.hemanthraj.fluttercompass.FlutterCompassPlugin());
|
flutterEngine.getPlugins().add(new com.hemanthraj.fluttercompass.FlutterCompassPlugin());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -80,5 +90,10 @@ public final class GeneratedPluginRegistrant {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e(TAG, "Error registering plugin url_launcher_android, io.flutter.plugins.urllauncher.UrlLauncherPlugin", e);
|
Log.e(TAG, "Error registering plugin url_launcher_android, io.flutter.plugins.urllauncher.UrlLauncherPlugin", e);
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
flutterEngine.getPlugins().add(new dev.fluttercommunity.workmanager.WorkmanagerPlugin());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error registering plugin workmanager_android, dev.fluttercommunity.workmanager.WorkmanagerPlugin", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,18 @@
|
|||||||
@import audio_session;
|
@import audio_session;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if __has_include(<firebase_core/FLTFirebaseCorePlugin.h>)
|
||||||
|
#import <firebase_core/FLTFirebaseCorePlugin.h>
|
||||||
|
#else
|
||||||
|
@import firebase_core;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if __has_include(<firebase_messaging/FLTFirebaseMessagingPlugin.h>)
|
||||||
|
#import <firebase_messaging/FLTFirebaseMessagingPlugin.h>
|
||||||
|
#else
|
||||||
|
@import firebase_messaging;
|
||||||
|
#endif
|
||||||
|
|
||||||
#if __has_include(<flutter_compass_v2/FlutterCompassPlugin.h>)
|
#if __has_include(<flutter_compass_v2/FlutterCompassPlugin.h>)
|
||||||
#import <flutter_compass_v2/FlutterCompassPlugin.h>
|
#import <flutter_compass_v2/FlutterCompassPlugin.h>
|
||||||
#else
|
#else
|
||||||
@@ -78,11 +90,19 @@
|
|||||||
@import url_launcher_ios;
|
@import url_launcher_ios;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if __has_include(<workmanager_apple/WorkmanagerPlugin.h>)
|
||||||
|
#import <workmanager_apple/WorkmanagerPlugin.h>
|
||||||
|
#else
|
||||||
|
@import workmanager_apple;
|
||||||
|
#endif
|
||||||
|
|
||||||
@implementation GeneratedPluginRegistrant
|
@implementation GeneratedPluginRegistrant
|
||||||
|
|
||||||
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
|
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
|
||||||
[AudioServicePlugin registerWithRegistrar:[registry registrarForPlugin:@"AudioServicePlugin"]];
|
[AudioServicePlugin registerWithRegistrar:[registry registrarForPlugin:@"AudioServicePlugin"]];
|
||||||
[AudioSessionPlugin registerWithRegistrar:[registry registrarForPlugin:@"AudioSessionPlugin"]];
|
[AudioSessionPlugin registerWithRegistrar:[registry registrarForPlugin:@"AudioSessionPlugin"]];
|
||||||
|
[FLTFirebaseCorePlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTFirebaseCorePlugin"]];
|
||||||
|
[FLTFirebaseMessagingPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTFirebaseMessagingPlugin"]];
|
||||||
[FlutterCompassPlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterCompassPlugin"]];
|
[FlutterCompassPlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterCompassPlugin"]];
|
||||||
[FlutterLocalNotificationsPlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterLocalNotificationsPlugin"]];
|
[FlutterLocalNotificationsPlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterLocalNotificationsPlugin"]];
|
||||||
[FlutterQiblahPlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterQiblahPlugin"]];
|
[FlutterQiblahPlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterQiblahPlugin"]];
|
||||||
@@ -93,6 +113,7 @@
|
|||||||
[FPPSharePlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPSharePlusPlugin"]];
|
[FPPSharePlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPSharePlusPlugin"]];
|
||||||
[SqflitePlugin registerWithRegistrar:[registry registrarForPlugin:@"SqflitePlugin"]];
|
[SqflitePlugin registerWithRegistrar:[registry registrarForPlugin:@"SqflitePlugin"]];
|
||||||
[URLLauncherPlugin registerWithRegistrar:[registry registrarForPlugin:@"URLLauncherPlugin"]];
|
[URLLauncherPlugin registerWithRegistrar:[registry registrarForPlugin:@"URLLauncherPlugin"]];
|
||||||
|
[WorkmanagerPlugin registerWithRegistrar:[registry registrarForPlugin:@"WorkmanagerPlugin"]];
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -104,6 +104,21 @@ class AppSettings extends HiveObject {
|
|||||||
@HiveField(32)
|
@HiveField(32)
|
||||||
bool mirrorAdzanToInbox;
|
bool mirrorAdzanToInbox;
|
||||||
|
|
||||||
|
@HiveField(33)
|
||||||
|
bool tilawahAutoContinueNextSurah;
|
||||||
|
|
||||||
|
@HiveField(34)
|
||||||
|
bool shalatReportReminderEnabled;
|
||||||
|
|
||||||
|
@HiveField(35)
|
||||||
|
int shalatReportReminderDelayMinutes;
|
||||||
|
|
||||||
|
@HiveField(36)
|
||||||
|
int shalatReportReminderRepeatCount;
|
||||||
|
|
||||||
|
@HiveField(37)
|
||||||
|
int shalatReportReminderRepeatIntervalMinutes;
|
||||||
|
|
||||||
AppSettings({
|
AppSettings({
|
||||||
this.userName = 'User',
|
this.userName = 'User',
|
||||||
this.userEmail = '',
|
this.userEmail = '',
|
||||||
@@ -138,6 +153,11 @@ class AppSettings extends HiveObject {
|
|||||||
this.quietHoursEnd = '05:00',
|
this.quietHoursEnd = '05:00',
|
||||||
this.maxNonPrayerPushPerDay = 2,
|
this.maxNonPrayerPushPerDay = 2,
|
||||||
this.mirrorAdzanToInbox = false,
|
this.mirrorAdzanToInbox = false,
|
||||||
|
this.tilawahAutoContinueNextSurah = true,
|
||||||
|
this.shalatReportReminderEnabled = true,
|
||||||
|
this.shalatReportReminderDelayMinutes = 30,
|
||||||
|
this.shalatReportReminderRepeatCount = 1,
|
||||||
|
this.shalatReportReminderRepeatIntervalMinutes = 15,
|
||||||
}) : adhanEnabled = adhanEnabled ??
|
}) : adhanEnabled = adhanEnabled ??
|
||||||
{
|
{
|
||||||
'fajr': true,
|
'fajr': true,
|
||||||
|
|||||||
@@ -72,13 +72,23 @@ class AppSettingsAdapter extends TypeAdapter<AppSettings> {
|
|||||||
fields.containsKey(31) ? fields[31] as int? ?? 2 : 2,
|
fields.containsKey(31) ? fields[31] as int? ?? 2 : 2,
|
||||||
mirrorAdzanToInbox:
|
mirrorAdzanToInbox:
|
||||||
fields.containsKey(32) ? fields[32] as bool? ?? false : false,
|
fields.containsKey(32) ? fields[32] as bool? ?? false : false,
|
||||||
|
tilawahAutoContinueNextSurah:
|
||||||
|
fields.containsKey(33) ? fields[33] as bool? ?? true : true,
|
||||||
|
shalatReportReminderEnabled:
|
||||||
|
fields.containsKey(34) ? fields[34] as bool? ?? true : true,
|
||||||
|
shalatReportReminderDelayMinutes:
|
||||||
|
fields.containsKey(35) ? fields[35] as int? ?? 30 : 30,
|
||||||
|
shalatReportReminderRepeatCount:
|
||||||
|
fields.containsKey(36) ? fields[36] as int? ?? 1 : 1,
|
||||||
|
shalatReportReminderRepeatIntervalMinutes:
|
||||||
|
fields.containsKey(37) ? fields[37] as int? ?? 15 : 15,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void write(BinaryWriter writer, AppSettings obj) {
|
void write(BinaryWriter writer, AppSettings obj) {
|
||||||
writer
|
writer
|
||||||
..writeByte(33)
|
..writeByte(38)
|
||||||
..writeByte(0)
|
..writeByte(0)
|
||||||
..write(obj.userName)
|
..write(obj.userName)
|
||||||
..writeByte(1)
|
..writeByte(1)
|
||||||
@@ -144,7 +154,17 @@ class AppSettingsAdapter extends TypeAdapter<AppSettings> {
|
|||||||
..writeByte(31)
|
..writeByte(31)
|
||||||
..write(obj.maxNonPrayerPushPerDay)
|
..write(obj.maxNonPrayerPushPerDay)
|
||||||
..writeByte(32)
|
..writeByte(32)
|
||||||
..write(obj.mirrorAdzanToInbox);
|
..write(obj.mirrorAdzanToInbox)
|
||||||
|
..writeByte(33)
|
||||||
|
..write(obj.tilawahAutoContinueNextSurah)
|
||||||
|
..writeByte(34)
|
||||||
|
..write(obj.shalatReportReminderEnabled)
|
||||||
|
..writeByte(35)
|
||||||
|
..write(obj.shalatReportReminderDelayMinutes)
|
||||||
|
..writeByte(36)
|
||||||
|
..write(obj.shalatReportReminderRepeatCount)
|
||||||
|
..writeByte(37)
|
||||||
|
..write(obj.shalatReportReminderRepeatIntervalMinutes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
117
lib/data/services/background_sync_service.dart
Normal file
117
lib/data/services/background_sync_service.dart
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:workmanager/workmanager.dart';
|
||||||
|
|
||||||
|
import '../local/hive_boxes.dart';
|
||||||
|
import '../local/models/app_settings.dart';
|
||||||
|
import 'myquran_sholat_service.dart';
|
||||||
|
import 'notification_orchestrator_service.dart';
|
||||||
|
import 'notification_service.dart';
|
||||||
|
|
||||||
|
class BackgroundSyncService {
|
||||||
|
BackgroundSyncService._();
|
||||||
|
static final BackgroundSyncService instance = BackgroundSyncService._();
|
||||||
|
|
||||||
|
static const String periodicTaskName = 'jamshalat_periodic_sync';
|
||||||
|
static const String periodicUniqueName = 'jamshalat_periodic_sync_unique';
|
||||||
|
|
||||||
|
Future<void> init() async {
|
||||||
|
await Workmanager().initialize(_workmanagerCallbackDispatcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> registerPeriodicSync() async {
|
||||||
|
await Workmanager().registerPeriodicTask(
|
||||||
|
periodicUniqueName,
|
||||||
|
periodicTaskName,
|
||||||
|
existingWorkPolicy: ExistingPeriodicWorkPolicy.update,
|
||||||
|
frequency: const Duration(hours: 6),
|
||||||
|
initialDelay: const Duration(minutes: 15),
|
||||||
|
constraints: Constraints(
|
||||||
|
networkType: NetworkType.connected,
|
||||||
|
),
|
||||||
|
backoffPolicy: BackoffPolicy.exponential,
|
||||||
|
backoffPolicyDelay: const Duration(minutes: 10),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> runSyncPass() async {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
await initHive();
|
||||||
|
|
||||||
|
final settingsBox = Hive.box<AppSettings>(HiveBoxes.settings);
|
||||||
|
final settings = settingsBox.get('default') ?? AppSettings();
|
||||||
|
final cityId = _resolveCityId(settings);
|
||||||
|
|
||||||
|
final schedulesByDate = await _buildWindowSchedules(cityId);
|
||||||
|
if (schedulesByDate.isNotEmpty) {
|
||||||
|
await NotificationService.instance.syncPrayerNotifications(
|
||||||
|
cityId: cityId,
|
||||||
|
adhanEnabled: settings.adhanEnabled,
|
||||||
|
iqamahOffset: settings.iqamahOffset,
|
||||||
|
schedulesByDate: schedulesByDate,
|
||||||
|
reportReminderEnabled: settings.shalatReportReminderEnabled,
|
||||||
|
reportReminderDelayMinutes: settings.shalatReportReminderDelayMinutes,
|
||||||
|
reportReminderRepeatCount: settings.shalatReportReminderRepeatCount,
|
||||||
|
reportReminderRepeatIntervalMinutes:
|
||||||
|
settings.shalatReportReminderRepeatIntervalMinutes,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await NotificationService.instance.syncHabitNotifications(
|
||||||
|
settings: settings,
|
||||||
|
);
|
||||||
|
await NotificationOrchestratorService.instance.runPassivePass(
|
||||||
|
settings: settings,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Map<String, Map<String, String>>> _buildWindowSchedules(
|
||||||
|
String cityId) async {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final startDate = DateTime(now.year, now.month, now.day);
|
||||||
|
final endDate = startDate.add(const Duration(days: 35));
|
||||||
|
|
||||||
|
final monthKeys = <String>{
|
||||||
|
DateFormat('yyyy-MM').format(startDate),
|
||||||
|
DateFormat('yyyy-MM').format(endDate),
|
||||||
|
};
|
||||||
|
|
||||||
|
final schedulesByDate = <String, Map<String, String>>{};
|
||||||
|
for (final monthKey in monthKeys) {
|
||||||
|
final monthly = await MyQuranSholatService.instance
|
||||||
|
.getMonthlySchedule(cityId, monthKey);
|
||||||
|
for (final entry in monthly.entries) {
|
||||||
|
final date = DateTime.tryParse(entry.key);
|
||||||
|
if (date == null) continue;
|
||||||
|
final normalized = DateTime(date.year, date.month, date.day);
|
||||||
|
if (normalized.isBefore(startDate) || normalized.isAfter(endDate)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
schedulesByDate[entry.key] = entry.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return schedulesByDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _resolveCityId(AppSettings settings) {
|
||||||
|
final stored = settings.lastCityName ?? '';
|
||||||
|
if (stored.contains('|')) {
|
||||||
|
return stored.split('|').last;
|
||||||
|
}
|
||||||
|
return '58a2fc6ed39fd083f55d4182bf88826d';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@pragma('vm:entry-point')
|
||||||
|
void _workmanagerCallbackDispatcher() {
|
||||||
|
Workmanager().executeTask((task, inputData) async {
|
||||||
|
if (task == BackgroundSyncService.periodicTaskName) {
|
||||||
|
await BackgroundSyncService.runSyncPass();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -211,21 +211,33 @@ class NotificationService {
|
|||||||
required Map<String, bool> adhanEnabled,
|
required Map<String, bool> adhanEnabled,
|
||||||
required Map<String, int> iqamahOffset,
|
required Map<String, int> iqamahOffset,
|
||||||
required Map<String, Map<String, String>> schedulesByDate,
|
required Map<String, Map<String, String>> schedulesByDate,
|
||||||
|
required bool reportReminderEnabled,
|
||||||
|
required int reportReminderDelayMinutes,
|
||||||
|
required int reportReminderRepeatCount,
|
||||||
|
required int reportReminderRepeatIntervalMinutes,
|
||||||
}) async {
|
}) async {
|
||||||
await init();
|
await init();
|
||||||
|
|
||||||
final hasAnyEnabled = adhanEnabled.values.any((v) => v);
|
final hasAnyEnabled = adhanEnabled.values.any((v) => v);
|
||||||
if (!hasAnyEnabled) {
|
if (!hasAnyEnabled) {
|
||||||
await cancelAllPending();
|
await _cancelPrayerPending();
|
||||||
_lastSyncSignature = null;
|
_lastSyncSignature = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final signature = _buildSyncSignature(
|
final signature = _buildSyncSignature(
|
||||||
cityId, adhanEnabled, iqamahOffset, schedulesByDate);
|
cityId,
|
||||||
|
adhanEnabled,
|
||||||
|
iqamahOffset,
|
||||||
|
schedulesByDate,
|
||||||
|
reportReminderEnabled: reportReminderEnabled,
|
||||||
|
reportReminderDelayMinutes: reportReminderDelayMinutes,
|
||||||
|
reportReminderRepeatCount: reportReminderRepeatCount,
|
||||||
|
reportReminderRepeatIntervalMinutes: reportReminderRepeatIntervalMinutes,
|
||||||
|
);
|
||||||
if (_lastSyncSignature == signature) return;
|
if (_lastSyncSignature == signature) return;
|
||||||
|
|
||||||
await cancelAllPending();
|
await _cancelPrayerPending();
|
||||||
|
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final dateEntries = schedulesByDate.entries.toList()
|
final dateEntries = schedulesByDate.entries.toList()
|
||||||
@@ -278,6 +290,30 @@ class NotificationService {
|
|||||||
iqamahTime: iqamahTime,
|
iqamahTime: iqamahTime,
|
||||||
offsetMinutes: offsetMinutes,
|
offsetMinutes: offsetMinutes,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!reportReminderEnabled || reportReminderDelayMinutes <= 0) continue;
|
||||||
|
final repeats =
|
||||||
|
reportReminderRepeatCount < 0 ? 0 : reportReminderRepeatCount;
|
||||||
|
final repeatGap = reportReminderRepeatIntervalMinutes <= 0
|
||||||
|
? 15
|
||||||
|
: reportReminderRepeatIntervalMinutes;
|
||||||
|
for (int index = 0; index <= repeats; index++) {
|
||||||
|
final reminderAt = prayerTime.add(
|
||||||
|
Duration(minutes: reportReminderDelayMinutes + (index * repeatGap)),
|
||||||
|
);
|
||||||
|
if (!reminderAt.isAfter(now)) continue;
|
||||||
|
await _scheduleShalatReportReminder(
|
||||||
|
id: _reportReminderId(
|
||||||
|
cityId: cityId,
|
||||||
|
dateKey: dateEntry.key,
|
||||||
|
prayerKey: canonicalPrayer,
|
||||||
|
reminderIndex: index,
|
||||||
|
),
|
||||||
|
prayerName: _localizedPrayerName(canonicalPrayer),
|
||||||
|
reminderTime: reminderAt,
|
||||||
|
reminderIndex: index,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,12 +431,30 @@ class NotificationService {
|
|||||||
return isIqamah ? bounded + 800000 : bounded + 100000;
|
return isIqamah ? bounded + 800000 : bounded + 100000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int _reportReminderId({
|
||||||
|
required String cityId,
|
||||||
|
required String dateKey,
|
||||||
|
required String prayerKey,
|
||||||
|
required int reminderIndex,
|
||||||
|
}) {
|
||||||
|
final seed = '$cityId|$dateKey|$prayerKey|report|$reminderIndex';
|
||||||
|
var hash = 23;
|
||||||
|
for (final rune in seed.runes) {
|
||||||
|
hash = 43 * hash + rune;
|
||||||
|
}
|
||||||
|
return 700000 + (hash.abs() % 90000);
|
||||||
|
}
|
||||||
|
|
||||||
String _buildSyncSignature(
|
String _buildSyncSignature(
|
||||||
String cityId,
|
String cityId,
|
||||||
Map<String, bool> adhanEnabled,
|
Map<String, bool> adhanEnabled,
|
||||||
Map<String, int> iqamahOffset,
|
Map<String, int> iqamahOffset,
|
||||||
Map<String, Map<String, String>> schedulesByDate,
|
Map<String, Map<String, String>> schedulesByDate, {
|
||||||
) {
|
required bool reportReminderEnabled,
|
||||||
|
required int reportReminderDelayMinutes,
|
||||||
|
required int reportReminderRepeatCount,
|
||||||
|
required int reportReminderRepeatIntervalMinutes,
|
||||||
|
}) {
|
||||||
final sortedAdhan = adhanEnabled.entries.toList()
|
final sortedAdhan = adhanEnabled.entries.toList()
|
||||||
..sort((a, b) => a.key.compareTo(b.key));
|
..sort((a, b) => a.key.compareTo(b.key));
|
||||||
final sortedIqamah = iqamahOffset.entries.toList()
|
final sortedIqamah = iqamahOffset.entries.toList()
|
||||||
@@ -415,6 +469,9 @@ class NotificationService {
|
|||||||
for (final e in sortedIqamah) {
|
for (final e in sortedIqamah) {
|
||||||
buffer.write('|${e.key}:${e.value}');
|
buffer.write('|${e.key}:${e.value}');
|
||||||
}
|
}
|
||||||
|
buffer.write(
|
||||||
|
'|report:${reportReminderEnabled ? 1 : 0}:$reportReminderDelayMinutes:$reportReminderRepeatCount:$reportReminderRepeatIntervalMinutes',
|
||||||
|
);
|
||||||
for (final dateEntry in sortedDates) {
|
for (final dateEntry in sortedDates) {
|
||||||
buffer.write('|${dateEntry.key}');
|
buffer.write('|${dateEntry.key}');
|
||||||
final times = dateEntry.value.entries.toList()
|
final times = dateEntry.value.entries.toList()
|
||||||
@@ -426,6 +483,43 @@ class NotificationService {
|
|||||||
return buffer.toString();
|
return buffer.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _scheduleShalatReportReminder({
|
||||||
|
required int id,
|
||||||
|
required String prayerName,
|
||||||
|
required DateTime reminderTime,
|
||||||
|
required int reminderIndex,
|
||||||
|
}) async {
|
||||||
|
final attemptLabel = reminderIndex == 0 ? '' : ' (#${reminderIndex + 1})';
|
||||||
|
await _plugin.zonedSchedule(
|
||||||
|
id: id,
|
||||||
|
title: 'Lapor Shalat • $prayerName$attemptLabel',
|
||||||
|
body: 'Sudah shalat $prayerName? Yuk tandai di checklist sekarang.',
|
||||||
|
scheduledDate: tz.TZDateTime.from(reminderTime, tz.local),
|
||||||
|
notificationDetails: _habitDetails,
|
||||||
|
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
|
||||||
|
payload: 'report|$prayerName|${reminderTime.toIso8601String()}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> cancelShalatReportReminders({
|
||||||
|
required String cityId,
|
||||||
|
required String dateKey,
|
||||||
|
required String canonicalPrayer,
|
||||||
|
required int repeatCount,
|
||||||
|
}) async {
|
||||||
|
final repeats = repeatCount < 0 ? 0 : repeatCount;
|
||||||
|
for (int index = 0; index <= repeats; index++) {
|
||||||
|
await _plugin.cancel(
|
||||||
|
id: _reportReminderId(
|
||||||
|
cityId: cityId,
|
||||||
|
dateKey: dateKey,
|
||||||
|
prayerKey: canonicalPrayer,
|
||||||
|
reminderIndex: index,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> cancelAllPending() async {
|
Future<void> cancelAllPending() async {
|
||||||
try {
|
try {
|
||||||
await _plugin.cancelAllPendingNotifications();
|
await _plugin.cancelAllPendingNotifications();
|
||||||
@@ -434,6 +528,17 @@ class NotificationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _cancelPrayerPending() async {
|
||||||
|
final pending = await _plugin.pendingNotificationRequests();
|
||||||
|
for (final request in pending) {
|
||||||
|
final id = request.id;
|
||||||
|
final isPrayerSchedule = id >= 100000 && id < 900000;
|
||||||
|
if (isPrayerSchedule) {
|
||||||
|
await _plugin.cancel(id: id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<int> pendingCount() async {
|
Future<int> pendingCount() async {
|
||||||
final pending = await _plugin.pendingNotificationRequests();
|
final pending = await _plugin.pendingNotificationRequests();
|
||||||
return pending.length;
|
return pending.length;
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
|
|
||||||
import '../local/models/app_settings.dart';
|
import '../local/models/app_settings.dart';
|
||||||
|
import '../local/hive_boxes.dart';
|
||||||
import 'notification_inbox_service.dart';
|
import 'notification_inbox_service.dart';
|
||||||
|
import 'notification_service.dart';
|
||||||
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
|
|
||||||
/// Phase-4 bridge for future FCM/APNs wiring.
|
/// Phase-4 bridge for future FCM/APNs wiring.
|
||||||
///
|
///
|
||||||
@@ -10,9 +19,30 @@ class RemotePushService {
|
|||||||
static final RemotePushService instance = RemotePushService._();
|
static final RemotePushService instance = RemotePushService._();
|
||||||
|
|
||||||
final NotificationInboxService _inbox = NotificationInboxService.instance;
|
final NotificationInboxService _inbox = NotificationInboxService.instance;
|
||||||
|
bool _initialized = false;
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
// Reserved for SDK wiring (FCM/APNs token registration, topic subscription).
|
if (_initialized) return;
|
||||||
|
try {
|
||||||
|
await Firebase.initializeApp();
|
||||||
|
final messaging = FirebaseMessaging.instance;
|
||||||
|
await messaging.requestPermission(
|
||||||
|
alert: true,
|
||||||
|
badge: true,
|
||||||
|
sound: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);
|
||||||
|
FirebaseMessaging.onMessage.listen((message) {
|
||||||
|
unawaited(_handleMessage(message, isForeground: true));
|
||||||
|
});
|
||||||
|
FirebaseMessaging.onMessageOpenedApp.listen((message) {
|
||||||
|
unawaited(_handleMessage(message, isForeground: false));
|
||||||
|
});
|
||||||
|
_initialized = true;
|
||||||
|
} catch (_) {
|
||||||
|
// Firebase may not be configured in all build variants yet.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> ingestPayload(
|
Future<void> ingestPayload(
|
||||||
@@ -44,4 +74,60 @@ class RemotePushService {
|
|||||||
meta: <String, dynamic>{'remoteId': id},
|
meta: <String, dynamic>{'remoteId': id},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _handleMessage(
|
||||||
|
RemoteMessage message, {
|
||||||
|
required bool isForeground,
|
||||||
|
}) async {
|
||||||
|
final payload = <String, dynamic>{
|
||||||
|
'id': message.messageId ?? message.data['id'] ?? '',
|
||||||
|
'title': message.notification?.title ?? message.data['title'] ?? '',
|
||||||
|
'body': message.notification?.body ?? message.data['body'] ?? '',
|
||||||
|
'type': message.data['type'] ?? 'content',
|
||||||
|
'deeplink': message.data['deeplink'] ?? '',
|
||||||
|
'expiresAt': message.data['expiresAt'] ?? '',
|
||||||
|
'isPinned': message.data['isPinned'] == 'true',
|
||||||
|
};
|
||||||
|
|
||||||
|
final settings = Hive.box<AppSettings>(HiveBoxes.settings).get('default') ??
|
||||||
|
AppSettings();
|
||||||
|
await ingestPayload(payload, settings: settings);
|
||||||
|
|
||||||
|
if (isForeground &&
|
||||||
|
settings.alertsEnabled &&
|
||||||
|
(payload['title'] as String).trim().isNotEmpty &&
|
||||||
|
(payload['body'] as String).trim().isNotEmpty) {
|
||||||
|
await NotificationService.instance.showNonPrayerAlert(
|
||||||
|
settings: settings,
|
||||||
|
id: NotificationService.instance
|
||||||
|
.nonPrayerNotificationId('remote.${payload['id']}'),
|
||||||
|
title: (payload['title'] as String).trim(),
|
||||||
|
body: (payload['body'] as String).trim(),
|
||||||
|
payloadType: 'remote',
|
||||||
|
bypassDailyCap: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@pragma('vm:entry-point')
|
||||||
|
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
await initHive();
|
||||||
|
try {
|
||||||
|
await Firebase.initializeApp();
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
final payload = <String, dynamic>{
|
||||||
|
'id': message.messageId ?? message.data['id'] ?? '',
|
||||||
|
'title': message.notification?.title ?? message.data['title'] ?? '',
|
||||||
|
'body': message.notification?.body ?? message.data['body'] ?? '',
|
||||||
|
'type': message.data['type'] ?? 'content',
|
||||||
|
'deeplink': message.data['deeplink'] ?? '',
|
||||||
|
'expiresAt': message.data['expiresAt'] ?? '',
|
||||||
|
'isPinned': message.data['isPinned'] == 'true',
|
||||||
|
};
|
||||||
|
final settings =
|
||||||
|
Hive.box<AppSettings>(HiveBoxes.settings).get('default') ?? AppSettings();
|
||||||
|
await RemotePushService.instance.ingestPayload(payload, settings: settings);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
@@ -14,6 +16,7 @@ import '../../../data/local/models/shalat_log.dart';
|
|||||||
import '../../../data/local/models/tilawah_log.dart';
|
import '../../../data/local/models/tilawah_log.dart';
|
||||||
import '../../../data/local/models/dzikir_log.dart';
|
import '../../../data/local/models/dzikir_log.dart';
|
||||||
import '../../../data/local/models/puasa_log.dart';
|
import '../../../data/local/models/puasa_log.dart';
|
||||||
|
import '../../../data/services/notification_service.dart';
|
||||||
|
|
||||||
class ChecklistScreen extends ConsumerStatefulWidget {
|
class ChecklistScreen extends ConsumerStatefulWidget {
|
||||||
const ChecklistScreen({super.key});
|
const ChecklistScreen({super.key});
|
||||||
@@ -72,6 +75,47 @@ class _ChecklistScreenState extends ConsumerState<ChecklistScreen> {
|
|||||||
|
|
||||||
DailyWorshipLog get _todayLog => _logBox.get(_todayKey)!;
|
DailyWorshipLog get _todayLog => _logBox.get(_todayKey)!;
|
||||||
|
|
||||||
|
Future<void> _cancelShalatReportReminderIfCompleted(String prayerKey) async {
|
||||||
|
final sLog = _todayLog.shalatLogs[prayerKey];
|
||||||
|
if (sLog == null || !sLog.completed) return;
|
||||||
|
final cityId = _resolveCityId();
|
||||||
|
final canonical = _canonicalPrayerKey(prayerKey);
|
||||||
|
if (canonical == null) return;
|
||||||
|
await NotificationService.instance.cancelShalatReportReminders(
|
||||||
|
cityId: cityId,
|
||||||
|
dateKey: _todayKey,
|
||||||
|
canonicalPrayer: canonical,
|
||||||
|
repeatCount: _settings.shalatReportReminderRepeatCount,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _resolveCityId() {
|
||||||
|
final stored = _settings.lastCityName ?? '';
|
||||||
|
if (stored.contains('|')) return stored.split('|').last;
|
||||||
|
return '58a2fc6ed39fd083f55d4182bf88826d';
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _canonicalPrayerKey(String key) {
|
||||||
|
switch (key) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _recalculateProgress() {
|
void _recalculateProgress() {
|
||||||
final log = _todayLog;
|
final log = _todayLog;
|
||||||
|
|
||||||
@@ -386,6 +430,9 @@ class _ChecklistScreenState extends ConsumerState<ChecklistScreen> {
|
|||||||
onChanged: (v) {
|
onChanged: (v) {
|
||||||
log.completed = v ?? false;
|
log.completed = v ?? false;
|
||||||
_recalculateProgress();
|
_recalculateProgress();
|
||||||
|
if (log.completed) {
|
||||||
|
unawaited(_cancelShalatReportReminderIfCompleted(pKey));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
childrenPadding:
|
childrenPadding:
|
||||||
@@ -404,12 +451,14 @@ class _ChecklistScreenState extends ConsumerState<ChecklistScreen> {
|
|||||||
log.location = 'Masjid';
|
log.location = 'Masjid';
|
||||||
log.completed = true; // Auto-check parent
|
log.completed = true; // Auto-check parent
|
||||||
_recalculateProgress();
|
_recalculateProgress();
|
||||||
|
unawaited(_cancelShalatReportReminderIfCompleted(pKey));
|
||||||
}),
|
}),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
_radioOption('Rumah', log, () {
|
_radioOption('Rumah', log, () {
|
||||||
log.location = 'Rumah';
|
log.location = 'Rumah';
|
||||||
log.completed = true; // Auto-check parent
|
log.completed = true; // Auto-check parent
|
||||||
_recalculateProgress();
|
_recalculateProgress();
|
||||||
|
unawaited(_cancelShalatReportReminderIfCompleted(pKey));
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -219,15 +219,36 @@ Future<void> _syncAdhanNotifications(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final schedulesByDate = <String, Map<String, String>>{
|
final schedulesByDate = <String, Map<String, String>>{};
|
||||||
schedule.date: schedule.times,
|
final today = DateTime.now();
|
||||||
|
final startDate = DateTime(today.year, today.month, today.day);
|
||||||
|
final endDate = startDate.add(const Duration(days: 35));
|
||||||
|
|
||||||
|
final monthKeys = <String>{
|
||||||
|
DateFormat('yyyy-MM').format(startDate),
|
||||||
|
DateFormat('yyyy-MM').format(endDate),
|
||||||
};
|
};
|
||||||
|
|
||||||
final baseDate = DateTime.tryParse(schedule.date);
|
for (final monthKey in monthKeys) {
|
||||||
if (baseDate != null) {
|
final monthly = await MyQuranSholatService.instance
|
||||||
final nextDate = DateFormat('yyyy-MM-dd')
|
.getMonthlySchedule(cityId, monthKey);
|
||||||
.format(baseDate.add(const Duration(days: 1)));
|
for (final entry in monthly.entries) {
|
||||||
if (!schedulesByDate.containsKey(nextDate)) {
|
final date = DateTime.tryParse(entry.key);
|
||||||
|
if (date == null) continue;
|
||||||
|
final normalized = DateTime(date.year, date.month, date.day);
|
||||||
|
if (normalized.isBefore(startDate) || normalized.isAfter(endDate)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
schedulesByDate[entry.key] = entry.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schedulesByDate.isEmpty) {
|
||||||
|
schedulesByDate[schedule.date] = schedule.times;
|
||||||
|
final baseDate = DateTime.tryParse(schedule.date);
|
||||||
|
if (baseDate != null) {
|
||||||
|
final nextDate = DateFormat('yyyy-MM-dd')
|
||||||
|
.format(baseDate.add(const Duration(days: 1)));
|
||||||
final nextSchedule = await MyQuranSholatService.instance
|
final nextSchedule = await MyQuranSholatService.instance
|
||||||
.getDailySchedule(cityId, nextDate);
|
.getDailySchedule(cityId, nextDate);
|
||||||
if (nextSchedule != null) {
|
if (nextSchedule != null) {
|
||||||
@@ -241,6 +262,11 @@ Future<void> _syncAdhanNotifications(
|
|||||||
adhanEnabled: settings.adhanEnabled,
|
adhanEnabled: settings.adhanEnabled,
|
||||||
iqamahOffset: settings.iqamahOffset,
|
iqamahOffset: settings.iqamahOffset,
|
||||||
schedulesByDate: schedulesByDate,
|
schedulesByDate: schedulesByDate,
|
||||||
|
reportReminderEnabled: settings.shalatReportReminderEnabled,
|
||||||
|
reportReminderDelayMinutes: settings.shalatReportReminderDelayMinutes,
|
||||||
|
reportReminderRepeatCount: settings.shalatReportReminderRepeatCount,
|
||||||
|
reportReminderRepeatIntervalMinutes:
|
||||||
|
settings.shalatReportReminderRepeatIntervalMinutes,
|
||||||
);
|
);
|
||||||
await NotificationService.instance.syncHabitNotifications(
|
await NotificationService.instance.syncHabitNotifications(
|
||||||
settings: settings,
|
settings: settings,
|
||||||
|
|||||||
@@ -17,11 +17,7 @@ import '../../../data/local/models/daily_worship_log.dart';
|
|||||||
import '../../../data/local/models/dzikir_counter.dart';
|
import '../../../data/local/models/dzikir_counter.dart';
|
||||||
import '../../../data/local/models/dzikir_log.dart';
|
import '../../../data/local/models/dzikir_log.dart';
|
||||||
import '../../../data/local/models/shalat_log.dart';
|
import '../../../data/local/models/shalat_log.dart';
|
||||||
import '../../../data/services/location_service.dart';
|
|
||||||
import '../../../data/services/myquran_sholat_service.dart';
|
|
||||||
import '../../../data/services/muslim_api_service.dart';
|
import '../../../data/services/muslim_api_service.dart';
|
||||||
import '../../../data/services/prayer_service.dart';
|
|
||||||
import '../../dashboard/data/prayer_times_provider.dart';
|
|
||||||
|
|
||||||
class DzikirScreen extends ConsumerStatefulWidget {
|
class DzikirScreen extends ConsumerStatefulWidget {
|
||||||
final bool isSimpleModeTab;
|
final bool isSimpleModeTab;
|
||||||
@@ -41,20 +37,17 @@ class _DzikirScreenState extends ConsumerState<DzikirScreen>
|
|||||||
'pagi': PageController(),
|
'pagi': PageController(),
|
||||||
'petang': PageController(),
|
'petang': PageController(),
|
||||||
'harian': PageController(),
|
'harian': PageController(),
|
||||||
'solat': PageController(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
final Map<String, int> _focusPageIndex = {
|
final Map<String, int> _focusPageIndex = {
|
||||||
'pagi': 0,
|
'pagi': 0,
|
||||||
'petang': 0,
|
'petang': 0,
|
||||||
'harian': 0,
|
'harian': 0,
|
||||||
'solat': 0,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
List<Map<String, dynamic>> _pagiItems = [];
|
List<Map<String, dynamic>> _pagiItems = [];
|
||||||
List<Map<String, dynamic>> _petangItems = [];
|
List<Map<String, dynamic>> _petangItems = [];
|
||||||
List<Map<String, dynamic>> _harianItems = [];
|
List<Map<String, dynamic>> _harianItems = [];
|
||||||
List<Map<String, dynamic>> _sesudahSholatItems = [];
|
|
||||||
Map<String, dynamic>? _pagiIntroItem;
|
Map<String, dynamic>? _pagiIntroItem;
|
||||||
Map<String, dynamic>? _petangIntroItem;
|
Map<String, dynamic>? _petangIntroItem;
|
||||||
bool _loading = true;
|
bool _loading = true;
|
||||||
@@ -63,29 +56,19 @@ class _DzikirScreenState extends ConsumerState<DzikirScreen>
|
|||||||
late Box<DzikirCounter> _counterBox;
|
late Box<DzikirCounter> _counterBox;
|
||||||
late String _todayKey;
|
late String _todayKey;
|
||||||
Timer? _dayResetTimer;
|
Timer? _dayResetTimer;
|
||||||
Timer? _solatResetTimer;
|
|
||||||
bool _refreshingSolatScope = false;
|
|
||||||
String _solatScopeKey = 'solat_bootstrap';
|
|
||||||
String _solatScopeDateKey = '';
|
|
||||||
DateTime? _nextSolatResetAt;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
_tabController = TabController(length: 4, vsync: this);
|
_tabController = TabController(length: 3, vsync: this);
|
||||||
_tabController.addListener(() {
|
_tabController.addListener(() {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
if (_tabController.index == 0) {
|
|
||||||
unawaited(_refreshSolatScope());
|
|
||||||
}
|
|
||||||
setState(() {});
|
setState(() {});
|
||||||
});
|
});
|
||||||
_counterBox = Hive.box<DzikirCounter>(HiveBoxes.dzikirCounters);
|
_counterBox = Hive.box<DzikirCounter>(HiveBoxes.dzikirCounters);
|
||||||
_todayKey = _currentTodayKey();
|
_todayKey = _currentTodayKey();
|
||||||
_solatScopeDateKey = _todayKey;
|
|
||||||
_scheduleDayResetTimer();
|
_scheduleDayResetTimer();
|
||||||
unawaited(_refreshSolatScope(forceSetState: false));
|
|
||||||
_loadData();
|
_loadData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +76,6 @@ class _DzikirScreenState extends ConsumerState<DzikirScreen>
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
_dayResetTimer?.cancel();
|
_dayResetTimer?.cancel();
|
||||||
_solatResetTimer?.cancel();
|
|
||||||
_tabController.dispose();
|
_tabController.dispose();
|
||||||
for (final controller in _pageControllers.values) {
|
for (final controller in _pageControllers.values) {
|
||||||
controller.dispose();
|
controller.dispose();
|
||||||
@@ -106,7 +88,6 @@ class _DzikirScreenState extends ConsumerState<DzikirScreen>
|
|||||||
if (state == AppLifecycleState.resumed) {
|
if (state == AppLifecycleState.resumed) {
|
||||||
_refreshTodayScope();
|
_refreshTodayScope();
|
||||||
_scheduleDayResetTimer();
|
_scheduleDayResetTimer();
|
||||||
unawaited(_refreshSolatScope());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,161 +112,15 @@ class _DzikirScreenState extends ConsumerState<DzikirScreen>
|
|||||||
if (_harianItems.isNotEmpty) {
|
if (_harianItems.isNotEmpty) {
|
||||||
_seedHarianProgressFromLinkedDzikir();
|
_seedHarianProgressFromLinkedDzikir();
|
||||||
}
|
}
|
||||||
unawaited(_refreshSolatScope());
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _scheduleSolatResetTimer() {
|
|
||||||
_solatResetTimer?.cancel();
|
|
||||||
final nextResetAt = _nextSolatResetAt;
|
|
||||||
if (nextResetAt == null) return;
|
|
||||||
|
|
||||||
var delay =
|
|
||||||
nextResetAt.difference(DateTime.now()) + const Duration(seconds: 1);
|
|
||||||
if (delay.isNegative) {
|
|
||||||
delay = const Duration(seconds: 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
_solatResetTimer = Timer(delay, () {
|
|
||||||
unawaited(_refreshSolatScope());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _refreshSolatScope({bool forceSetState = true}) async {
|
|
||||||
if (_refreshingSolatScope) return;
|
|
||||||
_refreshingSolatScope = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
final scope = await _loadCurrentSolatScope();
|
|
||||||
if (scope == null) return;
|
|
||||||
|
|
||||||
final scopeChanged = _solatScopeKey != scope.scopeKey;
|
|
||||||
_solatScopeKey = scope.scopeKey;
|
|
||||||
_solatScopeDateKey = scope.dateKey;
|
|
||||||
_nextSolatResetAt = scope.nextResetAt;
|
|
||||||
_scheduleSolatResetTimer();
|
|
||||||
|
|
||||||
if (scopeChanged) {
|
|
||||||
_focusPageIndex['solat'] = 0;
|
|
||||||
final controller = _pageControllers['solat'];
|
|
||||||
if (controller != null && controller.hasClients) {
|
|
||||||
controller.jumpToPage(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mounted) return;
|
|
||||||
if (scopeChanged || forceSetState) {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
_refreshingSolatScope = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<({String scopeKey, String dateKey, DateTime nextResetAt})?>
|
|
||||||
_loadCurrentSolatScope() async {
|
|
||||||
final now = DateTime.now();
|
|
||||||
final cityId = ref.read(selectedCityIdProvider);
|
|
||||||
final dates = [
|
|
||||||
DateTime(now.year, now.month, now.day - 1),
|
|
||||||
DateTime(now.year, now.month, now.day),
|
|
||||||
DateTime(now.year, now.month, now.day + 1),
|
|
||||||
];
|
|
||||||
|
|
||||||
final prayerEntries =
|
|
||||||
<({String prayerKey, String dateKey, DateTime time})>[];
|
|
||||||
for (final date in dates) {
|
|
||||||
final schedule = await _loadPrayerScheduleForDate(cityId, date);
|
|
||||||
final dateKey = DateFormat('yyyy-MM-dd').format(date);
|
|
||||||
for (final prayerKey in const [
|
|
||||||
'subuh',
|
|
||||||
'dzuhur',
|
|
||||||
'ashar',
|
|
||||||
'maghrib',
|
|
||||||
'isya',
|
|
||||||
]) {
|
|
||||||
final parsed = _parsePrayerDateTime(date, schedule[prayerKey]);
|
|
||||||
if (parsed == null) continue;
|
|
||||||
prayerEntries.add((
|
|
||||||
prayerKey: prayerKey,
|
|
||||||
dateKey: dateKey,
|
|
||||||
time: parsed,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prayerEntries.isEmpty) return null;
|
|
||||||
prayerEntries.sort((a, b) => a.time.compareTo(b.time));
|
|
||||||
|
|
||||||
({String prayerKey, String dateKey, DateTime time})? active;
|
|
||||||
({String prayerKey, String dateKey, DateTime time})? next;
|
|
||||||
|
|
||||||
for (final entry in prayerEntries) {
|
|
||||||
if (!entry.time.isAfter(now)) {
|
|
||||||
active = entry;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
next = entry;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
active ??= prayerEntries.first;
|
|
||||||
next ??= prayerEntries.last;
|
|
||||||
|
|
||||||
return (
|
|
||||||
scopeKey: '${active.dateKey}_${active.prayerKey}',
|
|
||||||
dateKey: active.dateKey,
|
|
||||||
nextResetAt: next.time,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Map<String, String>> _loadPrayerScheduleForDate(
|
|
||||||
String cityId,
|
|
||||||
DateTime date,
|
|
||||||
) async {
|
|
||||||
final dateKey = DateFormat('yyyy-MM-dd').format(date);
|
|
||||||
final jadwal =
|
|
||||||
await MyQuranSholatService.instance.getDailySchedule(cityId, dateKey);
|
|
||||||
if (jadwal != null) return jadwal;
|
|
||||||
return _buildFallbackPrayerSchedule(date);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, String> _buildFallbackPrayerSchedule(DateTime date) {
|
|
||||||
final lastKnown = LocationService.instance.getLastKnownLocation();
|
|
||||||
final lat = lastKnown?.lat ?? -6.2088;
|
|
||||||
final lng = lastKnown?.lng ?? 106.8456;
|
|
||||||
final result = PrayerService.instance.getPrayerTimes(lat, lng, date);
|
|
||||||
final timeFormat = DateFormat('HH:mm');
|
|
||||||
|
|
||||||
return {
|
|
||||||
'subuh': timeFormat.format(result.fajr),
|
|
||||||
'dzuhur': timeFormat.format(result.dhuhr),
|
|
||||||
'ashar': timeFormat.format(result.asr),
|
|
||||||
'maghrib': timeFormat.format(result.maghrib),
|
|
||||||
'isya': timeFormat.format(result.isha),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
DateTime? _parsePrayerDateTime(DateTime date, String? rawTime) {
|
|
||||||
if (rawTime == null || rawTime.trim().isEmpty || rawTime == '-') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final parts = rawTime.trim().split(':');
|
|
||||||
if (parts.length != 2) return null;
|
|
||||||
final hour = int.tryParse(parts[0]);
|
|
||||||
final minute = int.tryParse(parts[1]);
|
|
||||||
if (hour == null || minute == null) return null;
|
|
||||||
return DateTime(date.year, date.month, date.day, hour, minute);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _counterScopeKeyForPrefix(String prefix) {
|
String _counterScopeKeyForPrefix(String prefix) {
|
||||||
if (prefix == 'solat') return _solatScopeKey;
|
|
||||||
return _todayKey;
|
return _todayKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _counterDateKeyForPrefix(String prefix) {
|
String _counterDateKeyForPrefix(String prefix) {
|
||||||
if (prefix == 'solat') return _solatScopeDateKey;
|
|
||||||
return _todayKey;
|
return _todayKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,10 +140,6 @@ class _DzikirScreenState extends ConsumerState<DzikirScreen>
|
|||||||
'petang',
|
'petang',
|
||||||
strict: true,
|
strict: true,
|
||||||
);
|
);
|
||||||
final solat = await MuslimApiService.instance.getDzikirByType(
|
|
||||||
'solat',
|
|
||||||
strict: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final pagiNormalized = _normalizeRumayshoDzikir('pagi', pagi);
|
final pagiNormalized = _normalizeRumayshoDzikir('pagi', pagi);
|
||||||
@@ -336,7 +167,6 @@ class _DzikirScreenState extends ConsumerState<DzikirScreen>
|
|||||||
_pagiItems = pagiItems;
|
_pagiItems = pagiItems;
|
||||||
_petangItems = petangItems;
|
_petangItems = petangItems;
|
||||||
_harianItems = harianItems;
|
_harianItems = harianItems;
|
||||||
_sesudahSholatItems = solat;
|
|
||||||
_loading = false;
|
_loading = false;
|
||||||
});
|
});
|
||||||
_ensureValidFocusPages();
|
_ensureValidFocusPages();
|
||||||
@@ -691,7 +521,6 @@ class _DzikirScreenState extends ConsumerState<DzikirScreen>
|
|||||||
_petangItems.length + (_petangIntroItem != null ? 1 : 0),
|
_petangItems.length + (_petangIntroItem != null ? 1 : 0),
|
||||||
);
|
);
|
||||||
_clampFocusPageForPrefix('harian', _harianItems.length);
|
_clampFocusPageForPrefix('harian', _harianItems.length);
|
||||||
_clampFocusPageForPrefix('solat', _sesudahSholatItems.length);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _clampFocusPageForPrefix(String prefix, int itemLength) {
|
void _clampFocusPageForPrefix(String prefix, int itemLength) {
|
||||||
@@ -866,7 +695,6 @@ class _DzikirScreenState extends ConsumerState<DzikirScreen>
|
|||||||
labelStyle:
|
labelStyle:
|
||||||
const TextStyle(fontWeight: FontWeight.w700, fontSize: 13),
|
const TextStyle(fontWeight: FontWeight.w700, fontSize: 13),
|
||||||
tabs: const [
|
tabs: const [
|
||||||
Tab(text: 'Sesudah Sholat'),
|
|
||||||
Tab(text: 'Pagi'),
|
Tab(text: 'Pagi'),
|
||||||
Tab(text: 'Petang'),
|
Tab(text: 'Petang'),
|
||||||
Tab(text: 'Harian'),
|
Tab(text: 'Harian'),
|
||||||
@@ -883,28 +711,6 @@ class _DzikirScreenState extends ConsumerState<DzikirScreen>
|
|||||||
: TabBarView(
|
: TabBarView(
|
||||||
controller: _tabController,
|
controller: _tabController,
|
||||||
children: [
|
children: [
|
||||||
isFocusMode
|
|
||||||
? _buildFocusModeTab(
|
|
||||||
context,
|
|
||||||
isDark,
|
|
||||||
settings,
|
|
||||||
items: _sesudahSholatItems,
|
|
||||||
introItem: null,
|
|
||||||
prefix: 'solat',
|
|
||||||
title: 'Dzikir Sesudah Sholat',
|
|
||||||
subtitle:
|
|
||||||
'Dibaca setelah shalat fardhu. Hitungan akan dimulai ulang otomatis saat waktu shalat berikutnya masuk.',
|
|
||||||
)
|
|
||||||
: _buildDzikirList(
|
|
||||||
context,
|
|
||||||
isDark,
|
|
||||||
settings,
|
|
||||||
_sesudahSholatItems,
|
|
||||||
null,
|
|
||||||
'solat',
|
|
||||||
'Dzikir Sesudah Sholat',
|
|
||||||
'Dibaca setelah shalat fardhu. Hitungan akan dimulai ulang otomatis saat waktu shalat berikutnya masuk.',
|
|
||||||
),
|
|
||||||
isFocusMode
|
isFocusMode
|
||||||
? _buildFocusModeTab(
|
? _buildFocusModeTab(
|
||||||
context,
|
context,
|
||||||
@@ -1157,9 +963,6 @@ class _DzikirScreenState extends ConsumerState<DzikirScreen>
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
if (prefix == 'solat') {
|
|
||||||
await _refreshSolatScope();
|
|
||||||
}
|
|
||||||
final becameComplete = _increment(
|
final becameComplete = _increment(
|
||||||
dzikirId,
|
dzikirId,
|
||||||
target,
|
target,
|
||||||
@@ -1760,10 +1563,6 @@ class _DzikirScreenState extends ConsumerState<DzikirScreen>
|
|||||||
}) async {
|
}) async {
|
||||||
_refreshTodayScope();
|
_refreshTodayScope();
|
||||||
if (items.isEmpty) return;
|
if (items.isEmpty) return;
|
||||||
if (prefix == 'solat') {
|
|
||||||
await _refreshSolatScope();
|
|
||||||
if (!context.mounted) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final introOffset = introItem != null ? 1 : 0;
|
final introOffset = introItem != null ? 1 : 0;
|
||||||
final currentPage =
|
final currentPage =
|
||||||
|
|||||||
@@ -71,6 +71,13 @@ class _QuranReadingScreenState extends ConsumerState<QuranReadingScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _navigateToSurah(int surahNumber, {int? startVerse}) {
|
||||||
|
final base = widget.isSimpleModeTab ? '/quran' : '/tools/quran';
|
||||||
|
final verseQuery =
|
||||||
|
(startVerse != null && startVerse > 0) ? '?startVerse=$startVerse' : '';
|
||||||
|
context.pushReplacement('$base/$surahNumber$verseQuery');
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -679,6 +686,7 @@ class _QuranReadingScreenState extends ConsumerState<QuranReadingScreen> {
|
|||||||
TilawahSession session, int endVerseId) async {
|
TilawahSession session, int endVerseId) async {
|
||||||
final endSurahId = _surah!['nomor'] ?? 1;
|
final endSurahId = _surah!['nomor'] ?? 1;
|
||||||
final endSurahName = _surah!['namaLatin'] ?? '';
|
final endSurahName = _surah!['namaLatin'] ?? '';
|
||||||
|
final isLastAyat = endVerseId == _verses.length;
|
||||||
|
|
||||||
int calculatedAyat = 0;
|
int calculatedAyat = 0;
|
||||||
|
|
||||||
@@ -723,115 +731,155 @@ class _QuranReadingScreenState extends ConsumerState<QuranReadingScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
bool saveAsLastRead = true;
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => StatefulBuilder(
|
||||||
title: const Text('Catat Sesi Tilawah',
|
builder: (context, setModalState) => AlertDialog(
|
||||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
title: const Text('Catat Sesi Tilawah',
|
||||||
content: Column(
|
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
mainAxisSize: MainAxisSize.min,
|
content: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Container(
|
children: [
|
||||||
padding: const EdgeInsets.all(12),
|
Container(
|
||||||
decoration: BoxDecoration(
|
padding: const EdgeInsets.all(12),
|
||||||
color: AppColors.primary.withValues(alpha: 0.1),
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(8),
|
color: AppColors.primary.withValues(alpha: 0.1),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Column(children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const Text('Mulai:', style: TextStyle(fontSize: 13)),
|
||||||
|
Text(
|
||||||
|
'${session.startSurahName} : ${session.startVerseId}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold, fontSize: 13)),
|
||||||
|
]),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 4),
|
||||||
|
child: Divider()),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const Text('Selesai:',
|
||||||
|
style: TextStyle(fontSize: 13)),
|
||||||
|
Text('$endSurahName : $endVerseId',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold, fontSize: 13)),
|
||||||
|
]),
|
||||||
|
])),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(LucideIcons.bookOpen,
|
||||||
|
size: 20, color: AppColors.primary),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text('Total Dibaca: $calculatedAyat Ayat',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold, fontSize: 15)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
CheckboxListTile(
|
||||||
|
value: saveAsLastRead,
|
||||||
|
onChanged: (value) {
|
||||||
|
setModalState(() => saveAsLastRead = value ?? true);
|
||||||
|
},
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
|
title: const Text(
|
||||||
|
'Simpan juga sebagai Terakhir Dibaca',
|
||||||
|
style: TextStyle(fontSize: 13),
|
||||||
),
|
),
|
||||||
child: Column(children: [
|
),
|
||||||
Row(
|
],
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
),
|
||||||
children: [
|
actions: [
|
||||||
const Text('Mulai:', style: TextStyle(fontSize: 13)),
|
TextButton(
|
||||||
Text(
|
onPressed: () {
|
||||||
'${session.startSurahName} : ${session.startVerseId}',
|
ref.invalidate(tilawahTrackingProvider);
|
||||||
style: const TextStyle(
|
Navigator.pop(ctx);
|
||||||
fontWeight: FontWeight.bold, fontSize: 13)),
|
},
|
||||||
]),
|
child: const Text('Batal', style: TextStyle(color: Colors.red)),
|
||||||
const Padding(
|
),
|
||||||
padding: EdgeInsets.symmetric(vertical: 4),
|
FilledButton(
|
||||||
child: Divider()),
|
onPressed: () async {
|
||||||
Row(
|
final settingsBox = Hive.box<AppSettings>(HiveBoxes.settings);
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
final settings = settingsBox.get('default') ?? AppSettings();
|
||||||
children: [
|
final todayKey =
|
||||||
const Text('Selesai:', style: TextStyle(fontSize: 13)),
|
DateFormat('yyyy-MM-dd').format(DateTime.now());
|
||||||
Text('$endSurahName : $endVerseId',
|
final logBox = Hive.box<DailyWorshipLog>(HiveBoxes.worshipLogs);
|
||||||
style: const TextStyle(
|
var log = logBox.get(todayKey);
|
||||||
fontWeight: FontWeight.bold, fontSize: 13)),
|
|
||||||
]),
|
if (log == null) {
|
||||||
])),
|
log = DailyWorshipLog(
|
||||||
const SizedBox(height: 16),
|
date: todayKey,
|
||||||
Row(
|
shalatLogs: <String, ShalatLog>{
|
||||||
children: [
|
'subuh': ShalatLog(),
|
||||||
const Icon(LucideIcons.bookOpen,
|
'dzuhur': ShalatLog(),
|
||||||
size: 20, color: AppColors.primary),
|
'ashar': ShalatLog(),
|
||||||
const SizedBox(width: 8),
|
'maghrib': ShalatLog(),
|
||||||
Text('Total Dibaca: $calculatedAyat Ayat',
|
'isya': ShalatLog(),
|
||||||
style: const TextStyle(
|
},
|
||||||
fontWeight: FontWeight.bold, fontSize: 15)),
|
);
|
||||||
],
|
logBox.put(todayKey, log);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (log.tilawahLog == null) {
|
||||||
|
log.tilawahLog = TilawahLog(
|
||||||
|
targetValue: settings.tilawahTargetValue,
|
||||||
|
targetUnit: settings.tilawahTargetUnit,
|
||||||
|
autoSync: _autoSyncEnabled,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.tilawahLog!.rawAyatRead += calculatedAyat;
|
||||||
|
log.save();
|
||||||
|
|
||||||
|
if (saveAsLastRead) {
|
||||||
|
final verse = _verses.firstWhere(
|
||||||
|
(v) => (v['nomorAyat'] ?? 0) == endVerseId,
|
||||||
|
orElse: () => <String, dynamic>{},
|
||||||
|
);
|
||||||
|
if (verse.isNotEmpty) {
|
||||||
|
await _saveBookmark(
|
||||||
|
endSurahId,
|
||||||
|
endVerseId,
|
||||||
|
verse,
|
||||||
|
isLastRead: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('$calculatedAyat Ayat dicatat!'),
|
||||||
|
backgroundColor: AppColors.primary,
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ref.invalidate(tilawahTrackingProvider);
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
|
||||||
|
final isLastSurah = endSurahId >= 114;
|
||||||
|
if (settings.tilawahAutoContinueNextSurah &&
|
||||||
|
isLastAyat &&
|
||||||
|
!isLastSurah) {
|
||||||
|
_navigateToSurah(endSurahId + 1, startVerse: 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text('Simpan'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
ref.invalidate(tilawahTrackingProvider);
|
|
||||||
Navigator.pop(ctx);
|
|
||||||
},
|
|
||||||
child: const Text('Batal', style: TextStyle(color: Colors.red)),
|
|
||||||
),
|
|
||||||
FilledButton(
|
|
||||||
onPressed: () {
|
|
||||||
final todayKey = DateFormat('yyyy-MM-dd').format(DateTime.now());
|
|
||||||
final logBox = Hive.box<DailyWorshipLog>(HiveBoxes.worshipLogs);
|
|
||||||
var log = logBox.get(todayKey);
|
|
||||||
|
|
||||||
if (log == null) {
|
|
||||||
log = DailyWorshipLog(
|
|
||||||
date: todayKey,
|
|
||||||
shalatLogs: <String, ShalatLog>{
|
|
||||||
'subuh': ShalatLog(),
|
|
||||||
'dzuhur': ShalatLog(),
|
|
||||||
'ashar': ShalatLog(),
|
|
||||||
'maghrib': ShalatLog(),
|
|
||||||
'isya': ShalatLog(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
logBox.put(todayKey, log);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (log.tilawahLog == null) {
|
|
||||||
final settingsBox = Hive.box<AppSettings>(HiveBoxes.settings);
|
|
||||||
final settings = settingsBox.get('default') ?? AppSettings();
|
|
||||||
log.tilawahLog = TilawahLog(
|
|
||||||
targetValue: settings.tilawahTargetValue,
|
|
||||||
targetUnit: settings.tilawahTargetUnit,
|
|
||||||
autoSync: _autoSyncEnabled,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.tilawahLog!.rawAyatRead += calculatedAyat;
|
|
||||||
log.save();
|
|
||||||
|
|
||||||
if (mounted) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text('$calculatedAyat Ayat dicatat!'),
|
|
||||||
backgroundColor: AppColors.primary,
|
|
||||||
duration: const Duration(seconds: 2),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ref.invalidate(tilawahTrackingProvider);
|
|
||||||
Navigator.pop(ctx);
|
|
||||||
},
|
|
||||||
child: const Text('Simpan'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1156,6 +1204,42 @@ class _QuranReadingScreenState extends ConsumerState<QuranReadingScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildTrackingSessionBanner({
|
||||||
|
required bool isDark,
|
||||||
|
required TilawahSession session,
|
||||||
|
}) {
|
||||||
|
final currentSurah = _surah?['namaLatin']?.toString() ?? widget.surahId;
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.primary.withValues(alpha: isDark ? 0.16 : 0.1),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
color: AppColors.primary.withValues(alpha: 0.25),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(LucideIcons.flag, size: 16, color: AppColors.primary),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Sesi aktif: ${session.startSurahName}:${session.startVerseId} -> $currentSurah',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: isDark
|
||||||
|
? AppColors.textPrimaryDark
|
||||||
|
: AppColors.textPrimaryLight,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final trackingSession = ref.watch(tilawahTrackingProvider);
|
final trackingSession = ref.watch(tilawahTrackingProvider);
|
||||||
@@ -1271,6 +1355,11 @@ class _QuranReadingScreenState extends ConsumerState<QuranReadingScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
|
if (trackingSession != null)
|
||||||
|
_buildTrackingSessionBanner(
|
||||||
|
isDark: isDark,
|
||||||
|
session: trackingSession,
|
||||||
|
),
|
||||||
if (showBismillah) ...[
|
if (showBismillah) ...[
|
||||||
_buildBismillahSection(isDark: isDark),
|
_buildBismillahSection(isDark: isDark),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@@ -1292,6 +1381,25 @@ class _QuranReadingScreenState extends ConsumerState<QuranReadingScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
if ((_surah?['nomor'] as int? ?? 1) < 114)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(
|
||||||
|
16, 18, 16, 0),
|
||||||
|
child: FilledButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
final nextSurah =
|
||||||
|
(_surah?['nomor'] as int? ?? 1) +
|
||||||
|
1;
|
||||||
|
_navigateToSurah(nextSurah,
|
||||||
|
startVerse: 1);
|
||||||
|
},
|
||||||
|
icon:
|
||||||
|
const Icon(LucideIcons.arrowRight),
|
||||||
|
label: Text(trackingSession == null
|
||||||
|
? 'Lanjut ke Surah Berikutnya'
|
||||||
|
: 'Lanjut Surah (Sesi Tetap Aktif)'),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -101,6 +101,11 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|||||||
_saveSettings();
|
_saveSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _resyncPrayerNotifications() {
|
||||||
|
ref.invalidate(prayerTimesProvider);
|
||||||
|
unawaited(ref.read(prayerTimesProvider.future));
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _showQuietHoursDialog(BuildContext context) async {
|
Future<void> _showQuietHoursDialog(BuildContext context) async {
|
||||||
final startController =
|
final startController =
|
||||||
TextEditingController(text: _settings.quietHoursStart);
|
TextEditingController(text: _settings.quietHoursStart);
|
||||||
@@ -236,6 +241,97 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _showShalatReportDelayDialog(BuildContext context) async {
|
||||||
|
final controller = TextEditingController(
|
||||||
|
text: _settings.shalatReportReminderDelayMinutes.toString(),
|
||||||
|
);
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text('Jeda Pengingat Lapor Shalat'),
|
||||||
|
content: TextField(
|
||||||
|
controller: controller,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Menit setelah waktu shalat',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx),
|
||||||
|
child: const Text('Batal'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
final value = int.tryParse(controller.text.trim());
|
||||||
|
if (value == null || value < 5 || value > 240) return;
|
||||||
|
_settings.shalatReportReminderDelayMinutes = value;
|
||||||
|
_saveSettings();
|
||||||
|
_resyncPrayerNotifications();
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
},
|
||||||
|
child: const Text('Simpan'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showShalatReportRepeatDialog(BuildContext context) async {
|
||||||
|
final repeatController = TextEditingController(
|
||||||
|
text: _settings.shalatReportReminderRepeatCount.toString(),
|
||||||
|
);
|
||||||
|
final intervalController = TextEditingController(
|
||||||
|
text: _settings.shalatReportReminderRepeatIntervalMinutes.toString(),
|
||||||
|
);
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text('Pengulangan Pengingat Lapor'),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: repeatController,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Jumlah ulang (0-5)',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
TextField(
|
||||||
|
controller: intervalController,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Jeda antar ulang (menit)',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx),
|
||||||
|
child: const Text('Batal'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
final repeats = int.tryParse(repeatController.text.trim());
|
||||||
|
final interval = int.tryParse(intervalController.text.trim());
|
||||||
|
if (repeats == null || repeats < 0 || repeats > 5) return;
|
||||||
|
if (interval == null || interval < 5 || interval > 180) return;
|
||||||
|
_settings.shalatReportReminderRepeatCount = repeats;
|
||||||
|
_settings.shalatReportReminderRepeatIntervalMinutes = interval;
|
||||||
|
_saveSettings();
|
||||||
|
_resyncPrayerNotifications();
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
},
|
||||||
|
child: const Text('Simpan'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
@@ -446,6 +542,46 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
|
_settingRow(
|
||||||
|
isDark,
|
||||||
|
icon: LucideIcons.siren,
|
||||||
|
iconColor: const Color(0xFFC0392B),
|
||||||
|
title: 'Pengingat Lapor Shalat',
|
||||||
|
subtitle: _settings.shalatReportReminderEnabled
|
||||||
|
? 'Aktif • ${_settings.shalatReportReminderDelayMinutes} menit setelah adzan'
|
||||||
|
: 'Nonaktif',
|
||||||
|
trailing: IosToggle(
|
||||||
|
value: _settings.shalatReportReminderEnabled,
|
||||||
|
onChanged: (v) {
|
||||||
|
_settings.shalatReportReminderEnabled = v;
|
||||||
|
_saveSettings();
|
||||||
|
_resyncPrayerNotifications();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
_settingRow(
|
||||||
|
isDark,
|
||||||
|
icon: LucideIcons.clock3,
|
||||||
|
iconColor: const Color(0xFF16A085),
|
||||||
|
title: 'Jeda Pengingat Lapor',
|
||||||
|
subtitle:
|
||||||
|
'${_settings.shalatReportReminderDelayMinutes} menit setelah waktu shalat',
|
||||||
|
trailing: const Icon(LucideIcons.chevronRight, size: 20),
|
||||||
|
onTap: () => _showShalatReportDelayDialog(context),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
_settingRow(
|
||||||
|
isDark,
|
||||||
|
icon: LucideIcons.repeat,
|
||||||
|
iconColor: const Color(0xFF8E44AD),
|
||||||
|
title: 'Ulangi Pengingat Lapor',
|
||||||
|
subtitle:
|
||||||
|
'${_settings.shalatReportReminderRepeatCount}x • tiap ${_settings.shalatReportReminderRepeatIntervalMinutes} menit',
|
||||||
|
trailing: const Icon(LucideIcons.chevronRight, size: 20),
|
||||||
|
onTap: () => _showShalatReportRepeatDialog(context),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
_settingRow(
|
_settingRow(
|
||||||
isDark,
|
isDark,
|
||||||
icon: LucideIcons.moonStar,
|
icon: LucideIcons.moonStar,
|
||||||
@@ -521,6 +657,21 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
|
_settingRow(
|
||||||
|
isDark,
|
||||||
|
icon: LucideIcons.arrowRightCircle,
|
||||||
|
iconColor: Colors.green,
|
||||||
|
title: 'Lanjut Surah Otomatis',
|
||||||
|
subtitle: 'Saat simpan sesi di ayat terakhir',
|
||||||
|
trailing: IosToggle(
|
||||||
|
value: _settings.tilawahAutoContinueNextSurah,
|
||||||
|
onChanged: (v) {
|
||||||
|
_settings.tilawahAutoContinueNextSurah = v;
|
||||||
|
_saveSettings();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
_settingRow(
|
_settingRow(
|
||||||
isDark,
|
isDark,
|
||||||
icon: LucideIcons.listChecks,
|
icon: LucideIcons.listChecks,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:io' show Platform;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
@@ -13,6 +14,7 @@ import 'data/local/models/app_settings.dart';
|
|||||||
import 'data/services/notification_inbox_service.dart';
|
import 'data/services/notification_inbox_service.dart';
|
||||||
import 'data/services/notification_orchestrator_service.dart';
|
import 'data/services/notification_orchestrator_service.dart';
|
||||||
import 'data/services/remote_push_service.dart';
|
import 'data/services/remote_push_service.dart';
|
||||||
|
import 'data/services/background_sync_service.dart';
|
||||||
import 'data/services/notification_service.dart';
|
import 'data/services/notification_service.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
@@ -32,6 +34,10 @@ void main() async {
|
|||||||
|
|
||||||
// Initialize local notifications for adzan/iqamah scheduling
|
// Initialize local notifications for adzan/iqamah scheduling
|
||||||
await NotificationService.instance.init();
|
await NotificationService.instance.init();
|
||||||
|
if (Platform.isAndroid || Platform.isIOS) {
|
||||||
|
await BackgroundSyncService.instance.init();
|
||||||
|
await BackgroundSyncService.instance.registerPeriodicSync();
|
||||||
|
}
|
||||||
await RemotePushService.instance.init();
|
await RemotePushService.instance.init();
|
||||||
|
|
||||||
// Run passive notification checks at startup (inbox cleanup/content sync).
|
// Run passive notification checks at startup (inbox cleanup/content sync).
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import Foundation
|
|||||||
|
|
||||||
import audio_service
|
import audio_service
|
||||||
import audio_session
|
import audio_session
|
||||||
|
import firebase_core
|
||||||
|
import firebase_messaging
|
||||||
import flutter_local_notifications
|
import flutter_local_notifications
|
||||||
import geolocator_apple
|
import geolocator_apple
|
||||||
import just_audio
|
import just_audio
|
||||||
@@ -18,6 +20,8 @@ import url_launcher_macos
|
|||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin"))
|
AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin"))
|
||||||
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
|
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
|
||||||
|
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
||||||
|
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
|
||||||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||||
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
|
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
|
||||||
JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin"))
|
JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin"))
|
||||||
|
|||||||
@@ -4,17 +4,85 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- audio_session (0.0.1):
|
- audio_session (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
- Firebase/CoreOnly (12.12.1):
|
||||||
|
- FirebaseCore (~> 12.12.1)
|
||||||
|
- Firebase/Messaging (12.12.1):
|
||||||
|
- Firebase/CoreOnly
|
||||||
|
- FirebaseMessaging (~> 12.12.0)
|
||||||
|
- firebase_core (4.7.0):
|
||||||
|
- Firebase/CoreOnly (~> 12.12.0)
|
||||||
|
- FlutterMacOS
|
||||||
|
- firebase_messaging (16.2.0):
|
||||||
|
- Firebase/CoreOnly (~> 12.12.0)
|
||||||
|
- Firebase/Messaging (~> 12.12.0)
|
||||||
|
- firebase_core
|
||||||
|
- FlutterMacOS
|
||||||
|
- FirebaseCore (12.12.1):
|
||||||
|
- FirebaseCoreInternal (~> 12.12.0)
|
||||||
|
- GoogleUtilities/Environment (~> 8.1)
|
||||||
|
- GoogleUtilities/Logger (~> 8.1)
|
||||||
|
- FirebaseCoreInternal (12.12.0):
|
||||||
|
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||||
|
- FirebaseInstallations (12.12.0):
|
||||||
|
- FirebaseCore (~> 12.12.0)
|
||||||
|
- GoogleUtilities/Environment (~> 8.1)
|
||||||
|
- GoogleUtilities/UserDefaults (~> 8.1)
|
||||||
|
- PromisesObjC (~> 2.4)
|
||||||
|
- FirebaseMessaging (12.12.0):
|
||||||
|
- FirebaseCore (~> 12.12.0)
|
||||||
|
- FirebaseInstallations (~> 12.12.0)
|
||||||
|
- GoogleDataTransport (~> 10.1)
|
||||||
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||||
|
- GoogleUtilities/Environment (~> 8.1)
|
||||||
|
- GoogleUtilities/Reachability (~> 8.1)
|
||||||
|
- GoogleUtilities/UserDefaults (~> 8.1)
|
||||||
|
- nanopb (~> 3.30910.0)
|
||||||
- flutter_local_notifications (0.0.1):
|
- flutter_local_notifications (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- FlutterMacOS (1.0.0)
|
- FlutterMacOS (1.0.0)
|
||||||
- geolocator_apple (1.2.0):
|
- geolocator_apple (1.2.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
- GoogleDataTransport (10.1.0):
|
||||||
|
- nanopb (~> 3.30910.0)
|
||||||
|
- PromisesObjC (~> 2.4)
|
||||||
|
- GoogleUtilities/AppDelegateSwizzler (8.1.0):
|
||||||
|
- GoogleUtilities/Environment
|
||||||
|
- GoogleUtilities/Logger
|
||||||
|
- GoogleUtilities/Network
|
||||||
|
- GoogleUtilities/Privacy
|
||||||
|
- GoogleUtilities/Environment (8.1.0):
|
||||||
|
- GoogleUtilities/Privacy
|
||||||
|
- GoogleUtilities/Logger (8.1.0):
|
||||||
|
- GoogleUtilities/Environment
|
||||||
|
- GoogleUtilities/Privacy
|
||||||
|
- GoogleUtilities/Network (8.1.0):
|
||||||
|
- GoogleUtilities/Logger
|
||||||
|
- "GoogleUtilities/NSData+zlib"
|
||||||
|
- GoogleUtilities/Privacy
|
||||||
|
- GoogleUtilities/Reachability
|
||||||
|
- "GoogleUtilities/NSData+zlib (8.1.0)":
|
||||||
|
- GoogleUtilities/Privacy
|
||||||
|
- GoogleUtilities/Privacy (8.1.0)
|
||||||
|
- GoogleUtilities/Reachability (8.1.0):
|
||||||
|
- GoogleUtilities/Logger
|
||||||
|
- GoogleUtilities/Privacy
|
||||||
|
- GoogleUtilities/UserDefaults (8.1.0):
|
||||||
|
- GoogleUtilities/Logger
|
||||||
|
- GoogleUtilities/Privacy
|
||||||
- just_audio (0.0.1):
|
- just_audio (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
- nanopb (3.30910.0):
|
||||||
|
- nanopb/decode (= 3.30910.0)
|
||||||
|
- nanopb/encode (= 3.30910.0)
|
||||||
|
- nanopb/decode (3.30910.0)
|
||||||
|
- nanopb/encode (3.30910.0)
|
||||||
- package_info_plus (0.0.1):
|
- package_info_plus (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
- PromisesObjC (2.4.0)
|
||||||
|
- share_plus (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
- sqflite_darwin (0.0.4):
|
- sqflite_darwin (0.0.4):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
@@ -24,19 +92,38 @@ PODS:
|
|||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- audio_service (from `Flutter/ephemeral/.symlinks/plugins/audio_service/darwin`)
|
- audio_service (from `Flutter/ephemeral/.symlinks/plugins/audio_service/darwin`)
|
||||||
- audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`)
|
- audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`)
|
||||||
|
- firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`)
|
||||||
|
- firebase_messaging (from `Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos`)
|
||||||
- flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`)
|
- flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`)
|
||||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||||
- geolocator_apple (from `Flutter/ephemeral/.symlinks/plugins/geolocator_apple/darwin`)
|
- geolocator_apple (from `Flutter/ephemeral/.symlinks/plugins/geolocator_apple/darwin`)
|
||||||
- just_audio (from `Flutter/ephemeral/.symlinks/plugins/just_audio/darwin`)
|
- just_audio (from `Flutter/ephemeral/.symlinks/plugins/just_audio/darwin`)
|
||||||
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
|
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
|
||||||
|
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
|
||||||
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
|
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
|
||||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||||
|
|
||||||
|
SPEC REPOS:
|
||||||
|
trunk:
|
||||||
|
- Firebase
|
||||||
|
- FirebaseCore
|
||||||
|
- FirebaseCoreInternal
|
||||||
|
- FirebaseInstallations
|
||||||
|
- FirebaseMessaging
|
||||||
|
- GoogleDataTransport
|
||||||
|
- GoogleUtilities
|
||||||
|
- nanopb
|
||||||
|
- PromisesObjC
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
audio_service:
|
audio_service:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/audio_service/darwin
|
:path: Flutter/ephemeral/.symlinks/plugins/audio_service/darwin
|
||||||
audio_session:
|
audio_session:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos
|
||||||
|
firebase_core:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos
|
||||||
|
firebase_messaging:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos
|
||||||
flutter_local_notifications:
|
flutter_local_notifications:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos
|
||||||
FlutterMacOS:
|
FlutterMacOS:
|
||||||
@@ -47,6 +134,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: Flutter/ephemeral/.symlinks/plugins/just_audio/darwin
|
:path: Flutter/ephemeral/.symlinks/plugins/just_audio/darwin
|
||||||
package_info_plus:
|
package_info_plus:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
|
||||||
|
share_plus:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
|
||||||
sqflite_darwin:
|
sqflite_darwin:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
|
:path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
|
||||||
url_launcher_macos:
|
url_launcher_macos:
|
||||||
@@ -55,11 +144,23 @@ EXTERNAL SOURCES:
|
|||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
audio_service: aa99a6ba2ae7565996015322b0bb024e1d25c6fd
|
audio_service: aa99a6ba2ae7565996015322b0bb024e1d25c6fd
|
||||||
audio_session: eaca2512cf2b39212d724f35d11f46180ad3a33e
|
audio_session: eaca2512cf2b39212d724f35d11f46180ad3a33e
|
||||||
|
Firebase: 14f11e91129d246a8a6166b4c1c2ea61b56806ec
|
||||||
|
firebase_core: 8022171e82601bac2c79cfa04d69977f12595682
|
||||||
|
firebase_messaging: 5044cedfca0133cd38db45fc16c0d312bab00f1b
|
||||||
|
FirebaseCore: 86241206e656f5c80c995e370e6c975913b9b284
|
||||||
|
FirebaseCoreInternal: 7c12fc3011d889085e765e317d7b9fd1cef97af9
|
||||||
|
FirebaseInstallations: 4e6e162aa4abaaeeeb01dd00179dfc5ad9c2194e
|
||||||
|
FirebaseMessaging: 341004946fa7ffc741344b20f1b667514fc93e31
|
||||||
flutter_local_notifications: 1fc7ffb10a83d6a2eeeeddb152d43f1944b0aad0
|
flutter_local_notifications: 1fc7ffb10a83d6a2eeeeddb152d43f1944b0aad0
|
||||||
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
||||||
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
|
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
|
||||||
|
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||||
|
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||||
just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed
|
just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed
|
||||||
|
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||||
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
|
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
|
||||||
|
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||||
|
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
|
||||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||||
url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd
|
url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd
|
||||||
|
|
||||||
|
|||||||
@@ -241,6 +241,7 @@
|
|||||||
33CC110E2044A8840003C045 /* Bundle Framework */,
|
33CC110E2044A8840003C045 /* Bundle Framework */,
|
||||||
3399D490228B24CF009A79C7 /* ShellScript */,
|
3399D490228B24CF009A79C7 /* ShellScript */,
|
||||||
080782848EF12E763C1C7A22 /* [CP] Embed Pods Frameworks */,
|
080782848EF12E763C1C7A22 /* [CP] Embed Pods Frameworks */,
|
||||||
|
235A577E0BFC8CBB113CF429 /* [CP] Copy Pods Resources */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@@ -340,6 +341,23 @@
|
|||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||||
showEnvVarsInLog = 0;
|
showEnvVarsInLog = 0;
|
||||||
};
|
};
|
||||||
|
235A577E0BFC8CBB113CF429 /* [CP] Copy Pods Resources */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
|
);
|
||||||
|
name = "[CP] Copy Pods Resources";
|
||||||
|
outputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
3399D490228B24CF009A79C7 /* ShellScript */ = {
|
3399D490228B24CF009A79C7 /* ShellScript */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
alwaysOutOfDate = 1;
|
alwaysOutOfDate = 1;
|
||||||
|
|||||||
88
pubspec.lock
88
pubspec.lock
@@ -9,6 +9,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "67.0.0"
|
version: "67.0.0"
|
||||||
|
_flutterfire_internals:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: _flutterfire_internals
|
||||||
|
sha256: bda3b7b55958bfd867addc40d067b4b11f7b8846d57671f5b5a6e7f9a56fe3ad
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.69"
|
||||||
adhan:
|
adhan:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -321,6 +329,54 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.1"
|
version: "7.0.1"
|
||||||
|
firebase_core:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: firebase_core
|
||||||
|
sha256: d5a94b884dcb1e6d3430298e94bfe002238094cdfd5e29202d536ee2120f9158
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.7.0"
|
||||||
|
firebase_core_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: firebase_core_platform_interface
|
||||||
|
sha256: "0ecda14c1bfc9ed8cac303dd0f8d04a320811b479362a9a4efb14fd331a473ce"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.0.3"
|
||||||
|
firebase_core_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: firebase_core_web
|
||||||
|
sha256: dc5096257cd67292d34d78ceeb90836f02a4be921b5f3934311a02bb2376118c
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.6.0"
|
||||||
|
firebase_messaging:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: firebase_messaging
|
||||||
|
sha256: e5c93e8e7a9b0513f94bb684d2cf100e32e7dcdf2949574386b1955fc9a9b96a
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "16.2.0"
|
||||||
|
firebase_messaging_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: firebase_messaging_platform_interface
|
||||||
|
sha256: "8cbb7d842e5071bba836452aff262f7db4b14bb3a0d00c1896cf176df886d65a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.7.9"
|
||||||
|
firebase_messaging_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: firebase_messaging_web
|
||||||
|
sha256: "8750bacf50573c0383535fc3f9c58c6a2f9dff5320a16a82c30631b9dad894f1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.5"
|
||||||
fixnum:
|
fixnum:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1346,6 +1402,38 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.15.0"
|
version: "5.15.0"
|
||||||
|
workmanager:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: workmanager
|
||||||
|
sha256: "065673b2a465865183093806925419d311a9a5e0995aa74ccf8920fd695e2d10"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.0+3"
|
||||||
|
workmanager_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: workmanager_android
|
||||||
|
sha256: "9ae744db4ef891f5fcd2fb8671fccc712f4f96489a487a1411e0c8675e5e8cb7"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.0+2"
|
||||||
|
workmanager_apple:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: workmanager_apple
|
||||||
|
sha256: "1cc12ae3cbf5535e72f7ba4fde0c12dd11b757caf493a28e22d684052701f2ca"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.1+2"
|
||||||
|
workmanager_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: workmanager_platform_interface
|
||||||
|
sha256: f40422f10b970c67abb84230b44da22b075147637532ac501729256fcea10a47
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.1+1"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ dependencies:
|
|||||||
flutter_qiblah: ^3.0.0
|
flutter_qiblah: ^3.0.0
|
||||||
# Notifications
|
# Notifications
|
||||||
flutter_local_notifications: ^21.0.0
|
flutter_local_notifications: ^21.0.0
|
||||||
|
workmanager: ^0.9.0+3
|
||||||
|
firebase_core: ^4.1.1
|
||||||
|
firebase_messaging: ^16.0.1
|
||||||
|
|
||||||
# Audio
|
# Audio
|
||||||
just_audio: ^0.10.5
|
just_audio: ^0.10.5
|
||||||
|
|||||||
Reference in New Issue
Block a user