diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 48c6f31..a7b3413 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -976,6 +976,7 @@ "map_repeaters": "Repeaters", "map_otherNodes": "Other Nodes", "map_showOverlaps": "Repeater Key Overlaps", + "map_alwaysShowNames": "Always show names", "map_keyPrefix": "Key Prefix", "map_filterByKeyPrefix": "Filter by key prefix", "map_publicKeyPrefix": "Public key prefix", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 2c425e2..0ad7394 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3412,6 +3412,12 @@ abstract class AppLocalizations { /// **'Repeater Key Overlaps'** 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. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index ee62916..a379cc6 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -1890,6 +1890,9 @@ class AppLocalizationsBg extends AppLocalizations { @override String get map_showOverlaps => 'Покриване на ключа на повтаряча'; + @override + String get map_alwaysShowNames => 'Always show names'; + @override String get map_keyPrefix => 'Префикс на ключа'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 92c1102..b60bea3 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1887,6 +1887,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get map_showOverlaps => 'Überlappungen der Repeater-Taste'; + @override + String get map_alwaysShowNames => 'Always show names'; + @override String get map_keyPrefix => 'Schlüsselpräfix'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 7989c98..db7573f 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1854,6 +1854,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get map_showOverlaps => 'Repeater Key Overlaps'; + @override + String get map_alwaysShowNames => 'Always show names'; + @override String get map_keyPrefix => 'Key Prefix'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index df55c77..9253a00 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1885,6 +1885,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get map_showOverlaps => 'Superposiciones de tecla repetidora'; + @override + String get map_alwaysShowNames => 'Always show names'; + @override String get map_keyPrefix => 'Prefijo de clave'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index e02d168..19bd894 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1895,6 +1895,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get map_showOverlaps => 'Chevauchement de la touche répétitive'; + @override + String get map_alwaysShowNames => 'Always show names'; + @override String get map_keyPrefix => 'Préfixe clé'; diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index 3dabd04..98933ef 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -1896,6 +1896,9 @@ class AppLocalizationsHu extends AppLocalizations { @override String get map_showOverlaps => 'Az ismétlő kulcsok ütköznek'; + @override + String get map_alwaysShowNames => 'Always show names'; + @override String get map_keyPrefix => 'Kulcsfontosságú előtag'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index c4ad345..b0197a8 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -1888,6 +1888,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get map_showOverlaps => 'Sovrapposizioni della chiave ripetitore'; + @override + String get map_alwaysShowNames => 'Always show names'; + @override String get map_keyPrefix => 'Prefisso Chiave'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 1c2edbf..b244d39 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -1811,6 +1811,9 @@ class AppLocalizationsJa extends AppLocalizations { @override String get map_showOverlaps => 'リピーターキーの重複'; + @override + String get map_alwaysShowNames => 'Always show names'; + @override String get map_keyPrefix => '主要なプレフィックス'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 6403027..90228a2 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -1807,6 +1807,9 @@ class AppLocalizationsKo extends AppLocalizations { @override String get map_showOverlaps => '반복 키 중복'; + @override + String get map_alwaysShowNames => 'Always show names'; + @override String get map_keyPrefix => '핵심 접두사'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 8e34076..440530b 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1874,6 +1874,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get map_showOverlaps => 'Herhalingssleutel overlapt'; + @override + String get map_alwaysShowNames => 'Always show names'; + @override String get map_keyPrefix => 'Prefix sleutel'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 2a034a7..be65e06 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -1900,6 +1900,9 @@ class AppLocalizationsPl extends AppLocalizations { @override String get map_showOverlaps => 'Nakładające się klucze przekaźników'; + @override + String get map_alwaysShowNames => 'Always show names'; + @override String get map_keyPrefix => 'Prefiks klucza'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index b0851fe..efcfad0 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1885,6 +1885,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get map_showOverlaps => 'Sobreposições da Chave Repeater'; + @override + String get map_alwaysShowNames => 'Always show names'; + @override String get map_keyPrefix => 'Prefixo Chave'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 5ae0d11..63b5679 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -1889,6 +1889,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get map_showOverlaps => 'Перекрытия ключа повтора'; + @override + String get map_alwaysShowNames => 'Always show names'; + @override String get map_keyPrefix => 'Префикс ключа'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 0b885ac..be75989 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -1876,6 +1876,9 @@ class AppLocalizationsSk extends AppLocalizations { @override String get map_showOverlaps => 'Prekrývanie opakovača kľúča'; + @override + String get map_alwaysShowNames => 'Always show names'; + @override String get map_keyPrefix => 'Päťciferné predpona'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index fa6c120..4b9518c 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -1871,6 +1871,9 @@ class AppLocalizationsSl extends AppLocalizations { @override String get map_showOverlaps => 'Prekrivanje ključa ponovnega predvajanja'; + @override + String get map_alwaysShowNames => 'Always show names'; + @override String get map_keyPrefix => 'Predpona ključa'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index a4e67f6..ab1047d 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -1864,6 +1864,9 @@ class AppLocalizationsSv extends AppLocalizations { @override String get map_showOverlaps => 'Repeater-nyckelöverlappningar'; + @override + String get map_alwaysShowNames => 'Always show names'; + @override String get map_keyPrefix => 'Nyckelprefix'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 00aeeb1..ee32259 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -1884,6 +1884,9 @@ class AppLocalizationsUk extends AppLocalizations { @override String get map_showOverlaps => 'Перекриття ключів ретрансляторів'; + @override + String get map_alwaysShowNames => 'Always show names'; + @override String get map_keyPrefix => 'Префікс ключа'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 051db0e..7ae7266 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1778,6 +1778,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get map_showOverlaps => '重复键重叠'; + @override + String get map_alwaysShowNames => 'Always show names'; + @override String get map_keyPrefix => '关键字前缀'; diff --git a/lib/models/app_settings.dart b/lib/models/app_settings.dart index 005197f..782e78a 100644 --- a/lib/models/app_settings.dart +++ b/lib/models/app_settings.dart @@ -112,6 +112,9 @@ class AppSettings { final bool mapShowChatNodes; final bool mapShowOtherNodes; 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 bool mapKeyPrefixEnabled; final String mapKeyPrefix; @@ -188,6 +191,7 @@ class AppSettings { this.mapShowChatNodes = true, this.mapShowOtherNodes = true, this.mapShowOverlaps = false, + this.mapAlwaysShowNames = false, this.mapTimeFilterHours = 0, // Default to all time this.mapKeyPrefixEnabled = false, this.mapKeyPrefix = '', @@ -252,6 +256,7 @@ class AppSettings { 'map_show_chat_nodes': mapShowChatNodes, 'map_show_other_nodes': mapShowOtherNodes, 'map_show_overlaps': mapShowOverlaps, + 'map_always_show_names': mapAlwaysShowNames, 'map_time_filter_hours': mapTimeFilterHours, 'map_key_prefix_enabled': mapKeyPrefixEnabled, 'map_key_prefix': mapKeyPrefix, @@ -338,6 +343,7 @@ class AppSettings { mapShowChatNodes: json['map_show_chat_nodes'] as bool? ?? true, mapShowOtherNodes: json['map_show_other_nodes'] as bool? ?? true, mapShowOverlaps: json['map_show_overlaps'] as bool? ?? false, + mapAlwaysShowNames: json['map_always_show_names'] as bool? ?? false, mapTimeFilterHours: (json['map_time_filter_hours'] as num?)?.toDouble() ?? 0, mapKeyPrefixEnabled: json['map_key_prefix_enabled'] as bool? ?? false, @@ -462,6 +468,7 @@ class AppSettings { bool? mapShowChatNodes, bool? mapShowOtherNodes, bool? mapShowOverlaps, + bool? mapAlwaysShowNames, double? mapTimeFilterHours, bool? mapKeyPrefixEnabled, String? mapKeyPrefix, @@ -510,6 +517,7 @@ class AppSettings { mapShowChatNodes: mapShowChatNodes ?? this.mapShowChatNodes, mapShowOtherNodes: mapShowOtherNodes ?? this.mapShowOtherNodes, mapShowOverlaps: mapShowOverlaps ?? this.mapShowOverlaps, + mapAlwaysShowNames: mapAlwaysShowNames ?? this.mapAlwaysShowNames, mapTimeFilterHours: mapTimeFilterHours ?? this.mapTimeFilterHours, mapKeyPrefixEnabled: mapKeyPrefixEnabled ?? this.mapKeyPrefixEnabled, mapKeyPrefix: mapKeyPrefix ?? this.mapKeyPrefix, diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 68cc7dd..6917817 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -29,6 +29,8 @@ import '../services/chat_text_scale_service.dart'; import '../services/translation_service.dart'; import '../utils/emoji_utils.dart'; import '../utils/route_transitions.dart'; +import 'settings_screen.dart'; +import '../utils/dialog_utils.dart'; import '../widgets/app_shell.dart'; import '../widgets/channel_drawer_list.dart'; import '../widgets/mention_autocomplete.dart'; @@ -287,6 +289,11 @@ class _ChannelChatScreenState extends State { ); } + Future _disconnect(BuildContext context) async { + final connector = context.read(); + await showDisconnectDialog(context, connector); + } + /// Bottom-bar navigation out of an open channel. /// /// Tapping Channels returns to the channel list. Tapping Contacts or Map @@ -351,6 +358,13 @@ class _ChannelChatScreenState extends State { currentChannelIndex: _currentChannel.index, 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( // Pinned: the panel is already docked, so no hamburger is needed. // Unpinned: this is a pushed route, so Scaffold would put a back arrow diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 06c514e..eedfd71 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -97,234 +97,229 @@ class _ChannelsScreenState extends State return const SizedBox.shrink(); } - final allowBack = !connector.isConnected; - - return PopScope( - canPop: allowBack, - child: AppShell( - selectedIndex: 1, - onDestinationSelected: (index) => _handleQuickSwitch(index, context), - contactsUnreadCount: connector.getTotalContactsUnreadCount(), - channelsUnreadCount: connector.getTotalChannelsUnreadCount(), - drawerContent: ChannelDrawerList( - onChannelSelected: (channel) => _openChannel(context, channel), - ), - onDisconnect: () => _disconnect(context), - onSettings: () => Navigator.push( - context, - MaterialPageRoute(builder: (context) => const SettingsScreen()), - ), - appBar: AppBar( - title: AppBarTitle(context.l10n.channels_title), - centerTitle: true, - bottom: const SyncProgressAppBarBottom(), - actions: [ - // Disconnect and Settings moved to the panel footer (#290); only - // the screen-level Communities entry remains, and it already only - // appears once a community has been joined. - if (_communities.isNotEmpty) - PopupMenuButton( - itemBuilder: (context) => [ - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.groups), - const SizedBox(width: 8), - Text(context.l10n.community_manageCommunities), - ], - ), - onTap: () => _showManageCommunitiesDialog(context), + return AppShell( + selectedIndex: 1, + onDestinationSelected: (index) => _handleQuickSwitch(index, context), + contactsUnreadCount: connector.getTotalContactsUnreadCount(), + channelsUnreadCount: connector.getTotalChannelsUnreadCount(), + drawerContent: ChannelDrawerList( + onChannelSelected: (channel) => _openChannel(context, channel), + ), + onDisconnect: () => _disconnect(context), + onSettings: () => Navigator.push( + context, + MaterialPageRoute(builder: (context) => const SettingsScreen()), + ), + appBar: AppBar( + title: AppBarTitle(context.l10n.channels_title), + centerTitle: true, + bottom: const SyncProgressAppBarBottom(), + actions: [ + // Disconnect and Settings moved to the panel footer (#290); only + // the screen-level Communities entry remains, and it already only + // appears once a community has been joined. + if (_communities.isNotEmpty) + PopupMenuButton( + itemBuilder: (context) => [ + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.groups), + const SizedBox(width: 8), + Text(context.l10n.community_manageCommunities), + ], ), - ], - icon: const Icon(Icons.more_vert), - ), - ], - ), - body: RefreshIndicator( - onRefresh: () async { - await context.read().getChannels(force: true); - }, - child: () { - final channels = connector.channels; - final waitingForFirstChannel = - connector.isLoadingChannels && channels.isEmpty; - - // Only block the list while the first channel is actively loading. - // If the initial sync aborts, show cached/partial channels instead - // of trapping the user behind an idle spinner. - if (waitingForFirstChannel) { - return const Center(child: CircularProgressIndicator()); - } + onTap: () => _showManageCommunitiesDialog(context), + ), + ], + icon: const Icon(Icons.more_vert), + ), + ], + ), + body: RefreshIndicator( + onRefresh: () async { + await context.read().getChannels(force: true); + }, + child: () { + final channels = connector.channels; + final waitingForFirstChannel = + connector.isLoadingChannels && channels.isEmpty; + + // Only block the list while the first channel is actively loading. + // If the initial sync aborts, show cached/partial channels instead + // of trapping the user behind an idle spinner. + if (waitingForFirstChannel) { + return const Center(child: CircularProgressIndicator()); + } - if (channels.isEmpty) { - return ListView( - children: [ - SizedBox( - height: MediaQuery.of(context).size.height - 200, - child: EmptyState( - icon: Icons.tag, - title: context.l10n.channels_noChannelsConfigured, - action: FilledButton.icon( - onPressed: () => _addPublicChannel(context, connector), - icon: const Icon(Icons.public), - label: Text(context.l10n.channels_addPublicChannel), - ), + if (channels.isEmpty) { + return ListView( + children: [ + SizedBox( + height: MediaQuery.of(context).size.height - 200, + child: EmptyState( + icon: Icons.tag, + title: context.l10n.channels_noChannelsConfigured, + action: FilledButton.icon( + onPressed: () => _addPublicChannel(context, connector), + icon: const Icon(Icons.public), + label: Text(context.l10n.channels_addPublicChannel), ), ), - ], - ); - } - - final filteredChannels = _filterAndSortChannels( - channels, - connector, - viewState, + ), + ], ); + } - return Column( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: TextField( - controller: _searchController, - decoration: InputDecoration( - hintText: context.l10n.channels_searchChannels, - prefixIcon: const Icon(Icons.search), - suffixIcon: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (viewState.channelsSearchText.isNotEmpty) - IconButton( - icon: const Icon(Icons.clear), - onPressed: () { - _searchDebounce?.cancel(); - _searchDebounce = null; - _searchController.clear(); - context - .read() - .setChannelsSearchText(''); - }, - ), - _buildFilterButton(viewState), - ], - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), + final filteredChannels = _filterAndSortChannels( + channels, + connector, + viewState, + ); + + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + controller: _searchController, + decoration: InputDecoration( + hintText: context.l10n.channels_searchChannels, + prefixIcon: const Icon(Icons.search), + suffixIcon: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (viewState.channelsSearchText.isNotEmpty) + IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + _searchDebounce?.cancel(); + _searchDebounce = null; + _searchController.clear(); + context + .read() + .setChannelsSearchText(''); + }, + ), + _buildFilterButton(viewState), + ], + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, ), - onChanged: (value) { - _searchDebounce?.cancel(); - _searchDebounce = Timer( - const Duration(milliseconds: 300), - () { - if (!mounted) return; - context - .read() - .setChannelsSearchText(value); - }, - ); - }, ), + onChanged: (value) { + _searchDebounce?.cancel(); + _searchDebounce = Timer( + const Duration(milliseconds: 300), + () { + if (!mounted) return; + context + .read() + .setChannelsSearchText(value); + }, + ); + }, ), - Expanded( - child: filteredChannels.isEmpty - ? ListView( - children: [ - SizedBox( - height: MediaQuery.of(context).size.height - 300, - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.search_off, - size: 64, - color: Colors.grey[400], - ), - const SizedBox(height: 16), - Text( - context.l10n.channels_noChannelsFound, - style: TextStyle( - fontSize: 16, - color: Colors.grey[600], - ), + ), + Expanded( + child: filteredChannels.isEmpty + ? ListView( + children: [ + SizedBox( + height: MediaQuery.of(context).size.height - 300, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.search_off, + size: 64, + color: Colors.grey[400], + ), + const SizedBox(height: 16), + Text( + context.l10n.channels_noChannelsFound, + style: TextStyle( + fontSize: 16, + color: Colors.grey[600], ), - ], - ), + ), + ], ), ), - ], - ) - : (viewState.channelsSortOption == - ChannelSortOption.manual && - viewState.channelsSearchText.isEmpty) - ? ReorderableListView.builder( - padding: const EdgeInsets.only( - left: 16, - right: 16, - top: 8, - bottom: 88, - ), - buildDefaultDragHandles: false, - itemCount: filteredChannels.length, - onReorderItem: (oldIndex, newIndex) { - // onReorderItem already adjusts newIndex after the - // removed item, unlike the deprecated onReorder. - final reordered = List.from( - filteredChannels, - ); - final item = reordered.removeAt(oldIndex); - reordered.insert(newIndex, item); - unawaited( - connector.setChannelOrder( - reordered.map((c) => c.index).toList(), - ), - ); - }, - itemBuilder: (context, index) { - final channel = filteredChannels[index]; - return _buildChannelTile( - context, - connector, - channelMessageStore, - channel, - showDragHandle: true, - dragIndex: index, - ); - }, - ) - : ListView.builder( - padding: const EdgeInsets.only( - left: 16, - right: 16, - top: 8, - bottom: 88, ), - itemCount: filteredChannels.length, - itemBuilder: (context, index) { - final channel = filteredChannels[index]; - return _buildChannelTile( - context, - connector, - channelMessageStore, - channel, - ); - }, + ], + ) + : (viewState.channelsSortOption == + ChannelSortOption.manual && + viewState.channelsSearchText.isEmpty) + ? ReorderableListView.builder( + padding: const EdgeInsets.only( + left: 16, + right: 16, + top: 8, + bottom: 88, ), - ), - ], - ); - }(), - ), - floatingActionButton: FloatingActionButton( - onPressed: () => _showAddChannelDialog(context), - tooltip: context.l10n.channels_addChannel, - child: const Icon(Icons.add), - ), + buildDefaultDragHandles: false, + itemCount: filteredChannels.length, + onReorderItem: (oldIndex, newIndex) { + // onReorderItem already adjusts newIndex after the + // removed item, unlike the deprecated onReorder. + final reordered = List.from( + filteredChannels, + ); + final item = reordered.removeAt(oldIndex); + reordered.insert(newIndex, item); + unawaited( + connector.setChannelOrder( + reordered.map((c) => c.index).toList(), + ), + ); + }, + itemBuilder: (context, index) { + final channel = filteredChannels[index]; + return _buildChannelTile( + context, + connector, + channelMessageStore, + channel, + showDragHandle: true, + dragIndex: index, + ); + }, + ) + : ListView.builder( + padding: const EdgeInsets.only( + left: 16, + right: 16, + top: 8, + bottom: 88, + ), + itemCount: filteredChannels.length, + itemBuilder: (context, index) { + final channel = filteredChannels[index]; + return _buildChannelTile( + context, + connector, + channelMessageStore, + channel, + ); + }, + ), + ), + ], + ); + }(), + ), + floatingActionButton: FloatingActionButton( + onPressed: () => _showAddChannelDialog(context), + tooltip: context.l10n.channels_addChannel, + child: const Icon(Icons.add), ), ); } diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 7d2603b..ce32f4c 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -317,107 +317,103 @@ class _ContactsScreenState extends State return const SizedBox.shrink(); } - final allowBack = !connector.isConnected; - return PopScope( - canPop: allowBack, - child: AppShell( - selectedIndex: 0, - onDestinationSelected: (index) => _handleQuickSwitch(index, context), - contactsUnreadCount: connector.getTotalContactsUnreadCount(), - channelsUnreadCount: connector.getTotalChannelsUnreadCount(), - drawerContent: const ContactFilterRail(), - onDisconnect: () => _disconnect(context, connector), - onSettings: () => Navigator.push( - context, - MaterialPageRoute(builder: (context) => const SettingsScreen()), - ), - appBar: AppBar( - title: AppBarTitle(context.l10n.contacts_title), - bottom: const SyncProgressAppBarBottom(), - actions: [ - PopupMenuButton( - itemBuilder: (context) => [ - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.connect_without_contact), - const SizedBox(width: 8), - Text(context.l10n.contacts_zeroHopAdvert), - ], - ), - onTap: () => { - connector.sendSelfAdvert(flood: false), - showDismissibleSnackBar( - context, - content: Text(context.l10n.settings_advertisementSent), - ), - }, + return AppShell( + selectedIndex: 0, + onDestinationSelected: (index) => _handleQuickSwitch(index, context), + contactsUnreadCount: connector.getTotalContactsUnreadCount(), + channelsUnreadCount: connector.getTotalChannelsUnreadCount(), + drawerContent: const ContactFilterRail(), + onDisconnect: () => _disconnect(context, connector), + onSettings: () => Navigator.push( + context, + MaterialPageRoute(builder: (context) => const SettingsScreen()), + ), + appBar: AppBar( + title: AppBarTitle(context.l10n.contacts_title), + bottom: const SyncProgressAppBarBottom(), + actions: [ + PopupMenuButton( + itemBuilder: (context) => [ + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.connect_without_contact), + const SizedBox(width: 8), + Text(context.l10n.contacts_zeroHopAdvert), + ], ), - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.cell_tower), - const SizedBox(width: 8), - Text(context.l10n.contacts_floodAdvert), - ], + onTap: () => { + connector.sendSelfAdvert(flood: false), + showDismissibleSnackBar( + context, + content: Text(context.l10n.settings_advertisementSent), ), - onTap: () => { - connector.sendSelfAdvert(flood: true), - showDismissibleSnackBar( - context, - content: Text(context.l10n.settings_advertisementSent), - ), - }, + }, + ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.cell_tower), + const SizedBox(width: 8), + Text(context.l10n.contacts_floodAdvert), + ], ), - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.copy), - const SizedBox(width: 8), - Text(context.l10n.contacts_copyAdvertToClipboard), - ], + onTap: () => { + connector.sendSelfAdvert(flood: true), + showDismissibleSnackBar( + context, + content: Text(context.l10n.settings_advertisementSent), ), - onTap: () => _contactExport(Uint8List.fromList([])), + }, + ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.copy), + const SizedBox(width: 8), + Text(context.l10n.contacts_copyAdvertToClipboard), + ], ), - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.paste), - const SizedBox(width: 8), - Text(context.l10n.contacts_addContactFromClipboard), - ], - ), - onTap: () => _contactImport(), + onTap: () => _contactExport(Uint8List.fromList([])), + ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.paste), + const SizedBox(width: 8), + Text(context.l10n.contacts_addContactFromClipboard), + ], ), - ], - icon: const Icon(Icons.connect_without_contact), - ), - // Disconnect and Settings moved to the panel footer (#290). - // Discovered contacts is screen-level and stays here. - PopupMenuButton( - itemBuilder: (context) => [ - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.person_add_rounded), - const SizedBox(width: 8), - Text(context.l10n.discoveredContacts_Title), - ], - ), - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const DiscoveryScreen(), - ), + onTap: () => _contactImport(), + ), + ], + icon: const Icon(Icons.connect_without_contact), + ), + // Disconnect and Settings moved to the panel footer (#290). + // Discovered contacts is screen-level and stays here. + PopupMenuButton( + itemBuilder: (context) => [ + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.person_add_rounded), + const SizedBox(width: 8), + Text(context.l10n.discoveredContacts_Title), + ], + ), + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const DiscoveryScreen(), ), ), - ], - icon: const Icon(Icons.more_vert), - ), - ], - ), - body: _buildContactsBody(context, connector), + ), + ], + icon: const Icon(Icons.more_vert), + ), + ], ), + body: _buildContactsBody(context, connector), ); } diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index c902cb6..2b9ade1 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -399,7 +399,8 @@ class _MapScreenState extends State { // Re center map after removed markers have loaded if (!_hasInitializedMap && _removedMarkersLoaded) { _hasInitializedMap = true; - _showNodeLabels = initialZoom >= _labelZoomThreshold; + _showNodeLabels = + settings.mapAlwaysShowNames || initialZoom >= _labelZoomThreshold; if (hasMapContent) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { @@ -409,257 +410,253 @@ class _MapScreenState extends State { } } - final allowBack = !connector.isConnected; - - return PopScope( - canPop: allowBack, - child: AppShell( - selectedIndex: 2, - onDestinationSelected: (index) => - _handleQuickSwitch(index, context), - contactsUnreadCount: connector.getTotalContactsUnreadCount(), - channelsUnreadCount: connector.getTotalChannelsUnreadCount(), - drawerContent: const MapLayerPanel(), - onDisconnect: () => _disconnect(context, connector), - onSettings: () => Navigator.push( - context, - MaterialPageRoute(builder: (context) => const SettingsScreen()), - ), - appBar: AppBar( - title: AppBarTitle(context.l10n.map_title), - centerTitle: true, - bottom: const SyncProgressAppBarBottom(), - actions: [ - if (!_isBuildingPathTrace) - IconButton( - icon: const Icon(Icons.radar), - onPressed: () => _startPath( - LatLng(connector.selfLatitude!, connector.selfLongitude!), - ), - tooltip: context.l10n.contacts_pathTrace, + return AppShell( + selectedIndex: 2, + onDestinationSelected: (index) => _handleQuickSwitch(index, context), + contactsUnreadCount: connector.getTotalContactsUnreadCount(), + channelsUnreadCount: connector.getTotalChannelsUnreadCount(), + drawerContent: const MapLayerPanel(), + onDisconnect: () => _disconnect(context, connector), + onSettings: () => Navigator.push( + context, + MaterialPageRoute(builder: (context) => const SettingsScreen()), + ), + appBar: AppBar( + title: AppBarTitle(context.l10n.map_title), + centerTitle: true, + bottom: const SyncProgressAppBarBottom(), + actions: [ + if (!_isBuildingPathTrace) + IconButton( + icon: const Icon(Icons.radar), + onPressed: () => _startPath( + LatLng(connector.selfLatitude!, connector.selfLongitude!), ), - if (!_isBuildingPathTrace) - IconButton( - icon: const LosIcon(), - onPressed: () { - final candidates = []; - if (connector.selfLatitude != null && - connector.selfLongitude != null) { - candidates.add( - LineOfSightEndpoint( - label: context.l10n.pathTrace_you, - point: LatLng( - connector.selfLatitude!, - connector.selfLongitude!, - ), - color: Colors.teal, - icon: Icons.person_pin_circle, - ), - ); - } - for (final c in contactsWithLocation) { - candidates.add( - LineOfSightEndpoint( - label: c.name, - point: LatLng(c.latitude!, c.longitude!), - color: _getNodeColor(c.type), - icon: _getNodeIcon(c.type), - ), - ); - } - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => LineOfSightMapScreen( - title: context.l10n.map_losScreenTitle, - candidates: candidates, + tooltip: context.l10n.contacts_pathTrace, + ), + if (!_isBuildingPathTrace) + IconButton( + icon: const LosIcon(), + onPressed: () { + final candidates = []; + if (connector.selfLatitude != null && + connector.selfLongitude != null) { + candidates.add( + LineOfSightEndpoint( + label: context.l10n.pathTrace_you, + point: LatLng( + connector.selfLatitude!, + connector.selfLongitude!, ), + color: Colors.teal, + icon: Icons.person_pin_circle, ), ); - }, - tooltip: context.l10n.map_lineOfSight, + } + for (final c in contactsWithLocation) { + candidates.add( + LineOfSightEndpoint( + label: c.name, + point: LatLng(c.latitude!, c.longitude!), + color: _getNodeColor(c.type), + icon: _getNodeIcon(c.type), + ), + ); + } + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => LineOfSightMapScreen( + title: context.l10n.map_losScreenTitle, + candidates: candidates, + ), + ), + ); + }, + tooltip: context.l10n.map_lineOfSight, + ), + ], + ), + body: Stack( + children: [ + FlutterMap( + mapController: _mapController, + options: MapOptions( + initialCenter: center, + initialZoom: initialZoom, + minZoom: _mapMinZoom, + maxZoom: _mapMaxZoom, + interactionOptions: InteractionOptions( + flags: ~InteractiveFlag.rotate, + scrollWheelVelocity: isDesktop ? 0.012 : 0.005, + cursorKeyboardRotationOptions: + CursorKeyboardRotationOptions.disabled(), + keyboardOptions: isDesktop + ? const KeyboardOptions( + enableArrowKeysPanning: true, + enableWASDPanning: true, + enableRFZooming: true, + ) + : const KeyboardOptions.disabled(), ), - ], - ), - body: Stack( - children: [ - FlutterMap( - mapController: _mapController, - options: MapOptions( - initialCenter: center, - initialZoom: initialZoom, - minZoom: _mapMinZoom, - maxZoom: _mapMaxZoom, - interactionOptions: InteractionOptions( - flags: ~InteractiveFlag.rotate, - scrollWheelVelocity: isDesktop ? 0.012 : 0.005, - cursorKeyboardRotationOptions: - CursorKeyboardRotationOptions.disabled(), - keyboardOptions: isDesktop - ? const KeyboardOptions( - enableArrowKeysPanning: true, - enableWASDPanning: true, - enableRFZooming: true, - ) - : const KeyboardOptions.disabled(), - ), - onTap: (_, latLng) { - if (_isSelectingPoi) { - setState(() { - _isSelectingPoi = false; - }); - _shareMarker( - context: context, - connector: connector, - position: latLng, - defaultLabel: context.l10n.map_pointOfInterest, - flags: 'poi', - ); - } - }, - onLongPress: (_, latLng) { - if (_isSelectingPoi) { - setState(() { - _isSelectingPoi = false; - }); - _shareMarker( - context: context, - connector: connector, - position: latLng, - defaultLabel: context.l10n.map_pointOfInterest, - flags: 'poi', - ); - return; - } - _showShareMarkerAtPositionSheet( + onTap: (_, latLng) { + if (_isSelectingPoi) { + setState(() { + _isSelectingPoi = false; + }); + _shareMarker( context: context, connector: connector, position: latLng, + defaultLabel: context.l10n.map_pointOfInterest, + flags: 'poi', ); - }, - onPositionChanged: (camera, hasGesture) { - final shouldShow = camera.zoom >= _labelZoomThreshold; - if (shouldShow != _showNodeLabels && mounted) { - setState(() { - _showNodeLabels = shouldShow; - }); - } - }, + } + }, + onLongPress: (_, latLng) { + if (_isSelectingPoi) { + setState(() { + _isSelectingPoi = false; + }); + _shareMarker( + context: context, + connector: connector, + position: latLng, + defaultLabel: context.l10n.map_pointOfInterest, + flags: 'poi', + ); + return; + } + _showShareMarkerAtPositionSheet( + context: context, + connector: connector, + position: latLng, + ); + }, + onPositionChanged: (camera, hasGesture) { + final shouldShow = + settings.mapAlwaysShowNames || + camera.zoom >= _labelZoomThreshold; + if (shouldShow != _showNodeLabels && mounted) { + setState(() { + _showNodeLabels = shouldShow; + }); + } + }, + ), + children: [ + TileLayer( + urlTemplate: kMapTileUrlTemplate, + tileProvider: tileCache.tileProvider, + userAgentPackageName: + MapTileCacheService.userAgentPackageName, + maxZoom: 19, ), - children: [ - TileLayer( - urlTemplate: kMapTileUrlTemplate, - tileProvider: tileCache.tileProvider, - userAgentPackageName: - MapTileCacheService.userAgentPackageName, - maxZoom: 19, - ), - if (_polylines.isNotEmpty && _isBuildingPathTrace) - PolylineLayer(polylines: _polylines), - if (sharedMarkerPolylines.isNotEmpty) - PolylineLayer(polylines: sharedMarkerPolylines), - MarkerLayer( - markers: [ - if (highlightPosition != null) - Marker( - point: highlightPosition, - width: 40, - height: 40, - child: IgnorePointer( - child: Icon( - Icons.location_on_outlined, - color: Colors.red[600], - size: 34, - ), + if (_polylines.isNotEmpty && _isBuildingPathTrace) + PolylineLayer(polylines: _polylines), + if (sharedMarkerPolylines.isNotEmpty) + PolylineLayer(polylines: sharedMarkerPolylines), + MarkerLayer( + markers: [ + if (highlightPosition != null) + Marker( + point: highlightPosition, + width: 40, + height: 40, + child: IgnorePointer( + child: Icon( + Icons.location_on_outlined, + color: Colors.red[600], + size: 34, ), ), - if (!settings.mapShowOverlaps) - ..._buildGuessedMarker( - guessedLocations, - showLabels: _showNodeLabels, - ), - ..._buildMarkers( - contactsWithLocation, - settings, - showLabels: _showNodeLabels, ), - ...sharedMarkers.map(_buildSharedMarker), - if (connector.selfLatitude != null && - connector.selfLongitude != null) - Marker( - point: LatLng( - connector.selfLatitude!, - connector.selfLongitude!, - ), - width: 40, - height: 40, - child: IgnorePointer( - ignoring: true, - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: Colors.teal, - shape: BoxShape.circle, - border: Border.all( - color: Colors.white, - width: 2, - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues( - alpha: 0.3, - ), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - alignment: Alignment.center, - child: const Icon( - Icons.person_pin_circle, + if (!settings.mapShowOverlaps) + ..._buildGuessedMarker( + guessedLocations, + showLabels: + settings.mapAlwaysShowNames || _showNodeLabels, + ), + ..._buildMarkers( + contactsWithLocation, + settings, + showLabels: + settings.mapAlwaysShowNames || _showNodeLabels, + ), + ...sharedMarkers.map(_buildSharedMarker), + if (connector.selfLatitude != null && + connector.selfLongitude != null) + Marker( + point: LatLng( + connector.selfLatitude!, + connector.selfLongitude!, + ), + width: 40, + height: 40, + child: IgnorePointer( + ignoring: true, + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.teal, + shape: BoxShape.circle, + border: Border.all( color: Colors.white, - size: 20, + width: 2, ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + alignment: Alignment.center, + child: const Icon( + Icons.person_pin_circle, + color: Colors.white, + size: 20, ), ), ), - if (_showNodeLabels && - connector.selfLatitude != null && - connector.selfLongitude != null) - _buildNodeLabelMarker( - point: LatLng( - connector.selfLatitude!, - connector.selfLongitude!, - ), - label: context.l10n.pathTrace_you, + ), + if (_showNodeLabels && + connector.selfLatitude != null && + connector.selfLongitude != null) + _buildNodeLabelMarker( + point: LatLng( + connector.selfLatitude!, + connector.selfLongitude!, ), - ], - ), - ], - ), - if (!_isBuildingPathTrace) - _buildLegend( - contacts, - contactsWithLocation, - settings, - sharedMarkers.length, - guessedLocations.length, - ), - if (isDesktop) - _buildDesktopMapControls( - context, - center: center, - zoom: initialZoom, - hasPathSelector: _isBuildingPathTrace, + label: context.l10n.pathTrace_you, + ), + ], ), - if (_isBuildingPathTrace) _buildPathTraceOverlay(), - ], - ), - floatingActionButton: FloatingActionButton( - onPressed: () => _showFilterDialog(context, settingsService), - tooltip: context.l10n.map_filterNodes, - child: const Icon(Icons.filter_list), - ), + ], + ), + if (!_isBuildingPathTrace) + _buildLegend( + contacts, + contactsWithLocation, + settings, + sharedMarkers.length, + guessedLocations.length, + ), + if (isDesktop) + _buildDesktopMapControls( + context, + center: center, + zoom: initialZoom, + hasPathSelector: _isBuildingPathTrace, + ), + if (_isBuildingPathTrace) _buildPathTraceOverlay(), + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: () => _showFilterDialog(context, settingsService), + tooltip: context.l10n.map_filterNodes, + child: const Icon(Icons.filter_list), ), ); }, diff --git a/lib/services/app_settings_service.dart b/lib/services/app_settings_service.dart index 5e3d8c3..d0ea235 100644 --- a/lib/services/app_settings_service.dart +++ b/lib/services/app_settings_service.dart @@ -76,6 +76,10 @@ class AppSettingsService extends ChangeNotifier { await updateSettings(_settings.copyWith(mapShowOverlaps: value)); } + Future setMapAlwaysShowNames(bool value) async { + await updateSettings(_settings.copyWith(mapAlwaysShowNames: value)); + } + Future setMapTimeFilterHours(double value) async { await updateSettings(_settings.copyWith(mapTimeFilterHours: value)); } diff --git a/lib/widgets/app_shell.dart b/lib/widgets/app_shell.dart index 329cb59..689caa3 100644 --- a/lib/widgets/app_shell.dart +++ b/lib/widgets/app_shell.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../l10n/l10n.dart'; @@ -12,7 +13,7 @@ import 'quick_switch_bar.dart'; /// 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. -class AppShell extends StatelessWidget { +class AppShell extends StatefulWidget { static const double wideBreakpoint = 720; static const double _drawerWidth = 300; @@ -58,44 +59,82 @@ class AppShell extends StatelessWidget { this.channelsUnreadCount = 0, }); + @override + State createState() => _AppShellState(); +} + +class _AppShellState extends State { + final GlobalKey _scaffoldKey = GlobalKey(); + + /// 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 Widget build(BuildContext context) { - return LayoutBuilder( - builder: (context, constraints) { - final isWide = constraints.maxWidth >= wideBreakpoint; - final pinned = - isWide && context.watch().navDrawerPinned; - - return pinned - ? _buildPinned(context) - : _buildTransient(context, isWide); + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, _) { + if (didPop) return; + _handleBack(); }, + child: LayoutBuilder( + builder: (context, constraints) { + final isWide = constraints.maxWidth >= AppShell.wideBreakpoint; + final pinned = + isWide && context.watch().navDrawerPinned; + + return pinned + ? _buildPinned(context) + : _buildTransient(context, isWide); + }, + ), ); } /// Null on detail screens, which show the panel but no bottom bar. Widget? _bottomBar() { - final index = selectedIndex; - final onSelected = onDestinationSelected; + final index = widget.selectedIndex; + final onSelected = widget.onDestinationSelected; if (index == null || onSelected == null) return null; return SafeArea( top: false, child: QuickSwitchBar( selectedIndex: index, onDestinationSelected: onSelected, - contactsUnreadCount: contactsUnreadCount, - channelsUnreadCount: channelsUnreadCount, + contactsUnreadCount: widget.contactsUnreadCount, + channelsUnreadCount: widget.channelsUnreadCount, ), ); } 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. Widget _buildPinned(BuildContext context) { return Scaffold( + key: _scaffoldKey, appBar: _appBar(context, true), body: SafeArea( top: false, @@ -103,20 +142,20 @@ class AppShell extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ SizedBox( - width: _drawerWidth, + width: AppShell._drawerWidth, child: _NavPanel( isWide: true, - content: drawerContent, - onDisconnect: onDisconnect, - onSettings: onSettings, + content: widget.drawerContent, + onDisconnect: widget.onDisconnect, + onSettings: widget.onSettings, ), ), const VerticalDivider(width: 1), - Expanded(child: body), + Expanded(child: widget.body), ], ), ), - floatingActionButton: floatingActionButton, + floatingActionButton: widget.floatingActionButton, bottomNavigationBar: _bottomBar(), ); } @@ -125,18 +164,19 @@ class AppShell extends StatelessWidget { /// the hamburger into the app bar automatically. Widget _buildTransient(BuildContext context, bool isWide) { return Scaffold( + key: _scaffoldKey, appBar: _appBar(context, false), drawer: Drawer( - width: _drawerWidth, + width: AppShell._drawerWidth, child: _NavPanel( isWide: isWide, - content: drawerContent, - onDisconnect: onDisconnect, - onSettings: onSettings, + content: widget.drawerContent, + onDisconnect: widget.onDisconnect, + onSettings: widget.onSettings, ), ), - body: body, - floatingActionButton: floatingActionButton, + body: widget.body, + floatingActionButton: widget.floatingActionButton, bottomNavigationBar: _bottomBar(), ); } diff --git a/lib/widgets/map_layer_panel.dart b/lib/widgets/map_layer_panel.dart index 13df116..29c18ea 100644 --- a/lib/widgets/map_layer_panel.dart +++ b/lib/widgets/map_layer_panel.dart @@ -46,6 +46,13 @@ class MapLayerPanel extends StatelessWidget { onChanged: service.setMapShowOtherNodes, ), 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), _toggle( label: l10n.map_showDiscoveryContacts,