Files
jamshalat-masjid-screen/lib/data/services/sync_service.dart
2026-03-31 14:37:14 +07:00

291 lines
8.5 KiB
Dart

import 'package:hive_flutter/hive_flutter.dart';
import 'package:intl/intl.dart';
import '../local/models.dart';
import 'myquran_service.dart';
class ScheduleCacheStatus {
final DateTime? startDate;
final DateTime? endDate;
final int cachedDays;
final int daysUntilRefresh;
const ScheduleCacheStatus({
required this.startDate,
required this.endDate,
required this.cachedDays,
required this.daysUntilRefresh,
});
const ScheduleCacheStatus.empty()
: startDate = null,
endDate = null,
cachedDays = 0,
daysUntilRefresh = -1;
bool get hasData => startDate != null && endDate != null && cachedDays > 0;
bool get isExpired => hasData && daysUntilRefresh < 0;
bool get needsRefreshSoon => hasData && daysUntilRefresh <= 3;
static ScheduleCacheStatus fromSchedules(
Iterable<DailyPrayerSchedule> schedules,
DateTime referenceDate,
) {
DateTime? startDate;
DateTime? endDate;
var cachedDays = 0;
for (final schedule in schedules) {
final parsedDate = DateTime.tryParse(schedule.date);
if (parsedDate == null) continue;
final normalized = DateTime(
parsedDate.year,
parsedDate.month,
parsedDate.day,
);
cachedDays++;
startDate = startDate == null || normalized.isBefore(startDate)
? normalized
: startDate;
endDate = endDate == null || normalized.isAfter(endDate)
? normalized
: endDate;
}
if (startDate == null || endDate == null || cachedDays == 0) {
return const ScheduleCacheStatus.empty();
}
final today = DateTime(
referenceDate.year,
referenceDate.month,
referenceDate.day,
);
return ScheduleCacheStatus(
startDate: startDate,
endDate: endDate,
cachedDays: cachedDays,
daysUntilRefresh: endDate.difference(today).inDays,
);
}
}
/// Service to sync monthly prayer data from MyQuran API → Hive.
class SyncService {
SyncService._();
static final SyncService instance = SyncService._();
static const Duration _autoRefreshCooldown = Duration(hours: 12);
static Set<String> rollingWindowMonths(DateTime referenceDate) {
final currentMonth = DateTime(referenceDate.year, referenceDate.month, 1);
final nextMonth = DateTime(referenceDate.year, referenceDate.month + 1, 1);
return {
DateFormat('yyyy-MM').format(currentMonth),
DateFormat('yyyy-MM').format(nextMonth),
};
}
static List<String> staleScheduleKeys(
Iterable<String> keys,
DateTime referenceDate,
) {
final allowedMonths = rollingWindowMonths(referenceDate);
final staleKeys = <String>[];
for (final key in keys) {
final parsed = DateTime.tryParse(key);
if (parsed == null) {
staleKeys.add(key);
continue;
}
final monthKey = DateFormat('yyyy-MM').format(parsed);
if (!allowedMonths.contains(monthKey)) {
staleKeys.add(key);
}
}
return staleKeys;
}
Future<void> _pruneScheduleCache(
Box<DailyPrayerSchedule> scheduleBox,
DateTime referenceDate,
) async {
final staleKeys = staleScheduleKeys(
scheduleBox.keys.cast<String>(),
referenceDate,
);
if (staleKeys.isNotEmpty) {
await scheduleBox.deleteAll(staleKeys);
await scheduleBox.compact();
}
}
bool _shouldAttemptAutoRefresh({
required ScheduleCacheStatus status,
required bool hasTodayData,
}) {
return !hasTodayData || !status.hasData || status.needsRefreshSoon;
}
DateTime? _parseAttemptTimestamp(String? value) {
if (value == null || value.isEmpty) return null;
return DateTime.tryParse(value);
}
/// Sync current month + next month prayer data for the configured city.
/// Returns true on success.
Future<bool> syncMonthlyData({DateTime? referenceDate}) async {
final settingsBox = Hive.box<AppSettings>(HiveBoxes.settings);
final settings = settingsBox.get('default');
if (settings == null) return false;
final cityId = settings.cityIdApi;
final now = referenceDate ?? DateTime.now();
final currentMonth = DateFormat('yyyy-MM').format(now);
// Also fetch next month for continuity
final nextMonthDate = DateTime(now.year, now.month + 1, 1);
final nextMonth = DateFormat('yyyy-MM').format(nextMonthDate);
final api = MyQuranSholatService.instance;
final scheduleBox = Hive.box<DailyPrayerSchedule>(HiveBoxes.prayerSchedule);
var success = false;
var hasCurrentMonth = false;
var hasNextMonth = false;
// Fetch current month
final currentData = await api.getMonthlySchedule(cityId, currentMonth);
if (currentData.isNotEmpty) {
hasCurrentMonth = true;
for (final entry in currentData.entries) {
final jadwal = entry.value;
scheduleBox.put(
entry.key,
DailyPrayerSchedule(
date: entry.key,
imsak: jadwal['imsak'] ?? '00:00',
subuh: jadwal['subuh'] ?? '00:00',
terbit: jadwal['terbit'] ?? '00:00',
dhuha: jadwal['dhuha'] ?? '00:00',
dzuhur: jadwal['dzuhur'] ?? '00:00',
ashar: jadwal['ashar'] ?? '00:00',
maghrib: jadwal['maghrib'] ?? '00:00',
isya: jadwal['isya'] ?? '00:00',
),
);
}
success = true;
}
// Fetch next month
final nextData = await api.getMonthlySchedule(cityId, nextMonth);
if (nextData.isNotEmpty) {
hasNextMonth = true;
for (final entry in nextData.entries) {
final jadwal = entry.value;
scheduleBox.put(
entry.key,
DailyPrayerSchedule(
date: entry.key,
imsak: jadwal['imsak'] ?? '00:00',
subuh: jadwal['subuh'] ?? '00:00',
terbit: jadwal['terbit'] ?? '00:00',
dhuha: jadwal['dhuha'] ?? '00:00',
dzuhur: jadwal['dzuhur'] ?? '00:00',
ashar: jadwal['ashar'] ?? '00:00',
maghrib: jadwal['maghrib'] ?? '00:00',
isya: jadwal['isya'] ?? '00:00',
),
);
}
}
if (success) {
if (hasCurrentMonth && hasNextMonth) {
await _pruneScheduleCache(scheduleBox, now);
}
settings.lastSyncDate = DateFormat('yyyy-MM-dd HH:mm').format(now);
await settings.save();
}
return success;
}
Future<AutoRefreshResult> autoRefreshIfNeeded({
DateTime? referenceDate,
}) async {
final settingsBox = Hive.box<AppSettings>(HiveBoxes.settings);
final settings = settingsBox.get('default');
if (settings == null) {
return const AutoRefreshResult.skipped('settings-missing');
}
final now = referenceDate ?? DateTime.now();
final status = getCacheStatus(now);
final hasTodayData = getTodaySchedule(now) != null;
if (!_shouldAttemptAutoRefresh(status: status, hasTodayData: hasTodayData)) {
return const AutoRefreshResult.skipped('cache-fresh');
}
final lastAttempt = _parseAttemptTimestamp(settings.lastAutoSyncAttemptDate);
if (lastAttempt != null &&
now.difference(lastAttempt) < _autoRefreshCooldown) {
return const AutoRefreshResult.skipped('cooldown');
}
settings.lastAutoSyncAttemptDate = now.toIso8601String();
await settings.save();
final synced = await syncMonthlyData(referenceDate: now);
return synced
? const AutoRefreshResult.synced()
: const AutoRefreshResult.failed('sync-failed');
}
/// Get today's prayer schedule from local Hive cache.
DailyPrayerSchedule? getTodaySchedule([DateTime? targetDate]) {
final scheduleBox =
Hive.box<DailyPrayerSchedule>(HiveBoxes.prayerSchedule);
final dateToFetch = targetDate ?? DateTime.now();
final dateStr = DateFormat('yyyy-MM-dd').format(dateToFetch);
return scheduleBox.get(dateStr);
}
ScheduleCacheStatus getCacheStatus([DateTime? referenceDate]) {
final scheduleBox =
Hive.box<DailyPrayerSchedule>(HiveBoxes.prayerSchedule);
return ScheduleCacheStatus.fromSchedules(
scheduleBox.values,
referenceDate ?? DateTime.now(),
);
}
}
class AutoRefreshResult {
final bool attempted;
final bool synced;
final String reason;
const AutoRefreshResult._({
required this.attempted,
required this.synced,
required this.reason,
});
const AutoRefreshResult.skipped(String reason)
: this._(attempted: false, synced: false, reason: reason);
const AutoRefreshResult.synced()
: this._(attempted: true, synced: true, reason: 'synced');
const AutoRefreshResult.failed(String reason)
: this._(attempted: true, synced: false, reason: reason);
}