From 4ece02ceff3b83e2f4cae5f286dd0b8a8ac9ca0a Mon Sep 17 00:00:00 2001 From: Strycher Date: Mon, 20 Jul 2026 00:13:27 -0400 Subject: [PATCH] feat(#291): map layer toggles in the nav panel Epic D. The Map view has no list, so its nav panel was empty. It now carries the map's own layer controls. Those six toggles (chat nodes, repeaters, other nodes, discovery contacts, guessed locations, overlaps) previously existed only in a modal behind the floating button, so filtering the map meant covering the map with a dialog to do it. In the panel, and especially pinned on a wide screen, they stay visible and the map updates underneath as they are flipped. Both surfaces read and write the same AppSettings, so a change in one is reflected in the other. The floating-button dialog is deliberately left in place: on a phone it is fewer taps than opening the drawer, and the panel is the pinned-desktop path. If that duplication is unwanted, removing the dialog is a one-line follow-up. No new settings or storage; the existing setters were reused unchanged. flutter analyze clean, dart format clean, 469 tests pass. Not yet run on hardware. --- lib/screens/map_screen.dart | 2 + lib/widgets/map_layer_panel.dart | 99 ++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 lib/widgets/map_layer_panel.dart diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 7cb7d27..c902cb6 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -24,6 +24,7 @@ import '../services/map_tile_cache_service.dart'; import '../utils/contact_search.dart'; import '../utils/route_transitions.dart'; import '../widgets/app_shell.dart'; +import '../widgets/map_layer_panel.dart'; import '../widgets/sync_progress_overlay.dart'; import '../icons/los_icon.dart'; import 'channels_screen.dart'; @@ -418,6 +419,7 @@ class _MapScreenState extends State { _handleQuickSwitch(index, context), contactsUnreadCount: connector.getTotalContactsUnreadCount(), channelsUnreadCount: connector.getTotalChannelsUnreadCount(), + drawerContent: const MapLayerPanel(), onDisconnect: () => _disconnect(context, connector), onSettings: () => Navigator.push( context, diff --git a/lib/widgets/map_layer_panel.dart b/lib/widgets/map_layer_panel.dart new file mode 100644 index 0000000..13df116 --- /dev/null +++ b/lib/widgets/map_layer_panel.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../l10n/l10n.dart'; +import '../services/app_settings_service.dart'; + +/// Map layer toggles for the nav panel. +/// +/// The Map view has no list to show, so its panel carries the layer controls +/// that otherwise live in a modal behind the floating button. Pinned on a wide +/// screen these stay visible and the map updates underneath as they are +/// flipped, instead of a dialog covering the thing being filtered. +/// +/// The same settings back both surfaces, so a change here shows in the dialog +/// and vice versa. +class MapLayerPanel extends StatelessWidget { + const MapLayerPanel({super.key}); + + @override + Widget build(BuildContext context) { + final service = context.watch(); + final settings = service.settings; + final l10n = context.l10n; + final theme = Theme.of(context); + + return ListView( + padding: EdgeInsets.zero, + children: [ + _sectionHeader(theme, l10n.map_nodeTypes), + _toggle( + label: l10n.map_chatNodes, + icon: Icons.person_outline, + value: settings.mapShowChatNodes, + onChanged: service.setMapShowChatNodes, + ), + _toggle( + label: l10n.map_repeaters, + icon: Icons.cell_tower, + value: settings.mapShowRepeaters, + onChanged: service.setMapShowRepeaters, + ), + _toggle( + label: l10n.map_otherNodes, + icon: Icons.help_outline, + value: settings.mapShowOtherNodes, + onChanged: service.setMapShowOtherNodes, + ), + const Divider(height: 1), + _sectionHeader(theme, l10n.map_filterNodes), + _toggle( + label: l10n.map_showDiscoveryContacts, + icon: Icons.person_search, + value: settings.mapShowDiscoveryContacts, + onChanged: service.setMapShowDiscoveryContacts, + ), + _toggle( + label: l10n.map_showGuessedLocations, + icon: Icons.location_searching, + value: settings.mapShowGuessedLocations, + onChanged: service.setMapShowGuessedLocations, + ), + _toggle( + label: l10n.map_showOverlaps, + icon: Icons.layers, + value: settings.mapShowOverlaps, + onChanged: service.setMapShowOverlaps, + ), + ], + ); + } + + Widget _sectionHeader(ThemeData theme, String label) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), + child: Text( + label, + style: theme.textTheme.labelMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w700, + ), + ), + ); + } + + Widget _toggle({ + required String label, + required IconData icon, + required bool value, + required ValueChanged onChanged, + }) { + return SwitchListTile( + dense: true, + secondary: Icon(icon, size: 20), + title: Text(label, maxLines: 2, overflow: TextOverflow.ellipsis), + value: value, + onChanged: onChanged, + ); + } +}