164 lines
5.3 KiB
Dart
164 lines
5.3 KiB
Dart
import 'dart:convert';
|
|
import 'dart:math';
|
|
|
|
import 'package:hive_flutter/hive_flutter.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:intl/intl.dart';
|
|
|
|
import '../../core/hijri_date.dart';
|
|
import '../local/models.dart';
|
|
|
|
class HijriCalendarService {
|
|
HijriCalendarService._({http.Client? client})
|
|
: _client = client ?? http.Client();
|
|
|
|
static final HijriCalendarService instance = HijriCalendarService._();
|
|
static const String _baseUrl = 'https://api.myquran.com/v3/cal/hijr';
|
|
static const Duration _requestTimeout = Duration(seconds: 5);
|
|
static final RegExp _flexDateKeyRegex = RegExp(r'^(\d{4})-(\d{1,2})-(\d{1,2})$');
|
|
|
|
final http.Client _client;
|
|
final Map<String, String> _cache = {};
|
|
final DateFormat _dateFormat = DateFormat('yyyy-MM-dd');
|
|
|
|
DateTime _dateOnly(DateTime value) => DateTime(value.year, value.month, value.day);
|
|
|
|
String _dateKeyFromDate(DateTime value) => _dateFormat.format(_dateOnly(value));
|
|
|
|
String? _normalizeDateKey(String rawValue) {
|
|
final trimmed = rawValue.trim();
|
|
if (trimmed.isEmpty) return null;
|
|
|
|
final parsedIso = DateTime.tryParse(trimmed);
|
|
if (parsedIso != null) return _dateKeyFromDate(parsedIso);
|
|
|
|
final match = _flexDateKeyRegex.firstMatch(trimmed);
|
|
if (match == null) return null;
|
|
final year = int.tryParse(match.group(1) ?? '');
|
|
final month = int.tryParse(match.group(2) ?? '');
|
|
final day = int.tryParse(match.group(3) ?? '');
|
|
if (year == null || month == null || day == null) return null;
|
|
if (month < 1 || month > 12 || day < 1 || day > 31) return null;
|
|
return _dateKeyFromDate(DateTime(year, month, day));
|
|
}
|
|
|
|
Box<String>? _diskCacheBoxOrNull() {
|
|
if (!Hive.isBoxOpen(HiveBoxes.hijriCache)) return null;
|
|
return Hive.box<String>(HiveBoxes.hijriCache);
|
|
}
|
|
|
|
String? _readDiskCache(String dateKey) => _diskCacheBoxOrNull()?.get(dateKey);
|
|
|
|
Future<void> _writeDiskCache(String dateKey, String label) async {
|
|
final box = _diskCacheBoxOrNull();
|
|
if (box == null) return;
|
|
await box.put(dateKey, label);
|
|
}
|
|
|
|
Future<String?> _fetchHijriLabelFromApi(String dateKey) async {
|
|
try {
|
|
final response = await _client
|
|
.get(Uri.parse('$_baseUrl/$dateKey'))
|
|
.timeout(_requestTimeout);
|
|
if (response.statusCode != 200) return null;
|
|
|
|
final payload = json.decode(response.body) as Map<String, dynamic>;
|
|
return parseHijriLabel(payload);
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
Future<String> getHijriLabel(DateTime date) async {
|
|
final dateOnly = _dateOnly(date);
|
|
final dateKey = _dateKeyFromDate(dateOnly);
|
|
final cached = _cache[dateKey];
|
|
if (cached != null) return cached;
|
|
|
|
final diskCached = _readDiskCache(dateKey);
|
|
if (diskCached != null && diskCached.trim().isNotEmpty) {
|
|
_cache[dateKey] = diskCached;
|
|
return diskCached;
|
|
}
|
|
|
|
final label = await _fetchHijriLabelFromApi(dateKey);
|
|
if (label != null && label.trim().isNotEmpty) {
|
|
_cache[dateKey] = label;
|
|
await _writeDiskCache(dateKey, label);
|
|
return label;
|
|
}
|
|
|
|
// Keep UI usable when the device is offline.
|
|
final fallback = HijriDateFormatter.format(dateOnly);
|
|
_cache[dateKey] = fallback;
|
|
return fallback;
|
|
}
|
|
|
|
/// Prefetch and persist Hijri labels for a set of Gregorian date keys.
|
|
/// Date keys should be in `yyyy-MM-dd`, but this method normalizes inputs.
|
|
Future<void> warmCacheForDateKeys(
|
|
Iterable<String> rawDateKeys, {
|
|
int maxConcurrentRequests = 4,
|
|
}) async {
|
|
final uniqueKeys = <String>{};
|
|
for (final rawKey in rawDateKeys) {
|
|
final normalized = _normalizeDateKey(rawKey);
|
|
if (normalized != null) uniqueKeys.add(normalized);
|
|
}
|
|
if (uniqueKeys.isEmpty) return;
|
|
|
|
final pendingKeys = <String>[];
|
|
for (final dateKey in uniqueKeys) {
|
|
final memory = _cache[dateKey];
|
|
if (memory != null && memory.trim().isNotEmpty) continue;
|
|
|
|
final disk = _readDiskCache(dateKey);
|
|
if (disk != null && disk.trim().isNotEmpty) {
|
|
_cache[dateKey] = disk;
|
|
continue;
|
|
}
|
|
|
|
pendingKeys.add(dateKey);
|
|
}
|
|
if (pendingKeys.isEmpty) return;
|
|
|
|
final concurrency = maxConcurrentRequests.clamp(1, 8);
|
|
for (var index = 0; index < pendingKeys.length; index += concurrency) {
|
|
final end = min(index + concurrency, pendingKeys.length);
|
|
final chunk = pendingKeys.sublist(index, end);
|
|
await Future.wait(chunk.map((dateKey) async {
|
|
final label = await _fetchHijriLabelFromApi(dateKey);
|
|
if (label == null || label.trim().isEmpty) return;
|
|
_cache[dateKey] = label;
|
|
await _writeDiskCache(dateKey, label);
|
|
}));
|
|
}
|
|
}
|
|
|
|
static String? parseHijriLabel(Map<String, dynamic> payload) {
|
|
if (payload['status'] != true) return null;
|
|
|
|
final data = payload['data'];
|
|
if (data is! Map) return null;
|
|
|
|
final hijr = data['hijr'];
|
|
if (hijr is! Map) return null;
|
|
|
|
final today = hijr['today']?.toString().trim();
|
|
if (today != null && today.isNotEmpty) {
|
|
final parts = today.split(',');
|
|
return parts.length > 1 ? parts.last.trim() : today;
|
|
}
|
|
|
|
final day = hijr['day'];
|
|
final monthName = hijr['monthName']?.toString().trim();
|
|
final year = hijr['year'];
|
|
|
|
if (day != null && monthName != null && monthName.isNotEmpty && year != null) {
|
|
return '$day $monthName $year H';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|