diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index f7deb8b..06c514e 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -109,24 +109,22 @@ class _ChannelsScreenState extends State 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: [ - PopupMenuButton( - itemBuilder: (context) => [ - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.logout, color: Colors.red), - const SizedBox(width: 8), - Text(context.l10n.common_disconnect), - ], - ), - onTap: () => _disconnect(context), - ), - if (_communities.isNotEmpty) + // 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: [ @@ -137,24 +135,9 @@ class _ChannelsScreenState extends State ), onTap: () => _showManageCommunitiesDialog(context), ), - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.settings), - const SizedBox(width: 8), - Text(context.l10n.settings_title), - ], - ), - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const SettingsScreen(), - ), - ), - ), - ], - icon: const Icon(Icons.more_vert), - ), + ], + icon: const Icon(Icons.more_vert), + ), ], ), body: RefreshIndicator( diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 87fc7e4..7d2603b 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -326,6 +326,11 @@ class _ContactsScreenState extends State 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(), @@ -387,18 +392,10 @@ class _ContactsScreenState extends State ], 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.logout, color: Colors.red), - const SizedBox(width: 8), - Text(context.l10n.common_disconnect), - ], - ), - onTap: () => _disconnect(context, connector), - ), PopupMenuItem( child: Row( children: [ @@ -414,21 +411,6 @@ class _ContactsScreenState extends State ), ), ), - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.settings), - const SizedBox(width: 8), - Text(context.l10n.settings_title), - ], - ), - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const SettingsScreen(), - ), - ), - ), ], icon: const Icon(Icons.more_vert), ), diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 1444f5a..7cb7d27 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -418,6 +418,11 @@ class _MapScreenState extends State { _handleQuickSwitch(index, context), contactsUnreadCount: connector.getTotalContactsUnreadCount(), channelsUnreadCount: connector.getTotalChannelsUnreadCount(), + onDisconnect: () => _disconnect(context, connector), + onSettings: () => Navigator.push( + context, + MaterialPageRoute(builder: (context) => const SettingsScreen()), + ), appBar: AppBar( title: AppBarTitle(context.l10n.map_title), centerTitle: true, @@ -472,36 +477,6 @@ class _MapScreenState extends State { }, tooltip: context.l10n.map_lineOfSight, ), - PopupMenuButton( - itemBuilder: (context) => [ - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.logout, color: Colors.red), - const SizedBox(width: 8), - Text(context.l10n.common_disconnect), - ], - ), - onTap: () => _disconnect(context, connector), - ), - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.settings), - const SizedBox(width: 8), - Text(context.l10n.settings_title), - ], - ), - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const SettingsScreen(), - ), - ), - ), - ], - icon: const Icon(Icons.more_vert), - ), ], ), body: Stack( diff --git a/lib/widgets/app_shell.dart b/lib/widgets/app_shell.dart index cfc8ab9..329cb59 100644 --- a/lib/widgets/app_shell.dart +++ b/lib/widgets/app_shell.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import '../l10n/l10n.dart'; import '../services/ui_view_state_service.dart'; import 'quick_switch_bar.dart'; @@ -36,6 +37,12 @@ class AppShell extends StatelessWidget { /// List content for the active view. Null renders an empty drawer body. final Widget? drawerContent; + /// App-level actions, pinned to the bottom of the panel. These are not + /// contextual, which is why they were duplicated in three screens' overflow + /// menus before. Screen-level actions stay in their own overflow menu. + final VoidCallback? onDisconnect; + final VoidCallback? onSettings; + const AppShell({ super.key, required this.body, @@ -45,6 +52,8 @@ class AppShell extends StatelessWidget { this.appBarBuilder, this.floatingActionButton, this.drawerContent, + this.onDisconnect, + this.onSettings, this.contactsUnreadCount = 0, this.channelsUnreadCount = 0, }); @@ -95,7 +104,12 @@ class AppShell extends StatelessWidget { children: [ SizedBox( width: _drawerWidth, - child: _NavPanel(isWide: true, content: drawerContent), + child: _NavPanel( + isWide: true, + content: drawerContent, + onDisconnect: onDisconnect, + onSettings: onSettings, + ), ), const VerticalDivider(width: 1), Expanded(child: body), @@ -114,7 +128,12 @@ class AppShell extends StatelessWidget { appBar: _appBar(context, false), drawer: Drawer( width: _drawerWidth, - child: _NavPanel(isWide: isWide, content: drawerContent), + child: _NavPanel( + isWide: isWide, + content: drawerContent, + onDisconnect: onDisconnect, + onSettings: onSettings, + ), ), body: body, floatingActionButton: floatingActionButton, @@ -128,8 +147,15 @@ class AppShell extends StatelessWidget { class _NavPanel extends StatelessWidget { final bool isWide; final Widget? content; - - const _NavPanel({required this.isWide, this.content}); + final VoidCallback? onDisconnect; + final VoidCallback? onSettings; + + const _NavPanel({ + required this.isWide, + this.content, + this.onDisconnect, + this.onSettings, + }); @override Widget build(BuildContext context) { @@ -164,6 +190,51 @@ class _NavPanel extends StatelessWidget { ), if (isWide) const Divider(height: 1), Expanded(child: content ?? const SizedBox.shrink()), + if (onDisconnect != null || onSettings != null) ...[ + const Divider(height: 1), + _Footer(onDisconnect: onDisconnect, onSettings: onSettings), + ], + ], + ), + ); + } +} + +/// App-level actions pinned to the bottom of the panel. +class _Footer extends StatelessWidget { + final VoidCallback? onDisconnect; + final VoidCallback? onSettings; + + const _Footer({this.onDisconnect, this.onSettings}); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final colors = Theme.of(context).colorScheme; + + return Padding( + padding: const EdgeInsets.fromLTRB(8, 6, 8, 8), + child: Row( + children: [ + if (onDisconnect != null) + Expanded( + child: TextButton.icon( + // Kept visually distinct: this drops the radio connection, + // and it was a red menu entry before the move. + style: TextButton.styleFrom(foregroundColor: colors.error), + icon: const Icon(Icons.logout, size: 18), + label: Text(l10n.common_disconnect), + onPressed: onDisconnect, + ), + ), + if (onSettings != null) + Expanded( + child: TextButton.icon( + icon: const Icon(Icons.settings, size: 18), + label: Text(l10n.settings_title), + onPressed: onSettings, + ), + ), ], ), );