287 lines
9.3 KiB
Dart
287 lines
9.3 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:hive_flutter/hive_flutter.dart';
|
|
import 'package:intl/intl.dart';
|
|
import '../../../data/services/notification_orchestrator_service.dart';
|
|
import '../../../data/services/notification_event_producer_service.dart';
|
|
import '../../../data/services/myquran_sholat_service.dart';
|
|
import '../../../data/services/notification_service.dart';
|
|
import '../../../data/services/prayer_service.dart';
|
|
import '../../../data/services/location_service.dart';
|
|
import '../../../data/local/hive_boxes.dart';
|
|
import '../../../data/local/models/app_settings.dart';
|
|
|
|
/// Represents a single prayer time entry.
|
|
class PrayerTimeEntry {
|
|
final String name;
|
|
final String time; // "HH:mm"
|
|
final bool isActive;
|
|
PrayerTimeEntry({
|
|
required this.name,
|
|
required this.time,
|
|
this.isActive = false,
|
|
});
|
|
}
|
|
|
|
/// Full day prayer schedule from myQuran API.
|
|
class DaySchedule {
|
|
final String cityName;
|
|
final String province;
|
|
final String date; // yyyy-MM-dd
|
|
final String tanggal; // formatted date from API
|
|
final Map<String, String>
|
|
times; // {imsak, subuh, terbit, dhuha, dzuhur, ashar, maghrib, isya}
|
|
|
|
DaySchedule({
|
|
required this.cityName,
|
|
required this.province,
|
|
required this.date,
|
|
required this.tanggal,
|
|
required this.times,
|
|
});
|
|
|
|
/// Is this schedule for tomorrow?
|
|
bool get isTomorrow {
|
|
final todayStr = DateFormat('yyyy-MM-dd').format(DateTime.now());
|
|
return date.compareTo(todayStr) > 0;
|
|
}
|
|
|
|
/// Get prayer time entries as a list.
|
|
List<PrayerTimeEntry> get prayerList {
|
|
final now = DateTime.now();
|
|
final formatter = DateFormat('HH:mm');
|
|
final currentTime = formatter.format(now);
|
|
|
|
final prayers = [
|
|
PrayerTimeEntry(name: 'Imsak', time: times['imsak'] ?? '-'),
|
|
PrayerTimeEntry(name: 'Subuh', time: times['subuh'] ?? '-'),
|
|
PrayerTimeEntry(name: 'Terbit', time: times['terbit'] ?? '-'),
|
|
PrayerTimeEntry(name: 'Dhuha', time: times['dhuha'] ?? '-'),
|
|
PrayerTimeEntry(name: 'Dzuhur', time: times['dzuhur'] ?? '-'),
|
|
PrayerTimeEntry(name: 'Ashar', time: times['ashar'] ?? '-'),
|
|
PrayerTimeEntry(name: 'Maghrib', time: times['maghrib'] ?? '-'),
|
|
PrayerTimeEntry(name: 'Isya', time: times['isya'] ?? '-'),
|
|
];
|
|
|
|
// Find the next prayer
|
|
int activeIndex = -1;
|
|
if (isTomorrow) {
|
|
// User specifically requested to show tomorrow's Subuh as upcoming
|
|
activeIndex = 1; // 0=Imsak, 1=Subuh
|
|
} else {
|
|
for (int i = 0; i < prayers.length; i++) {
|
|
if (prayers[i].time != '-' &&
|
|
prayers[i].time.compareTo(currentTime) > 0) {
|
|
activeIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (activeIndex >= 0) {
|
|
prayers[activeIndex] = PrayerTimeEntry(
|
|
name: prayers[activeIndex].name,
|
|
time: prayers[activeIndex].time,
|
|
isActive: true,
|
|
);
|
|
}
|
|
|
|
return prayers;
|
|
}
|
|
|
|
/// Get the next prayer name and time.
|
|
PrayerTimeEntry? get nextPrayer {
|
|
final list = prayerList;
|
|
for (final p in list) {
|
|
if (p.isActive) return p;
|
|
}
|
|
// If none active and it's today, all prayers have passed
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// Default Jakarta city ID from myQuran API.
|
|
const _defaultCityId = '58a2fc6ed39fd083f55d4182bf88826d';
|
|
|
|
/// Provider for the user's selected city ID (stored in Hive settings).
|
|
final selectedCityIdProvider = StateProvider<String>((ref) {
|
|
final box = Hive.box<AppSettings>(HiveBoxes.settings);
|
|
final settings = box.get('default');
|
|
final stored = settings?.lastCityName ?? '';
|
|
if (stored.contains('|')) {
|
|
return stored.split('|').last;
|
|
}
|
|
return _defaultCityId;
|
|
});
|
|
|
|
/// Provider for today's prayer times using myQuran API.
|
|
final prayerTimesProvider = FutureProvider<DaySchedule?>((ref) async {
|
|
final cityId = ref.watch(selectedCityIdProvider);
|
|
final today = DateFormat('yyyy-MM-dd').format(DateTime.now());
|
|
|
|
DaySchedule? schedule;
|
|
|
|
// Try API first
|
|
final jadwal =
|
|
await MyQuranSholatService.instance.getDailySchedule(cityId, today);
|
|
|
|
if (jadwal != null) {
|
|
final cityInfo = await MyQuranSholatService.instance.getCityInfo(cityId);
|
|
schedule = DaySchedule(
|
|
cityName: cityInfo?['kabko'] ?? 'Jakarta',
|
|
province: cityInfo?['prov'] ?? 'DKI Jakarta',
|
|
date: today,
|
|
tanggal: jadwal['tanggal'] ?? today,
|
|
times: jadwal,
|
|
);
|
|
}
|
|
|
|
// Check if all prayers today have passed
|
|
if (schedule != null && !schedule.isTomorrow && schedule.nextPrayer == null) {
|
|
// All prayers passed, fetch tomorrow's schedule
|
|
final tomorrow = DateTime.now().add(const Duration(days: 1));
|
|
final tomorrowStr = DateFormat('yyyy-MM-dd').format(tomorrow);
|
|
|
|
final tmrwJadwal = await MyQuranSholatService.instance
|
|
.getDailySchedule(cityId, tomorrowStr);
|
|
|
|
if (tmrwJadwal != null) {
|
|
final cityInfo = await MyQuranSholatService.instance.getCityInfo(cityId);
|
|
schedule = DaySchedule(
|
|
cityName: cityInfo?['kabko'] ?? 'Jakarta',
|
|
province: cityInfo?['prov'] ?? 'DKI Jakarta',
|
|
date: tomorrowStr,
|
|
tanggal: tmrwJadwal['tanggal'] ?? tomorrowStr,
|
|
times: tmrwJadwal,
|
|
);
|
|
}
|
|
}
|
|
|
|
if (schedule != null) {
|
|
unawaited(_syncAdhanNotifications(cityId, schedule));
|
|
return schedule;
|
|
}
|
|
|
|
// Fallback to adhan package
|
|
final position = await LocationService.instance.getCurrentLocation();
|
|
final lat = position?.latitude ?? -6.2088;
|
|
final lng = position?.longitude ?? 106.8456;
|
|
final locationUnavailable = position == null;
|
|
|
|
final result =
|
|
PrayerService.instance.getPrayerTimes(lat, lng, DateTime.now());
|
|
final timeFormat = DateFormat('HH:mm');
|
|
final fallbackSchedule = DaySchedule(
|
|
cityName: 'Jakarta',
|
|
province: 'DKI Jakarta',
|
|
date: today,
|
|
tanggal: DateFormat('EEEE, dd/MM/yyyy').format(DateTime.now()),
|
|
times: {
|
|
'imsak':
|
|
timeFormat.format(result.fajr.subtract(const Duration(minutes: 10))),
|
|
'subuh': timeFormat.format(result.fajr),
|
|
'terbit': timeFormat.format(result.sunrise),
|
|
'dhuha':
|
|
timeFormat.format(result.sunrise.add(const Duration(minutes: 15))),
|
|
'dzuhur': timeFormat.format(result.dhuhr),
|
|
'ashar': timeFormat.format(result.asr),
|
|
'maghrib': timeFormat.format(result.maghrib),
|
|
'isya': timeFormat.format(result.isha),
|
|
},
|
|
);
|
|
unawaited(
|
|
NotificationEventProducerService.instance.emitScheduleFallback(
|
|
settings: Hive.box<AppSettings>(HiveBoxes.settings).get('default') ??
|
|
AppSettings(),
|
|
cityId: cityId,
|
|
locationUnavailable: locationUnavailable,
|
|
),
|
|
);
|
|
unawaited(_syncAdhanNotifications(cityId, fallbackSchedule));
|
|
return fallbackSchedule;
|
|
});
|
|
|
|
Future<void> _syncAdhanNotifications(
|
|
String cityId, DaySchedule schedule) async {
|
|
try {
|
|
final settingsBox = Hive.box<AppSettings>(HiveBoxes.settings);
|
|
final settings = settingsBox.get('default') ?? AppSettings();
|
|
final adhanEnabled = settings.adhanEnabled.values.any((v) => v);
|
|
|
|
if (adhanEnabled) {
|
|
final permissionStatus =
|
|
await NotificationService.instance.getPermissionStatus();
|
|
await NotificationEventProducerService.instance
|
|
.emitPermissionWarningsIfNeeded(
|
|
settings: settings,
|
|
permissionStatus: permissionStatus,
|
|
);
|
|
}
|
|
|
|
final schedulesByDate = <String, Map<String, String>>{
|
|
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)));
|
|
if (!schedulesByDate.containsKey(nextDate)) {
|
|
final nextSchedule = await MyQuranSholatService.instance
|
|
.getDailySchedule(cityId, nextDate);
|
|
if (nextSchedule != null) {
|
|
schedulesByDate[nextDate] = nextSchedule;
|
|
}
|
|
}
|
|
}
|
|
|
|
await NotificationService.instance.syncPrayerNotifications(
|
|
cityId: cityId,
|
|
adhanEnabled: settings.adhanEnabled,
|
|
iqamahOffset: settings.iqamahOffset,
|
|
schedulesByDate: schedulesByDate,
|
|
);
|
|
await NotificationService.instance.syncHabitNotifications(
|
|
settings: settings,
|
|
);
|
|
await NotificationOrchestratorService.instance.runPassivePass(
|
|
settings: settings,
|
|
);
|
|
} catch (_) {
|
|
// Don't block UI when scheduling notifications fails.
|
|
unawaited(
|
|
NotificationEventProducerService.instance.emitNotificationSyncFailed(
|
|
settings: Hive.box<AppSettings>(HiveBoxes.settings).get('default') ??
|
|
AppSettings(),
|
|
cityId: cityId,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Provider for monthly prayer schedule (for Imsakiyah screen).
|
|
final monthlyScheduleProvider =
|
|
FutureProvider.family<Map<String, Map<String, String>>, String>(
|
|
(ref, month) async {
|
|
final cityId = ref.watch(selectedCityIdProvider);
|
|
return MyQuranSholatService.instance.getMonthlySchedule(cityId, month);
|
|
});
|
|
|
|
/// Provider for current city name.
|
|
final cityNameProvider = FutureProvider<String>((ref) async {
|
|
final box = Hive.box<AppSettings>(HiveBoxes.settings);
|
|
final settings = box.get('default');
|
|
final stored = settings?.lastCityName ?? '';
|
|
if (stored.contains('|')) {
|
|
return stored.split('|').first;
|
|
}
|
|
|
|
final cityId = ref.watch(selectedCityIdProvider);
|
|
final info = await MyQuranSholatService.instance.getCityInfo(cityId);
|
|
if (info != null) {
|
|
return '${info['kabko']}, ${info['prov']}';
|
|
}
|
|
return 'Kota Jakarta, DKI Jakarta';
|
|
});
|