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 Notifier { late OfflineRepository _repository; @override OfflineState build() { _repository = ref.watch(offlineRepositoryProvider); final subscription = _repository.watchDownloadedRegions().listen((regions) { state = state.copyWith(downloadedRegions: regions); }); ref.onDispose(subscription.cancel); Future.microtask(loadAvailableRegions); return const OfflineState(); } Future loadAvailableRegions() async { state = state.copyWith(isLoading: true, clearError: true); try { final regions = await _repository.getAvailableRegions(); state = state.copyWith(availableRegions: regions, isLoading: false); } catch (e) { 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) { state = state.copyWith( downloadProgress: { ...state.downloadProgress, region.id: DownloadProgress( regionId: region.id, component: component, received: received, total: total, ), }, ); }, ); final newProgress = Map.from(state.downloadProgress); newProgress.remove(region.id); state = state.copyWith(downloadProgress: newProgress); } catch (e) { 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) { state = state.copyWith(error: 'Could not delete region.'); } } } final offlineProvider = NotifierProvider(OfflineNotifier.new);