mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-11-04 00:33:12 +00:00
feat: request approval (#16784)
* feat: add connections without sending passwords
Add Without password actions and pass the selection through desktop,
mobile and Web sessions. Omit the password when constructing LoginRequest,
preserve saved credentials, and reuse the existing approval and OS login
dialogs. Keep protocol and controlled-side authentication changes separate.
* fix: preserve login method across session windows
* fix: skip password persistence for consent connections
* refactor: unify request approval menus and naming
* chore: move request approval translation key to the end
* i18n: translate request approval menu label
* fix: align request approval menu layout and icons
* fix: avoid duplicate focus stops in request approval menus
* fix: distinguish request approval submenus on mobile
* fix: open overflowing submenus to the left
* Revert "fix: distinguish request approval submenus on mobile"
This reverts commit 7f0e50d4e4.
This commit is contained in:
1 parent
037b8787fe
commit
a85ad89ffc
89 files changed
+1110
-152
No files matched your search
+44
-4
@@ -2286,6 +2286,7 @@ bool handleUriLink({List<String>? cmdArgs, Uri? uri, String? uriString}) {
|
||||
String? password;
|
||||
String? switchUuid;
|
||||
bool? forceRelay;
|
||||
var loginMethod = '';
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
switch (args[i]) {
|
||||
case '--connect':
|
||||
@@ -2329,6 +2330,9 @@ bool handleUriLink({List<String>? cmdArgs, Uri? uri, String? uriString}) {
|
||||
password = args[i + 1];
|
||||
i++;
|
||||
break;
|
||||
case '--request-approval':
|
||||
loginMethod = 'request-approval';
|
||||
break;
|
||||
case '--switch_uuid':
|
||||
switchUuid = args[i + 1];
|
||||
i++;
|
||||
@@ -2340,11 +2344,15 @@ bool handleUriLink({List<String>? cmdArgs, Uri? uri, String? uriString}) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (loginMethod == 'request-approval') {
|
||||
password = null;
|
||||
}
|
||||
if (type != null && id != null) {
|
||||
switch (type) {
|
||||
case UriLinkType.remoteDesktop:
|
||||
Future.delayed(Duration.zero, () {
|
||||
rustDeskWinManager.newRemoteDesktop(id!,
|
||||
loginMethod: loginMethod,
|
||||
password: password,
|
||||
switchUuid: switchUuid,
|
||||
forceRelay: forceRelay);
|
||||
@@ -2353,30 +2361,35 @@ bool handleUriLink({List<String>? cmdArgs, Uri? uri, String? uriString}) {
|
||||
case UriLinkType.fileTransfer:
|
||||
Future.delayed(Duration.zero, () {
|
||||
rustDeskWinManager.newFileTransfer(id!,
|
||||
loginMethod: loginMethod,
|
||||
password: password, forceRelay: forceRelay);
|
||||
});
|
||||
break;
|
||||
case UriLinkType.viewCamera:
|
||||
Future.delayed(Duration.zero, () {
|
||||
rustDeskWinManager.newViewCamera(id!,
|
||||
loginMethod: loginMethod,
|
||||
password: password, forceRelay: forceRelay);
|
||||
});
|
||||
break;
|
||||
case UriLinkType.portForward:
|
||||
Future.delayed(Duration.zero, () {
|
||||
rustDeskWinManager.newPortForward(id!, false,
|
||||
loginMethod: loginMethod,
|
||||
password: password, forceRelay: forceRelay);
|
||||
});
|
||||
break;
|
||||
case UriLinkType.rdp:
|
||||
Future.delayed(Duration.zero, () {
|
||||
rustDeskWinManager.newPortForward(id!, true,
|
||||
loginMethod: loginMethod,
|
||||
password: password, forceRelay: forceRelay);
|
||||
});
|
||||
break;
|
||||
case UriLinkType.terminal:
|
||||
Future.delayed(Duration.zero, () {
|
||||
rustDeskWinManager.newTerminal(id!,
|
||||
loginMethod: loginMethod,
|
||||
password: password, forceRelay: forceRelay);
|
||||
});
|
||||
break;
|
||||
@@ -2473,6 +2486,10 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
|
||||
|
||||
var queryParameters =
|
||||
uri.queryParameters.map((k, v) => MapEntry(k.toLowerCase(), v));
|
||||
final requestApproval = queryParameters['request-approval'] == 'true';
|
||||
if (requestApproval) {
|
||||
queryParameters.remove('password');
|
||||
}
|
||||
|
||||
var key = queryParameters["key"];
|
||||
if (id != null) {
|
||||
@@ -2484,24 +2501,30 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
|
||||
if (isMobile && id != null) {
|
||||
final forceRelay = queryParameters["relay"] != null;
|
||||
final password = queryParameters["password"];
|
||||
final loginMethod = requestApproval ? 'request-approval' : '';
|
||||
|
||||
// Determine connection type based on command
|
||||
if (command == '--file-transfer') {
|
||||
connect(Get.context!, id,
|
||||
loginMethod: loginMethod,
|
||||
isFileTransfer: true, forceRelay: forceRelay, password: password);
|
||||
} else if (command == '--view-camera') {
|
||||
connect(Get.context!, id,
|
||||
loginMethod: loginMethod,
|
||||
isViewCamera: true, forceRelay: forceRelay, password: password);
|
||||
} else if (command == '--terminal') {
|
||||
connect(Get.context!, id,
|
||||
loginMethod: loginMethod,
|
||||
isTerminal: true, forceRelay: forceRelay, password: password);
|
||||
} else if (command == 'terminal-admin') {
|
||||
} else if (command == '--terminal-admin') {
|
||||
setEnvTerminalAdmin();
|
||||
connect(Get.context!, id,
|
||||
loginMethod: loginMethod,
|
||||
isTerminal: true, forceRelay: forceRelay, password: password);
|
||||
} else {
|
||||
// Default to remote desktop for '--connect', '--play', or direct connection
|
||||
connect(Get.context!, id, forceRelay: forceRelay, password: password);
|
||||
connect(Get.context!, id,
|
||||
loginMethod: loginMethod, forceRelay: forceRelay, password: password);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -2513,6 +2536,7 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
|
||||
var param = queryParameters;
|
||||
String? password = param["password"];
|
||||
if (password != null) args.addAll(['--password', password]);
|
||||
if (requestApproval) args.add('--request-approval');
|
||||
String? switch_uuid = param["switch_uuid"];
|
||||
if (switch_uuid != null) args.addAll(['--switch_uuid', switch_uuid]);
|
||||
if (param["relay"] != null) args.add("--relay");
|
||||
@@ -2531,33 +2555,39 @@ connectMainDesktop(String id,
|
||||
bool? forceRelay,
|
||||
String? password,
|
||||
String? connToken,
|
||||
bool? isSharedPassword}) async {
|
||||
bool? isSharedPassword,
|
||||
String loginMethod = ''}) async {
|
||||
if (isFileTransfer) {
|
||||
await rustDeskWinManager.newFileTransfer(id,
|
||||
loginMethod: loginMethod,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
connToken: connToken,
|
||||
forceRelay: forceRelay);
|
||||
} else if (isViewCamera) {
|
||||
await rustDeskWinManager.newViewCamera(id,
|
||||
loginMethod: loginMethod,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
connToken: connToken,
|
||||
forceRelay: forceRelay);
|
||||
} else if (isTcpTunneling || isRDP) {
|
||||
await rustDeskWinManager.newPortForward(id, isRDP,
|
||||
loginMethod: loginMethod,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
connToken: connToken,
|
||||
forceRelay: forceRelay);
|
||||
} else if (isTerminal) {
|
||||
await rustDeskWinManager.newTerminal(id,
|
||||
loginMethod: loginMethod,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
connToken: connToken,
|
||||
forceRelay: forceRelay);
|
||||
} else {
|
||||
await rustDeskWinManager.newRemoteDesktop(id,
|
||||
loginMethod: loginMethod,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
forceRelay: forceRelay);
|
||||
@@ -2578,7 +2608,8 @@ connect(BuildContext context, String id,
|
||||
bool forceRelay = false,
|
||||
String? password,
|
||||
String? connToken,
|
||||
bool? isSharedPassword}) async {
|
||||
bool? isSharedPassword,
|
||||
String loginMethod = ''}) async {
|
||||
if (id == '') return;
|
||||
if (!isDesktop || desktopType == DesktopType.main) {
|
||||
try {
|
||||
@@ -2603,6 +2634,7 @@ connect(BuildContext context, String id,
|
||||
if (desktopType == DesktopType.main) {
|
||||
await connectMainDesktop(
|
||||
id,
|
||||
loginMethod: loginMethod,
|
||||
isFileTransfer: isFileTransfer,
|
||||
isViewCamera: isViewCamera,
|
||||
isTerminal: isTerminal,
|
||||
@@ -2624,6 +2656,7 @@ connect(BuildContext context, String id,
|
||||
'isSharedPassword': isSharedPassword,
|
||||
'forceRelay': forceRelay,
|
||||
'connToken': connToken,
|
||||
'loginMethod': loginMethod,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -2635,6 +2668,7 @@ connect(BuildContext context, String id,
|
||||
builder: (BuildContext context) =>
|
||||
desktop_file_manager.FileManagerPage(
|
||||
id: id,
|
||||
loginMethod: loginMethod,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword),
|
||||
),
|
||||
@@ -2645,6 +2679,7 @@ connect(BuildContext context, String id,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) => FileManagerPage(
|
||||
id: id,
|
||||
loginMethod: loginMethod,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
forceRelay: forceRelay),
|
||||
@@ -2660,6 +2695,7 @@ connect(BuildContext context, String id,
|
||||
desktop_view_camera.ViewCameraPage(
|
||||
key: ValueKey(id),
|
||||
id: id,
|
||||
loginMethod: loginMethod,
|
||||
toolbarState: ToolbarState(),
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
@@ -2672,6 +2708,7 @@ connect(BuildContext context, String id,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) => ViewCameraPage(
|
||||
id: id,
|
||||
loginMethod: loginMethod,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
forceRelay: forceRelay),
|
||||
@@ -2684,6 +2721,7 @@ connect(BuildContext context, String id,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) => TerminalPage(
|
||||
id: id,
|
||||
loginMethod: loginMethod,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
forceRelay: forceRelay,
|
||||
@@ -2698,6 +2736,7 @@ connect(BuildContext context, String id,
|
||||
builder: (BuildContext context) => desktop_remote.RemotePage(
|
||||
key: ValueKey(id),
|
||||
id: id,
|
||||
loginMethod: loginMethod,
|
||||
toolbarState: ToolbarState(),
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
@@ -2710,6 +2749,7 @@ connect(BuildContext context, String id,
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) => RemotePage(
|
||||
id: id,
|
||||
loginMethod: loginMethod,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
forceRelay: forceRelay),
|
||||
|
||||
@@ -937,13 +937,15 @@ void enterPasswordDialog(
|
||||
}
|
||||
|
||||
void enterUserLoginDialog(SessionID sessionId,
|
||||
OverlayDialogManager dialogManager, String osAccountDescTip) async {
|
||||
OverlayDialogManager dialogManager, String osAccountDescTip,
|
||||
{bool waitForAccept = false}) async {
|
||||
await _connectDialog(
|
||||
sessionId,
|
||||
dialogManager,
|
||||
osUsernameController: TextEditingController(),
|
||||
osPasswordController: TextEditingController(),
|
||||
osAccountDescTip: osAccountDescTip,
|
||||
waitForAccept: waitForAccept,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -966,6 +968,7 @@ _connectDialog(
|
||||
TextEditingController? osPasswordController,
|
||||
TextEditingController? passwordController,
|
||||
String? osAccountDescTip,
|
||||
bool waitForAccept = false,
|
||||
}) async {
|
||||
final errUsername = ''.obs;
|
||||
var rememberPassword = false;
|
||||
@@ -1008,8 +1011,17 @@ _connectDialog(
|
||||
rememberPassword,
|
||||
);
|
||||
close();
|
||||
dialogManager.showLoading(translate('Logging in...'),
|
||||
onCancel: closeConnection);
|
||||
if (waitForAccept) {
|
||||
showWaitAcceptDialog(
|
||||
sessionId,
|
||||
'wait-remote-accept-nook',
|
||||
'Prompt',
|
||||
'Please wait for the remote side to accept your session request...',
|
||||
dialogManager);
|
||||
} else {
|
||||
dialogManager.showLoading(translate('Logging in...'),
|
||||
onCancel: closeConnection);
|
||||
}
|
||||
}
|
||||
|
||||
descWidget(String text) {
|
||||
|
||||
@@ -15,6 +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 'request_approval_menu.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
typedef PopupMenuEntryBuilder = Future<List<mod_menu.PopupMenuEntry<String>>>
|
||||
@@ -572,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,
|
||||
@@ -581,12 +611,10 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
bool isRDP = false,
|
||||
bool isTerminal = false,
|
||||
bool isTerminalRunAsAdmin = false,
|
||||
String loginMethod = '',
|
||||
}) {
|
||||
return MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
title,
|
||||
style: style,
|
||||
),
|
||||
childBuilder: (TextStyle? style) => Text(title, style: style),
|
||||
proc: () {
|
||||
if (isTerminalRunAsAdmin) {
|
||||
setEnvTerminalAdmin();
|
||||
@@ -600,6 +628,7 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
isTcpTunneling: isTcpTunneling,
|
||||
isRDP: isRDP,
|
||||
isTerminal: isTerminal || isTerminalRunAsAdmin,
|
||||
loginMethod: loginMethod,
|
||||
);
|
||||
},
|
||||
padding: menuPadding,
|
||||
@@ -607,13 +636,53 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
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),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
MenuEntryBase<String> _connectAction(BuildContext context) {
|
||||
return _connectCommonAction(
|
||||
context,
|
||||
(peer.alias.isEmpty
|
||||
peer.alias.isEmpty
|
||||
? translate('Connect')
|
||||
: '${translate('Connect')} ${peer.id}'),
|
||||
: '${translate('Connect')} ${peer.id}',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -682,6 +751,10 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: kMinInteractiveDimension - 8,
|
||||
minHeight: kMinInteractiveDimension,
|
||||
),
|
||||
onPressed: () {
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
@@ -1000,33 +1073,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));
|
||||
}
|
||||
@@ -1065,31 +1118,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));
|
||||
}
|
||||
@@ -1125,29 +1158,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));
|
||||
@@ -1184,32 +1198,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));
|
||||
}
|
||||
@@ -1341,26 +1335,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));
|
||||
@@ -1591,7 +1568,8 @@ void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab,
|
||||
bool isViewCamera = false,
|
||||
bool isTcpTunneling = false,
|
||||
bool isRDP = false,
|
||||
bool isTerminal = false}) async {
|
||||
bool isTerminal = false,
|
||||
String loginMethod = ''}) async {
|
||||
var password = '';
|
||||
bool isSharedPassword = false;
|
||||
if (tab == PeerTabIndex.ab) {
|
||||
@@ -1619,6 +1597,7 @@ void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab,
|
||||
}
|
||||
}
|
||||
connect(context, peer.id,
|
||||
loginMethod: loginMethod,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
isFileTransfer: isFileTransfer,
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
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;
|
||||
final double arrowScale;
|
||||
|
||||
RequestApprovalMenu(
|
||||
{required this.title,
|
||||
required this.entries,
|
||||
this.padding,
|
||||
this.arrowScale = 1.0});
|
||||
|
||||
@override
|
||||
List<mod_menu.PopupMenuEntry<String>> build(
|
||||
BuildContext context, MenuConfig conf) {
|
||||
final theme = Theme.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,
|
||||
arrowScale: arrowScale,
|
||||
padding: (padding ?? EdgeInsets.zero)
|
||||
.copyWith(right: (padding?.right ?? 0) + arrowInset),
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -579,6 +579,9 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
bool isTerminal = false}) {
|
||||
final connToken = bind.sessionGetConnToken(sessionId: ffi.sessionId);
|
||||
connect(context, id,
|
||||
loginMethod: bind.sessionGetCommonSync(
|
||||
sessionId: ffi.sessionId, key: 'login_method', param: '') ??
|
||||
'',
|
||||
isFileTransfer: isFileTransfer,
|
||||
isViewCamera: isViewCamera,
|
||||
isTerminal: isTerminal,
|
||||
|
||||
@@ -18,6 +18,7 @@ import '../../common.dart';
|
||||
import '../../common/formatter/id_formatter.dart';
|
||||
import '../../common/widgets/peer_tab_page.dart';
|
||||
import '../../common/widgets/autocomplete.dart';
|
||||
import '../../common/widgets/request_approval_menu.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
|
||||
|
||||
@@ -340,6 +341,38 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
isTcpTunneling: isTcpTunneling);
|
||||
}
|
||||
|
||||
RequestApprovalMenu _requestApprovalMenu(BuildContext context) {
|
||||
MenuEntryButton<String> action(String title,
|
||||
{bool isFileTransfer = false,
|
||||
bool isViewCamera = false,
|
||||
bool isTerminal = false,
|
||||
bool isTcpTunneling = false}) =>
|
||||
MenuEntryButton<String>(
|
||||
childBuilder: (style) => Text(translate(title), style: style),
|
||||
proc: () => connect(context, _idController.id,
|
||||
loginMethod: 'request-approval',
|
||||
isFileTransfer: isFileTransfer,
|
||||
isViewCamera: isViewCamera,
|
||||
isTerminal: isTerminal,
|
||||
isTcpTunneling: isTcpTunneling),
|
||||
padding: EdgeInsets.symmetric(horizontal: kDesktopMenuPadding.left),
|
||||
dismissOnClicked: true,
|
||||
);
|
||||
|
||||
return RequestApprovalMenu(
|
||||
title: translate('Request approval'),
|
||||
padding: EdgeInsets.symmetric(horizontal: kDesktopMenuPadding.left),
|
||||
arrowScale: MenuConfig.iconScale,
|
||||
entries: [
|
||||
action('Connect'),
|
||||
action('Transfer file', isFileTransfer: true),
|
||||
action('View camera', isViewCamera: true),
|
||||
action('${translate('Terminal')} (beta)', isTerminal: true),
|
||||
if (isDesktop) action('TCP tunneling', isTcpTunneling: true),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// UI for the remote ID TextField.
|
||||
/// Search for a peer.
|
||||
Widget _buildRemoteIDTextField(BuildContext context) {
|
||||
@@ -579,7 +612,8 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
() => onConnect(isTcpTunneling: true)
|
||||
),
|
||||
]
|
||||
.map((e) => MenuEntryButton<String>(
|
||||
.map<MenuEntryBase<String>>(
|
||||
(e) => MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) =>
|
||||
Text(
|
||||
translate(e.$1),
|
||||
@@ -591,6 +625,10 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
kDesktopMenuPadding.left),
|
||||
dismissOnClicked: true,
|
||||
))
|
||||
.followedBy([
|
||||
MenuEntryDivider<String>(),
|
||||
_requestApprovalMenu(context),
|
||||
])
|
||||
.map((e) => e.build(
|
||||
context,
|
||||
const MenuConfig(
|
||||
|
||||
@@ -804,6 +804,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
password: call.arguments['password'],
|
||||
forceRelay: call.arguments['forceRelay'],
|
||||
connToken: call.arguments['connToken'],
|
||||
loginMethod: call.arguments['loginMethod'] ?? '',
|
||||
);
|
||||
} else if (call.method == kWindowBumpMouse) {
|
||||
return RdPlatformChannel.instance.bumpMouse(
|
||||
|
||||
@@ -56,6 +56,7 @@ class FileManagerPage extends StatefulWidget {
|
||||
{Key? key,
|
||||
required this.id,
|
||||
required this.password,
|
||||
this.loginMethod = '',
|
||||
required this.isSharedPassword,
|
||||
this.tabController,
|
||||
this.connToken,
|
||||
@@ -63,6 +64,7 @@ class FileManagerPage extends StatefulWidget {
|
||||
: super(key: key);
|
||||
final String id;
|
||||
final String? password;
|
||||
final String loginMethod;
|
||||
final bool? isSharedPassword;
|
||||
final bool? forceRelay;
|
||||
final String? connToken;
|
||||
@@ -99,6 +101,7 @@ class _FileManagerPageState extends State<FileManagerPage>
|
||||
_ffi.start(widget.id,
|
||||
isFileTransfer: true,
|
||||
password: widget.password,
|
||||
loginMethod: widget.loginMethod,
|
||||
isSharedPassword: widget.isSharedPassword,
|
||||
connToken: widget.connToken,
|
||||
forceRelay: widget.forceRelay);
|
||||
|
||||
@@ -54,6 +54,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
key: ValueKey(params['id']),
|
||||
id: params['id'],
|
||||
password: params['password'],
|
||||
loginMethod: params['loginMethod'] ?? '',
|
||||
isSharedPassword: params['isSharedPassword'],
|
||||
tabController: tabController,
|
||||
forceRelay: params['forceRelay'],
|
||||
@@ -91,6 +92,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
key: ValueKey(id),
|
||||
id: id,
|
||||
password: args['password'],
|
||||
loginMethod: args['loginMethod'] ?? '',
|
||||
isSharedPassword: args['isSharedPassword'],
|
||||
tabController: tabController,
|
||||
forceRelay: args['forceRelay'],
|
||||
|
||||
@@ -29,6 +29,7 @@ class PortForwardPage extends StatefulWidget {
|
||||
Key? key,
|
||||
required this.id,
|
||||
required this.password,
|
||||
this.loginMethod = '',
|
||||
required this.tabController,
|
||||
required this.isRDP,
|
||||
required this.isSharedPassword,
|
||||
@@ -37,6 +38,7 @@ class PortForwardPage extends StatefulWidget {
|
||||
}) : super(key: key);
|
||||
final String id;
|
||||
final String? password;
|
||||
final String loginMethod;
|
||||
final DesktopTabController tabController;
|
||||
final bool isRDP;
|
||||
final bool? forceRelay;
|
||||
@@ -69,6 +71,7 @@ class _PortForwardPageState extends State<PortForwardPage>
|
||||
_ffi.start(widget.id,
|
||||
isPortForward: true,
|
||||
password: widget.password,
|
||||
loginMethod: widget.loginMethod,
|
||||
isSharedPassword: widget.isSharedPassword,
|
||||
forceRelay: widget.forceRelay,
|
||||
connToken: widget.connToken,
|
||||
|
||||
@@ -44,6 +44,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||
key: ValueKey(params['id']),
|
||||
id: params['id'],
|
||||
password: params['password'],
|
||||
loginMethod: params['loginMethod'] ?? '',
|
||||
isSharedPassword: params['isSharedPassword'],
|
||||
tabController: tabController,
|
||||
isRDP: isRDP,
|
||||
@@ -79,6 +80,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||
key: ValueKey(args['id']),
|
||||
id: id,
|
||||
password: args['password'],
|
||||
loginMethod: args['loginMethod'] ?? '',
|
||||
isSharedPassword: args['isSharedPassword'],
|
||||
isRDP: isRDP,
|
||||
tabController: tabController,
|
||||
|
||||
@@ -41,6 +41,7 @@ class RemotePage extends StatefulWidget {
|
||||
this.sessionId,
|
||||
this.tabWindowId,
|
||||
this.password,
|
||||
this.loginMethod = '',
|
||||
this.display,
|
||||
this.displays,
|
||||
this.tabController,
|
||||
@@ -57,6 +58,7 @@ class RemotePage extends StatefulWidget {
|
||||
final int? display;
|
||||
final List<int>? displays;
|
||||
final String? password;
|
||||
final String loginMethod;
|
||||
final ToolbarState toolbarState;
|
||||
final String? switchUuid;
|
||||
final bool? forceRelay;
|
||||
@@ -180,6 +182,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
_ffi.start(
|
||||
widget.id,
|
||||
password: widget.password,
|
||||
loginMethod: widget.loginMethod,
|
||||
isSharedPassword: widget.isSharedPassword,
|
||||
switchUuid: widget.switchUuid,
|
||||
forceRelay: widget.forceRelay,
|
||||
|
||||
@@ -97,6 +97,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
display: display,
|
||||
displays: displays?.cast<int>(),
|
||||
password: params['password'],
|
||||
loginMethod: params['loginMethod'] ?? '',
|
||||
toolbarState: ToolbarState(),
|
||||
tabController: tabController,
|
||||
switchUuid: params['switch_uuid'],
|
||||
@@ -470,6 +471,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
display: display,
|
||||
displays: displays?.cast<int>(),
|
||||
password: args['password'],
|
||||
loginMethod: args['loginMethod'] ?? '',
|
||||
toolbarState: ToolbarState(),
|
||||
tabController: tabController,
|
||||
switchUuid: switchUuid,
|
||||
|
||||
@@ -14,6 +14,7 @@ class TerminalConnectionManager {
|
||||
static FFI getConnection({
|
||||
required String peerId,
|
||||
required String? password,
|
||||
String loginMethod = '',
|
||||
required bool? isSharedPassword,
|
||||
required bool? forceRelay,
|
||||
required String? connToken,
|
||||
@@ -32,6 +33,7 @@ class TerminalConnectionManager {
|
||||
ffi.start(
|
||||
peerId,
|
||||
password: password,
|
||||
loginMethod: loginMethod,
|
||||
isSharedPassword: isSharedPassword,
|
||||
forceRelay: forceRelay,
|
||||
connToken: connToken,
|
||||
|
||||
@@ -13,6 +13,7 @@ class TerminalPage extends StatefulWidget {
|
||||
Key? key,
|
||||
required this.id,
|
||||
required this.password,
|
||||
this.loginMethod = '',
|
||||
required this.tabController,
|
||||
required this.isSharedPassword,
|
||||
required this.terminalId,
|
||||
@@ -24,6 +25,7 @@ class TerminalPage extends StatefulWidget {
|
||||
}) : super(key: key);
|
||||
final String id;
|
||||
final String? password;
|
||||
final String loginMethod;
|
||||
final DesktopTabController tabController;
|
||||
final bool? forceRelay;
|
||||
final bool? isSharedPassword;
|
||||
@@ -68,6 +70,7 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
_ffi = TerminalConnectionManager.getConnection(
|
||||
peerId: widget.id,
|
||||
password: widget.password,
|
||||
loginMethod: widget.loginMethod,
|
||||
isSharedPassword: widget.isSharedPassword,
|
||||
forceRelay: widget.forceRelay,
|
||||
connToken: widget.connToken,
|
||||
|
||||
@@ -79,6 +79,7 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
||||
peerId: params['id'],
|
||||
terminalId: terminalId,
|
||||
password: params['password'],
|
||||
loginMethod: params['loginMethod'] ?? '',
|
||||
isSharedPassword: params['isSharedPassword'],
|
||||
forceRelay: params['forceRelay'],
|
||||
connToken: params['connToken'],
|
||||
@@ -89,6 +90,7 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
||||
required String peerId,
|
||||
required int terminalId,
|
||||
String? password,
|
||||
String loginMethod = '',
|
||||
bool? isSharedPassword,
|
||||
bool? forceRelay,
|
||||
String? connToken,
|
||||
@@ -114,6 +116,7 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
||||
terminalId: terminalId,
|
||||
tabKey: tabKey,
|
||||
password: password,
|
||||
loginMethod: loginMethod,
|
||||
isSharedPassword: isSharedPassword,
|
||||
tabController: tabController,
|
||||
forceRelay: forceRelay,
|
||||
@@ -514,6 +517,7 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
||||
peerId: id,
|
||||
terminalId: terminalId,
|
||||
password: args['password'],
|
||||
loginMethod: args['loginMethod'] ?? '',
|
||||
isSharedPassword: args['isSharedPassword'],
|
||||
forceRelay: args['forceRelay'],
|
||||
connToken: args['connToken'],
|
||||
@@ -713,6 +717,7 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
||||
peerId: peerId,
|
||||
terminalId: newTerminalId,
|
||||
password: page.password,
|
||||
loginMethod: page.loginMethod,
|
||||
isSharedPassword: page.isSharedPassword,
|
||||
forceRelay: page.forceRelay,
|
||||
connToken: page.connToken,
|
||||
|
||||
@@ -37,6 +37,7 @@ class ViewCameraPage extends StatefulWidget {
|
||||
this.sessionId,
|
||||
this.tabWindowId,
|
||||
this.password,
|
||||
this.loginMethod = '',
|
||||
this.display,
|
||||
this.displays,
|
||||
this.tabController,
|
||||
@@ -53,6 +54,7 @@ class ViewCameraPage extends StatefulWidget {
|
||||
final int? display;
|
||||
final List<int>? displays;
|
||||
final String? password;
|
||||
final String loginMethod;
|
||||
final ToolbarState toolbarState;
|
||||
final bool? forceRelay;
|
||||
final bool? isSharedPassword;
|
||||
@@ -112,6 +114,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
||||
widget.id,
|
||||
isViewCamera: true,
|
||||
password: widget.password,
|
||||
loginMethod: widget.loginMethod,
|
||||
isSharedPassword: widget.isSharedPassword,
|
||||
forceRelay: widget.forceRelay,
|
||||
tabWindowId: widget.tabWindowId,
|
||||
|
||||
@@ -97,6 +97,7 @@ class _ViewCameraTabPageState extends State<ViewCameraTabPage> {
|
||||
display: display,
|
||||
displays: displays?.cast<int>(),
|
||||
password: params['password'],
|
||||
loginMethod: params['loginMethod'] ?? '',
|
||||
toolbarState: ToolbarState(),
|
||||
tabController: tabController,
|
||||
connToken: params['connToken'],
|
||||
@@ -433,6 +434,7 @@ class _ViewCameraTabPageState extends State<ViewCameraTabPage> {
|
||||
display: display,
|
||||
displays: displays?.cast<int>(),
|
||||
password: args['password'],
|
||||
loginMethod: args['loginMethod'] ?? '',
|
||||
toolbarState: ToolbarState(),
|
||||
tabController: tabController,
|
||||
connToken: args['connToken'],
|
||||
|
||||
@@ -656,11 +656,13 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate {
|
||||
this.selectedItemIndex,
|
||||
this.textDirection,
|
||||
this.padding,
|
||||
this.avoidBounds,
|
||||
);
|
||||
this.avoidBounds, {
|
||||
this.submenuAnchor,
|
||||
});
|
||||
|
||||
// Rectangle of underlying button, relative to the overlay's dimensions.
|
||||
final RelativeRect position;
|
||||
final Rect? submenuAnchor;
|
||||
|
||||
// The sizes of each item are computed when the menu is laid out, and before
|
||||
// the route is laid out.
|
||||
@@ -735,6 +737,14 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate {
|
||||
DisplayFeatureSubScreen.subScreensInBounds(
|
||||
Offset.zero & size, avoidBounds);
|
||||
final Rect subScreen = _closestScreen(subScreens, originCenter);
|
||||
final anchor = submenuAnchor;
|
||||
if (anchor != null) {
|
||||
final left = anchor.left - childSize.width;
|
||||
if (wantedPosition.dx + childSize.width >
|
||||
subScreen.right - _kMenuScreenPadding - padding.right) {
|
||||
return _fitInsideScreen(subScreen, childSize, Offset(left, y));
|
||||
}
|
||||
}
|
||||
return _fitInsideScreen(subScreen, childSize, wantedPosition);
|
||||
}
|
||||
|
||||
@@ -781,6 +791,7 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate {
|
||||
assert(itemSizes.length == oldDelegate.itemSizes.length);
|
||||
|
||||
return position != oldDelegate.position ||
|
||||
submenuAnchor != oldDelegate.submenuAnchor ||
|
||||
selectedItemIndex != oldDelegate.selectedItemIndex ||
|
||||
textDirection != oldDelegate.textDirection ||
|
||||
!listEquals(itemSizes, oldDelegate.itemSizes) ||
|
||||
@@ -792,6 +803,7 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate {
|
||||
class _PopupMenuRoute<T> extends PopupRoute<T> {
|
||||
_PopupMenuRoute({
|
||||
required this.position,
|
||||
this.submenuAnchor,
|
||||
required this.items,
|
||||
this.menuWrapper,
|
||||
this.initialValue,
|
||||
@@ -805,6 +817,7 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
|
||||
}) : itemSizes = List<Size?>.filled(items.length, null);
|
||||
|
||||
final RelativeRect position;
|
||||
final Rect? submenuAnchor;
|
||||
final List<PopupMenuEntry<T>> items;
|
||||
final MenuWrapper? menuWrapper;
|
||||
final List<Size?> itemSizes;
|
||||
@@ -874,6 +887,7 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
|
||||
Directionality.of(context),
|
||||
mediaQuery.padding,
|
||||
_avoidBounds(mediaQuery),
|
||||
submenuAnchor: submenuAnchor,
|
||||
),
|
||||
child: capturedThemes.wrap(menu),
|
||||
);
|
||||
@@ -1042,6 +1056,7 @@ class PopupMenu<T> extends StatelessWidget {
|
||||
Future<T?> showMenu<T>({
|
||||
required BuildContext context,
|
||||
required RelativeRect position,
|
||||
Rect? submenuAnchor,
|
||||
required List<PopupMenuEntry<T>> items,
|
||||
MenuWrapper? menuWrapper,
|
||||
T? initialValue,
|
||||
@@ -1070,6 +1085,7 @@ Future<T?> showMenu<T>({
|
||||
Navigator.of(context, rootNavigator: useRootNavigator);
|
||||
return navigator.push(_PopupMenuRoute<T>(
|
||||
position: position,
|
||||
submenuAnchor: submenuAnchor,
|
||||
items: items,
|
||||
menuWrapper: menuWrapper,
|
||||
initialValue: initialValue,
|
||||
@@ -1352,6 +1368,9 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
|
||||
menuWrapper: widget.menuWrapper,
|
||||
initialValue: widget.initialValue,
|
||||
position: position,
|
||||
submenuAnchor: widget.position == PopupMenuPosition.overSide
|
||||
? button.localToGlobal(Offset.zero, ancestor: overlay) & button.size
|
||||
: null,
|
||||
shape: widget.shape ?? popupMenuTheme.shape,
|
||||
color: widget.color ?? popupMenuTheme.color,
|
||||
constraints: widget.constraints,
|
||||
|
||||
@@ -15,6 +15,8 @@ class PopupMenuChildrenItem<T> extends mod_menu.PopupMenuEntry<T> {
|
||||
this.enabled,
|
||||
this.textStyle,
|
||||
this.onTap,
|
||||
this.dismissParentOnCanceled = false,
|
||||
this.useButtonLayout = false,
|
||||
this.position = mod_menu.PopupMenuPosition.overSide,
|
||||
this.offset = Offset.zero,
|
||||
required this.itemBuilder,
|
||||
@@ -27,6 +29,8 @@ class PopupMenuChildrenItem<T> extends mod_menu.PopupMenuEntry<T> {
|
||||
final EdgeInsets? padding;
|
||||
final RxBool? enabled;
|
||||
final void Function()? onTap;
|
||||
final bool dismissParentOnCanceled;
|
||||
final bool useButtonLayout;
|
||||
final List<mod_menu.PopupMenuEntry<T>> Function(BuildContext) itemBuilder;
|
||||
final Widget child;
|
||||
|
||||
@@ -69,20 +73,42 @@ class MyPopupMenuItemState<T, W extends PopupMenuChildrenItem<T>>
|
||||
position: widget.position,
|
||||
offset: widget.offset,
|
||||
onSelected: handleTap,
|
||||
onCanceled: widget.dismissParentOnCanceled
|
||||
? () {
|
||||
if (ModalRoute.of(context)?.isCurrent == true) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
itemBuilder: widget.itemBuilder,
|
||||
padding: EdgeInsets.zero,
|
||||
child: AnimatedDefaultTextStyle(
|
||||
style: style,
|
||||
duration: kThemeChangeDuration,
|
||||
child: Container(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
constraints: BoxConstraints(
|
||||
minHeight: widget.height, maxHeight: widget.height),
|
||||
padding:
|
||||
widget.padding ?? const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
child: Builder(builder: (context) {
|
||||
final child = AnimatedDefaultTextStyle(
|
||||
style: style,
|
||||
duration: kThemeChangeDuration,
|
||||
child: Container(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
constraints: BoxConstraints(
|
||||
minHeight: widget.height, maxHeight: widget.height),
|
||||
padding: widget.padding ??
|
||||
const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
if (!widget.useButtonLayout) return child;
|
||||
final onPressed = enabled.value
|
||||
? () => context
|
||||
.findAncestorStateOfType<mod_menu.PopupMenuButtonState<T>>()
|
||||
?.showButtonMenu()
|
||||
: null;
|
||||
// The popup trigger owns keyboard focus; retain the menu button sizing.
|
||||
return ExcludeFocus(
|
||||
child: TextButton(
|
||||
onPressed: onPressed,
|
||||
child: TextButton(onPressed: onPressed, child: child),
|
||||
),
|
||||
);
|
||||
}),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -645,11 +671,15 @@ class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
|
||||
final String text;
|
||||
final List<MenuEntryBase<T>> entries;
|
||||
final EdgeInsets? padding;
|
||||
final bool dismissParentOnCanceled;
|
||||
final double arrowScale;
|
||||
|
||||
MenuEntrySubMenu({
|
||||
required this.text,
|
||||
required this.entries,
|
||||
this.padding,
|
||||
this.dismissParentOnCanceled = false,
|
||||
this.arrowScale = MenuConfig.iconScale,
|
||||
RxBool? enabled,
|
||||
}) : super(enabled: enabled);
|
||||
|
||||
@@ -662,13 +692,14 @@ class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
|
||||
enabled: super.enabled,
|
||||
height: conf.height,
|
||||
padding: padding,
|
||||
dismissParentOnCanceled: dismissParentOnCanceled,
|
||||
useButtonLayout: true,
|
||||
position: mod_menu.PopupMenuPosition.overSide,
|
||||
itemBuilder: (BuildContext context) => entries
|
||||
.map((entry) => entry.build(context, conf))
|
||||
.expand((i) => i)
|
||||
.toList(),
|
||||
child: Row(children: [
|
||||
const SizedBox(width: MenuConfig.midPadding),
|
||||
Obx(() => Text(
|
||||
text,
|
||||
style: super.enabled!.value
|
||||
@@ -678,10 +709,15 @@ class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Obx(() => Icon(
|
||||
Icons.keyboard_arrow_right,
|
||||
color: super.enabled!.value ? conf.commonColor : Colors.grey,
|
||||
)),
|
||||
child: Transform.scale(
|
||||
scale: arrowScale,
|
||||
child: Obx(() => Icon(
|
||||
Icons.keyboard_arrow_right,
|
||||
color: super.enabled!.value
|
||||
? Theme.of(context).unselectedWidgetColor
|
||||
: disabledStyle().color,
|
||||
)),
|
||||
),
|
||||
))
|
||||
]),
|
||||
)
|
||||
|
||||
@@ -16,11 +16,13 @@ class FileManagerPage extends StatefulWidget {
|
||||
FileManagerPage(
|
||||
{Key? key,
|
||||
required this.id,
|
||||
this.loginMethod = '',
|
||||
this.password,
|
||||
this.isSharedPassword,
|
||||
this.forceRelay})
|
||||
: super(key: key);
|
||||
final String id;
|
||||
final String loginMethod;
|
||||
final String? password;
|
||||
final bool? isSharedPassword;
|
||||
final bool? forceRelay;
|
||||
@@ -276,6 +278,7 @@ class _FileManagerPageState extends State<FileManagerPage> {
|
||||
super.initState();
|
||||
gFFI.start(widget.id,
|
||||
isFileTransfer: true,
|
||||
loginMethod: widget.loginMethod,
|
||||
password: widget.password,
|
||||
isSharedPassword: widget.isSharedPassword,
|
||||
forceRelay: widget.forceRelay);
|
||||
|
||||
@@ -208,6 +208,7 @@ class WebHomePage extends StatelessWidget {
|
||||
bool isTerminal = false;
|
||||
String? id;
|
||||
String? password;
|
||||
var loginMethod = '';
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
switch (args[i]) {
|
||||
case '--connect':
|
||||
@@ -240,6 +241,9 @@ class WebHomePage extends StatelessWidget {
|
||||
password = args[i + 1];
|
||||
i++;
|
||||
break;
|
||||
case '--request-approval':
|
||||
loginMethod = 'request-approval';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -249,6 +253,7 @@ class WebHomePage extends StatelessWidget {
|
||||
isFileTransfer: isFileTransfer,
|
||||
isViewCamera: isViewCamera,
|
||||
isTerminal: isTerminal,
|
||||
loginMethod: loginMethod,
|
||||
password: password);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,12 +71,14 @@ class RemotePage extends StatefulWidget {
|
||||
RemotePage(
|
||||
{Key? key,
|
||||
required this.id,
|
||||
this.loginMethod = '',
|
||||
this.password,
|
||||
this.isSharedPassword,
|
||||
this.forceRelay})
|
||||
: super(key: key);
|
||||
|
||||
final String id;
|
||||
final String loginMethod;
|
||||
final String? password;
|
||||
final bool? isSharedPassword;
|
||||
final bool? forceRelay;
|
||||
@@ -123,6 +125,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||
gFFI.ffiModel.updateEventListener(sessionId, widget.id);
|
||||
gFFI.start(
|
||||
widget.id,
|
||||
loginMethod: widget.loginMethod,
|
||||
password: widget.password,
|
||||
isSharedPassword: widget.isSharedPassword,
|
||||
forceRelay: widget.forceRelay,
|
||||
|
||||
@@ -71,8 +71,10 @@ class TerminalPage extends StatefulWidget {
|
||||
required this.isSharedPassword,
|
||||
this.forceRelay,
|
||||
this.connToken,
|
||||
this.loginMethod = '',
|
||||
}) : super(key: key);
|
||||
final String id;
|
||||
final String loginMethod;
|
||||
final String? password;
|
||||
final bool? forceRelay;
|
||||
final bool? isSharedPassword;
|
||||
@@ -141,6 +143,7 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
// Use shared FFI instance from connection manager
|
||||
_ffi = TerminalConnectionManager.getConnection(
|
||||
peerId: widget.id,
|
||||
loginMethod: widget.loginMethod,
|
||||
password: widget.password,
|
||||
isSharedPassword: widget.isSharedPassword,
|
||||
forceRelay: widget.forceRelay,
|
||||
|
||||
@@ -40,12 +40,14 @@ class ViewCameraPage extends StatefulWidget {
|
||||
ViewCameraPage(
|
||||
{Key? key,
|
||||
required this.id,
|
||||
this.loginMethod = '',
|
||||
this.password,
|
||||
this.isSharedPassword,
|
||||
this.forceRelay})
|
||||
: super(key: key);
|
||||
|
||||
final String id;
|
||||
final String loginMethod;
|
||||
final String? password;
|
||||
final bool? isSharedPassword;
|
||||
final bool? forceRelay;
|
||||
@@ -90,6 +92,7 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
||||
gFFI.start(
|
||||
widget.id,
|
||||
isViewCamera: true,
|
||||
loginMethod: widget.loginMethod,
|
||||
password: widget.password,
|
||||
isSharedPassword: widget.isSharedPassword,
|
||||
forceRelay: widget.forceRelay,
|
||||
|
||||
@@ -920,9 +920,10 @@ class FfiModel with ChangeNotifier {
|
||||
enter2FaDialog(sessionId, dialogManager);
|
||||
} else if (type == 'input-password') {
|
||||
enterPasswordDialog(sessionId, dialogManager);
|
||||
} else if (type == 'terminal-admin-login') {
|
||||
enterUserLoginDialog(
|
||||
sessionId, dialogManager, 'terminal-admin-login-tip');
|
||||
} else if (type == 'terminal-admin-login' ||
|
||||
type == 'terminal-admin-login-request-approval') {
|
||||
enterUserLoginDialog(sessionId, dialogManager, 'terminal-admin-login-tip',
|
||||
waitForAccept: type == 'terminal-admin-login-request-approval');
|
||||
} else if (type == 'terminal-admin-login-password') {
|
||||
enterUserLoginAndPasswordDialog(
|
||||
sessionId, dialogManager, 'terminal-admin-login-tip');
|
||||
@@ -4123,6 +4124,7 @@ class FFI {
|
||||
int? tabWindowId,
|
||||
int? display,
|
||||
List<int>? displays,
|
||||
String loginMethod = '',
|
||||
}) {
|
||||
closed = false;
|
||||
if (isMobile) mobileReset();
|
||||
@@ -4168,6 +4170,7 @@ class FFI {
|
||||
password: password ?? '',
|
||||
isSharedPassword: isSharedPassword ?? false,
|
||||
connToken: connToken,
|
||||
loginMethod: loginMethod,
|
||||
);
|
||||
} else if (display != null) {
|
||||
if (displays == null) {
|
||||
|
||||
@@ -225,6 +225,7 @@ class RustDeskMultiWindowManager {
|
||||
String remoteId,
|
||||
List<int> windows, {
|
||||
String? password,
|
||||
String loginMethod = '',
|
||||
bool? forceRelay,
|
||||
String? switchUuid,
|
||||
bool? isRDP,
|
||||
@@ -249,6 +250,9 @@ class RustDeskMultiWindowManager {
|
||||
if (connToken != null) {
|
||||
params['connToken'] = connToken;
|
||||
}
|
||||
if (loginMethod.isNotEmpty) {
|
||||
params['loginMethod'] = loginMethod;
|
||||
}
|
||||
final msg = jsonEncode(params);
|
||||
|
||||
// separate window for file transfer is not supported
|
||||
@@ -270,6 +274,7 @@ class RustDeskMultiWindowManager {
|
||||
Future<MultiWindowCallResult> newRemoteDesktop(
|
||||
String remoteId, {
|
||||
String? password,
|
||||
String loginMethod = '',
|
||||
bool? isSharedPassword,
|
||||
String? switchUuid,
|
||||
bool? forceRelay,
|
||||
@@ -280,6 +285,7 @@ class RustDeskMultiWindowManager {
|
||||
remoteId,
|
||||
_remoteDesktopWindows,
|
||||
password: password,
|
||||
loginMethod: loginMethod,
|
||||
forceRelay: forceRelay,
|
||||
switchUuid: switchUuid,
|
||||
isSharedPassword: isSharedPassword,
|
||||
@@ -289,6 +295,7 @@ class RustDeskMultiWindowManager {
|
||||
Future<MultiWindowCallResult> newFileTransfer(
|
||||
String remoteId, {
|
||||
String? password,
|
||||
String loginMethod = '',
|
||||
bool? isSharedPassword,
|
||||
bool? forceRelay,
|
||||
String? connToken,
|
||||
@@ -299,6 +306,7 @@ class RustDeskMultiWindowManager {
|
||||
remoteId,
|
||||
_fileTransferWindows,
|
||||
password: password,
|
||||
loginMethod: loginMethod,
|
||||
forceRelay: forceRelay,
|
||||
isSharedPassword: isSharedPassword,
|
||||
connToken: connToken,
|
||||
@@ -308,6 +316,7 @@ class RustDeskMultiWindowManager {
|
||||
Future<MultiWindowCallResult> newViewCamera(
|
||||
String remoteId, {
|
||||
String? password,
|
||||
String loginMethod = '',
|
||||
bool? isSharedPassword,
|
||||
String? switchUuid,
|
||||
bool? forceRelay,
|
||||
@@ -319,6 +328,7 @@ class RustDeskMultiWindowManager {
|
||||
remoteId,
|
||||
_viewCameraWindows,
|
||||
password: password,
|
||||
loginMethod: loginMethod,
|
||||
forceRelay: forceRelay,
|
||||
switchUuid: switchUuid,
|
||||
isSharedPassword: isSharedPassword,
|
||||
@@ -330,6 +340,7 @@ class RustDeskMultiWindowManager {
|
||||
String remoteId,
|
||||
bool isRDP, {
|
||||
String? password,
|
||||
String loginMethod = '',
|
||||
bool? isSharedPassword,
|
||||
bool? forceRelay,
|
||||
String? connToken,
|
||||
@@ -340,6 +351,7 @@ class RustDeskMultiWindowManager {
|
||||
remoteId,
|
||||
_portForwardWindows,
|
||||
password: password,
|
||||
loginMethod: loginMethod,
|
||||
forceRelay: forceRelay,
|
||||
isRDP: isRDP,
|
||||
isSharedPassword: isSharedPassword,
|
||||
@@ -350,6 +362,7 @@ class RustDeskMultiWindowManager {
|
||||
Future<MultiWindowCallResult> newTerminal(
|
||||
String remoteId, {
|
||||
String? password,
|
||||
String loginMethod = '',
|
||||
bool? isSharedPassword,
|
||||
bool? forceRelay,
|
||||
String? connToken,
|
||||
@@ -375,6 +388,9 @@ class RustDeskMultiWindowManager {
|
||||
"isSharedPassword": isSharedPassword,
|
||||
"connToken": connToken,
|
||||
};
|
||||
if (loginMethod.isNotEmpty) {
|
||||
params['loginMethod'] = loginMethod;
|
||||
}
|
||||
final msg = jsonEncode(params);
|
||||
|
||||
// Always create a new window for terminal
|
||||
|
||||
@@ -149,6 +149,7 @@ class RustdeskImpl {
|
||||
required String password,
|
||||
required bool isSharedPassword,
|
||||
String? connToken,
|
||||
required String loginMethod,
|
||||
dynamic hint}) {
|
||||
return js.context.callMethod('setByName', [
|
||||
'session_add_sync',
|
||||
@@ -156,6 +157,7 @@ class RustdeskImpl {
|
||||
'id': id,
|
||||
'password': password,
|
||||
'is_shared_password': isSharedPassword,
|
||||
'login_method': loginMethod,
|
||||
'isFileTransfer': isFileTransfer,
|
||||
'isViewCamera': isViewCamera,
|
||||
'isTerminal': isTerminal
|
||||
@@ -1964,6 +1966,9 @@ class RustdeskImpl {
|
||||
required String key,
|
||||
required String param,
|
||||
dynamic hint}) {
|
||||
if (key == 'login_method') {
|
||||
return js.context.callMethod('getByName', ['login_method']);
|
||||
}
|
||||
throw UnimplementedError("sessionGetCommonSync");
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user