refactor: unify request approval menus and naming

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

No files matched your search

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