add stadiamaps.com

This commit is contained in:
ericz
2026-05-23 11:41:06 +02:00
parent a50c0d0b2d
commit 225d07b440
10 changed files with 507 additions and 32 deletions
+217
View File
@@ -8,6 +8,7 @@ import '../l10n/l10n.dart';
import '../models/app_settings.dart';
import '../models/translation_support.dart';
import '../services/app_settings_service.dart';
import '../services/map_tile_cache_service.dart';
import '../services/notification_service.dart';
import '../services/translation_service.dart';
import '../widgets/adaptive_app_bar_title.dart';
@@ -529,6 +530,39 @@ class AppSettingsScreen extends StatelessWidget {
onTap: () => _showUnitsDialog(context, settingsService),
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.layers_outlined),
title: const Text('Raster Tile Source'),
subtitle: Text(
_mapRasterSourceSummary(settingsService.settings),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showMapRasterSourceDialog(context, settingsService),
),
if (_isStadiaSource(settingsService.settings)) ...[
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.public_outlined),
title: const Text('Stadia Endpoint'),
subtitle: Text(
_mapRasterEndpointSummary(settingsService.settings),
),
trailing: const Icon(Icons.chevron_right),
onTap: () =>
_showMapRasterEndpointDialog(context, settingsService),
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.key_outlined),
title: const Text('Stadia API Key'),
subtitle: Text(_mapApiKeySummary(settingsService.settings)),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showMapApiKeyDialog(context, settingsService),
),
],
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.download_outlined),
title: Text(context.l10n.appSettings_offlineMapCache),
@@ -553,6 +587,189 @@ class AppSettingsScreen extends StatelessWidget {
);
}
String _mapRasterSourceSummary(AppSettings settings) {
final source = MapRasterSourceCatalog.fromSettings(settings);
return '${source.label} - ${source.description}';
}
bool _isStadiaSource(AppSettings settings) {
return MapRasterSourceCatalog.fromSettings(settings).isStadia;
}
String _mapRasterEndpointSummary(AppSettings settings) {
final endpoint = MapRasterEndpointCatalog.fromSettings(settings);
return '${endpoint.label} - ${endpoint.description}';
}
String _mapApiKeySummary(AppSettings settings) {
final apiKey = settings.mapTileApiKey?.trim();
if (apiKey == null || apiKey.isEmpty) {
return 'Required for mobile and non-local Stadia Maps usage';
}
return 'Configured: ${_maskApiKey(apiKey)}';
}
String _maskApiKey(String value) {
if (value.length <= 8) return '********';
return '${value.substring(0, 4)}...${value.substring(value.length - 4)}';
}
void _showMapRasterSourceDialog(
BuildContext context,
AppSettingsService settingsService,
) {
String selectedId = settingsService.settings.mapRasterSourceId;
showDialog(
context: context,
builder: (dialogContext) => StatefulBuilder(
builder: (dialogContext, setState) => AlertDialog(
title: const Text('Raster Tile Source'),
content: SizedBox(
width: 360,
child: RadioGroup<String>(
groupValue: selectedId,
onChanged: (value) {
if (value == null) return;
setState(() {
selectedId = value;
});
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
for (final option in MapRasterSourceCatalog.presets)
RadioListTile<String>(
value: option.id,
title: Text(option.label),
subtitle: Text(option.description),
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: Text(context.l10n.common_cancel),
),
TextButton(
onPressed: () async {
await settingsService.setMapRasterSourceId(selectedId);
if (!dialogContext.mounted) return;
Navigator.pop(dialogContext);
},
child: Text(context.l10n.common_save),
),
],
),
),
);
}
void _showMapRasterEndpointDialog(
BuildContext context,
AppSettingsService settingsService,
) {
String selectedId = settingsService.settings.mapTileEndpointId;
showDialog(
context: context,
builder: (dialogContext) => StatefulBuilder(
builder: (dialogContext, setState) => AlertDialog(
title: const Text('Stadia Endpoint'),
content: SizedBox(
width: 360,
child: RadioGroup<String>(
groupValue: selectedId,
onChanged: (value) {
if (value == null) return;
setState(() {
selectedId = value;
});
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
for (final option in MapRasterEndpointCatalog.presets)
RadioListTile<String>(
value: option.id,
title: Text(option.label),
subtitle: Text(option.description),
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: Text(context.l10n.common_cancel),
),
TextButton(
onPressed: () async {
await settingsService.setMapTileEndpointId(selectedId);
if (!dialogContext.mounted) return;
Navigator.pop(dialogContext);
},
child: Text(context.l10n.common_save),
),
],
),
),
);
}
void _showMapApiKeyDialog(
BuildContext context,
AppSettingsService settingsService,
) {
final controller = TextEditingController(
text: settingsService.settings.mapTileApiKey ?? '',
);
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
title: const Text('Stadia API Key'),
content: SizedBox(
width: 420,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Enter your Stadia Maps API key. This app uses it for raster tile requests on mobile and other non-local environments.',
),
const SizedBox(height: 12),
TextField(
controller: controller,
autofocus: true,
autocorrect: false,
enableSuggestions: false,
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: 'sk_...',
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: Text(context.l10n.common_cancel),
),
TextButton(
onPressed: () async {
await settingsService.setMapTileApiKey(controller.text);
if (!dialogContext.mounted) return;
Navigator.pop(dialogContext);
},
child: Text(context.l10n.common_save),
),
],
),
);
}
Widget _buildTranslationCard(
BuildContext context,
AppSettingsService settingsService,
+2 -2
View File
@@ -441,7 +441,7 @@ class _ChannelMessagePathMapScreenState
builder: (context, connector, _) {
final settings = context.watch<AppSettingsService>().settings;
final isImperial = settings.unitSystem == UnitSystem.imperial;
final tileCache = context.read<MapTileCacheService>();
final tileCache = context.watch<MapTileCacheService>();
final primaryPath = _selectPrimaryPath(
widget.message.pathBytes,
widget.message.pathVariants,
@@ -559,7 +559,7 @@ class _ChannelMessagePathMapScreenState
),
children: [
TileLayer(
urlTemplate: kMapTileUrlTemplate,
urlTemplate: tileCache.urlTemplate,
tileProvider: tileCache.tileProvider,
userAgentPackageName:
MapTileCacheService.userAgentPackageName,
+2 -2
View File
@@ -391,7 +391,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
Widget build(BuildContext context) {
final settings = context.watch<AppSettingsService>().settings;
final isImperial = settings.unitSystem == UnitSystem.imperial;
final tileCache = context.read<MapTileCacheService>();
final tileCache = context.watch<MapTileCacheService>();
final endpoints = _visibleEndpoints();
final mapPoints = [
if (_start != null) _start!.point,
@@ -470,7 +470,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
),
children: [
TileLayer(
urlTemplate: kMapTileUrlTemplate,
urlTemplate: tileCache.urlTemplate,
tileProvider: tileCache.tileProvider,
userAgentPackageName: MapTileCacheService.userAgentPackageName,
maxZoom: 19,
+25 -5
View File
@@ -171,6 +171,7 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
Future<void> _startDownload() async {
final bounds = _selectedBounds;
final cacheService = context.read<MapTileCacheService>();
if (bounds == null) {
showDismissibleSnackBar(
context,
@@ -187,6 +188,16 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
return;
}
if (!cacheService.source.allowsBulkDownload) {
showDismissibleSnackBar(
context,
content: Text(
'Offline bulk downloads are disabled for ${cacheService.source.label} in this app configuration.',
),
);
return;
}
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
@@ -209,8 +220,6 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
if (confirmed != true || !mounted) return;
final cacheService = context.read<MapTileCacheService>();
setState(() {
_isDownloading = true;
_completedTiles = 0;
@@ -278,7 +287,8 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
@override
Widget build(BuildContext context) {
final tileCache = context.read<MapTileCacheService>();
final tileCache = context.watch<MapTileCacheService>();
final source = tileCache.source;
final selectedBounds = _selectedBounds;
final l10n = context.l10n;
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
@@ -319,7 +329,7 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
),
children: [
TileLayer(
urlTemplate: kMapTileUrlTemplate,
urlTemplate: tileCache.urlTemplate,
tileProvider: tileCache.tileProvider,
userAgentPackageName:
MapTileCacheService.userAgentPackageName,
@@ -423,6 +433,13 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
},
),
Text(l10n.mapCache_estimatedTiles(_estimatedTiles)),
if (!source.allowsBulkDownload) ...[
const SizedBox(height: 8),
Text(
'Offline bulk downloads are disabled for ${source.label}.',
style: TextStyle(color: Colors.orange[800]),
),
],
if (_isDownloading) ...[
const SizedBox(height: 8),
LinearProgressIndicator(value: progressValue),
@@ -441,7 +458,10 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
child: ElevatedButton.icon(
icon: const Icon(Icons.download),
label: Text(l10n.mapCache_downloadTilesButton),
onPressed: _isDownloading || selectedBounds == null
onPressed:
_isDownloading ||
selectedBounds == null ||
!source.allowsBulkDownload
? null
: _startDownload,
),
+2 -2
View File
@@ -205,7 +205,7 @@ class _MapScreenState extends State<MapScreen> {
Widget build(BuildContext context) {
return Consumer3<MeshCoreConnector, AppSettingsService, PathHistoryService>(
builder: (context, connector, settingsService, pathHistory, child) {
final tileCache = context.read<MapTileCacheService>();
final tileCache = context.watch<MapTileCacheService>();
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
final settings = settingsService.settings;
final allContacts = connector.allContacts;
@@ -563,7 +563,7 @@ class _MapScreenState extends State<MapScreen> {
),
children: [
TileLayer(
urlTemplate: kMapTileUrlTemplate,
urlTemplate: tileCache.urlTemplate,
tileProvider: tileCache.tileProvider,
userAgentPackageName:
MapTileCacheService.userAgentPackageName,
+2 -2
View File
@@ -535,7 +535,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
builder: (context, connector, _) {
final settings = context.watch<AppSettingsService>().settings;
final isImperial = settings.unitSystem == UnitSystem.imperial;
final tileCache = context.read<MapTileCacheService>();
final tileCache = context.watch<MapTileCacheService>();
return Scaffold(
appBar: AppBar(
@@ -909,7 +909,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
),
children: [
TileLayer(
urlTemplate: kMapTileUrlTemplate,
urlTemplate: tileCache.urlTemplate,
tileProvider: tileCache.tileProvider,
userAgentPackageName: MapTileCacheService.userAgentPackageName,
maxZoom: 19,