From 63ebc2b68245eda2cef5690c2f397a3628a43be2 Mon Sep 17 00:00:00 2001 From: stopflock Date: Fri, 22 Aug 2025 21:04:30 -0500 Subject: [PATCH] ho lee shet --- lib/app_state.dart | 2 + .../offline_mode_section.dart | 49 ++++++++++++++++++- lib/services/offline_area_service.dart | 21 ++++++++ lib/state/settings_state.dart | 1 + lib/widgets/download_area_dialog.dart | 33 ++++++++++++- lib/widgets/tile_provider_with_cache.dart | 17 ++++++- 6 files changed, 119 insertions(+), 4 deletions(-) diff --git a/lib/app_state.dart b/lib/app_state.dart index cfa9c1d..b7e836c 100644 --- a/lib/app_state.dart +++ b/lib/app_state.dart @@ -161,6 +161,8 @@ class AppState extends ChangeNotifier { _startUploader(); // Resume upload queue processing as we leave offline mode } else { _uploadQueueState.stopUploader(); // Stop uploader in offline mode + // Cancel any active area downloads + await OfflineAreaService().cancelActiveDownloads(); } } diff --git a/lib/screens/settings_screen_sections/offline_mode_section.dart b/lib/screens/settings_screen_sections/offline_mode_section.dart index 36a734a..3db49ef 100644 --- a/lib/screens/settings_screen_sections/offline_mode_section.dart +++ b/lib/screens/settings_screen_sections/offline_mode_section.dart @@ -1,10 +1,57 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../app_state.dart'; +import '../../services/offline_area_service.dart'; class OfflineModeSection extends StatelessWidget { const OfflineModeSection({super.key}); + Future _handleOfflineModeChange(BuildContext context, AppState appState, bool value) async { + // If enabling offline mode, check for active downloads + if (value && !appState.offlineMode) { + final offlineService = OfflineAreaService(); + if (offlineService.hasActiveDownloads) { + // Show confirmation dialog + final shouldProceed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Row( + children: const [ + Icon(Icons.warning, color: Colors.orange), + SizedBox(width: 8), + Text('Active Downloads'), + ], + ), + content: const Text( + 'Enabling offline mode will cancel any active area downloads. Do you want to continue?', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () => Navigator.of(context).pop(true), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orange, + foregroundColor: Colors.white, + ), + child: const Text('Enable Offline Mode'), + ), + ], + ), + ); + + if (shouldProceed != true) { + return; // User cancelled + } + } + } + + // Proceed with the change + await appState.setOfflineMode(value); + } + @override Widget build(BuildContext context) { final appState = context.watch(); @@ -14,7 +61,7 @@ class OfflineModeSection extends StatelessWidget { subtitle: const Text('Disable all network requests except for local/offline areas.'), trailing: Switch( value: appState.offlineMode, - onChanged: (value) async => await appState.setOfflineMode(value), + onChanged: (value) => _handleOfflineModeChange(context, appState, value), ), ); } diff --git a/lib/services/offline_area_service.dart b/lib/services/offline_area_service.dart index 1a54191..59da1df 100644 --- a/lib/services/offline_area_service.dart +++ b/lib/services/offline_area_service.dart @@ -26,6 +26,27 @@ class OfflineAreaService { final List _areas = []; List get offlineAreas => List.unmodifiable(_areas); + /// Check if any areas are currently downloading + bool get hasActiveDownloads => _areas.any((area) => area.status == OfflineAreaStatus.downloading); + + /// Cancel all active downloads (used when enabling offline mode) + Future cancelActiveDownloads() async { + final activeAreas = _areas.where((area) => area.status == OfflineAreaStatus.downloading).toList(); + for (final area in activeAreas) { + area.status = OfflineAreaStatus.cancelled; + if (!area.isPermanent) { + // Clean up non-permanent areas + final dir = Directory(area.directory); + if (await dir.exists()) { + await dir.delete(recursive: true); + } + _areas.remove(area); + } + } + await saveAreasToDisk(); + debugPrint('OfflineAreaService: Cancelled ${activeAreas.length} active downloads due to offline mode'); + } + /// Ensure the service is initialized (areas loaded from disk) Future ensureInitialized() async { if (_initialized) return; diff --git a/lib/state/settings_state.dart b/lib/state/settings_state.dart index 67b24e4..5918f1c 100644 --- a/lib/state/settings_state.dart +++ b/lib/state/settings_state.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../widgets/tile_provider_with_cache.dart'; +import '../widgets/camera_provider_with_cache.dart'; // Enum for upload mode (Production, OSM Sandbox, Simulate) enum UploadMode { production, sandbox, simulate } diff --git a/lib/widgets/download_area_dialog.dart b/lib/widgets/download_area_dialog.dart index 99cba63..1cb56e4 100644 --- a/lib/widgets/download_area_dialog.dart +++ b/lib/widgets/download_area_dialog.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; import 'package:latlong2/latlong.dart'; import 'package:flutter_map/flutter_map.dart'; +import 'package:provider/provider.dart'; import 'dart:math' as math; +import '../app_state.dart'; import '../dev_config.dart'; import '../services/offline_area_service.dart'; import '../services/offline_areas/offline_tile_utils.dart'; @@ -85,8 +87,10 @@ class _DownloadAreaDialogState extends State { @override Widget build(BuildContext context) { + final appState = context.watch(); final bounds = widget.controller.camera.visibleBounds; final maxZoom = _zoom.toInt(); + final isOfflineMode = appState.offlineMode; // Use the calculated max possible zoom instead of fixed span final sliderMin = _minZoom?.toDouble() ?? 12.0; @@ -190,6 +194,33 @@ class _DownloadAreaDialogState extends State { ), ), ), + if (isOfflineMode) + Padding( + padding: const EdgeInsets.only(top: 12.0), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.orange.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.orange.withOpacity(0.3)), + ), + child: Row( + children: [ + Icon(Icons.wifi_off, color: Colors.orange[700], size: 20), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Downloads disabled while in offline mode. Disable offline mode to download new areas.', + style: TextStyle( + fontSize: 12, + color: Colors.orange[700], + ), + ), + ), + ], + ), + ), + ), ], ), ), @@ -199,7 +230,7 @@ class _DownloadAreaDialogState extends State { child: const Text('Cancel'), ), ElevatedButton( - onPressed: () async { + onPressed: isOfflineMode ? null : () async { try { final id = DateTime.now().toIso8601String().replaceAll(':', '-'); final appDocDir = await OfflineAreaService().getOfflineAreaDir(); diff --git a/lib/widgets/tile_provider_with_cache.dart b/lib/widgets/tile_provider_with_cache.dart index 7fd04a3..9cf353c 100644 --- a/lib/widgets/tile_provider_with_cache.dart +++ b/lib/widgets/tile_provider_with_cache.dart @@ -5,16 +5,25 @@ import 'package:flutter/services.dart'; import '../services/map_data_provider.dart'; import '../app_state.dart'; -/// Singleton in-memory tile cache and async provider for custom tiles. +/// In-memory tile cache and async provider for custom tiles. class TileProviderWithCache extends TileProvider with ChangeNotifier { static final Map _tileCache = {}; static Map get tileCache => _tileCache; + + bool _disposed = false; TileProviderWithCache(); + + @override + void dispose() { + _disposed = true; + super.dispose(); + } @override ImageProvider getImage(TileCoordinates coords, TileLayer options, {MapSource source = MapSource.auto}) { final key = '${coords.z}/${coords.x}/${coords.y}'; + if (_tileCache.containsKey(key)) { final bytes = _tileCache[key]!; return MemoryImage(bytes); @@ -33,6 +42,7 @@ class TileProviderWithCache extends TileProvider with ChangeNotifier { void _fetchAndCacheTile(TileCoordinates coords, String key, {MapSource source = MapSource.auto}) async { // Don't fire multiple fetches for the same tile simultaneously if (_tileCache.containsKey(key)) return; + try { final bytes = await MapDataProvider().getTile( z: coords.z, x: coords.x, y: coords.y, source: source, @@ -40,7 +50,10 @@ class TileProviderWithCache extends TileProvider with ChangeNotifier { if (bytes.isNotEmpty) { _tileCache[key] = Uint8List.fromList(bytes); print('[TileProviderWithCache] Cached tile $key, bytes=${bytes.length}'); - notifyListeners(); // This updates any listening widgets + // Only notify listeners if not disposed + if (!_disposed) { + notifyListeners(); // This updates any listening widgets + } } // If bytes were empty, don't cache (will re-attempt next time) } catch (e) {