feat(#290): move Disconnect and Settings into the panel footer

Epic C, rescoped. The original plan was to eliminate the overflow menu and
move everything into the drawer. An inventory showed that premise was wrong:
the ellipsis is six different menus sharing an icon, and most of what they
hold is screen-level (force flood mode, path management, delete all
discovered). Those cannot live in a global nav panel, which has no notion of
which contact or channel is meant.

Only Disconnect and Settings are app-level, which is exactly why they were
duplicated across three screens. Those move; everything else stays put.

- AppShell gains optional onDisconnect / onSettings, rendered as a pinned
  footer in the nav panel. Disconnect keeps the error colour it had as a red
  menu entry, since it drops the radio connection.
- Channels, Contacts and Map pass both and drop those two menu entries.
- Map's overflow menu is removed entirely, having nothing left.
- Channels keeps its menu only for Manage Communities, which was already
  conditional on having joined a community.
- Contacts keeps its menu for Discovered contacts.
- No detail screen menu is touched.

Pinned on a wide screen, Disconnect and Settings are now visible without
opening anything.

flutter analyze clean, dart format clean, 469 tests pass. Not yet run on
hardware.
integration/290-306-335
Strycher 2 days ago
parent e17f539d7e
commit 6ccbc71c20

@ -109,24 +109,22 @@ class _ChannelsScreenState extends State<ChannelsScreen>
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.logout, color: Colors.red),
const SizedBox(width: 8),
Text(context.l10n.common_disconnect),
],
),
onTap: () => _disconnect(context),
),
if (_communities.isNotEmpty)
PopupMenuItem(
child: Row(
children: [
@ -137,21 +135,6 @@ class _ChannelsScreenState extends State<ChannelsScreen>
),
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),
),

@ -326,6 +326,11 @@ class _ContactsScreenState extends State<ContactsScreen>
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<ContactsScreen>
],
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<ContactsScreen>
),
),
),
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),
),

@ -418,6 +418,11 @@ class _MapScreenState extends State<MapScreen> {
_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<MapScreen> {
},
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(

@ -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,
),
),
],
),
);

Loading…
Cancel
Save

Powered by TurnKey Linux.