refactor: unify request approval menus and naming

This commit is contained in:
21pages committed 2026-11-01 15:45:13 +08:00
1 parent 417dcdf7bb
commit c43443b1a1
67 files changed
+535 -559

No files matched your search

@@ -1,75 +0,0 @@
import 'package:flutter/material.dart';
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
import '../../desktop/widgets/popup_menu.dart';
class LoginMethodMenu extends MenuEntryBase<String> {
final MenuEntryButton<String> defaultEntry;
final List<MenuEntryButton<String>> methods;
LoginMethodMenu({required this.defaultEntry, required this.methods});
@override
List<mod_menu.PopupMenuEntry<String>> build(
BuildContext context, MenuConfig conf) {
return [
_LoginMethodMenuItem(
height: conf.height,
child: IntrinsicHeight(
child:
Row(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
Expanded(
child: MenuEntryButton<String>(
padding: defaultEntry.padding,
enabled: defaultEntry.enabled,
dismissOnClicked: true,
proc: defaultEntry.proc,
childBuilder: defaultEntry.childBuilder,
).build(context, conf).single),
PopupMenuChildrenItem<String>(
height: conf.height,
padding: EdgeInsets.zero,
enabled: defaultEntry.enabled,
itemBuilder: (submenuContext) => methods
.expand((entry) => MenuEntryButton<String>(
childBuilder: entry.childBuilder,
proc: entry.proc,
padding: entry.padding,
enabled: entry.enabled,
dismissOnClicked: true,
dismissCallback: () {
if (Navigator.canPop(context)) Navigator.pop(context);
},
).build(submenuContext, conf))
.toList(),
child: SizedBox(
width: 56,
child: Icon(Icons.keyboard_arrow_right, color: conf.commonColor),
),
),
])),
)
];
}
}
// PopupMenuItem adds a TextButton around its child; these actions need separate hit targets.
class _LoginMethodMenuItem extends mod_menu.PopupMenuEntry<String> {
const _LoginMethodMenuItem({required this.height, required this.child});
final Widget child;
@override
final double height;
@override
bool represents(String? value) => false;
@override
State<_LoginMethodMenuItem> createState() => _LoginMethodMenuItemState();
}
class _LoginMethodMenuItemState extends State<_LoginMethodMenuItem> {
@override
Widget build(BuildContext context) => widget.child;
}
+84 -125
View File
@@ -15,7 +15,7 @@ import '../../models/peer_model.dart';
import '../../models/platform_model.dart';
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
import '../../desktop/widgets/popup_menu.dart';
import 'login_method_menu.dart';
import 'request_approval_menu.dart';
import 'dart:math' as math;
typedef PopupMenuEntryBuilder = Future<List<mod_menu.PopupMenuEntry<String>>>
@@ -573,7 +573,36 @@ abstract class BasePeerCard extends StatelessWidget {
@protected
Future<List<MenuEntryBase<String>>> _buildMenuItems(BuildContext context);
MenuEntryBase<String> _connectCommonAction(
@protected
Future<List<MenuEntryBase<String>>> _buildConnectionMenuItems(
BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [
_connectAction(context),
_transferFileAction(context),
_viewCameraAction(context),
_terminalAction(context),
];
if (peer.platform == kPeerPlatformWindows) {
menuItems.add(_terminalRunAsAdminAction(context));
}
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context));
}
if (isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id));
}
menuItems.add(MenuEntryDivider<String>());
menuItems.add(_requestApprovalAction(context));
// menuItems.add(await _openNewConnInOptAction(peer.id));
if (!isWeb) {
menuItems.add(await _forceAlwaysRelayAction(peer.id));
}
return menuItems;
}
MenuEntryButton<String> _connectCommonAction(
BuildContext context,
String title, {
bool isFileTransfer = false,
@@ -582,16 +611,10 @@ abstract class BasePeerCard extends StatelessWidget {
bool isRDP = false,
bool isTerminal = false,
bool isTerminalRunAsAdmin = false,
MenuEntryButton<String>? defaultEntry,
String loginMethod = '',
}) {
action(String text, String loginMethod) => MenuEntryButton<String>(
childBuilder: (TextStyle? style) => loginMethod == 'consent'
? Tooltip(
message: translate(
'Skip saved passwords and ask the remote user to accept the connection.'),
child: Text(text, style: style),
)
: Text(text, style: style),
return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(title, style: style),
proc: () {
if (isTerminalRunAsAdmin) {
setEnvTerminalAdmin();
@@ -611,10 +634,44 @@ abstract class BasePeerCard extends StatelessWidget {
padding: menuPadding,
dismissOnClicked: true,
);
return LoginMethodMenu(
defaultEntry: defaultEntry ?? action(title, ''),
methods: [
action(translate('Without password'), 'consent'),
}
MenuEntryBase<String> _requestApprovalAction(BuildContext context) {
MenuEntryButton<String> action(String title,
{bool isFileTransfer = false,
bool isViewCamera = false,
bool isTcpTunneling = false,
bool isRDP = false,
bool isTerminal = false,
bool isTerminalRunAsAdmin = false}) =>
_connectCommonAction(
context,
isTerminal || isTerminalRunAsAdmin
? '${translate(title)} (beta)'
: translate(title),
isFileTransfer: isFileTransfer,
isViewCamera: isViewCamera,
isTcpTunneling: isTcpTunneling,
isRDP: isRDP,
isTerminal: isTerminal,
isTerminalRunAsAdmin: isTerminalRunAsAdmin,
loginMethod: 'request-approval',
);
return RequestApprovalMenu(
title: translate('Request approval'),
padding: menuPadding,
entries: [
action('Connect'),
action('Transfer file', isFileTransfer: true),
action('View camera', isViewCamera: true),
action('Terminal', isTerminal: true),
if (peer.platform == kPeerPlatformWindows)
action('Terminal (Run as administrator)', isTerminalRunAsAdmin: true),
if (isDesktop && peer.platform != kPeerPlatformAndroid)
action('TCP tunneling', isTcpTunneling: true),
if (isWindows && peer.platform == kPeerPlatformWindows)
action('RDP', isRDP: true),
],
);
}
@@ -676,7 +733,7 @@ abstract class BasePeerCard extends StatelessWidget {
@protected
MenuEntryBase<String> _rdpAction(BuildContext context, String id) {
final action = MenuEntryButton<String>(
return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Container(
alignment: AlignmentDirectional.center,
height: CustomPopupMenuTheme.height,
@@ -710,8 +767,6 @@ abstract class BasePeerCard extends StatelessWidget {
padding: menuPadding,
dismissOnClicked: true,
);
return _connectCommonAction(context, translate('RDP'),
isRDP: true, defaultEntry: action);
}
@protected
@@ -1014,33 +1069,13 @@ class RecentPeerCard extends BasePeerCard {
@override
Future<List<MenuEntryBase<String>>> _buildMenuItems(
BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [
_connectAction(context),
_transferFileAction(context),
_viewCameraAction(context),
_terminalAction(context),
];
if (peer.platform == kPeerPlatformWindows) {
menuItems.add(_terminalRunAsAdminAction(context));
}
final List favs = (await bind.mainGetFav()).toList();
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context));
}
// menuItems.add(await _openNewConnInOptAction(peer.id));
if (!isWeb) {
menuItems.add(await _forceAlwaysRelayAction(peer.id));
}
if (isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id));
}
final menuItems = await _buildConnectionMenuItems(context);
menuItems.add(MenuEntryDivider());
if (isWindows) {
menuItems.add(_createShortCutAction(peer.id));
}
menuItems.add(MenuEntryDivider());
if (isMobile || isDesktop || isWebDesktop) {
menuItems.add(_renameAction(peer.id));
}
@@ -1079,31 +1114,11 @@ class FavoritePeerCard extends BasePeerCard {
@override
Future<List<MenuEntryBase<String>>> _buildMenuItems(
BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [
_connectAction(context),
_transferFileAction(context),
_viewCameraAction(context),
_terminalAction(context),
];
if (peer.platform == kPeerPlatformWindows) {
menuItems.add(_terminalRunAsAdminAction(context));
}
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context));
}
// menuItems.add(await _openNewConnInOptAction(peer.id));
if (!isWeb) {
menuItems.add(await _forceAlwaysRelayAction(peer.id));
}
if (isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id));
}
final menuItems = await _buildConnectionMenuItems(context);
menuItems.add(MenuEntryDivider());
if (isWindows) {
menuItems.add(_createShortCutAction(peer.id));
}
menuItems.add(MenuEntryDivider());
if (isMobile || isDesktop || isWebDesktop) {
menuItems.add(_renameAction(peer.id));
}
@@ -1139,29 +1154,10 @@ class DiscoveredPeerCard extends BasePeerCard {
@override
Future<List<MenuEntryBase<String>>> _buildMenuItems(
BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [
_connectAction(context),
_transferFileAction(context),
_viewCameraAction(context),
_terminalAction(context),
];
if (peer.platform == kPeerPlatformWindows) {
menuItems.add(_terminalRunAsAdminAction(context));
}
final List favs = (await bind.mainGetFav()).toList();
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context));
}
// menuItems.add(await _openNewConnInOptAction(peer.id));
if (!isWeb) {
menuItems.add(await _forceAlwaysRelayAction(peer.id));
}
if (isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id));
}
final menuItems = await _buildConnectionMenuItems(context);
menuItems.add(MenuEntryDivider());
menuItems.add(_wolAction(peer.id));
if (isWindows) {
menuItems.add(_createShortCutAction(peer.id));
@@ -1198,32 +1194,12 @@ class AddressBookPeerCard extends BasePeerCard {
@override
Future<List<MenuEntryBase<String>>> _buildMenuItems(
BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [
_connectAction(context),
_transferFileAction(context),
_viewCameraAction(context),
_terminalAction(context),
];
if (peer.platform == kPeerPlatformWindows) {
menuItems.add(_terminalRunAsAdminAction(context));
}
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context));
}
// menuItems.add(await _openNewConnInOptAction(peer.id));
if (!isWeb) {
menuItems.add(await _forceAlwaysRelayAction(peer.id));
}
if (isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id));
}
final menuItems = await _buildConnectionMenuItems(context);
menuItems.add(MenuEntryDivider());
if (isWindows) {
menuItems.add(_createShortCutAction(peer.id));
}
if (gFFI.abModel.current.canWrite()) {
menuItems.add(MenuEntryDivider());
if (isMobile || isDesktop || isWebDesktop) {
menuItems.add(_renameAction(peer.id));
}
@@ -1355,26 +1331,9 @@ class MyGroupPeerCard extends BasePeerCard {
@override
Future<List<MenuEntryBase<String>>> _buildMenuItems(
BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [
_connectAction(context),
_transferFileAction(context),
_viewCameraAction(context),
_terminalAction(context),
];
if (peer.platform == kPeerPlatformWindows) {
menuItems.add(_terminalRunAsAdminAction(context));
}
if (isDesktop && peer.platform != kPeerPlatformAndroid) {
menuItems.add(_tcpTunnelingAction(context));
}
// menuItems.add(await _openNewConnInOptAction(peer.id));
if (!isWeb) {
menuItems.add(await _forceAlwaysRelayAction(peer.id));
}
if (isWindows && peer.platform == kPeerPlatformWindows) {
menuItems.add(_rdpAction(context, peer.id));
final menuItems = await _buildConnectionMenuItems(context);
if (isWindows || gFFI.userModel.userName.isNotEmpty) {
menuItems.add(MenuEntryDivider());
}
if (isWindows) {
menuItems.add(_createShortCutAction(peer.id));
@@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
import '../../desktop/widgets/popup_menu.dart';
class RequestApprovalMenu extends MenuEntryBase<String> {
final String title;
final List<MenuEntryButton<String>> entries;
final EdgeInsets? padding;
RequestApprovalMenu(
{required this.title, required this.entries, this.padding});
@override
List<mod_menu.PopupMenuEntry<String>> build(
BuildContext context, MenuConfig conf) {
final theme = Theme.of(context);
final buttonPadding =
(TextButtonTheme.of(context).style?.padding?.resolve({}) ??
ButtonStyleButton.scaledPadding(
theme.useMaterial3
? const EdgeInsets.symmetric(horizontal: 12, vertical: 8)
: const EdgeInsets.all(8),
const EdgeInsets.symmetric(horizontal: 8),
const EdgeInsets.symmetric(horizontal: 4),
MediaQuery.textScalerOf(context)
.scale(theme.textTheme.labelLarge?.fontSize ?? 14) /
14,
))
.resolve(Directionality.of(context));
final checkboxTheme = CheckboxTheme.of(context);
final checkboxWidth =
((checkboxTheme.materialTapTargetSize ?? theme.materialTapTargetSize) ==
MaterialTapTargetSize.padded
? kMinInteractiveDimension
: kMinInteractiveDimension - 8) +
(checkboxTheme.visualDensity ?? theme.visualDensity)
.baseSizeAdjustment
.dx;
final arrowInset = (checkboxWidth - (IconTheme.of(context).size ?? 24)) / 2;
return MenuEntrySubMenu<String>(
text: title,
dismissParentOnCanceled: true,
// Normal entries contain two TextButtons; the submenu adds a leading gap.
padding: (padding ?? EdgeInsets.zero)
.add(EdgeInsets.only(
left: buttonPadding.left * 2,
right: buttonPadding.right * 2 + arrowInset))
.subtract(
const EdgeInsetsDirectional.only(start: MenuConfig.midPadding))
.resolve(Directionality.of(context)),
entries: entries
.map((entry) => MenuEntryButton<String>(
childBuilder: entry.childBuilder,
proc: entry.proc,
padding: entry.padding,
enabled: entry.enabled,
dismissOnClicked: true,
dismissCallback: () {
if (Navigator.canPop(context)) Navigator.pop(context);
},
))
.toList(),
).build(context, conf);
}
}