import 'dart:typed_data'; import 'package:hive/hive.dart'; import 'package:http/http.dart' as http; class HiveTileCache { HiveTileCache._(); static const String _boxName = 'map_tile_cache_v1'; static const int _maxEntries = 6000; static const Duration _staleAfter = Duration(days: 14); static final Map> _inFlight = >{}; static Future init() async { if (!Hive.isBoxOpen(_boxName)) { await Hive.openBox(_boxName); } } static Box? get _boxOrNull { try { if (!Hive.isBoxOpen(_boxName)) return null; return Hive.box(_boxName); } catch (_) { return null; } } static Future getOrFetch(String url) async { final now = DateTime.now().millisecondsSinceEpoch; final box = _boxOrNull; if (box == null) return _fetchWithoutCache(url); final cached = _freshCachedBytes(box, url, now); if (cached != null) return cached; return _fetchAndStore(box, url, now); } static void prefetchUrls( Iterable urls, { int maxCount = 96, }) { final box = _boxOrNull; if (box == null) return; final now = DateTime.now().millisecondsSinceEpoch; var queued = 0; for (final url in urls) { if (queued >= maxCount) break; if (_freshCachedBytes(box, url, now) != null) continue; if (_inFlight.containsKey(url)) continue; queued += 1; _fetchAndStore(box, url, now); } } static Uint8List? _freshCachedBytes(Box box, String url, int now) { final cached = box.get(url); if (cached is! Map) return null; final ts = cached['ts']; final bytes = cached['bytes']; if (ts is int && bytes is Uint8List && now - ts <= _staleAfter.inMilliseconds) { return bytes; } return null; } static Future _fetchAndStore(Box box, String url, int now) { final inFlight = _inFlight[url]; if (inFlight != null) return inFlight; final future = _fetchAndStoreInternal(box, url, now); _inFlight[url] = future; future.whenComplete(() { _inFlight.remove(url); }); return future; } static Future _fetchAndStoreInternal( Box box, String url, int now, ) async { try { final response = await http.get(Uri.parse(url)); if (response.statusCode != 200) return null; final bytes = response.bodyBytes; await box.put(url, {'ts': now, 'bytes': bytes}); await _pruneIfNeeded(box); return bytes; } catch (_) { return null; } } static Future _fetchWithoutCache(String url) async { try { final response = await http.get(Uri.parse(url)); if (response.statusCode != 200) return null; return response.bodyBytes; } catch (_) { return null; } } static Future _pruneIfNeeded(Box box) async { final overflow = box.length - _maxEntries; if (overflow <= 0) return; final entries = >[]; for (final key in box.keys) { entries.add(MapEntry(key, box.get(key))); } entries.sort((a, b) { final ats = (a.value is Map && a.value['ts'] is int) ? a.value['ts'] as int : 0; final bts = (b.value is Map && b.value['ts'] is int) ? b.value['ts'] as int : 0; return ats.compareTo(bts); }); for (var i = 0; i < overflow; i++) { await box.delete(entries[i].key); } } }