import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/database/app_database.dart'; import '../data/offline_repository.dart'; class DownloadProgress { final String regionId; final String component; final int received; final int total; DownloadProgress({ required this.regionId, required this.component, required this.received, required this.total, }); double get fraction => total > 0 ? received / total : 0; } class OfflineState { final List availableRegions; final List downloadedRegions; final Map downloadProgress; // regionId -> progress final bool isLoading; final String? error; const OfflineState({ this.availableRegions = const [], this.downloadedRegions = const [], this.downloadProgress = const {}, this.isLoading = false, this.error, }); OfflineState copyWith({ List? availableRegions, List? downloadedRegions, Map? downloadProgress, bool? isLoading, String? error, bool clearError = false, }) { return OfflineState( availableRegions: availableRegions ?? this.availableRegions, downloadedRegions: downloadedRegions ?? this.downloadedRegions, downloadProgress: downloadProgress ?? this.downloadProgress, isLoading: isLoading ?? this.isLoading, error: clearError ? null : (error ?? this.error), ); } bool isRegionDownloaded(String regionId) { return downloadedRegions.any((r) => r.id == regionId); } bool isRegionDownloading(String regionId) { return downloadProgress.containsKey(regionId); } int get totalStorageUsedMb { int total = 0; for (final r in downloadedRegions) { total += r.tilesSizeBytes + r.routingSizeBytes + r.poisSizeBytes; } return total ~/ (1024 * 1024); } } class OfflineNotifier extends StateNotifier { final OfflineRepository _repository; OfflineNotifier(this._repository) : super(const OfflineState()) { _init(); } Future _init() async { await loadAvailableRegions(); _repository.watchDownloadedRegions().listen((regions) { if (mounted) { state = state.copyWith(downloadedRegions: regions); } }); } Future loadAvailableRegions() async { state = state.copyWith(isLoading: true, clearError: true); try { final regions = await _repository.getAvailableRegions(); if (mounted) { state = state.copyWith( availableRegions: regions, isLoading: false, ); } } catch (e) { if (mounted) { state = state.copyWith( isLoading: false, error: 'Could not load available regions.', ); } } } Future downloadRegion(OfflineRegionInfo region) async { state = state.copyWith( downloadProgress: { ...state.downloadProgress, region.id: DownloadProgress( regionId: region.id, component: 'starting', received: 0, total: region.sizeMb * 1024 * 1024, ), }, ); try { await _repository.downloadRegion( region: region, onProgress: (component, received, total) { if (mounted) { state = state.copyWith( downloadProgress: { ...state.downloadProgress, region.id: DownloadProgress( regionId: region.id, component: component, received: received, total: total, ), }, ); } }, ); if (mounted) { final newProgress = Map.from( state.downloadProgress); newProgress.remove(region.id); state = state.copyWith(downloadProgress: newProgress); } } catch (e) { if (mounted) { final newProgress = Map.from( state.downloadProgress); newProgress.remove(region.id); state = state.copyWith( downloadProgress: newProgress, error: 'Download failed for ${region.name}.', ); } } } Future deleteRegion(String regionId) async { try { await _repository.deleteRegion(regionId); } catch (e) { if (mounted) { state = state.copyWith(error: 'Could not delete region.'); } } } } final offlineProvider = StateNotifierProvider((ref) { return OfflineNotifier(ref.watch(offlineRepositoryProvider)); });