From f3f40f36ef866b4de5489d1de1235af333ec27ba Mon Sep 17 00:00:00 2001 From: Doug Borg Date: Wed, 4 Mar 2026 09:47:42 -0700 Subject: [PATCH] Allow OSM offline downloads, disable button for restricted providers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allow offline area downloads for OSM tile server. Move the "downloads not permitted" check from inside the download dialog to the download button itself — the button is now disabled (greyed out) when the current tile type doesn't support offline downloads. Co-Authored-By: Claude Opus 4.6 --- lib/screens/home_screen.dart | 60 ++++++++++++++------------ lib/services/service_policy.dart | 4 +- lib/widgets/download_area_dialog.dart | 36 ---------------- test/services/service_policy_test.dart | 8 ++-- 4 files changed, 38 insertions(+), 70 deletions(-) diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index ff9702e..acc3e03 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -578,37 +578,41 @@ class _HomeScreenState extends State with TickerProviderStateMixin { flex: 3, // 30% for secondary action child: AnimatedBuilder( animation: LocalizationService.instance, - builder: (context, child) => FittedBox( - fit: BoxFit.scaleDown, - child: ElevatedButton.icon( - icon: Icon(Icons.download_for_offline), - label: Text(LocalizationService.instance.download), - onPressed: () { - // Check minimum zoom level before opening download dialog - final currentZoom = _mapController.mapController.camera.zoom; - if (currentZoom < kMinZoomForOfflineDownload) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - LocalizationService.instance.t('download.areaTooBigMessage', - params: [kMinZoomForOfflineDownload.toString()]) + builder: (context, child) { + final appState = context.watch(); + final canDownload = appState.selectedTileType?.allowsOfflineDownload ?? false; + return FittedBox( + fit: BoxFit.scaleDown, + child: ElevatedButton.icon( + icon: Icon(Icons.download_for_offline), + label: Text(LocalizationService.instance.download), + onPressed: canDownload ? () { + // Check minimum zoom level before opening download dialog + final currentZoom = _mapController.mapController.camera.zoom; + if (currentZoom < kMinZoomForOfflineDownload) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + LocalizationService.instance.t('download.areaTooBigMessage', + params: [kMinZoomForOfflineDownload.toString()]) + ), ), - ), + ); + return; + } + + showDialog( + context: context, + builder: (ctx) => DownloadAreaDialog(controller: _mapController.mapController), ); - return; - } - - showDialog( - context: context, - builder: (ctx) => DownloadAreaDialog(controller: _mapController.mapController), - ); - }, - style: ElevatedButton.styleFrom( - minimumSize: Size(0, 48), - textStyle: TextStyle(fontSize: 16), + } : null, + style: ElevatedButton.styleFrom( + minimumSize: Size(0, 48), + textStyle: TextStyle(fontSize: 16), + ), ), - ), - ), + ); + }, ), ), ], diff --git a/lib/services/service_policy.dart b/lib/services/service_policy.dart index e8990a3..078e7a7 100644 --- a/lib/services/service_policy.dart +++ b/lib/services/service_policy.dart @@ -70,13 +70,13 @@ class ServicePolicy { attributionUrl = null; /// OSM tile server (tile.openstreetmap.org) - /// Policy: no offline/bulk downloading, min 7-day cache, must honor cache headers. + /// Policy: min 7-day cache, must honor cache headers. /// Concurrency managed by flutter_map's NetworkTileProvider. /// https://operations.osmfoundation.org/policies/tiles/ const ServicePolicy.osmTileServer() : maxConcurrentRequests = 0, // managed by flutter_map minRequestInterval = null, - allowsOfflineDownload = false, + allowsOfflineDownload = true, requiresClientCaching = true, minCacheTtl = const Duration(days: 7), attributionUrl = 'https://www.openstreetmap.org/copyright'; diff --git a/lib/widgets/download_area_dialog.dart b/lib/widgets/download_area_dialog.dart index cdda1d0..a370aaf 100644 --- a/lib/widgets/download_area_dialog.dart +++ b/lib/widgets/download_area_dialog.dart @@ -267,42 +267,6 @@ class _DownloadAreaDialogState extends State { final selectedProvider = appState.selectedTileProvider; final selectedTileType = appState.selectedTileType; - // Check if the tile provider allows offline downloads - if (selectedTileType != null && !selectedTileType.allowsOfflineDownload) { - if (!context.mounted) return; - // Capture navigator before popping, since context is - // deactivated after Navigator.pop. - final navigator = Navigator.of(context); - navigator.pop(); - showDialog( - context: navigator.context, - builder: (context) => AlertDialog( - title: Row( - children: [ - const Icon(Icons.block, color: Colors.orange), - const SizedBox(width: 10), - Text(locService.t('download.title')), - ], - ), - content: Text( - locService.t( - 'download.offlineNotPermitted', - params: [ - selectedProvider?.name ?? locService.t('download.currentTileProvider'), - ], - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(locService.t('actions.ok')), - ), - ], - ), - ); - return; - } - // Guard: provider and tile type must be non-null for a // useful offline area (fetchLocalTile requires exact match). if (selectedProvider == null || selectedTileType == null) { diff --git a/test/services/service_policy_test.dart b/test/services/service_policy_test.dart index 59a1287..bfe31e4 100644 --- a/test/services/service_policy_test.dart +++ b/test/services/service_policy_test.dart @@ -96,11 +96,11 @@ void main() { }); group('resolve', () { - test('OSM tile server policy disallows offline download', () { + test('OSM tile server policy allows offline download', () { final policy = ServicePolicyResolver.resolve( 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', ); - expect(policy.allowsOfflineDownload, false); + expect(policy.allowsOfflineDownload, true); }); test('OSM tile server policy requires 7-day min cache TTL', () { @@ -175,7 +175,7 @@ void main() { final policy = ServicePolicyResolver.resolve( 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', ); - expect(policy.allowsOfflineDownload, false); + expect(policy.allowsOfflineDownload, true); }); test('handles {quadkey} template variable', () { @@ -381,7 +381,7 @@ void main() { group('ServicePolicy', () { test('osmTileServer policy has correct values', () { const policy = ServicePolicy.osmTileServer(); - expect(policy.allowsOfflineDownload, false); + expect(policy.allowsOfflineDownload, true); expect(policy.minCacheTtl, const Duration(days: 7)); expect(policy.requiresClientCaching, true); expect(policy.attributionUrl, 'https://www.openstreetmap.org/copyright');