mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-07-12 20:02:01 +10:00
Merge pull request #461 from ericszimmermann/ez_alternative_maps
Alternative Maps
This commit is contained in:
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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, _) {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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, _) {
|
||||
|
||||
Reference in New Issue
Block a user