mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-11-04 00:33:12 +00:00
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.
This commit is contained in:
1 parent
ef827fdb34
commit
956031d109
87 files changed
+853
-23
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 '--without-password':
|
||||
loginMethod = 'consent';
|
||||
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 == 'consent') {
|
||||
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 withoutPassword = queryParameters['without-password'] == 'true';
|
||||
if (withoutPassword) {
|
||||
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 = withoutPassword ? 'consent' : '';
|
||||
|
||||
// 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 (withoutPassword) args.add('--without-password');
|
||||
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) {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
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,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 'login_method_menu.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
typedef PopupMenuEntryBuilder = Future<List<mod_menu.PopupMenuEntry<String>>>
|
||||
@@ -581,12 +582,16 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
bool isRDP = false,
|
||||
bool isTerminal = false,
|
||||
bool isTerminalRunAsAdmin = false,
|
||||
MenuEntryButton<String>? defaultEntry,
|
||||
}) {
|
||||
return MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Text(
|
||||
title,
|
||||
style: style,
|
||||
),
|
||||
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),
|
||||
proc: () {
|
||||
if (isTerminalRunAsAdmin) {
|
||||
setEnvTerminalAdmin();
|
||||
@@ -600,20 +605,27 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
isTcpTunneling: isTcpTunneling,
|
||||
isRDP: isRDP,
|
||||
isTerminal: isTerminal || isTerminalRunAsAdmin,
|
||||
loginMethod: loginMethod,
|
||||
);
|
||||
},
|
||||
padding: menuPadding,
|
||||
dismissOnClicked: true,
|
||||
);
|
||||
return LoginMethodMenu(
|
||||
defaultEntry: defaultEntry ?? action(title, ''),
|
||||
methods: [
|
||||
action(translate('Without password'), 'consent'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@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}',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -664,7 +676,7 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
|
||||
@protected
|
||||
MenuEntryBase<String> _rdpAction(BuildContext context, String id) {
|
||||
return MenuEntryButton<String>(
|
||||
final action = MenuEntryButton<String>(
|
||||
childBuilder: (TextStyle? style) => Container(
|
||||
alignment: AlignmentDirectional.center,
|
||||
height: CustomPopupMenuTheme.height,
|
||||
@@ -698,6 +710,8 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
padding: menuPadding,
|
||||
dismissOnClicked: true,
|
||||
);
|
||||
return _connectCommonAction(context, translate('RDP'),
|
||||
isRDP: true, defaultEntry: action);
|
||||
}
|
||||
|
||||
@protected
|
||||
@@ -1591,7 +1605,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 +1634,7 @@ void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab,
|
||||
}
|
||||
}
|
||||
connect(context, peer.id,
|
||||
loginMethod: loginMethod,
|
||||
password: password,
|
||||
isSharedPassword: isSharedPassword,
|
||||
isFileTransfer: isFileTransfer,
|
||||
|
||||
@@ -579,6 +579,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
bool isTerminal = false}) {
|
||||
final connToken = bind.sessionGetConnToken(sessionId: ffi.sessionId);
|
||||
connect(context, id,
|
||||
loginMethod: ffi.loginMethod,
|
||||
isFileTransfer: isFileTransfer,
|
||||
isViewCamera: isViewCamera,
|
||||
isTerminal: isTerminal,
|
||||
|
||||
@@ -558,6 +558,11 @@ 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)
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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 '--without-password':
|
||||
loginMethod = 'consent';
|
||||
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-consent') {
|
||||
enterUserLoginDialog(sessionId, dialogManager, 'terminal-admin-login-tip',
|
||||
waitForAccept: type == 'terminal-admin-login-consent');
|
||||
} else if (type == 'terminal-admin-login-password') {
|
||||
enterUserLoginAndPasswordDialog(
|
||||
sessionId, dialogManager, 'terminal-admin-login-tip');
|
||||
@@ -4031,6 +4032,7 @@ class FFI {
|
||||
var id = '';
|
||||
var version = '';
|
||||
var connType = ConnType.defaultConn;
|
||||
var loginMethod = '';
|
||||
var closed = false;
|
||||
|
||||
/// dialogManager use late to ensure init after main page binding [globalKey]
|
||||
@@ -4123,7 +4125,9 @@ class FFI {
|
||||
int? tabWindowId,
|
||||
int? display,
|
||||
List<int>? displays,
|
||||
String loginMethod = '',
|
||||
}) {
|
||||
this.loginMethod = loginMethod;
|
||||
closed = false;
|
||||
if (isMobile) mobileReset();
|
||||
assert(
|
||||
@@ -4168,6 +4172,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
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
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,46 @@
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
test('without-password links preserve their connection type', () {
|
||||
for (final type in [
|
||||
'connect',
|
||||
'play',
|
||||
'file-transfer',
|
||||
'view-camera',
|
||||
'port-forward',
|
||||
'rdp',
|
||||
'terminal',
|
||||
'terminal-admin',
|
||||
]) {
|
||||
expect(
|
||||
urlLinkToCmdArgs(
|
||||
Uri.parse('rustdesk://$type/123456789?without-password=true')),
|
||||
['--$type', '123456789', '--without-password'],
|
||||
);
|
||||
}
|
||||
expect(
|
||||
urlLinkToCmdArgs(Uri.parse('rustdesk://123456789?without-password=true')),
|
||||
['--connect', '123456789', '--without-password'],
|
||||
);
|
||||
});
|
||||
|
||||
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']) {
|
||||
expect(urlLinkToCmdArgs(Uri.parse('rustdesk://123456789?$query')),
|
||||
['--connect', '123456789', '--password', '']);
|
||||
}
|
||||
});
|
||||
|
||||
test('without-password ignores an explicit password in either order', () {
|
||||
for (final query in [
|
||||
'password=provided&without-password=true',
|
||||
'without-password=true&password=provided',
|
||||
]) {
|
||||
expect(urlLinkToCmdArgs(Uri.parse('rustdesk://123456789?$query')),
|
||||
['--connect', '123456789', '--without-password']);
|
||||
}
|
||||
});
|
||||
}
|
||||
+28
-2
@@ -93,6 +93,8 @@ use crate::ui_session_interface::SessionPermissionConfig;
|
||||
|
||||
pub use super::lang::*;
|
||||
|
||||
mod login_method;
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
mod audio_playback;
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -2834,6 +2836,7 @@ pub struct LoginConfigHandler {
|
||||
hash: Hash,
|
||||
password: Vec<u8>, // remember password for reconnect
|
||||
pub remember: bool,
|
||||
login_method: login_method::LoginMethod,
|
||||
config: PeerConfig,
|
||||
pub port_forward: (String, i32),
|
||||
/// This login's `multiplex`, filled with `port_forward` under the turn
|
||||
@@ -2911,6 +2914,7 @@ impl LoginConfigHandler {
|
||||
adapter_luid: Option<i64>,
|
||||
shared_password: Option<String>,
|
||||
conn_token: Option<String>,
|
||||
login_method: &str,
|
||||
) {
|
||||
let mut id = id;
|
||||
if id.contains("@") {
|
||||
@@ -2951,6 +2955,10 @@ impl LoginConfigHandler {
|
||||
|
||||
self.id = id;
|
||||
self.conn_type = conn_type;
|
||||
self.login_method = Default::default();
|
||||
if !login_method.is_empty() && !self.set_login_method(login_method) {
|
||||
log::warn!("Failed to initialize login method: {}", login_method);
|
||||
}
|
||||
let config = self.load_config();
|
||||
self.remember = !config.password.is_empty();
|
||||
self.config = config;
|
||||
@@ -3860,6 +3868,9 @@ impl LoginConfigHandler {
|
||||
avatar,
|
||||
..Default::default()
|
||||
};
|
||||
if self.skips_saved_password() {
|
||||
lr.password.clear();
|
||||
}
|
||||
match self.conn_type {
|
||||
ConnType::FILE_TRANSFER => lr.set_file_transfer(FileTransfer {
|
||||
dir: self.get_remote_dir(),
|
||||
@@ -4733,10 +4744,14 @@ pub async fn handle_hash(
|
||||
|
||||
lc.write().unwrap().password = password.clone();
|
||||
|
||||
let request_consent = 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 password.is_empty() {
|
||||
if request_consent {
|
||||
interface.msgbox("terminal-admin-login-consent", "", "", "");
|
||||
} else if password.is_empty() {
|
||||
interface.msgbox("terminal-admin-login-password", "", "", "");
|
||||
} else {
|
||||
interface.msgbox("terminal-admin-login", "", "", "");
|
||||
@@ -4747,7 +4762,9 @@ pub async fn handle_hash(
|
||||
|
||||
let password = if password.is_empty() {
|
||||
// login without password, the remote side can click accept
|
||||
interface.msgbox("input-password", "Password Required", "", "");
|
||||
if !request_consent {
|
||||
interface.msgbox("input-password", "Password Required", "", "");
|
||||
}
|
||||
Vec::new()
|
||||
} else {
|
||||
let mut hasher = Sha256::new();
|
||||
@@ -4756,6 +4773,15 @@ pub async fn handle_hash(
|
||||
hasher.finalize()[..].into()
|
||||
};
|
||||
|
||||
if request_consent {
|
||||
interface.msgbox(
|
||||
"wait-remote-accept-nook",
|
||||
"Prompt",
|
||||
"Please wait for the remote side to accept your session request...",
|
||||
"",
|
||||
);
|
||||
}
|
||||
|
||||
if let Err(error) = send_login(
|
||||
lc.clone(),
|
||||
String::new(),
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
use super::LoginConfigHandler;
|
||||
|
||||
#[derive(Default, PartialEq, Eq)]
|
||||
pub(super) enum LoginMethod {
|
||||
#[default]
|
||||
Automatic,
|
||||
Consent,
|
||||
}
|
||||
|
||||
impl LoginConfigHandler {
|
||||
pub(crate) fn set_login_method(&mut self, method: &str) -> bool {
|
||||
self.login_method = match method {
|
||||
"consent" => LoginMethod::Consent,
|
||||
_ => return false,
|
||||
};
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn skips_saved_password(&self) -> bool {
|
||||
self.login_method != LoginMethod::Automatic
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "flutter"))]
|
||||
mod tests {
|
||||
use crate::{client, flutter::FlutterHandler, ui_session_interface::Session};
|
||||
use base::message_proto::{Hash, Message, PeerInfo};
|
||||
use hbb_common::{
|
||||
config::PeerConfig, protobuf::Message as _, rendezvous_proto::ConnType, tcp::FramedStream,
|
||||
tokio, Stream,
|
||||
};
|
||||
|
||||
async fn connection() -> (Stream, Stream) {
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let client = tokio::net::TcpStream::connect(addr).await.unwrap();
|
||||
let (socket, addr) = listener.accept().await.unwrap();
|
||||
(
|
||||
Stream::Tcp(FramedStream::from(client, listener.local_addr().unwrap())),
|
||||
Stream::Tcp(FramedStream::from(socket, addr)),
|
||||
)
|
||||
}
|
||||
|
||||
async fn received_password(peer: &mut Stream) -> Vec<u8> {
|
||||
let bytes = peer.next_timeout(3000).await.unwrap().unwrap();
|
||||
Message::parse_from_bytes(&bytes)
|
||||
.unwrap()
|
||||
.login_request()
|
||||
.password
|
||||
.to_vec()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_creation_initializes_login_method() {
|
||||
let remote_id = hbb_common::config::LocalConfig::get_remote_id();
|
||||
for method in ["", "consent"] {
|
||||
let session_id = uuid::Uuid::new_v4();
|
||||
let result = crate::flutter_ffi::session_add_sync(
|
||||
session_id,
|
||||
format!("login-method-init-{}", session_id),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
String::new(),
|
||||
false,
|
||||
String::new(),
|
||||
false,
|
||||
None,
|
||||
method.to_owned(),
|
||||
);
|
||||
let session = crate::flutter::sessions::remove_session_by_session_id(&session_id);
|
||||
hbb_common::config::LocalConfig::set_remote_id(&remote_id);
|
||||
assert!(result.0.is_empty(), "{}", result.0);
|
||||
let session = session.unwrap();
|
||||
let request = session
|
||||
.lc
|
||||
.read()
|
||||
.unwrap()
|
||||
.create_login_msg(String::new(), String::new(), vec![1; 32]);
|
||||
assert_eq!(
|
||||
request.login_request().password.is_empty(),
|
||||
method == "consent"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn configured_passwords_are_skipped_only_for_the_selected_connection() {
|
||||
for conn_type in [
|
||||
ConnType::DEFAULT_CONN,
|
||||
ConnType::FILE_TRANSFER,
|
||||
ConnType::VIEW_CAMERA,
|
||||
ConnType::PORT_FORWARD,
|
||||
ConnType::RDP,
|
||||
ConnType::TERMINAL,
|
||||
] {
|
||||
for skip in [false, true] {
|
||||
let session = Session::<FlutterHandler>::default();
|
||||
{
|
||||
let mut lc = session.lc.write().unwrap();
|
||||
lc.id = format!("skip-saved-password-test-{}", uuid::Uuid::new_v4());
|
||||
lc.conn_type = conn_type;
|
||||
lc.config.password = vec![1; 32];
|
||||
lc.password = vec![2; 32];
|
||||
lc.shared_password = Some("shared password".into());
|
||||
let token = lc.get_conn_token();
|
||||
if skip {
|
||||
assert!(lc.set_login_method("consent"));
|
||||
}
|
||||
assert_eq!(lc.get_conn_token(), token);
|
||||
let request = lc.create_login_msg(String::new(), String::new(), vec![4; 32]);
|
||||
assert_eq!(request.login_request().password.is_empty(), skip);
|
||||
}
|
||||
let (mut stream, mut peer) = connection().await;
|
||||
assert!(
|
||||
client::handle_hash(
|
||||
session.lc.clone(),
|
||||
"preset password",
|
||||
Hash {
|
||||
salt: "salt".into(),
|
||||
challenge: "challenge".into(),
|
||||
..Default::default()
|
||||
},
|
||||
&session,
|
||||
&mut Default::default(),
|
||||
&mut stream,
|
||||
)
|
||||
.await
|
||||
);
|
||||
let password = received_password(&mut peer).await;
|
||||
assert_eq!(password.is_empty(), skip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn admin_terminal_waits_for_os_credentials_before_requesting_consent() {
|
||||
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"));
|
||||
}
|
||||
let (mut stream, mut peer) = connection().await;
|
||||
let mut trust = Default::default();
|
||||
assert!(
|
||||
client::handle_hash(
|
||||
session.lc.clone(),
|
||||
"preset password",
|
||||
Hash::default(),
|
||||
&session,
|
||||
&mut trust,
|
||||
&mut stream,
|
||||
)
|
||||
.await
|
||||
);
|
||||
assert!(peer.next_timeout(50).await.is_none());
|
||||
client::handle_login_from_ui(
|
||||
session.lc.clone(),
|
||||
"administrator".into(),
|
||||
"os password".into(),
|
||||
String::new(),
|
||||
false,
|
||||
&mut trust,
|
||||
&mut stream,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let bytes = peer.next_timeout(3000).await.unwrap().unwrap();
|
||||
let message = Message::parse_from_bytes(&bytes).unwrap();
|
||||
let login = message.login_request();
|
||||
assert!(login.password.is_empty());
|
||||
assert_eq!(login.os_login.username, "administrator");
|
||||
assert_eq!(login.os_login.password, "os password");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn consent_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();
|
||||
config.password = vec![3; 32];
|
||||
config.store(&id);
|
||||
let session = Session::<FlutterHandler>::default();
|
||||
{
|
||||
let mut lc = session.lc.write().unwrap();
|
||||
lc.initialize(
|
||||
id.clone(),
|
||||
ConnType::DEFAULT_CONN,
|
||||
None,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
"",
|
||||
);
|
||||
assert!(!lc.set_login_method("password"));
|
||||
assert!(lc.set_login_method("consent"));
|
||||
}
|
||||
assert!(
|
||||
client::handle_hash(
|
||||
session.lc.clone(),
|
||||
"",
|
||||
Hash::default(),
|
||||
&session,
|
||||
&mut Default::default(),
|
||||
&mut stream,
|
||||
)
|
||||
.await
|
||||
);
|
||||
let first = received_password(&mut peer).await;
|
||||
assert!(first.is_empty());
|
||||
session
|
||||
.lc
|
||||
.write()
|
||||
.unwrap()
|
||||
.handle_peer_info(&PeerInfo::default());
|
||||
let saved = PeerConfig::load(&id).password;
|
||||
PeerConfig::remove(&id);
|
||||
assert_eq!(saved, config.password);
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -73,6 +73,7 @@ pub fn core_main() -> Option<CoreMainAction> {
|
||||
"--view-camera",
|
||||
"--port-forward",
|
||||
"--terminal",
|
||||
"--terminal-admin",
|
||||
"--rdp",
|
||||
]
|
||||
.contains(&arg.as_str())
|
||||
@@ -763,7 +764,7 @@ fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option<Vec<Strin
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--connect" | "--play" | "--file-transfer" | "--view-camera" | "--port-forward"
|
||||
| "--terminal" | "--rdp" => {
|
||||
| "--terminal" | "--terminal-admin" | "--rdp" => {
|
||||
authority = Some((&arg.to_string()[2..]).to_owned());
|
||||
id = args.next();
|
||||
}
|
||||
@@ -775,6 +776,9 @@ 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());
|
||||
}
|
||||
// inner
|
||||
"--switch_uuid" => {
|
||||
if let Some(switch_uuid) = args.next() {
|
||||
|
||||
@@ -1316,6 +1316,7 @@ pub fn session_add(
|
||||
password: String,
|
||||
is_shared_password: bool,
|
||||
conn_token: Option<String>,
|
||||
login_method: &str,
|
||||
) -> ResultType<FlutterSession> {
|
||||
let conn_type = if is_file_transfer {
|
||||
ConnType::FILE_TRANSFER
|
||||
@@ -1376,6 +1377,7 @@ pub fn session_add(
|
||||
get_adapter_luid(),
|
||||
shared_password,
|
||||
conn_token,
|
||||
login_method,
|
||||
);
|
||||
|
||||
let session = Arc::new(session.clone());
|
||||
|
||||
@@ -160,6 +160,7 @@ pub fn session_add_sync(
|
||||
password: String,
|
||||
is_shared_password: bool,
|
||||
conn_token: Option<String>,
|
||||
login_method: String,
|
||||
) -> SyncReturn<String> {
|
||||
let add_res = session_add(
|
||||
&session_id,
|
||||
@@ -174,6 +175,7 @@ pub fn session_add_sync(
|
||||
password,
|
||||
is_shared_password,
|
||||
conn_token,
|
||||
&login_method,
|
||||
);
|
||||
// We can't put the remove call together with `std::env::var("IS_TERMINAL_ADMIN")`.
|
||||
// Because there are some `bail!` in `session_add()`, we must make sure `IS_TERMINAL_ADMIN` is removed at last.
|
||||
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", "不使用已保存的密码,请求对方接受连接。"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", "不使用已儲存的密碼,請求對方接受連線。"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
@@ -823,5 +823,8 @@ 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.", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
+1
-1
@@ -621,7 +621,7 @@ impl SciterSession {
|
||||
.lc
|
||||
.write()
|
||||
.unwrap()
|
||||
.initialize(id, conn_type, None, force_relay, None, None, None);
|
||||
.initialize(id, conn_type, None, force_relay, None, None, None, "");
|
||||
|
||||
Self(session)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user