mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-11-04 00:33:12 +00:00
refactor: unify request approval menus and naming
This commit is contained in:
1 parent
417dcdf7bb
commit
c43443b1a1
67 files changed
+535
-559
No files matched your search
@@ -2330,8 +2330,8 @@ bool handleUriLink({List<String>? cmdArgs, Uri? uri, String? uriString}) {
|
||||
password = args[i + 1];
|
||||
i++;
|
||||
break;
|
||||
case '--without-password':
|
||||
loginMethod = 'consent';
|
||||
case '--request-approval':
|
||||
loginMethod = 'request-approval';
|
||||
break;
|
||||
case '--switch_uuid':
|
||||
switchUuid = args[i + 1];
|
||||
@@ -2344,7 +2344,7 @@ bool handleUriLink({List<String>? cmdArgs, Uri? uri, String? uriString}) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (loginMethod == 'consent') {
|
||||
if (loginMethod == 'request-approval') {
|
||||
password = null;
|
||||
}
|
||||
if (type != null && id != null) {
|
||||
@@ -2486,8 +2486,8 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
|
||||
|
||||
var queryParameters =
|
||||
uri.queryParameters.map((k, v) => MapEntry(k.toLowerCase(), v));
|
||||
final withoutPassword = queryParameters['without-password'] == 'true';
|
||||
if (withoutPassword) {
|
||||
final requestApproval = queryParameters['request-approval'] == 'true';
|
||||
if (requestApproval) {
|
||||
queryParameters.remove('password');
|
||||
}
|
||||
|
||||
@@ -2501,7 +2501,7 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
|
||||
if (isMobile && id != null) {
|
||||
final forceRelay = queryParameters["relay"] != null;
|
||||
final password = queryParameters["password"];
|
||||
final loginMethod = withoutPassword ? 'consent' : '';
|
||||
final loginMethod = requestApproval ? 'request-approval' : '';
|
||||
|
||||
// Determine connection type based on command
|
||||
if (command == '--file-transfer') {
|
||||
@@ -2536,7 +2536,7 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
|
||||
var param = queryParameters;
|
||||
String? password = param["password"];
|
||||
if (password != null) args.addAll(['--password', password]);
|
||||
if (withoutPassword) args.add('--without-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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,37 @@ 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),
|
||||
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) {
|
||||
@@ -558,11 +590,6 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
context: context,
|
||||
position: RelativeRect.fromLTRB(x, y, x, y),
|
||||
items: [
|
||||
(
|
||||
'Connect without password',
|
||||
() => connect(context, _idController.id,
|
||||
loginMethod: 'consent')
|
||||
),
|
||||
(
|
||||
'Transfer file',
|
||||
() => onConnect(isFileTransfer: true)
|
||||
@@ -584,7 +611,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),
|
||||
@@ -596,6 +624,10 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
kDesktopMenuPadding.left),
|
||||
dismissOnClicked: true,
|
||||
))
|
||||
.followedBy([
|
||||
MenuEntryDivider<String>(),
|
||||
_requestApprovalMenu(context),
|
||||
])
|
||||
.map((e) => e.build(
|
||||
context,
|
||||
const MenuConfig(
|
||||
|
||||
@@ -15,6 +15,7 @@ class PopupMenuChildrenItem<T> extends mod_menu.PopupMenuEntry<T> {
|
||||
this.enabled,
|
||||
this.textStyle,
|
||||
this.onTap,
|
||||
this.dismissParentOnCanceled = false,
|
||||
this.position = mod_menu.PopupMenuPosition.overSide,
|
||||
this.offset = Offset.zero,
|
||||
required this.itemBuilder,
|
||||
@@ -27,6 +28,7 @@ class PopupMenuChildrenItem<T> extends mod_menu.PopupMenuEntry<T> {
|
||||
final EdgeInsets? padding;
|
||||
final RxBool? enabled;
|
||||
final void Function()? onTap;
|
||||
final bool dismissParentOnCanceled;
|
||||
final List<mod_menu.PopupMenuEntry<T>> Function(BuildContext) itemBuilder;
|
||||
final Widget child;
|
||||
|
||||
@@ -69,6 +71,13 @@ 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(
|
||||
@@ -645,11 +654,13 @@ class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
|
||||
final String text;
|
||||
final List<MenuEntryBase<T>> entries;
|
||||
final EdgeInsets? padding;
|
||||
final bool dismissParentOnCanceled;
|
||||
|
||||
MenuEntrySubMenu({
|
||||
required this.text,
|
||||
required this.entries,
|
||||
this.padding,
|
||||
this.dismissParentOnCanceled = false,
|
||||
RxBool? enabled,
|
||||
}) : super(enabled: enabled);
|
||||
|
||||
@@ -662,6 +673,7 @@ class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
|
||||
enabled: super.enabled,
|
||||
height: conf.height,
|
||||
padding: padding,
|
||||
dismissParentOnCanceled: dismissParentOnCanceled,
|
||||
position: mod_menu.PopupMenuPosition.overSide,
|
||||
itemBuilder: (BuildContext context) => entries
|
||||
.map((entry) => entry.build(context, conf))
|
||||
@@ -678,10 +690,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: MenuConfig.iconScale,
|
||||
child: Obx(() => Icon(
|
||||
Icons.keyboard_arrow_right,
|
||||
color: super.enabled!.value
|
||||
? Theme.of(context).unselectedWidgetColor
|
||||
: disabledStyle().color,
|
||||
)),
|
||||
),
|
||||
))
|
||||
]),
|
||||
)
|
||||
|
||||
@@ -241,8 +241,8 @@ class WebHomePage extends StatelessWidget {
|
||||
password = args[i + 1];
|
||||
i++;
|
||||
break;
|
||||
case '--without-password':
|
||||
loginMethod = 'consent';
|
||||
case '--request-approval':
|
||||
loginMethod = 'request-approval';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -921,9 +921,9 @@ class FfiModel with ChangeNotifier {
|
||||
} else if (type == 'input-password') {
|
||||
enterPasswordDialog(sessionId, dialogManager);
|
||||
} else if (type == 'terminal-admin-login' ||
|
||||
type == 'terminal-admin-login-consent') {
|
||||
type == 'terminal-admin-login-request-approval') {
|
||||
enterUserLoginDialog(sessionId, dialogManager, 'terminal-admin-login-tip',
|
||||
waitForAccept: type == 'terminal-admin-login-consent');
|
||||
waitForAccept: type == 'terminal-admin-login-request-approval');
|
||||
} else if (type == 'terminal-admin-login-password') {
|
||||
enterUserLoginAndPasswordDialog(
|
||||
sessionId, dialogManager, 'terminal-admin-login-tip');
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_hbb/common/widgets/login_method_menu.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart'
|
||||
as mod_menu;
|
||||
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
|
||||
|
||||
Future<void> openMenu(WidgetTester tester, List<String> actions) async {
|
||||
MenuEntryButton<String> entry(String title) => MenuEntryButton<String>(
|
||||
childBuilder: (style) => Text(title, style: style),
|
||||
proc: () => actions.add(title),
|
||||
dismissOnClicked: true,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Builder(builder: (context) {
|
||||
return TextButton(
|
||||
onPressed: () => mod_menu.showMenu<String>(
|
||||
context: context,
|
||||
position: const RelativeRect.fromLTRB(20, 20, 780, 580),
|
||||
items: [
|
||||
LoginMethodMenu(
|
||||
defaultEntry: entry('Connect'),
|
||||
methods: [entry('Without password')],
|
||||
),
|
||||
LoginMethodMenu(
|
||||
defaultEntry: entry('View camera'),
|
||||
methods: [entry('Without password')],
|
||||
),
|
||||
LoginMethodMenu(
|
||||
defaultEntry: MenuEntryButton<String>(
|
||||
childBuilder: (style) => Row(children: [
|
||||
Text('RDP', style: style),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
actions.add('Edit RDP');
|
||||
},
|
||||
),
|
||||
]),
|
||||
proc: () => actions.add('RDP'),
|
||||
),
|
||||
methods: [entry('Without password')],
|
||||
),
|
||||
entry('Rename'),
|
||||
]
|
||||
.expand((entry) => entry.build(context,
|
||||
const MenuConfig(commonColor: Colors.black, height: 20)))
|
||||
.toList(),
|
||||
),
|
||||
child: const Text('Open menu'),
|
||||
);
|
||||
}),
|
||||
),
|
||||
));
|
||||
await tester.tap(find.text('Open menu'));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('main rows align and clicking them connects by default',
|
||||
(tester) async {
|
||||
for (final title in ['Connect', 'View camera', 'RDP']) {
|
||||
final actions = <String>[];
|
||||
await openMenu(tester, actions);
|
||||
final left = tester.getTopLeft(find.text('Rename')).dx;
|
||||
for (final row in ['Connect', 'View camera', 'RDP']) {
|
||||
expect(tester.getTopLeft(find.text(row)).dx, left);
|
||||
}
|
||||
await tester.tap(find.text(title));
|
||||
await tester.pumpAndSettle();
|
||||
expect(actions, [title]);
|
||||
expect(find.text('Without password'), findsNothing);
|
||||
expect(find.text('Rename'), findsNothing);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'arrow offers connection without password and dismisses both menus',
|
||||
(tester) async {
|
||||
final actions = <String>[];
|
||||
await openMenu(tester, actions);
|
||||
await tester.tap(find.byIcon(Icons.keyboard_arrow_right).first);
|
||||
await tester.pumpAndSettle();
|
||||
expect(actions, isEmpty);
|
||||
expect(find.text('Without password'), findsOneWidget);
|
||||
expect(find.text('Enter password manually'), findsNothing);
|
||||
await tester.tap(find.text('Without password'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(actions, ['Without password']);
|
||||
expect(find.text('Without password'), findsNothing);
|
||||
expect(find.text('Rename'), findsNothing);
|
||||
},
|
||||
variant: TargetPlatformVariant({
|
||||
TargetPlatform.android,
|
||||
TargetPlatform.iOS,
|
||||
TargetPlatform.linux,
|
||||
TargetPlatform.macOS,
|
||||
TargetPlatform.windows,
|
||||
}));
|
||||
|
||||
testWidgets('RDP edit remains independent of the connection action',
|
||||
(tester) async {
|
||||
final actions = <String>[];
|
||||
await openMenu(tester, actions);
|
||||
await tester.tap(find.byIcon(Icons.edit));
|
||||
await tester.pumpAndSettle();
|
||||
expect(actions, ['Edit RDP']);
|
||||
expect(find.text('Rename'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('touches around the arrow open the submenu without connecting',
|
||||
(tester) async {
|
||||
tester.view.physicalSize = const Size(360, 640);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
for (final corner in [
|
||||
Alignment.topLeft,
|
||||
Alignment.topRight,
|
||||
Alignment.bottomLeft,
|
||||
Alignment.bottomRight,
|
||||
]) {
|
||||
final actions = <String>[];
|
||||
await openMenu(tester, actions);
|
||||
final arrow = find.byIcon(Icons.keyboard_arrow_right).first;
|
||||
final row = tester
|
||||
.getRect(find.ancestor(of: arrow, matching: find.byType(Row)).first);
|
||||
final target = Rect.fromCenter(
|
||||
center: tester.getCenter(arrow), width: 56, height: row.height);
|
||||
await tester.tapAt(corner.withinRect(target.deflate(2)));
|
||||
await tester.pumpAndSettle();
|
||||
expect(actions, isEmpty);
|
||||
expect(find.text('Without password'), findsOneWidget);
|
||||
await tester.tap(find.text('Without password'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(actions, ['Without password']);
|
||||
expect(find.text('Rename'), findsNothing);
|
||||
}
|
||||
},
|
||||
variant:
|
||||
TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_hbb/common/widgets/request_approval_menu.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart'
|
||||
as mod_menu;
|
||||
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
|
||||
|
||||
const connectionTypes = [
|
||||
'Connect',
|
||||
'Transfer file',
|
||||
'View camera',
|
||||
'Terminal',
|
||||
'Terminal (Run as administrator)',
|
||||
'TCP tunneling',
|
||||
'RDP',
|
||||
];
|
||||
|
||||
Future<void> openMenu(WidgetTester tester, List<String> actions,
|
||||
{ThemeData? theme, EdgeInsets? padding, bool includeRelay = false}) async {
|
||||
MenuEntryButton<String> entry(String title, {bool requestApproval = false}) =>
|
||||
MenuEntryButton<String>(
|
||||
childBuilder: (style) => Text(title, style: style),
|
||||
proc: () =>
|
||||
actions.add(requestApproval ? '$title:request-approval' : title),
|
||||
padding: padding,
|
||||
dismissOnClicked: true,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
theme: theme,
|
||||
home: Scaffold(
|
||||
body: Builder(builder: (context) {
|
||||
return TextButton(
|
||||
onPressed: () => mod_menu.showMenu<String>(
|
||||
context: context,
|
||||
position: const RelativeRect.fromLTRB(20, 20, 780, 580),
|
||||
items: [
|
||||
entry('Connect'),
|
||||
entry('Transfer file'),
|
||||
entry('View camera'),
|
||||
MenuEntryButton<String>(
|
||||
childBuilder: (style) => Row(children: [
|
||||
Text('RDP', style: style),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
actions.add('Edit RDP');
|
||||
},
|
||||
),
|
||||
]),
|
||||
proc: () => actions.add('RDP'),
|
||||
padding: padding,
|
||||
dismissOnClicked: true,
|
||||
),
|
||||
RequestApprovalMenu(
|
||||
title: 'Request approval',
|
||||
padding: padding,
|
||||
entries: [
|
||||
for (final title in connectionTypes)
|
||||
entry(title, requestApproval: true),
|
||||
],
|
||||
),
|
||||
if (includeRelay)
|
||||
MenuEntrySwitchSync<String>(
|
||||
switchType: SwitchType.scheckbox,
|
||||
text: 'Always connect via relay',
|
||||
currentValue: false,
|
||||
setter: (_) async {},
|
||||
padding: padding,
|
||||
dismissOnClicked: true,
|
||||
),
|
||||
entry('Rename'),
|
||||
]
|
||||
.expand((entry) => entry.build(context,
|
||||
const MenuConfig(commonColor: Colors.black, height: 20)))
|
||||
.toList(),
|
||||
),
|
||||
child: const Text('Open menu'),
|
||||
);
|
||||
}),
|
||||
),
|
||||
));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Open menu'));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('tapping outside the approval submenu closes both menus',
|
||||
(tester) async {
|
||||
final actions = <String>[];
|
||||
await openMenu(tester, actions);
|
||||
await tester.tap(find.text('Request approval'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapAt(const Offset(2, 2));
|
||||
await tester.pumpAndSettle();
|
||||
expect(actions, isEmpty);
|
||||
expect(find.text('Request approval'), findsNothing);
|
||||
expect(find.text('Rename'), findsNothing);
|
||||
expect(find.text('Open menu'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('approval text and arrow align with normal menu items',
|
||||
(tester) async {
|
||||
for (final (theme, padding) in [
|
||||
(ThemeData(), null),
|
||||
(
|
||||
ThemeData(
|
||||
useMaterial3: false,
|
||||
platform: TargetPlatform.windows,
|
||||
visualDensity: VisualDensity.compact),
|
||||
const EdgeInsets.only(left: 12, right: 3)
|
||||
),
|
||||
(
|
||||
ThemeData(useMaterial3: false),
|
||||
const EdgeInsets.symmetric(horizontal: 12)
|
||||
),
|
||||
(
|
||||
ThemeData(
|
||||
useMaterial3: false,
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20)),
|
||||
)),
|
||||
null
|
||||
),
|
||||
]) {
|
||||
await openMenu(tester, [],
|
||||
theme: theme, padding: padding, includeRelay: true);
|
||||
expect(tester.getTopLeft(find.text('Request approval')).dx,
|
||||
tester.getTopLeft(find.text('Connect')).dx);
|
||||
expect(tester.getCenter(find.byIcon(Icons.keyboard_arrow_right)).dx,
|
||||
closeTo(tester.getCenter(find.byType(Checkbox)).dx, 0.01));
|
||||
await tester.tap(find.text('Rename'));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('normal connections and RDP editing remain direct actions',
|
||||
(tester) async {
|
||||
for (final title in [
|
||||
'Connect',
|
||||
'Transfer file',
|
||||
'View camera',
|
||||
'RDP',
|
||||
'Edit RDP'
|
||||
]) {
|
||||
final actions = <String>[];
|
||||
await openMenu(tester, actions);
|
||||
expect(find.byIcon(Icons.keyboard_arrow_right), findsOneWidget);
|
||||
final left = tester.getTopLeft(find.text('Rename')).dx;
|
||||
for (final row in ['Connect', 'Transfer file', 'View camera', 'RDP']) {
|
||||
expect(tester.getTopLeft(find.text(row)).dx, left);
|
||||
}
|
||||
await tester.tap(
|
||||
title == 'Edit RDP' ? find.byIcon(Icons.edit) : find.text(title));
|
||||
await tester.pumpAndSettle();
|
||||
expect(actions, [title]);
|
||||
expect(find.text('Request approval'), findsNothing);
|
||||
expect(find.text('Rename'), findsNothing);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'one approval menu offers all connection types and closes both menus',
|
||||
(tester) async {
|
||||
for (var i = 0; i < connectionTypes.length; i++) {
|
||||
final actions = <String>[];
|
||||
await openMenu(tester, actions);
|
||||
await tester.tap(i.isEven
|
||||
? find.text('Request approval')
|
||||
: find.byIcon(Icons.keyboard_arrow_right));
|
||||
await tester.pumpAndSettle();
|
||||
expect(actions, isEmpty);
|
||||
for (final title in connectionTypes) {
|
||||
expect(find.text(title), findsWidgets);
|
||||
}
|
||||
await tester.tap(find.text(connectionTypes[i]).last);
|
||||
await tester.pumpAndSettle();
|
||||
expect(actions, ['${connectionTypes[i]}:request-approval']);
|
||||
expect(find.text('Request approval'), findsNothing);
|
||||
expect(find.text('Rename'), findsNothing);
|
||||
}
|
||||
},
|
||||
variant: TargetPlatformVariant({
|
||||
TargetPlatform.android,
|
||||
TargetPlatform.iOS,
|
||||
TargetPlatform.linux,
|
||||
TargetPlatform.macOS,
|
||||
TargetPlatform.windows,
|
||||
}));
|
||||
|
||||
testWidgets('the whole approval row opens the submenu on a narrow screen',
|
||||
(tester) async {
|
||||
tester.view.physicalSize = const Size(360, 640);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
for (final corner in [
|
||||
Alignment.topLeft,
|
||||
Alignment.topRight,
|
||||
Alignment.bottomLeft,
|
||||
Alignment.bottomRight,
|
||||
]) {
|
||||
final actions = <String>[];
|
||||
await openMenu(tester, actions);
|
||||
final row = tester.getRect(find.byType(PopupMenuChildrenItem<String>));
|
||||
await tester.tapAt(corner.withinRect(row.deflate(2)));
|
||||
await tester.pumpAndSettle();
|
||||
expect(actions, isEmpty);
|
||||
expect(find.text('Terminal (Run as administrator)'), findsOneWidget);
|
||||
await tester.tap(find.text('Connect').last);
|
||||
await tester.pumpAndSettle();
|
||||
expect(actions, ['Connect:request-approval']);
|
||||
expect(find.text('Rename'), findsNothing);
|
||||
}
|
||||
},
|
||||
variant:
|
||||
TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
test('without-password links preserve their connection type', () {
|
||||
test('request-approval links preserve their connection type', () {
|
||||
for (final type in [
|
||||
'connect',
|
||||
'play',
|
||||
@@ -15,32 +15,32 @@ void main() {
|
||||
]) {
|
||||
expect(
|
||||
urlLinkToCmdArgs(
|
||||
Uri.parse('rustdesk://$type/123456789?without-password=true')),
|
||||
['--$type', '123456789', '--without-password'],
|
||||
Uri.parse('rustdesk://$type/123456789?request-approval=true')),
|
||||
['--$type', '123456789', '--request-approval'],
|
||||
);
|
||||
}
|
||||
expect(
|
||||
urlLinkToCmdArgs(Uri.parse('rustdesk://123456789?without-password=true')),
|
||||
['--connect', '123456789', '--without-password'],
|
||||
urlLinkToCmdArgs(Uri.parse('rustdesk://123456789?request-approval=true')),
|
||||
['--connect', '123456789', '--request-approval'],
|
||||
);
|
||||
});
|
||||
|
||||
test('omitted and empty passwords retain automatic login', () {
|
||||
expect(urlLinkToCmdArgs(Uri.parse('rustdesk://123456789')),
|
||||
['--connect', '123456789']);
|
||||
for (final query in ['password=', 'password=&without-password=false']) {
|
||||
for (final query in ['password=', 'password=&request-approval=false']) {
|
||||
expect(urlLinkToCmdArgs(Uri.parse('rustdesk://123456789?$query')),
|
||||
['--connect', '123456789', '--password', '']);
|
||||
}
|
||||
});
|
||||
|
||||
test('without-password ignores an explicit password in either order', () {
|
||||
test('request-approval ignores an explicit password in either order', () {
|
||||
for (final query in [
|
||||
'password=provided&without-password=true',
|
||||
'without-password=true&password=provided',
|
||||
'password=provided&request-approval=true',
|
||||
'request-approval=true&password=provided',
|
||||
]) {
|
||||
expect(urlLinkToCmdArgs(Uri.parse('rustdesk://123456789?$query')),
|
||||
['--connect', '123456789', '--without-password']);
|
||||
['--connect', '123456789', '--request-approval']);
|
||||
}
|
||||
});
|
||||
}
|
||||
+5
-5
@@ -4745,13 +4745,13 @@ pub async fn handle_hash(
|
||||
|
||||
lc.write().unwrap().password = password.clone();
|
||||
|
||||
let request_consent = lc.read().unwrap().skips_saved_password();
|
||||
let request_approval = lc.read().unwrap().skips_saved_password();
|
||||
|
||||
let is_terminal_admin = lc.read().unwrap().is_terminal_admin;
|
||||
let is_terminal = lc.read().unwrap().conn_type.eq(&ConnType::TERMINAL);
|
||||
if is_terminal && is_terminal_admin {
|
||||
if request_consent {
|
||||
interface.msgbox("terminal-admin-login-consent", "", "", "");
|
||||
if request_approval {
|
||||
interface.msgbox("terminal-admin-login-request-approval", "", "", "");
|
||||
} else if password.is_empty() {
|
||||
interface.msgbox("terminal-admin-login-password", "", "", "");
|
||||
} else {
|
||||
@@ -4763,7 +4763,7 @@ pub async fn handle_hash(
|
||||
|
||||
let password = if password.is_empty() {
|
||||
// login without password, the remote side can click accept
|
||||
if !request_consent {
|
||||
if !request_approval {
|
||||
interface.msgbox("input-password", "Password Required", "", "");
|
||||
}
|
||||
Vec::new()
|
||||
@@ -4774,7 +4774,7 @@ pub async fn handle_hash(
|
||||
hasher.finalize()[..].into()
|
||||
};
|
||||
|
||||
if request_consent {
|
||||
if request_approval {
|
||||
interface.msgbox(
|
||||
"wait-remote-accept-nook",
|
||||
"Prompt",
|
||||
|
||||
+23
-16
@@ -4,13 +4,13 @@ use super::LoginConfigHandler;
|
||||
pub(super) enum LoginMethod {
|
||||
#[default]
|
||||
Automatic,
|
||||
Consent,
|
||||
RequestApproval,
|
||||
}
|
||||
|
||||
impl LoginConfigHandler {
|
||||
pub(crate) fn set_login_method(&mut self, method: &str) -> bool {
|
||||
self.login_method = match method {
|
||||
"consent" => LoginMethod::Consent,
|
||||
"request-approval" => LoginMethod::RequestApproval,
|
||||
_ => return false,
|
||||
};
|
||||
true
|
||||
@@ -23,7 +23,7 @@ impl LoginConfigHandler {
|
||||
pub(crate) fn get_login_method(&self) -> &'static str {
|
||||
match self.login_method {
|
||||
LoginMethod::Automatic => "",
|
||||
LoginMethod::Consent => "consent",
|
||||
LoginMethod::RequestApproval => "request-approval",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,7 @@ mod tests {
|
||||
#[test]
|
||||
fn session_creation_initializes_login_method() {
|
||||
let remote_id = hbb_common::config::LocalConfig::get_remote_id();
|
||||
for method in ["", "consent"] {
|
||||
for method in ["", "request-approval"] {
|
||||
let session_id = uuid::Uuid::new_v4();
|
||||
let result = crate::flutter_ffi::session_add_sync(
|
||||
session_id,
|
||||
@@ -106,7 +106,7 @@ mod tests {
|
||||
.create_login_msg(String::new(), String::new(), vec![1; 32]);
|
||||
assert_eq!(
|
||||
request.login_request().password.is_empty(),
|
||||
method == "consent"
|
||||
method == "request-approval"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -132,7 +132,7 @@ mod tests {
|
||||
lc.shared_password = Some("shared password".into());
|
||||
let token = lc.get_conn_token();
|
||||
if skip {
|
||||
assert!(lc.set_login_method("consent"));
|
||||
assert!(lc.set_login_method("request-approval"));
|
||||
}
|
||||
assert_eq!(lc.get_conn_token(), token);
|
||||
let request = lc.create_login_msg(String::new(), String::new(), vec![4; 32]);
|
||||
@@ -161,13 +161,13 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn admin_terminal_waits_for_os_credentials_before_requesting_consent() {
|
||||
async fn admin_terminal_waits_for_os_credentials_before_requesting_approval() {
|
||||
let session = Session::<FlutterHandler>::default();
|
||||
{
|
||||
let mut lc = session.lc.write().unwrap();
|
||||
lc.conn_type = ConnType::TERMINAL;
|
||||
lc.is_terminal_admin = true;
|
||||
assert!(lc.set_login_method("consent"));
|
||||
assert!(lc.set_login_method("request-approval"));
|
||||
}
|
||||
let (mut stream, mut peer) = connection().await;
|
||||
let mut trust = Default::default();
|
||||
@@ -203,24 +203,31 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn consent_does_not_save_personal_ab_password() {
|
||||
for consent in [false, true] {
|
||||
fn request_approval_does_not_save_personal_ab_password() {
|
||||
for request_approval in [false, true] {
|
||||
let mut lc = client::LoginConfigHandler::default();
|
||||
lc.id = format!("consent-ab-password-test-{}", uuid::Uuid::new_v4());
|
||||
lc.id = format!("request-approval-ab-password-test-{}", uuid::Uuid::new_v4());
|
||||
lc.password = vec![3; 32];
|
||||
lc.password_source = client::PasswordSource::PersonalAb(lc.password.clone());
|
||||
if consent {
|
||||
assert!(lc.set_login_method("consent"));
|
||||
if request_approval {
|
||||
assert!(lc.set_login_method("request-approval"));
|
||||
}
|
||||
lc.handle_peer_info(&PeerInfo::default());
|
||||
let saved = PeerConfig::load(&lc.id).password;
|
||||
PeerConfig::remove(&lc.id);
|
||||
assert_eq!(saved, if consent { Vec::new() } else { lc.password });
|
||||
assert_eq!(
|
||||
saved,
|
||||
if request_approval {
|
||||
Vec::new()
|
||||
} else {
|
||||
lc.password
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn consent_preserves_saved_password() {
|
||||
async fn request_approval_preserves_saved_password() {
|
||||
let (mut stream, mut peer) = connection().await;
|
||||
let id = format!("skip-saved-password-test-{}", uuid::Uuid::new_v4());
|
||||
let mut config = PeerConfig::default();
|
||||
@@ -240,7 +247,7 @@ mod tests {
|
||||
"",
|
||||
);
|
||||
assert!(!lc.set_login_method("password"));
|
||||
assert!(lc.set_login_method("consent"));
|
||||
assert!(lc.set_login_method("request-approval"));
|
||||
}
|
||||
assert!(
|
||||
client::handle_hash(
|
||||
|
||||
+2
-2
@@ -776,8 +776,8 @@ fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option<Vec<Strin
|
||||
"--relay" => {
|
||||
param_array.push(format!("relay=true"));
|
||||
}
|
||||
"--without-password" => {
|
||||
param_array.push("without-password=true".to_owned());
|
||||
"--request-approval" => {
|
||||
param_array.push("request-approval=true".to_owned());
|
||||
}
|
||||
// inner
|
||||
"--switch_uuid" => {
|
||||
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "الرجاء اختيار Android → media."),
|
||||
("Trust duration (days)", "مدة الثقة (بالأيام)"),
|
||||
("Renew trust", "تجديد الثقة"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Zəhmət olmasa, Android → media seçin."),
|
||||
("Trust duration (days)", "Etibar müddəti (gün)"),
|
||||
("Renew trust", "Etibarı yenilə"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Выберыце Android → media."),
|
||||
("Trust duration (days)", "Тэрмін даверу (у днях)"),
|
||||
("Renew trust", "Падоўжыць давер"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Моля, изберете Android → media."),
|
||||
("Trust duration (days)", "Срок на доверие (дни)"),
|
||||
("Renew trust", "Подновяване на доверието"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Seleccioneu Android → media."),
|
||||
("Trust duration (days)", "Durada de la confiança (dies)"),
|
||||
("Renew trust", "Renova la confiança"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "请选择 Android → media。"),
|
||||
("Trust duration (days)", "信任有效期(天)"),
|
||||
("Renew trust", "延长信任有效期"),
|
||||
("Without password", "不使用密码"),
|
||||
("Connect without password", "免密连接"),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", "不使用已保存的密码,请求对方接受连接。"),
|
||||
("Request approval", "请求批准"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Vyberte prosím Android → media."),
|
||||
("Trust duration (days)", "Doba důvěry (dny)"),
|
||||
("Renew trust", "Obnovit důvěru"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Vælg Android → media."),
|
||||
("Trust duration (days)", "Tillidsperiode (dage)"),
|
||||
("Renew trust", "Forny tillid"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Bitte wählen Sie Android → media."),
|
||||
("Trust duration (days)", "Vertrauensdauer (Tage)"),
|
||||
("Renew trust", "Vertrauen erneuern"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Παρακαλώ επιλέξτε Android → media."),
|
||||
("Trust duration (days)", "Διάρκεια εμπιστοσύνης (ημέρες)"),
|
||||
("Renew trust", "Ανανέωση εμπιστοσύνης"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Bonvolu elekti Android → media."),
|
||||
("Trust duration (days)", "Daŭro de fido (tagoj)"),
|
||||
("Renew trust", "Renovigi fidon"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Seleccione Android → media."),
|
||||
("Trust duration (days)", "Duración de la confianza (días)"),
|
||||
("Renew trust", "Renovar confianza"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Palun vali Android → media."),
|
||||
("Trust duration (days)", "Usalduse kestus (päevades)"),
|
||||
("Renew trust", "Uuenda usaldust"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Hautatu Android → media."),
|
||||
("Trust duration (days)", "Konfiantzaren iraupena (egunak)"),
|
||||
("Renew trust", "Berritu konfiantza"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "لطفاً Android → media را انتخاب کنید."),
|
||||
("Trust duration (days)", "مدت اعتماد (روز)"),
|
||||
("Renew trust", "تمدید اعتماد"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Valitse Android → media."),
|
||||
("Trust duration (days)", "Luottamuksen kesto (päivää)"),
|
||||
("Renew trust", "Jatka luottamusta"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Veuillez sélectionner Android → media."),
|
||||
("Trust duration (days)", "Durée de confiance (jours)"),
|
||||
("Renew trust", "Renouveler la confiance"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "გთხოვთ, აირჩიოთ Android → media."),
|
||||
("Trust duration (days)", "ნდობის ხანგრძლივობა (დღე)"),
|
||||
("Renew trust", "ნდობის განახლება"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Selecciona Android → media."),
|
||||
("Trust duration (days)", "Duración da confianza (días)"),
|
||||
("Renew trust", "Renovar a confianza"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "કૃપા કરીને Android → media પસંદ કરો."),
|
||||
("Trust duration (days)", "વિશ્વાસ અવધિ (દિવસો)"),
|
||||
("Renew trust", "વિશ્વાસ નવીકરણ કરો"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "אנא בחר Android → media."),
|
||||
("Trust duration (days)", "משך האמון (ימים)"),
|
||||
("Renew trust", "חידוש אמון"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "कृपया Android → media चुनें।"),
|
||||
("Trust duration (days)", "विश्वास अवधि (दिन)"),
|
||||
("Renew trust", "विश्वास नवीनीकृत करें"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Odaberite Android → media."),
|
||||
("Trust duration (days)", "Trajanje povjerenja (dani)"),
|
||||
("Renew trust", "Obnovi povjerenje"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Válassza az Android → media mappát."),
|
||||
("Trust duration (days)", "Megbízhatóság időtartama (nap)"),
|
||||
("Renew trust", "Megbízhatóság megújítása"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Silakan pilih Android → media."),
|
||||
("Trust duration (days)", "Durasi kepercayaan (hari)"),
|
||||
("Renew trust", "Perbarui kepercayaan"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Seleziona Android → media."),
|
||||
("Trust duration (days)", "Durata attendibilità (giorni)"),
|
||||
("Renew trust", "Rinnova attendibilità"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Android → media を選択してください。"),
|
||||
("Trust duration (days)", "信頼期間 (日)"),
|
||||
("Renew trust", "信頼を更新"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Android → media를 선택해 주시기 바랍니다."),
|
||||
("Trust duration (days)", "신뢰 유지 기간 (일)"),
|
||||
("Renew trust", "신뢰 갱신"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Android → media таңдаңыз."),
|
||||
("Trust duration (days)", "Сенім мерзімі (күн)"),
|
||||
("Renew trust", "Сенімді жаңарту"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Pasirinkite Android → media."),
|
||||
("Trust duration (days)", "Pasitikėjimo trukmė (dienomis)"),
|
||||
("Renew trust", "Atnaujinti pasitikėjimą"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Lūdzu, atlasiet Android → media."),
|
||||
("Trust duration (days)", "Uzticēšanās ilgums (dienas)"),
|
||||
("Renew trust", "Atjaunot uzticēšanos"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "ദയവായി Android → media തിരഞ്ഞെടുക്കുക."),
|
||||
("Trust duration (days)", "വിശ്വാസ കാലാവധി (ദിവസം)"),
|
||||
("Renew trust", "വിശ്വാസം പുതുക്കുക"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Velg Android → media."),
|
||||
("Trust duration (days)", "Klareringsperiode (dager)"),
|
||||
("Renew trust", "Forny klarering"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Selecteer Android → media."),
|
||||
("Trust duration (days)", "Vertrouwensperiode (dagen)"),
|
||||
("Renew trust", "Vertrouwen hernieuwen"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Wybierz Android → media."),
|
||||
("Trust duration (days)", "Okres zaufania (dni)"),
|
||||
("Renew trust", "Odnów zaufanie"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Selecione Android → media."),
|
||||
("Trust duration (days)", "Duração da confiança (dias)"),
|
||||
("Renew trust", "Renovar confiança"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Por favor selecione Android → media."),
|
||||
("Trust duration (days)", "Duração da confiança (dias)"),
|
||||
("Renew trust", "Renovar confiança"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Selectează Android → media."),
|
||||
("Trust duration (days)", "Durata încrederii (zile)"),
|
||||
("Renew trust", "Reînnoiește încrederea"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Выберите Android → media."),
|
||||
("Trust duration (days)", "Срок доверия (в днях)"),
|
||||
("Renew trust", "Продлить доверие"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Seletziona Android → media."),
|
||||
("Trust duration (days)", "Durada de sa fidùtzia (dies)"),
|
||||
("Renew trust", "Annoa sa fidùtzia"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Vyberte Android → media."),
|
||||
("Trust duration (days)", "Doba dôvery (dni)"),
|
||||
("Renew trust", "Obnoviť dôveru"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Izberite Android → media."),
|
||||
("Trust duration (days)", "Trajanje zaupanja (dni)"),
|
||||
("Renew trust", "Obnovi zaupanje"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Ju lutemi zgjidhni Android → media."),
|
||||
("Trust duration (days)", "Kohëzgjatja e besimit (ditë)"),
|
||||
("Renew trust", "Rinovo besimin"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Izaberite Android → media."),
|
||||
("Trust duration (days)", "Trajanje poverenja (dani)"),
|
||||
("Renew trust", "Obnovi poverenje"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Välj Android → media."),
|
||||
("Trust duration (days)", "Förtroendeperiod (dagar)"),
|
||||
("Renew trust", "Förnya förtroende"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Android → media என்பதைத் தேர்ந்தெடுக்கவும்."),
|
||||
("Trust duration (days)", "நம்பிக்கைக் காலம் (நாட்கள்)"),
|
||||
("Renew trust", "நம்பிக்கையைப் புதுப்பி"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", ""),
|
||||
("Trust duration (days)", ""),
|
||||
("Renew trust", ""),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "กรุณาเลือก Android → media"),
|
||||
("Trust duration (days)", "ระยะเวลาการเชื่อถือ (วัน)"),
|
||||
("Renew trust", "ต่ออายุการเชื่อถือ"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Lütfen Android → media konumunu seçin."),
|
||||
("Trust duration (days)", "Güven süresi (gün)"),
|
||||
("Renew trust", "Güveni yenile"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "請選擇 Android → media。"),
|
||||
("Trust duration (days)", "信任有效期(天)"),
|
||||
("Renew trust", "延長信任期限"),
|
||||
("Without password", "不使用密碼"),
|
||||
("Connect without password", "免密連線"),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", "不使用已儲存的密碼,請求對方接受連線。"),
|
||||
("Request approval", "請求批准"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Будь ласка, виберіть Android → media."),
|
||||
("Trust duration (days)", "Термін довіри (у днях)"),
|
||||
("Renew trust", "Подовжити довіру"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "براہ کرم Android → media منتخب کریں۔"),
|
||||
("Trust duration (days)", "اعتماد کی مدت (دن)"),
|
||||
("Renew trust", "اعتماد کی تجدید کریں"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-3
@@ -823,8 +823,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please select Android → media.", "Vui lòng chọn Android → media."),
|
||||
("Trust duration (days)", "Thời hạn tin cậy (ngày)"),
|
||||
("Renew trust", "Gia hạn tin cậy"),
|
||||
("Without password", ""),
|
||||
("Connect without password", ""),
|
||||
("Skip saved passwords and ask the remote user to accept the connection.", ""),
|
||||
("Request approval", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
Reference in new issue
Block a user