fix(#291): map name toggle, panel footer everywhere, and back-button behavior

Owner hardware test on an S25 FE and Windows turned up five issues. Three of
them shared one root cause.

**Map: always show names.** New mapAlwaysShowNames setting, toggled from the
map panel. Names were shown only past zoom 14; the override wins at any zoom.
Evaluated at build rather than only on camera movement, so flipping the switch
repaints immediately instead of waiting for the next pan.

**Panel footer on every screen.** Disconnect and Settings now also appear
inside a channel. The footer is app-level, so it belongs anywhere the panel
is, not just the primary views.

**Back button (three reported symptoms, one cause).** Channels, Contacts and
Map each wrapped themselves in `PopScope(canPop: !isConnected)`, so while
connected the system back press was swallowed whole. That explains all three:
an open drawer would not close, Android would not background the app, and on
Windows the pinned layout has no Scaffold drawer, so the app bar auto-implied
a back arrow whose press PopScope then discarded - a button that visibly did
nothing.

AppShell now owns back handling for every screen that uses it, in order:
close an open drawer, else pop the route (a pushed chat returns to its list),
else hand back to the OS via SystemNavigator.pop() so Android returns to the
home screen or the previous app. The three per-screen PopScope wrappers are
removed. AppShell becomes stateful to hold the scaffold key this needs.

flutter analyze clean, dart format clean, 469 tests pass. Not yet re-tested on
hardware.
integration/290-306-335
Strycher 2 days ago
parent 4ece02ceff
commit 9aaebce77c

@ -976,6 +976,7 @@
"map_repeaters": "Repeaters", "map_repeaters": "Repeaters",
"map_otherNodes": "Other Nodes", "map_otherNodes": "Other Nodes",
"map_showOverlaps": "Repeater Key Overlaps", "map_showOverlaps": "Repeater Key Overlaps",
"map_alwaysShowNames": "Always show names",
"map_keyPrefix": "Key Prefix", "map_keyPrefix": "Key Prefix",
"map_filterByKeyPrefix": "Filter by key prefix", "map_filterByKeyPrefix": "Filter by key prefix",
"map_publicKeyPrefix": "Public key prefix", "map_publicKeyPrefix": "Public key prefix",

@ -3412,6 +3412,12 @@ abstract class AppLocalizations {
/// **'Repeater Key Overlaps'** /// **'Repeater Key Overlaps'**
String get map_showOverlaps; String get map_showOverlaps;
/// No description provided for @map_alwaysShowNames.
///
/// In en, this message translates to:
/// **'Always show names'**
String get map_alwaysShowNames;
/// No description provided for @map_keyPrefix. /// No description provided for @map_keyPrefix.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

@ -1890,6 +1890,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get map_showOverlaps => 'Покриване на ключа на повтаряча'; String get map_showOverlaps => 'Покриване на ключа на повтаряча';
@override
String get map_alwaysShowNames => 'Always show names';
@override @override
String get map_keyPrefix => 'Префикс на ключа'; String get map_keyPrefix => 'Префикс на ключа';

@ -1887,6 +1887,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get map_showOverlaps => 'Überlappungen der Repeater-Taste'; String get map_showOverlaps => 'Überlappungen der Repeater-Taste';
@override
String get map_alwaysShowNames => 'Always show names';
@override @override
String get map_keyPrefix => 'Schlüsselpräfix'; String get map_keyPrefix => 'Schlüsselpräfix';

@ -1854,6 +1854,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get map_showOverlaps => 'Repeater Key Overlaps'; String get map_showOverlaps => 'Repeater Key Overlaps';
@override
String get map_alwaysShowNames => 'Always show names';
@override @override
String get map_keyPrefix => 'Key Prefix'; String get map_keyPrefix => 'Key Prefix';

@ -1885,6 +1885,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get map_showOverlaps => 'Superposiciones de tecla repetidora'; String get map_showOverlaps => 'Superposiciones de tecla repetidora';
@override
String get map_alwaysShowNames => 'Always show names';
@override @override
String get map_keyPrefix => 'Prefijo de clave'; String get map_keyPrefix => 'Prefijo de clave';

@ -1895,6 +1895,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get map_showOverlaps => 'Chevauchement de la touche répétitive'; String get map_showOverlaps => 'Chevauchement de la touche répétitive';
@override
String get map_alwaysShowNames => 'Always show names';
@override @override
String get map_keyPrefix => 'Préfixe clé'; String get map_keyPrefix => 'Préfixe clé';

@ -1896,6 +1896,9 @@ class AppLocalizationsHu extends AppLocalizations {
@override @override
String get map_showOverlaps => 'Az ismétlő kulcsok ütköznek'; String get map_showOverlaps => 'Az ismétlő kulcsok ütköznek';
@override
String get map_alwaysShowNames => 'Always show names';
@override @override
String get map_keyPrefix => 'Kulcsfontosságú előtag'; String get map_keyPrefix => 'Kulcsfontosságú előtag';

@ -1888,6 +1888,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get map_showOverlaps => 'Sovrapposizioni della chiave ripetitore'; String get map_showOverlaps => 'Sovrapposizioni della chiave ripetitore';
@override
String get map_alwaysShowNames => 'Always show names';
@override @override
String get map_keyPrefix => 'Prefisso Chiave'; String get map_keyPrefix => 'Prefisso Chiave';

@ -1811,6 +1811,9 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get map_showOverlaps => 'リピーターキーの重複'; String get map_showOverlaps => 'リピーターキーの重複';
@override
String get map_alwaysShowNames => 'Always show names';
@override @override
String get map_keyPrefix => '主要なプレフィックス'; String get map_keyPrefix => '主要なプレフィックス';

@ -1807,6 +1807,9 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get map_showOverlaps => '반복 키 중복'; String get map_showOverlaps => '반복 키 중복';
@override
String get map_alwaysShowNames => 'Always show names';
@override @override
String get map_keyPrefix => '핵심 접두사'; String get map_keyPrefix => '핵심 접두사';

@ -1874,6 +1874,9 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get map_showOverlaps => 'Herhalingssleutel overlapt'; String get map_showOverlaps => 'Herhalingssleutel overlapt';
@override
String get map_alwaysShowNames => 'Always show names';
@override @override
String get map_keyPrefix => 'Prefix sleutel'; String get map_keyPrefix => 'Prefix sleutel';

@ -1900,6 +1900,9 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get map_showOverlaps => 'Nakładające się klucze przekaźników'; String get map_showOverlaps => 'Nakładające się klucze przekaźników';
@override
String get map_alwaysShowNames => 'Always show names';
@override @override
String get map_keyPrefix => 'Prefiks klucza'; String get map_keyPrefix => 'Prefiks klucza';

@ -1885,6 +1885,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get map_showOverlaps => 'Sobreposições da Chave Repeater'; String get map_showOverlaps => 'Sobreposições da Chave Repeater';
@override
String get map_alwaysShowNames => 'Always show names';
@override @override
String get map_keyPrefix => 'Prefixo Chave'; String get map_keyPrefix => 'Prefixo Chave';

@ -1889,6 +1889,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get map_showOverlaps => 'Перекрытия ключа повтора'; String get map_showOverlaps => 'Перекрытия ключа повтора';
@override
String get map_alwaysShowNames => 'Always show names';
@override @override
String get map_keyPrefix => 'Префикс ключа'; String get map_keyPrefix => 'Префикс ключа';

@ -1876,6 +1876,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get map_showOverlaps => 'Prekrývanie opakovača kľúča'; String get map_showOverlaps => 'Prekrývanie opakovača kľúča';
@override
String get map_alwaysShowNames => 'Always show names';
@override @override
String get map_keyPrefix => 'Päťciferné predpona'; String get map_keyPrefix => 'Päťciferné predpona';

@ -1871,6 +1871,9 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get map_showOverlaps => 'Prekrivanje ključa ponovnega predvajanja'; String get map_showOverlaps => 'Prekrivanje ključa ponovnega predvajanja';
@override
String get map_alwaysShowNames => 'Always show names';
@override @override
String get map_keyPrefix => 'Predpona ključa'; String get map_keyPrefix => 'Predpona ključa';

@ -1864,6 +1864,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get map_showOverlaps => 'Repeater-nyckelöverlappningar'; String get map_showOverlaps => 'Repeater-nyckelöverlappningar';
@override
String get map_alwaysShowNames => 'Always show names';
@override @override
String get map_keyPrefix => 'Nyckelprefix'; String get map_keyPrefix => 'Nyckelprefix';

@ -1884,6 +1884,9 @@ class AppLocalizationsUk extends AppLocalizations {
@override @override
String get map_showOverlaps => 'Перекриття ключів ретрансляторів'; String get map_showOverlaps => 'Перекриття ключів ретрансляторів';
@override
String get map_alwaysShowNames => 'Always show names';
@override @override
String get map_keyPrefix => 'Префікс ключа'; String get map_keyPrefix => 'Префікс ключа';

@ -1778,6 +1778,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get map_showOverlaps => '重复键重叠'; String get map_showOverlaps => '重复键重叠';
@override
String get map_alwaysShowNames => 'Always show names';
@override @override
String get map_keyPrefix => '关键字前缀'; String get map_keyPrefix => '关键字前缀';

@ -112,6 +112,9 @@ class AppSettings {
final bool mapShowChatNodes; final bool mapShowChatNodes;
final bool mapShowOtherNodes; final bool mapShowOtherNodes;
final bool mapShowOverlaps; final bool mapShowOverlaps;
/// Show node names at any zoom, instead of only past the zoom threshold.
final bool mapAlwaysShowNames;
final double mapTimeFilterHours; // 0 = all time final double mapTimeFilterHours; // 0 = all time
final bool mapKeyPrefixEnabled; final bool mapKeyPrefixEnabled;
final String mapKeyPrefix; final String mapKeyPrefix;
@ -188,6 +191,7 @@ class AppSettings {
this.mapShowChatNodes = true, this.mapShowChatNodes = true,
this.mapShowOtherNodes = true, this.mapShowOtherNodes = true,
this.mapShowOverlaps = false, this.mapShowOverlaps = false,
this.mapAlwaysShowNames = false,
this.mapTimeFilterHours = 0, // Default to all time this.mapTimeFilterHours = 0, // Default to all time
this.mapKeyPrefixEnabled = false, this.mapKeyPrefixEnabled = false,
this.mapKeyPrefix = '', this.mapKeyPrefix = '',
@ -252,6 +256,7 @@ class AppSettings {
'map_show_chat_nodes': mapShowChatNodes, 'map_show_chat_nodes': mapShowChatNodes,
'map_show_other_nodes': mapShowOtherNodes, 'map_show_other_nodes': mapShowOtherNodes,
'map_show_overlaps': mapShowOverlaps, 'map_show_overlaps': mapShowOverlaps,
'map_always_show_names': mapAlwaysShowNames,
'map_time_filter_hours': mapTimeFilterHours, 'map_time_filter_hours': mapTimeFilterHours,
'map_key_prefix_enabled': mapKeyPrefixEnabled, 'map_key_prefix_enabled': mapKeyPrefixEnabled,
'map_key_prefix': mapKeyPrefix, 'map_key_prefix': mapKeyPrefix,
@ -338,6 +343,7 @@ class AppSettings {
mapShowChatNodes: json['map_show_chat_nodes'] as bool? ?? true, mapShowChatNodes: json['map_show_chat_nodes'] as bool? ?? true,
mapShowOtherNodes: json['map_show_other_nodes'] as bool? ?? true, mapShowOtherNodes: json['map_show_other_nodes'] as bool? ?? true,
mapShowOverlaps: json['map_show_overlaps'] as bool? ?? false, mapShowOverlaps: json['map_show_overlaps'] as bool? ?? false,
mapAlwaysShowNames: json['map_always_show_names'] as bool? ?? false,
mapTimeFilterHours: mapTimeFilterHours:
(json['map_time_filter_hours'] as num?)?.toDouble() ?? 0, (json['map_time_filter_hours'] as num?)?.toDouble() ?? 0,
mapKeyPrefixEnabled: json['map_key_prefix_enabled'] as bool? ?? false, mapKeyPrefixEnabled: json['map_key_prefix_enabled'] as bool? ?? false,
@ -462,6 +468,7 @@ class AppSettings {
bool? mapShowChatNodes, bool? mapShowChatNodes,
bool? mapShowOtherNodes, bool? mapShowOtherNodes,
bool? mapShowOverlaps, bool? mapShowOverlaps,
bool? mapAlwaysShowNames,
double? mapTimeFilterHours, double? mapTimeFilterHours,
bool? mapKeyPrefixEnabled, bool? mapKeyPrefixEnabled,
String? mapKeyPrefix, String? mapKeyPrefix,
@ -510,6 +517,7 @@ class AppSettings {
mapShowChatNodes: mapShowChatNodes ?? this.mapShowChatNodes, mapShowChatNodes: mapShowChatNodes ?? this.mapShowChatNodes,
mapShowOtherNodes: mapShowOtherNodes ?? this.mapShowOtherNodes, mapShowOtherNodes: mapShowOtherNodes ?? this.mapShowOtherNodes,
mapShowOverlaps: mapShowOverlaps ?? this.mapShowOverlaps, mapShowOverlaps: mapShowOverlaps ?? this.mapShowOverlaps,
mapAlwaysShowNames: mapAlwaysShowNames ?? this.mapAlwaysShowNames,
mapTimeFilterHours: mapTimeFilterHours ?? this.mapTimeFilterHours, mapTimeFilterHours: mapTimeFilterHours ?? this.mapTimeFilterHours,
mapKeyPrefixEnabled: mapKeyPrefixEnabled ?? this.mapKeyPrefixEnabled, mapKeyPrefixEnabled: mapKeyPrefixEnabled ?? this.mapKeyPrefixEnabled,
mapKeyPrefix: mapKeyPrefix ?? this.mapKeyPrefix, mapKeyPrefix: mapKeyPrefix ?? this.mapKeyPrefix,

@ -29,6 +29,8 @@ import '../services/chat_text_scale_service.dart';
import '../services/translation_service.dart'; import '../services/translation_service.dart';
import '../utils/emoji_utils.dart'; import '../utils/emoji_utils.dart';
import '../utils/route_transitions.dart'; import '../utils/route_transitions.dart';
import 'settings_screen.dart';
import '../utils/dialog_utils.dart';
import '../widgets/app_shell.dart'; import '../widgets/app_shell.dart';
import '../widgets/channel_drawer_list.dart'; import '../widgets/channel_drawer_list.dart';
import '../widgets/mention_autocomplete.dart'; import '../widgets/mention_autocomplete.dart';
@ -287,6 +289,11 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
); );
} }
Future<void> _disconnect(BuildContext context) async {
final connector = context.read<MeshCoreConnector>();
await showDisconnectDialog(context, connector);
}
/// Bottom-bar navigation out of an open channel. /// Bottom-bar navigation out of an open channel.
/// ///
/// Tapping Channels returns to the channel list. Tapping Contacts or Map /// Tapping Channels returns to the channel list. Tapping Contacts or Map
@ -351,6 +358,13 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
currentChannelIndex: _currentChannel.index, currentChannelIndex: _currentChannel.index,
onChannelSelected: _switchChannel, onChannelSelected: _switchChannel,
), ),
// Present here too: the footer is app-level, so it belongs on every
// screen carrying the panel, not just the primary views.
onDisconnect: () => _disconnect(context),
onSettings: () => Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SettingsScreen()),
),
appBarBuilder: (context, pinned) => AppBar( appBarBuilder: (context, pinned) => AppBar(
// Pinned: the panel is already docked, so no hamburger is needed. // Pinned: the panel is already docked, so no hamburger is needed.
// Unpinned: this is a pushed route, so Scaffold would put a back arrow // Unpinned: this is a pushed route, so Scaffold would put a back arrow

@ -97,11 +97,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
final allowBack = !connector.isConnected; return AppShell(
return PopScope(
canPop: allowBack,
child: AppShell(
selectedIndex: 1, selectedIndex: 1,
onDestinationSelected: (index) => _handleQuickSwitch(index, context), onDestinationSelected: (index) => _handleQuickSwitch(index, context),
contactsUnreadCount: connector.getTotalContactsUnreadCount(), contactsUnreadCount: connector.getTotalContactsUnreadCount(),
@ -325,7 +321,6 @@ class _ChannelsScreenState extends State<ChannelsScreen>
tooltip: context.l10n.channels_addChannel, tooltip: context.l10n.channels_addChannel,
child: const Icon(Icons.add), child: const Icon(Icons.add),
), ),
),
); );
} }

@ -317,10 +317,7 @@ class _ContactsScreenState extends State<ContactsScreen>
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
final allowBack = !connector.isConnected; return AppShell(
return PopScope(
canPop: allowBack,
child: AppShell(
selectedIndex: 0, selectedIndex: 0,
onDestinationSelected: (index) => _handleQuickSwitch(index, context), onDestinationSelected: (index) => _handleQuickSwitch(index, context),
contactsUnreadCount: connector.getTotalContactsUnreadCount(), contactsUnreadCount: connector.getTotalContactsUnreadCount(),
@ -417,7 +414,6 @@ class _ContactsScreenState extends State<ContactsScreen>
], ],
), ),
body: _buildContactsBody(context, connector), body: _buildContactsBody(context, connector),
),
); );
} }

@ -399,7 +399,8 @@ class _MapScreenState extends State<MapScreen> {
// Re center map after removed markers have loaded // Re center map after removed markers have loaded
if (!_hasInitializedMap && _removedMarkersLoaded) { if (!_hasInitializedMap && _removedMarkersLoaded) {
_hasInitializedMap = true; _hasInitializedMap = true;
_showNodeLabels = initialZoom >= _labelZoomThreshold; _showNodeLabels =
settings.mapAlwaysShowNames || initialZoom >= _labelZoomThreshold;
if (hasMapContent) { if (hasMapContent) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) { if (mounted) {
@ -409,14 +410,9 @@ class _MapScreenState extends State<MapScreen> {
} }
} }
final allowBack = !connector.isConnected; return AppShell(
return PopScope(
canPop: allowBack,
child: AppShell(
selectedIndex: 2, selectedIndex: 2,
onDestinationSelected: (index) => onDestinationSelected: (index) => _handleQuickSwitch(index, context),
_handleQuickSwitch(index, context),
contactsUnreadCount: connector.getTotalContactsUnreadCount(), contactsUnreadCount: connector.getTotalContactsUnreadCount(),
channelsUnreadCount: connector.getTotalChannelsUnreadCount(), channelsUnreadCount: connector.getTotalChannelsUnreadCount(),
drawerContent: const MapLayerPanel(), drawerContent: const MapLayerPanel(),
@ -538,7 +534,9 @@ class _MapScreenState extends State<MapScreen> {
); );
}, },
onPositionChanged: (camera, hasGesture) { onPositionChanged: (camera, hasGesture) {
final shouldShow = camera.zoom >= _labelZoomThreshold; final shouldShow =
settings.mapAlwaysShowNames ||
camera.zoom >= _labelZoomThreshold;
if (shouldShow != _showNodeLabels && mounted) { if (shouldShow != _showNodeLabels && mounted) {
setState(() { setState(() {
_showNodeLabels = shouldShow; _showNodeLabels = shouldShow;
@ -576,12 +574,14 @@ class _MapScreenState extends State<MapScreen> {
if (!settings.mapShowOverlaps) if (!settings.mapShowOverlaps)
..._buildGuessedMarker( ..._buildGuessedMarker(
guessedLocations, guessedLocations,
showLabels: _showNodeLabels, showLabels:
settings.mapAlwaysShowNames || _showNodeLabels,
), ),
..._buildMarkers( ..._buildMarkers(
contactsWithLocation, contactsWithLocation,
settings, settings,
showLabels: _showNodeLabels, showLabels:
settings.mapAlwaysShowNames || _showNodeLabels,
), ),
...sharedMarkers.map(_buildSharedMarker), ...sharedMarkers.map(_buildSharedMarker),
if (connector.selfLatitude != null && if (connector.selfLatitude != null &&
@ -606,9 +606,7 @@ class _MapScreenState extends State<MapScreen> {
), ),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withValues( color: Colors.black.withValues(alpha: 0.3),
alpha: 0.3,
),
blurRadius: 4, blurRadius: 4,
offset: const Offset(0, 2), offset: const Offset(0, 2),
), ),
@ -660,7 +658,6 @@ class _MapScreenState extends State<MapScreen> {
tooltip: context.l10n.map_filterNodes, tooltip: context.l10n.map_filterNodes,
child: const Icon(Icons.filter_list), child: const Icon(Icons.filter_list),
), ),
),
); );
}, },
); );

@ -76,6 +76,10 @@ class AppSettingsService extends ChangeNotifier {
await updateSettings(_settings.copyWith(mapShowOverlaps: value)); await updateSettings(_settings.copyWith(mapShowOverlaps: value));
} }
Future<void> setMapAlwaysShowNames(bool value) async {
await updateSettings(_settings.copyWith(mapAlwaysShowNames: value));
}
Future<void> setMapTimeFilterHours(double value) async { Future<void> setMapTimeFilterHours(double value) async {
await updateSettings(_settings.copyWith(mapTimeFilterHours: value)); await updateSettings(_settings.copyWith(mapTimeFilterHours: value));
} }

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
@ -12,7 +13,7 @@ import 'quick_switch_bar.dart';
/// dockable pane that can be pinned open on wide ones. /// dockable pane that can be pinned open on wide ones.
/// ///
/// The bottom bar stays a bottom bar at every width; it never becomes a rail. /// The bottom bar stays a bottom bar at every width; it never becomes a rail.
class AppShell extends StatelessWidget { class AppShell extends StatefulWidget {
static const double wideBreakpoint = 720; static const double wideBreakpoint = 720;
static const double _drawerWidth = 300; static const double _drawerWidth = 300;
@ -58,11 +59,47 @@ class AppShell extends StatelessWidget {
this.channelsUnreadCount = 0, this.channelsUnreadCount = 0,
}); });
@override
State<AppShell> createState() => _AppShellState();
}
class _AppShellState extends State<AppShell> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
/// System back, in priority order:
/// 1. an open drawer closes,
/// 2. otherwise pop the route (a pushed chat returns to its list),
/// 3. otherwise hand back to the OS, so Android returns to the home
/// screen or the previous app rather than sitting on a dead press.
///
/// Screens previously did this with `PopScope(canPop: !isConnected)`, which
/// swallowed back entirely while connected: the drawer would not close and
/// the app would never background.
void _handleBack() {
final scaffold = _scaffoldKey.currentState;
if (scaffold?.isDrawerOpen ?? false) {
scaffold!.closeDrawer();
return;
}
final navigator = Navigator.of(context);
if (navigator.canPop()) {
navigator.pop();
return;
}
SystemNavigator.pop();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LayoutBuilder( return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, _) {
if (didPop) return;
_handleBack();
},
child: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final isWide = constraints.maxWidth >= wideBreakpoint; final isWide = constraints.maxWidth >= AppShell.wideBreakpoint;
final pinned = final pinned =
isWide && context.watch<UiViewStateService>().navDrawerPinned; isWide && context.watch<UiViewStateService>().navDrawerPinned;
@ -70,32 +107,34 @@ class AppShell extends StatelessWidget {
? _buildPinned(context) ? _buildPinned(context)
: _buildTransient(context, isWide); : _buildTransient(context, isWide);
}, },
),
); );
} }
/// Null on detail screens, which show the panel but no bottom bar. /// Null on detail screens, which show the panel but no bottom bar.
Widget? _bottomBar() { Widget? _bottomBar() {
final index = selectedIndex; final index = widget.selectedIndex;
final onSelected = onDestinationSelected; final onSelected = widget.onDestinationSelected;
if (index == null || onSelected == null) return null; if (index == null || onSelected == null) return null;
return SafeArea( return SafeArea(
top: false, top: false,
child: QuickSwitchBar( child: QuickSwitchBar(
selectedIndex: index, selectedIndex: index,
onDestinationSelected: onSelected, onDestinationSelected: onSelected,
contactsUnreadCount: contactsUnreadCount, contactsUnreadCount: widget.contactsUnreadCount,
channelsUnreadCount: channelsUnreadCount, channelsUnreadCount: widget.channelsUnreadCount,
), ),
); );
} }
PreferredSizeWidget? _appBar(BuildContext context, bool pinned) { PreferredSizeWidget? _appBar(BuildContext context, bool pinned) {
return appBarBuilder?.call(context, pinned) ?? appBar; return widget.appBarBuilder?.call(context, pinned) ?? widget.appBar;
} }
/// Wide + pinned: the panel is laid out beside the body, not overlaid. /// Wide + pinned: the panel is laid out beside the body, not overlaid.
Widget _buildPinned(BuildContext context) { Widget _buildPinned(BuildContext context) {
return Scaffold( return Scaffold(
key: _scaffoldKey,
appBar: _appBar(context, true), appBar: _appBar(context, true),
body: SafeArea( body: SafeArea(
top: false, top: false,
@ -103,20 +142,20 @@ class AppShell extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
SizedBox( SizedBox(
width: _drawerWidth, width: AppShell._drawerWidth,
child: _NavPanel( child: _NavPanel(
isWide: true, isWide: true,
content: drawerContent, content: widget.drawerContent,
onDisconnect: onDisconnect, onDisconnect: widget.onDisconnect,
onSettings: onSettings, onSettings: widget.onSettings,
), ),
), ),
const VerticalDivider(width: 1), const VerticalDivider(width: 1),
Expanded(child: body), Expanded(child: widget.body),
], ],
), ),
), ),
floatingActionButton: floatingActionButton, floatingActionButton: widget.floatingActionButton,
bottomNavigationBar: _bottomBar(), bottomNavigationBar: _bottomBar(),
); );
} }
@ -125,18 +164,19 @@ class AppShell extends StatelessWidget {
/// the hamburger into the app bar automatically. /// the hamburger into the app bar automatically.
Widget _buildTransient(BuildContext context, bool isWide) { Widget _buildTransient(BuildContext context, bool isWide) {
return Scaffold( return Scaffold(
key: _scaffoldKey,
appBar: _appBar(context, false), appBar: _appBar(context, false),
drawer: Drawer( drawer: Drawer(
width: _drawerWidth, width: AppShell._drawerWidth,
child: _NavPanel( child: _NavPanel(
isWide: isWide, isWide: isWide,
content: drawerContent, content: widget.drawerContent,
onDisconnect: onDisconnect, onDisconnect: widget.onDisconnect,
onSettings: onSettings, onSettings: widget.onSettings,
), ),
), ),
body: body, body: widget.body,
floatingActionButton: floatingActionButton, floatingActionButton: widget.floatingActionButton,
bottomNavigationBar: _bottomBar(), bottomNavigationBar: _bottomBar(),
); );
} }

@ -46,6 +46,13 @@ class MapLayerPanel extends StatelessWidget {
onChanged: service.setMapShowOtherNodes, onChanged: service.setMapShowOtherNodes,
), ),
const Divider(height: 1), const Divider(height: 1),
_toggle(
label: l10n.map_alwaysShowNames,
icon: Icons.label_outline,
value: settings.mapAlwaysShowNames,
onChanged: service.setMapAlwaysShowNames,
),
const Divider(height: 1),
_sectionHeader(theme, l10n.map_filterNodes), _sectionHeader(theme, l10n.map_filterNodes),
_toggle( _toggle(
label: l10n.map_showDiscoveryContacts, label: l10n.map_showDiscoveryContacts,

Loading…
Cancel
Save

Powered by TurnKey Linux.