Allow OSM offline downloads, disable button for restricted providers

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 <noreply@anthropic.com>
This commit is contained in:
Doug Borg
2026-03-04 09:47:42 -07:00
parent 2d92214bed
commit f3f40f36ef
4 changed files with 38 additions and 70 deletions

View File

@@ -578,37 +578,41 @@ class _HomeScreenState extends State<HomeScreen> 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<AppState>();
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),
),
),
),
),
);
},
),
),
],

View File

@@ -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';

View File

@@ -267,42 +267,6 @@ class _DownloadAreaDialogState extends State<DownloadAreaDialog> {
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) {

View File

@@ -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');