Files
jamshalat-diary/lib/features/dashboard/data/prayer_times_provider.dart
2026-05-31 20:40:20 +07:00

349 lines
12 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 = await _resolveCityIdWithAutoDetect(ref);
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<String> _resolveCityIdWithAutoDetect(Ref ref) async {
final settingsBox = Hive.box<AppSettings>(HiveBoxes.settings);
final settings = settingsBox.get('default') ?? AppSettings();
final stored = settings.lastCityName ?? '';
if (stored.contains('|')) {
return stored.split('|').last;
}
final position = await LocationService.instance.getCurrentLocation();
final fallbackLocation =
position == null ? LocationService.instance.getLastKnownLocation() : null;
final lat = position?.latitude ?? fallbackLocation?.lat;
final lng = position?.longitude ?? fallbackLocation?.lng;
if (lat != null && lng != null) {
try {
final resolved = await LocationService.instance
.resolveMyQuranCityFromCoordinates(lat: lat, lng: lng);
if (resolved != null) {
settings.lastCityName = '${resolved.name}|${resolved.id}';
if (settings.isInBox) {
await settings.save();
} else {
await settingsBox.put('default', settings);
}
ref.read(selectedCityIdProvider.notifier).state = resolved.id;
return resolved.id;
}
} catch (_) {
// Non-fatal: fallback to default city id.
}
}
return _defaultCityId;
}
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>>{};
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),
};
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;
}
}
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
.getDailySchedule(cityId, nextDate);
if (nextSchedule != null) {
schedulesByDate[nextDate] = nextSchedule;
}
}
}
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,
);
} 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';
});