Merge pull request #461 from ericszimmermann/ez_alternative_maps

Alternative Maps
This commit is contained in:
zjs81
2026-07-04 00:32:58 -07:00
committed by GitHub
55 changed files with 3537 additions and 282 deletions
+412 -91
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 '../theme/mesh_theme.dart';
@@ -699,52 +700,229 @@ class AppSettingsScreen extends StatelessWidget {
) {
final scheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
return Column(
children: [
SwitchListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
final children = <Widget>[
SwitchListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
secondary: const Icon(Icons.router_outlined, size: 20),
title: Text(context.l10n.appSettings_showRepeaters),
subtitle: Text(context.l10n.appSettings_showRepeatersSubtitle),
value: settingsService.settings.mapShowRepeaters,
onChanged: (value) => settingsService.setMapShowRepeaters(value),
),
const Divider(height: 1, indent: 16),
SwitchListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
secondary: const Icon(Icons.chat_outlined, size: 20),
title: Text(context.l10n.appSettings_showChatNodes),
subtitle: Text(context.l10n.appSettings_showChatNodesSubtitle),
value: settingsService.settings.mapShowChatNodes,
onChanged: (value) => settingsService.setMapShowChatNodes(value),
),
const Divider(height: 1, indent: 16),
SwitchListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
secondary: const Icon(Icons.people_outline, size: 20),
title: Text(context.l10n.appSettings_showOtherNodes),
subtitle: Text(context.l10n.appSettings_showOtherNodesSubtitle),
value: settingsService.settings.mapShowOtherNodes,
onChanged: (value) => settingsService.setMapShowOtherNodes(value),
),
const Divider(height: 1, indent: 16),
InkWell(
onTap: () => _showTimeFilterSheet(context, settingsService),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Icon(
Icons.timer_outlined,
size: 20,
color: scheme.onSurfaceVariant,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.appSettings_timeFilter,
style: textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 2),
Text(
settingsService.settings.mapTimeFilterHours == 0
? context.l10n.appSettings_timeFilterShowAll
: context.l10n.appSettings_timeFilterShowLast(
settingsService.settings.mapTimeFilterHours
.toInt(),
),
style: textTheme.bodySmall?.copyWith(
color: scheme.onSurfaceVariant,
),
),
],
),
),
Icon(
Icons.chevron_right,
color: scheme.onSurfaceVariant,
size: 16,
),
],
),
secondary: const Icon(Icons.router_outlined, size: 20),
title: Text(context.l10n.appSettings_showRepeaters),
subtitle: Text(context.l10n.appSettings_showRepeatersSubtitle),
value: settingsService.settings.mapShowRepeaters,
onChanged: (value) => settingsService.setMapShowRepeaters(value),
),
const Divider(height: 1, indent: 16),
SwitchListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
const Divider(height: 1, indent: 16),
InkWell(
onTap: () => _showUnitsSheet(context, settingsService),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Icon(Icons.straighten, size: 20, color: scheme.onSurfaceVariant),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.appSettings_unitsTitle,
style: textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 2),
Text(
settingsService.settings.unitSystem == UnitSystem.imperial
? context.l10n.appSettings_unitsImperial
: context.l10n.appSettings_unitsMetric,
style: textTheme.bodySmall?.copyWith(
color: scheme.onSurfaceVariant,
),
),
],
),
),
Icon(
Icons.chevron_right,
color: scheme.onSurfaceVariant,
size: 16,
),
],
),
secondary: const Icon(Icons.chat_outlined, size: 20),
title: Text(context.l10n.appSettings_showChatNodes),
subtitle: Text(context.l10n.appSettings_showChatNodesSubtitle),
value: settingsService.settings.mapShowChatNodes,
onChanged: (value) => settingsService.setMapShowChatNodes(value),
),
const Divider(height: 1, indent: 16),
SwitchListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
),
const Divider(height: 1, indent: 16),
InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const MapCacheScreen()),
);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Icon(
Icons.download_outlined,
size: 20,
color: scheme.onSurfaceVariant,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.appSettings_offlineMapCache,
style: textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 2),
Text(
settingsService.settings.mapCacheBounds == null
? context.l10n.appSettings_noAreaSelected
: context.l10n.appSettings_areaSelectedZoom(
settingsService.settings.mapCacheMinZoom,
settingsService.settings.mapCacheMaxZoom,
),
style: textTheme.bodySmall?.copyWith(
color: scheme.onSurfaceVariant,
),
),
],
),
),
Icon(
Icons.chevron_right,
color: scheme.onSurfaceVariant,
size: 16,
),
],
),
secondary: const Icon(Icons.people_outline, size: 20),
title: Text(context.l10n.appSettings_showOtherNodes),
subtitle: Text(context.l10n.appSettings_showOtherNodesSubtitle),
value: settingsService.settings.mapShowOtherNodes,
onChanged: (value) => settingsService.setMapShowOtherNodes(value),
),
),
const Divider(height: 1, indent: 16),
InkWell(
onTap: () => _showMapRasterSourceDialog(context, settingsService),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Icon(
Icons.layers_outlined,
size: 20,
color: scheme.onSurfaceVariant,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.appSettings_rasterTileSource,
style: textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 2),
Text(
_mapRasterSourceSummary(settingsService.settings),
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: textTheme.bodySmall?.copyWith(
color: scheme.onSurfaceVariant,
),
),
],
),
),
Icon(
Icons.chevron_right,
color: scheme.onSurfaceVariant,
size: 16,
),
],
),
),
),
];
if (_isStadiaSource(settingsService.settings)) {
children.addAll([
const Divider(height: 1, indent: 16),
InkWell(
onTap: () => _showTimeFilterSheet(context, settingsService),
onTap: () => _showMapRasterEndpointDialog(context, settingsService),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Icon(
Icons.timer_outlined,
Icons.public_outlined,
size: 20,
color: scheme.onSurfaceVariant,
),
@@ -754,19 +932,14 @@ class AppSettingsScreen extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.appSettings_timeFilter,
context.l10n.appSettings_stadiaEndpoint,
style: textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 2),
Text(
settingsService.settings.mapTimeFilterHours == 0
? context.l10n.appSettings_timeFilterShowAll
: context.l10n.appSettings_timeFilterShowLast(
settingsService.settings.mapTimeFilterHours
.toInt(),
),
_mapRasterEndpointSummary(settingsService.settings),
style: textTheme.bodySmall?.copyWith(
color: scheme.onSurfaceVariant,
),
@@ -785,13 +958,13 @@ class AppSettingsScreen extends StatelessWidget {
),
const Divider(height: 1, indent: 16),
InkWell(
onTap: () => _showUnitsSheet(context, settingsService),
onTap: () => _showMapApiKeyDialog(context, settingsService),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Icon(
Icons.straighten,
Icons.key_outlined,
size: 20,
color: scheme.onSurfaceVariant,
),
@@ -801,17 +974,14 @@ class AppSettingsScreen extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.appSettings_unitsTitle,
context.l10n.appSettings_stadiaApiKey,
style: textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 2),
Text(
settingsService.settings.unitSystem ==
UnitSystem.imperial
? context.l10n.appSettings_unitsImperial
: context.l10n.appSettings_unitsMetric,
_mapApiKeySummary(context, settingsService.settings),
style: textTheme.bodySmall?.copyWith(
color: scheme.onSurfaceVariant,
),
@@ -828,59 +998,210 @@ class AppSettingsScreen extends StatelessWidget {
),
),
),
const Divider(height: 1, indent: 16),
InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const MapCacheScreen()),
);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Icon(
Icons.download_outlined,
size: 20,
color: scheme.onSurfaceVariant,
),
const SizedBox(width: 12),
Expanded(
]);
}
return Column(children: children);
}
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(BuildContext context, AppSettings settings) {
return context.l10n.appSettings_stadiaApiKeyConfigured(
_maskApiKey(settings.effectiveMapTileApiKey),
);
}
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: Text(context.l10n.appSettings_rasterTileSource),
content: SizedBox(
width: 360,
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(dialogContext).size.height * 0.6,
),
child: SingleChildScrollView(
child: RadioGroup<String>(
groupValue: selectedId,
onChanged: (value) {
if (value == null) return;
setState(() {
selectedId = value;
});
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
context.l10n.appSettings_offlineMapCache,
style: textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
for (final preset in MapRasterSourcePreset.values)
Builder(
builder: (context) {
final option = MapRasterSourceCatalog.fromPreset(
preset,
);
return RadioListTile<String>(
value: preset.id,
title: Text(option.label),
subtitle: Text(option.description),
);
},
),
),
const SizedBox(height: 2),
Text(
settingsService.settings.mapCacheBounds == null
? context.l10n.appSettings_noAreaSelected
: context.l10n.appSettings_areaSelectedZoom(
settingsService.settings.mapCacheMinZoom,
settingsService.settings.mapCacheMaxZoom,
),
style: textTheme.bodySmall?.copyWith(
color: scheme.onSurfaceVariant,
),
),
],
),
),
Icon(
Icons.chevron_right,
color: scheme.onSurfaceVariant,
size: 16,
),
],
),
),
),
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: Text(context.l10n.appSettings_stadiaEndpoint),
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 currentApiKey = settingsService.settings.mapTileApiKey?.trim() ?? '';
final maskedApiKey = _maskApiKey(
currentApiKey.isEmpty
? AppSettings.stadiaDemo
: currentApiKey,
);
final controller = TextEditingController(text: maskedApiKey);
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(context.l10n.appSettings_stadiaApiKey),
content: SizedBox(
width: 420,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(context.l10n.appSettings_stadiaApiKeyDialogDescription),
const SizedBox(height: 12),
TextField(
controller: controller,
autofocus: true,
autocorrect: false,
enableSuggestions: false,
autofillHints: const [AutofillHints.password],
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: '4e1bf343-3d91-4d9c-a8e1-1234567890ab',
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: Text(context.l10n.common_cancel),
),
TextButton(
onPressed: () async {
final apiKey = controller.text.trim();
await settingsService.setMapTileApiKey(
apiKey == maskedApiKey ? currentApiKey : apiKey,
);
if (!dialogContext.mounted) return;
Navigator.pop(dialogContext);
},
child: Text(context.l10n.common_save),
),
],
),
);
}
+1 -2
View File
@@ -22,7 +22,6 @@ import '../theme/mesh_theme.dart';
import '../widgets/adaptive_app_bar_title.dart';
import '../widgets/mesh_ui.dart';
import '../widgets/path_map_ui.dart';
import '../widgets/themed_map_tile_layer.dart';
class ChannelMessagePathScreen extends StatelessWidget {
final ChannelMessage message;
@@ -967,7 +966,7 @@ class _ChannelMessagePathMapScreenState
},
),
children: [
ThemedMapTileLayer(tileCache: tileCache),
tileCache.buildTileLayer(context),
AnimatedBuilder(
animation: _playback,
builder: (context, _) {
-2
View File
@@ -282,8 +282,6 @@ class _ChannelsScreenState extends State<ChannelsScreen>
buildDefaultDragHandles: false,
itemCount: filteredChannels.length,
onReorderItem: (oldIndex, newIndex) {
// onReorderItem already adjusts newIndex after the
// removed item, unlike the deprecated onReorder.
final reordered = List<Channel>.from(
filteredChannels,
);
+3 -4
View File
@@ -20,7 +20,6 @@ import '../widgets/app_bar.dart';
import '../widgets/quick_switch_bar.dart';
import '../icons/los_icon.dart';
import '../theme/mesh_theme.dart';
import '../widgets/themed_map_tile_layer.dart';
class LineOfSightEndpoint {
final String label;
@@ -357,7 +356,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,
@@ -443,8 +442,8 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
},
),
children: [
ThemedMapTileLayer(
tileCache: tileCache,
tileCache.buildTileLayer(
context,
opacity: _showTerrainLayer ? 1 : 0.72,
),
if (_result != null && _result!.segments.isNotEmpty)
+130 -6
View File
@@ -12,7 +12,6 @@ import '../widgets/adaptive_app_bar_title.dart';
import '../helpers/snack_bar_builder.dart';
import '../theme/mesh_theme.dart';
import '../widgets/mesh_ui.dart';
import '../widgets/themed_map_tile_layer.dart';
class MapCacheScreen extends StatefulWidget {
const MapCacheScreen({super.key});
@@ -34,6 +33,11 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
bool _isDownloading = false;
int _completedTiles = 0;
int _failedTiles = 0;
List<CachedTileInfo> _cachedTiles = const [];
int _cachedTileBytes = 0;
bool _isLoadingCachedTiles = false;
double _overlayZoom = 2.0;
LatLngBounds? _visibleBounds;
@override
void initState() {
@@ -123,8 +127,10 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
_minZoom = safeMin;
_maxZoom = safeMax;
_selectedBounds = bounds;
_visibleBounds = bounds;
});
_updateEstimate();
_refreshCachedTiles();
if (bounds != null) {
_mapController.fitCamera(
CameraFit.bounds(bounds: bounds, padding: const EdgeInsets.all(48)),
@@ -181,6 +187,7 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
Future<void> _startDownload() async {
final bounds = _selectedBounds;
final cacheService = context.read<MapTileCacheService>();
if (bounds == null) {
showDismissibleSnackBar(
context,
@@ -197,6 +204,18 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
return;
}
if (!cacheService.source.allowsBulkDownload) {
showDismissibleSnackBar(
context,
content: Text(
context.l10n.mapCache_bulkDownloadDisabledInConfig(
cacheService.source.label,
),
),
);
return;
}
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
@@ -219,8 +238,6 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
if (confirmed != true || !mounted) return;
final cacheService = context.read<MapTileCacheService>();
setState(() {
_isDownloading = true;
_completedTiles = 0;
@@ -254,6 +271,8 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
result.failed,
)
: context.l10n.mapCache_cachedTiles(result.downloaded);
await _refreshCachedTiles();
if (!mounted) return;
showDismissibleSnackBar(context, content: Text(message));
}
@@ -280,16 +299,61 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
final cacheService = context.read<MapTileCacheService>();
await cacheService.clearCache();
if (!mounted) return;
await _refreshCachedTiles();
if (!mounted) return;
showDismissibleSnackBar(
context,
content: Text(context.l10n.mapCache_offlineCacheCleared),
);
}
Future<void> _refreshCachedTiles() async {
if (!mounted) return;
setState(() {
_isLoadingCachedTiles = true;
});
final cacheService = context.read<MapTileCacheService>();
final inventory = await cacheService.getCachedTileInventory();
if (!mounted) return;
setState(() {
_cachedTiles = inventory.tiles;
_cachedTileBytes = inventory.totalBytes;
_isLoadingCachedTiles = false;
});
}
String _formatBytes(int bytes) {
if (bytes < 1024) return '$bytes B';
if (bytes < 1024 * 1024) {
return '${(bytes / 1024).toStringAsFixed(1)} KB';
}
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
}
@override
Widget build(BuildContext context) {
final tileCache = context.read<MapTileCacheService>();
final tileCache = context.watch<MapTileCacheService>();
final source = tileCache.source;
final selectedBounds = _selectedBounds;
final activeCachedTiles = tileCache.filterTilesForActiveSource(
_cachedTiles,
);
final cachedInSelection = tileCache.countTilesForBounds(
activeCachedTiles,
bounds: selectedBounds,
minZoom: _minZoom,
maxZoom: _maxZoom,
);
final overlayPolygons = tileCache.buildCachedTilePolygons(
activeCachedTiles,
zoom: _overlayZoom.round().clamp(0, 19).toInt(),
visibleBounds: _visibleBounds,
);
final cacheSummary =
'${context.l10n.mapCache_summarySource(source.label)}\n'
'${context.l10n.mapCache_summaryCachedTilesForSource(activeCachedTiles.length)}\n'
'${context.l10n.mapCache_summaryCachedInSelection(cachedInSelection)}\n'
'${context.l10n.mapCache_summaryApproxCacheSize(_formatBytes(_cachedTileBytes))}';
final l10n = context.l10n;
final scheme = Theme.of(context).colorScheme;
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
@@ -327,9 +391,23 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
)
: const KeyboardOptions.disabled(),
),
onPositionChanged: (camera, hasGesture) {
final nextZoom = camera.zoom;
final nextBounds = camera.visibleBounds;
if (_overlayZoom != nextZoom ||
_visibleBounds?.north != nextBounds.north ||
_visibleBounds?.south != nextBounds.south ||
_visibleBounds?.east != nextBounds.east ||
_visibleBounds?.west != nextBounds.west) {
setState(() {
_overlayZoom = nextZoom;
_visibleBounds = nextBounds;
});
}
},
),
children: [
ThemedMapTileLayer(tileCache: tileCache),
tileCache.buildTileLayer(context),
if (selectedBounds != null)
PolygonLayer(
polygons: [
@@ -341,9 +419,24 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
),
],
),
if (overlayPolygons.isNotEmpty)
PolygonLayer(polygons: overlayPolygons),
],
),
if (isDesktop) _buildDesktopMapControls(),
Positioned(
top: 12,
left: isDesktop ? 84 : 12,
child: Card(
child: Padding(
padding: const EdgeInsets.all(8),
child: Text(
'Z: ${_overlayZoom.round()}:',
style: const TextStyle(fontSize: 12),
),
),
),
),
Positioned(
top: 12,
right: 12,
@@ -445,6 +538,34 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
color: scheme.onSurfaceVariant,
),
),
const SizedBox(height: 12),
TextFormField(
key: ValueKey(
'$cacheSummary|${activeCachedTiles.length}|$cachedInSelection|$_cachedTileBytes',
),
initialValue: cacheSummary,
readOnly: true,
minLines: 4,
maxLines: 4,
decoration: InputDecoration(
labelText: _isLoadingCachedTiles
? l10n.mapCache_cachedTilesLabel
: l10n.mapCache_cachedTileSummaryLabel,
border: const OutlineInputBorder(),
),
),
if (!source.allowsBulkDownload) ...[
const SizedBox(height: 8),
Text(
l10n.mapCache_bulkDownloadDisabledForSource(
source.label,
),
style: MeshTheme.mono(
fontSize: 12,
color: MeshPalette.alert,
),
),
],
if (_isDownloading) ...[
const SizedBox(height: 8),
LinearProgressIndicator(
@@ -471,7 +592,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,
),
+124 -90
View File
@@ -25,7 +25,6 @@ import '../utils/battery_utils.dart';
import '../utils/route_transitions.dart';
import '../widgets/quick_switch_bar.dart';
import '../widgets/sync_progress_overlay.dart';
import '../widgets/themed_map_tile_layer.dart';
import '../icons/los_icon.dart';
import 'channels_screen.dart';
import 'chat_screen.dart';
@@ -107,6 +106,31 @@ class _MapScreenState extends State<MapScreen> {
super.dispose();
}
ColorScheme get _overlayScheme => Theme.of(context).colorScheme;
bool get _useDarkOverlay => Theme.of(context).brightness == Brightness.dark;
Color get _overlayPanelColor => _useDarkOverlay
? MapPalette.panelDark
: _overlayScheme.surfaceContainerLow.withValues(alpha: 0.96);
Color get _overlayPrimaryTextColor =>
_useDarkOverlay ? MapPalette.textPrimary : _overlayScheme.onSurface;
Color get _overlaySecondaryTextColor => _useDarkOverlay
? MapPalette.textSecondary
: _overlayScheme.onSurfaceVariant;
Color get _overlayMutedTextColor =>
_useDarkOverlay ? MapPalette.textMuted : _overlayScheme.onSurfaceVariant;
Color get _overlayBorderColor =>
_useDarkOverlay ? MapPalette.border : _overlayScheme.outlineVariant;
Color get _overlayShadowColor => _useDarkOverlay
? MapPalette.markerShadow
: Colors.black.withValues(alpha: 0.18);
_NodeAge _ageOf(Contact contact) {
final d = DateTime.now().difference(contact.lastSeen);
if (d.inMinutes <= 60) return _NodeAge.online;
@@ -233,12 +257,12 @@ class _MapScreenState extends State<MapScreen> {
bottom: 96,
child: DecoratedBox(
decoration: BoxDecoration(
color: MapPalette.panelDark,
color: _overlayPanelColor,
borderRadius: BorderRadius.circular(MeshRadii.md),
border: Border.all(color: MapPalette.border),
boxShadow: const [
border: Border.all(color: _overlayBorderColor),
boxShadow: [
BoxShadow(
color: MapPalette.markerShadow,
color: _overlayShadowColor,
blurRadius: 8,
offset: Offset(0, 3),
),
@@ -250,20 +274,20 @@ class _MapScreenState extends State<MapScreen> {
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
color: MapPalette.textPrimary,
color: _overlayPrimaryTextColor,
icon: const Icon(Icons.add),
visualDensity: VisualDensity.standard,
tooltip: context.l10n.map_zoomIn,
onPressed: () => _zoomMapBy(1),
),
IconButton(
color: MapPalette.textPrimary,
color: _overlayPrimaryTextColor,
icon: const Icon(Icons.remove),
tooltip: context.l10n.map_zoomOut,
onPressed: () => _zoomMapBy(-1),
),
IconButton(
color: MapPalette.textPrimary,
color: _overlayPrimaryTextColor,
icon: const Icon(Icons.crop_free),
tooltip: context.l10n.map_centerMap,
onPressed: () => _mapController.move(center, zoom),
@@ -328,6 +352,7 @@ class _MapScreenState extends State<MapScreen> {
final settingsService = context.read<AppSettingsService>();
final pathHistory = context.read<PathHistoryService>();
final tileCache = context.read<MapTileCacheService>();
final scheme = Theme.of(context).colorScheme;
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
final allContacts = connector.allContacts;
@@ -579,8 +604,8 @@ class _MapScreenState extends State<MapScreen> {
canPop: allowBack,
child: Scaffold(
appBar: AppBar(
backgroundColor: MapPalette.panelDark,
foregroundColor: MapPalette.textPrimary,
backgroundColor: scheme.surface,
foregroundColor: scheme.onSurface,
title: AppBarTitle(context.l10n.map_title),
centerTitle: true,
automaticallyImplyLeading: false,
@@ -753,7 +778,7 @@ class _MapScreenState extends State<MapScreen> {
},
),
children: [
ThemedMapTileLayer(tileCache: tileCache),
tileCache.buildTileLayer(context),
if (_polylines.isNotEmpty && _isBuildingPathTrace)
PolylineLayer(polylines: _polylines),
if (sharedMarkerPolylines.isNotEmpty)
@@ -906,7 +931,7 @@ class _MapScreenState extends State<MapScreen> {
_handleQuickSwitch(index, context),
contactsUnreadCount: connector.getTotalContactsUnreadCount(),
channelsUnreadCount: connector.getTotalChannelsUnreadCount(),
highContrast: true,
highContrast: _useDarkOverlay,
),
),
floatingActionButton:
@@ -1207,14 +1232,14 @@ class _MapScreenState extends State<MapScreen> {
height: 36,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: MapPalette.panelDark,
color: _overlayPanelColor,
border: Border.all(
color: guess.highConfidence ? color : MapPalette.textMuted,
color: guess.highConfidence ? color : _overlayMutedTextColor,
width: guess.highConfidence ? 2.5 : 2,
),
boxShadow: const [
boxShadow: [
BoxShadow(
color: MapPalette.markerShadow,
color: _overlayShadowColor,
blurRadius: 7,
offset: Offset(0, 2),
),
@@ -1223,7 +1248,7 @@ class _MapScreenState extends State<MapScreen> {
alignment: Alignment.center,
child: Icon(
Icons.not_listed_location,
color: MapPalette.textPrimary,
color: _overlayPrimaryTextColor,
size: 19,
),
),
@@ -1541,12 +1566,12 @@ class _MapScreenState extends State<MapScreen> {
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: MapPalette.panelDark,
color: _overlayPanelColor,
borderRadius: BorderRadius.circular(MeshRadii.xs),
border: Border.all(color: MapPalette.border),
boxShadow: const [
border: Border.all(color: _overlayBorderColor),
boxShadow: [
BoxShadow(
color: MapPalette.markerShadow,
color: _overlayShadowColor,
blurRadius: 4,
offset: Offset(0, 1),
),
@@ -1560,7 +1585,7 @@ class _MapScreenState extends State<MapScreen> {
style: MeshTheme.mono(
fontSize: 10,
fontWeight: FontWeight.w700,
color: MapPalette.textPrimary,
color: _overlayPrimaryTextColor,
),
),
),
@@ -1681,7 +1706,7 @@ class _MapScreenState extends State<MapScreen> {
decoration: BoxDecoration(
shape: BoxShape.circle,
color: statusColor,
border: Border.all(color: MapPalette.panelDark, width: 2),
border: Border.all(color: _overlayPanelColor, width: 2),
),
alignment: Alignment.center,
child: batteryLow
@@ -1704,10 +1729,10 @@ class _MapScreenState extends State<MapScreen> {
Expanded(
child: Text(
label,
style: const TextStyle(
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: MapPalette.textSecondary,
color: _overlaySecondaryTextColor,
),
overflow: TextOverflow.ellipsis,
),
@@ -1765,9 +1790,9 @@ class _MapScreenState extends State<MapScreen> {
children: [
Expanded(
child: Material(
color: MapPalette.panelDark,
color: _overlayPanelColor,
shape: StadiumBorder(
side: const BorderSide(color: MapPalette.border),
side: BorderSide(color: _overlayBorderColor),
),
clipBehavior: Clip.antiAlias,
child: TextField(
@@ -1775,17 +1800,15 @@ class _MapScreenState extends State<MapScreen> {
focusNode: _searchFocus,
decoration: InputDecoration(
hintText: context.l10n.map_searchHint,
hintStyle: const TextStyle(
color: MapPalette.textSecondary,
),
prefixIcon: const Icon(
hintStyle: TextStyle(color: _overlaySecondaryTextColor),
prefixIcon: Icon(
Icons.search,
size: 20,
color: MapPalette.textPrimary,
color: _overlayPrimaryTextColor,
),
suffixIcon: hasQuery
? IconButton(
color: MapPalette.textPrimary,
color: _overlayPrimaryTextColor,
icon: const Icon(Icons.close, size: 18),
onPressed: () {
setState(() {
@@ -1805,9 +1828,9 @@ class _MapScreenState extends State<MapScreen> {
vertical: 12,
),
),
style: const TextStyle(
style: TextStyle(
fontSize: 14,
color: MapPalette.textPrimary,
color: _overlayPrimaryTextColor,
fontWeight: FontWeight.w600,
),
cursorColor: MapPalette.selected,
@@ -1819,9 +1842,9 @@ class _MapScreenState extends State<MapScreen> {
),
const SizedBox(width: 8),
Material(
color: MapPalette.panelDark,
color: _overlayPanelColor,
shape: StadiumBorder(
side: const BorderSide(color: MapPalette.border),
side: BorderSide(color: _overlayBorderColor),
),
clipBehavior: Clip.antiAlias,
child: InkWell(
@@ -1845,17 +1868,17 @@ class _MapScreenState extends State<MapScreen> {
style: MeshTheme.mono(
fontSize: 13,
fontWeight: FontWeight.w700,
color: MapPalette.textPrimary,
color: _overlayPrimaryTextColor,
),
),
const SizedBox(width: 2),
AnimatedRotation(
turns: _statsExpanded ? 0.5 : 0,
duration: const Duration(milliseconds: 200),
child: const Icon(
child: Icon(
Icons.expand_more,
size: 16,
color: MapPalette.textPrimary,
color: _overlayPrimaryTextColor,
),
),
],
@@ -1954,12 +1977,12 @@ class _MapScreenState extends State<MapScreen> {
color: selected
? Color.alphaBlend(
accent.withValues(alpha: 0.34),
MapPalette.panelDark,
_overlayPanelColor,
)
: MapPalette.panelDark,
: _overlayPanelColor,
shape: StadiumBorder(
side: BorderSide(
color: selected ? accent : MapPalette.border,
color: selected ? accent : _overlayBorderColor,
width: selected ? 1.5 : 1,
),
),
@@ -1975,11 +1998,7 @@ class _MapScreenState extends State<MapScreen> {
mainAxisSize: MainAxisSize.min,
children: [
if (selected) ...[
const Icon(
Icons.check,
size: 13,
color: MapPalette.textPrimary,
),
Icon(Icons.check, size: 13, color: _overlayPrimaryTextColor),
const SizedBox(width: 4),
],
Text(
@@ -1988,8 +2007,8 @@ class _MapScreenState extends State<MapScreen> {
fontSize: 12.5,
fontWeight: FontWeight.w600,
color: selected
? MapPalette.textPrimary
: MapPalette.textSecondary,
? _overlayPrimaryTextColor
: _overlaySecondaryTextColor,
),
),
],
@@ -2019,14 +2038,14 @@ class _MapScreenState extends State<MapScreen> {
margin: const EdgeInsets.only(top: 6),
constraints: const BoxConstraints(maxHeight: 300),
decoration: BoxDecoration(
color: MapPalette.panelDark,
color: _overlayPanelColor,
borderRadius: BorderRadius.circular(MeshRadii.md),
border: Border.all(color: MapPalette.border),
boxShadow: const [
border: Border.all(color: _overlayBorderColor),
boxShadow: [
BoxShadow(
color: MapPalette.markerShadow,
color: _overlayShadowColor,
blurRadius: 10,
offset: Offset(0, 4),
offset: const Offset(0, 4),
),
],
),
@@ -2035,8 +2054,8 @@ class _MapScreenState extends State<MapScreen> {
padding: const EdgeInsets.all(16),
child: Text(
context.l10n.map_noResults,
style: const TextStyle(
color: MapPalette.textSecondary,
style: TextStyle(
color: _overlaySecondaryTextColor,
fontSize: 13,
),
),
@@ -2046,7 +2065,7 @@ class _MapScreenState extends State<MapScreen> {
padding: const EdgeInsets.symmetric(vertical: 4),
itemCount: results.length,
separatorBuilder: (_, _) =>
const Divider(height: 1, color: MapPalette.border),
Divider(height: 1, color: _overlayBorderColor),
itemBuilder: (context, index) {
final c = results[index];
final color = _getNodeColor(c.type);
@@ -2067,10 +2086,10 @@ class _MapScreenState extends State<MapScreen> {
children: [
Text(
c.name,
style: const TextStyle(
style: TextStyle(
fontSize: 13.5,
fontWeight: FontWeight.w600,
color: MapPalette.textPrimary,
color: _overlayPrimaryTextColor,
),
overflow: TextOverflow.ellipsis,
),
@@ -2078,7 +2097,7 @@ class _MapScreenState extends State<MapScreen> {
c.publicKeyHex.substring(0, 12),
style: MeshTheme.mono(
fontSize: 10.5,
color: MapPalette.textSecondary,
color: _overlaySecondaryTextColor,
),
),
],
@@ -2088,13 +2107,13 @@ class _MapScreenState extends State<MapScreen> {
Icon(
Icons.chevron_right,
size: 18,
color: MapPalette.textSecondary,
color: _overlaySecondaryTextColor,
)
else
Text(
context.l10n.map_noGps.toUpperCase(),
style: MeshTheme.accentLabel(
color: MapPalette.textMuted,
color: _overlayMutedTextColor,
fontSize: 8.5,
),
),
@@ -2154,14 +2173,14 @@ class _MapScreenState extends State<MapScreen> {
width: 230,
padding: const EdgeInsets.fromLTRB(14, 12, 14, 12),
decoration: BoxDecoration(
color: MapPalette.panelDark,
color: _overlayPanelColor,
borderRadius: BorderRadius.circular(MeshRadii.md),
border: Border.all(color: MapPalette.border),
boxShadow: const [
border: Border.all(color: _overlayBorderColor),
boxShadow: [
BoxShadow(
color: MapPalette.markerShadow,
color: _overlayShadowColor,
blurRadius: 10,
offset: Offset(0, 4),
offset: const Offset(0, 4),
),
],
),
@@ -2178,7 +2197,7 @@ class _MapScreenState extends State<MapScreen> {
),
_statRow(context.l10n.map_hidden, hiddenCount, MapPalette.offline),
_statRow(context.l10n.map_markers, pinCount, MapPalette.shared),
const Divider(height: 16, color: MapPalette.border),
Divider(height: 16, color: _overlayBorderColor),
_buildLegendItem(
Icons.person,
context.l10n.map_chat,
@@ -2208,7 +2227,7 @@ class _MapScreenState extends State<MapScreen> {
_buildLegendItem(
Icons.not_listed_location,
context.l10n.map_guessedLocation,
MapPalette.textMuted,
_overlayMutedTextColor,
),
],
),
@@ -2229,7 +2248,10 @@ class _MapScreenState extends State<MapScreen> {
Expanded(
child: Text(
label,
style: TextStyle(fontSize: 12.5, color: MapPalette.textSecondary),
style: TextStyle(
fontSize: 12.5,
color: _overlaySecondaryTextColor,
),
overflow: TextOverflow.ellipsis,
),
),
@@ -2238,7 +2260,7 @@ class _MapScreenState extends State<MapScreen> {
style: MeshTheme.mono(
fontSize: 13,
fontWeight: FontWeight.w700,
color: MapPalette.textPrimary,
color: _overlayPrimaryTextColor,
),
),
],
@@ -2271,8 +2293,8 @@ class _MapScreenState extends State<MapScreen> {
child: MeshCard(
margin: EdgeInsets.zero,
padding: const EdgeInsets.fromLTRB(14, 12, 8, 12),
color: MapPalette.panelDark,
borderColor: MapPalette.border,
color: _overlayPanelColor,
borderColor: _overlayBorderColor,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
@@ -2295,10 +2317,10 @@ class _MapScreenState extends State<MapScreen> {
Flexible(
child: Text(
contact.name,
style: const TextStyle(
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
color: MapPalette.textPrimary,
color: _overlayPrimaryTextColor,
),
overflow: TextOverflow.ellipsis,
),
@@ -2326,9 +2348,9 @@ class _MapScreenState extends State<MapScreen> {
Flexible(
child: Text(
contact.typeLabel(context.l10n),
style: const TextStyle(
style: TextStyle(
fontSize: 11.5,
color: MapPalette.textSecondary,
color: _overlaySecondaryTextColor,
),
overflow: TextOverflow.ellipsis,
),
@@ -2340,13 +2362,13 @@ class _MapScreenState extends State<MapScreen> {
),
if (pos != null)
IconButton(
color: MapPalette.textPrimary,
color: _overlayPrimaryTextColor,
icon: const Icon(Icons.center_focus_strong, size: 20),
tooltip: context.l10n.map_centerOnNode,
onPressed: () => _mapController.move(pos, max(_zoom, 15)),
),
IconButton(
color: MapPalette.textPrimary,
color: _overlayPrimaryTextColor,
icon: const Icon(Icons.close, size: 20),
onPressed: _clearSelection,
),
@@ -2407,14 +2429,17 @@ class _MapScreenState extends State<MapScreen> {
Text(
label.toUpperCase(),
style: MeshTheme.accentLabel(
color: MapPalette.textMuted,
color: _overlayMutedTextColor,
fontSize: 8,
),
),
const SizedBox(height: 1),
Text(
value,
style: MeshTheme.mono(fontSize: 11.5, color: MapPalette.textPrimary),
style: MeshTheme.mono(
fontSize: 11.5,
color: _overlayPrimaryTextColor,
),
),
],
);
@@ -3555,14 +3580,14 @@ class _MapScreenState extends State<MapScreen> {
right: 16,
child: DecoratedBox(
decoration: BoxDecoration(
color: MapPalette.panelDark,
color: _overlayPanelColor,
borderRadius: BorderRadius.circular(MeshRadii.md),
border: Border.all(color: MapPalette.border),
boxShadow: const [
border: Border.all(color: _overlayBorderColor),
boxShadow: [
BoxShadow(
color: MapPalette.markerShadow,
color: _overlayShadowColor,
blurRadius: 10,
offset: Offset(0, 4),
offset: const Offset(0, 4),
),
],
),
@@ -3575,18 +3600,27 @@ class _MapScreenState extends State<MapScreen> {
children: [
Text(
l10n.contacts_pathTrace,
style: TextStyle(fontWeight: FontWeight.bold),
style: TextStyle(
fontWeight: FontWeight.bold,
color: _overlayPrimaryTextColor,
),
),
if (_pathTrace.isEmpty) const SizedBox(height: 8),
if (_pathTrace.isEmpty)
Text(l10n.map_tapToAdd, style: TextStyle(fontSize: 12)),
Text(
l10n.map_tapToAdd,
style: TextStyle(
fontSize: 12,
color: _overlaySecondaryTextColor,
),
),
const SizedBox(height: 6),
if (_pathTrace.isNotEmpty)
Text(
"${l10n.path_currentPathLabel} ${formatDistance(getPathDistanceMeters(_points), isImperial: isImperial)}",
style: MeshTheme.mono(
fontSize: 12,
color: MapPalette.textSecondary,
color: _overlaySecondaryTextColor,
),
),
SelectableText(
+1 -2
View File
@@ -19,7 +19,6 @@ import 'package:meshcore_open/services/path_history_service.dart';
import 'package:meshcore_open/utils/app_logger.dart';
import 'package:meshcore_open/widgets/path_map_ui.dart';
import 'package:meshcore_open/widgets/snr_indicator.dart';
import 'package:meshcore_open/widgets/themed_map_tile_layer.dart';
import 'package:provider/provider.dart';
import '../theme/mesh_theme.dart';
@@ -1399,7 +1398,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
},
),
children: [
ThemedMapTileLayer(tileCache: tileCache),
tileCache.buildTileLayer(context),
AnimatedBuilder(
animation: _playback,
builder: (context, _) {