Merge pull request #2741 from Heap-Hop/master

refactor user login, add two-step verification (email)
This commit is contained in:
RustDesk 2023-01-09 14:04:57 +08:00 committed by GitHub
commit 20a4550cce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 1378 additions and 1059 deletions

View File

@ -1367,7 +1367,7 @@ connect(BuildContext context, String id,
} }
} }
Future<Map<String, String>> getHttpHeaders() async { Map<String, String> getHttpHeaders() {
return { return {
'Authorization': 'Bearer ${bind.mainGetLocalOption(key: 'access_token')}' 'Authorization': 'Bearer ${bind.mainGetLocalOption(key: 'access_token')}'
}; };

View File

@ -1,12 +1,22 @@
import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_model.dart';
class HttpType {
static const kAuthReqTypeAccount = "account";
static const kAuthReqTypeMobile = "mobile";
static const kAuthReqTypeSMSCode = "sms_code";
static const kAuthReqTypeEmailCode = "email_code";
static const kAuthResTypeToken = "access_token";
static const kAuthResTypeEmailCheck = "email_check";
}
class UserPayload { class UserPayload {
String name = ''; String name = '';
String email = ''; String email = '';
String note = ''; String note = '';
int? status; int? status;
String grp = ''; String grp = '';
bool is_admin = false; bool isAdmin = false;
UserPayload.fromJson(Map<String, dynamic> json) UserPayload.fromJson(Map<String, dynamic> json)
: name = json['name'] ?? '', : name = json['name'] ?? '',
@ -14,7 +24,7 @@ class UserPayload {
note = json['note'] ?? '', note = json['note'] ?? '',
status = json['status'], status = json['status'],
grp = json['grp'] ?? '', grp = json['grp'] ?? '',
is_admin = json['is_admin'] == true; isAdmin = json['is_admin'] == true;
} }
class PeerPayload { class PeerPayload {
@ -37,3 +47,73 @@ class PeerPayload {
return Peer.fromJson({"id": p.id}); return Peer.fromJson({"id": p.id});
} }
} }
class LoginRequest {
String? username;
String? password;
String? id;
String? uuid;
bool? autoLogin;
String? type;
String? verificationCode;
String? deviceInfo;
LoginRequest(
{this.username,
this.password,
this.id,
this.uuid,
this.autoLogin,
this.type,
this.verificationCode,
this.deviceInfo});
LoginRequest.fromJson(Map<String, dynamic> json) {
username = json['username'];
password = json['password'];
id = json['id'];
uuid = json['uuid'];
autoLogin = json['autoLogin'];
type = json['type'];
verificationCode = json['verificationCode'];
deviceInfo = json['deviceInfo'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['username'] = username ?? '';
data['password'] = password ?? '';
data['id'] = id ?? '';
data['uuid'] = uuid ?? '';
data['autoLogin'] = autoLogin ?? '';
data['type'] = type ?? '';
data['verificationCode'] = verificationCode ?? '';
data['deviceInfo'] = deviceInfo ?? '';
return data;
}
}
class LoginResponse {
String? access_token;
String? type;
UserPayload? user;
LoginResponse({this.access_token, this.type, this.user});
LoginResponse.fromJson(Map<String, dynamic> json) {
access_token = json['access_token'];
type = json['type'];
user = json['user'] != null ? UserPayload.fromJson(json['user']) : null;
}
}
class RequestException implements Exception {
int statusCode;
String cause;
RequestException(this.statusCode, this.cause);
@override
String toString() {
return "RequestException, statusCode: $statusCode, error: $cause";
}
}

View File

@ -3,14 +3,12 @@ import 'package:flutter_hbb/common/formatter/id_formatter.dart';
import 'package:flutter_hbb/common/widgets/peer_card.dart'; import 'package:flutter_hbb/common/widgets/peer_card.dart';
import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/common/widgets/peers_view.dart';
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
import 'package:flutter_hbb/desktop/widgets/login.dart';
import '../../consts.dart'; import '../../consts.dart';
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../common.dart'; import '../../common.dart';
import '../../desktop/pages/desktop_home_page.dart'; import 'login.dart';
import '../../mobile/pages/settings_page.dart';
class AddressBook extends StatefulWidget { class AddressBook extends StatefulWidget {
final EdgeInsets? menuPadding; final EdgeInsets? menuPadding;
@ -41,21 +39,12 @@ class _AddressBookState extends State<AddressBook> {
} }
}); });
handleLogin() {
// TODO refactor login dialog for desktop and mobile
if (isDesktop) {
loginDialog();
} else {
showLogin(gFFI.dialogManager);
}
}
Future<Widget> buildBody(BuildContext context) async { Future<Widget> buildBody(BuildContext context) async {
return Obx(() { return Obx(() {
if (gFFI.userModel.userName.value.isEmpty) { if (gFFI.userModel.userName.value.isEmpty) {
return Center( return Center(
child: InkWell( child: InkWell(
onTap: handleLogin, onTap: loginDialog,
child: Text( child: Text(
translate("Login"), translate("Login"),
style: const TextStyle(decoration: TextDecoration.underline), style: const TextStyle(decoration: TextDecoration.underline),

View File

@ -0,0 +1,676 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../common.dart';
class _IconOP extends StatelessWidget {
final String icon;
final double iconWidth;
final EdgeInsets margin;
const _IconOP(
{Key? key,
required this.icon,
required this.iconWidth,
this.margin = const EdgeInsets.symmetric(horizontal: 4.0)})
: super(key: key);
@override
Widget build(BuildContext context) {
return Container(
margin: margin,
child: SvgPicture.asset(
'assets/$icon.svg',
width: iconWidth,
),
);
}
}
class ButtonOP extends StatelessWidget {
final String op;
final RxString curOP;
final double iconWidth;
final Color primaryColor;
final double height;
final Function() onTap;
const ButtonOP({
Key? key,
required this.op,
required this.curOP,
required this.iconWidth,
required this.primaryColor,
required this.height,
required this.onTap,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(children: [
Container(
height: height,
width: 200,
child: Obx(() => ElevatedButton(
style: ElevatedButton.styleFrom(
primary: curOP.value.isEmpty || curOP.value == op
? primaryColor
: Colors.grey,
).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)),
onPressed: curOP.value.isEmpty || curOP.value == op ? onTap : null,
child: Row(
children: [
SizedBox(
width: 30,
child: _IconOP(
icon: op,
iconWidth: iconWidth,
margin: EdgeInsets.only(right: 5),
)),
Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
child: Center(
child: Text('${translate("Continue with")} $op')))),
],
))),
),
]);
}
}
class ConfigOP {
final String op;
final double iconWidth;
ConfigOP({required this.op, required this.iconWidth});
}
class WidgetOP extends StatefulWidget {
final ConfigOP config;
final RxString curOP;
final Function(String) cbLogin;
const WidgetOP({
Key? key,
required this.config,
required this.curOP,
required this.cbLogin,
}) : super(key: key);
@override
State<StatefulWidget> createState() {
return _WidgetOPState();
}
}
class _WidgetOPState extends State<WidgetOP> {
Timer? _updateTimer;
String _stateMsg = '';
String _failedMsg = '';
String _url = '';
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
_updateTimer?.cancel();
}
_beginQueryState() {
_updateTimer = Timer.periodic(Duration(seconds: 1), (timer) {
_updateState();
});
}
_updateState() {
bind.mainAccountAuthResult().then((result) {
if (result.isEmpty) {
return;
}
final resultMap = jsonDecode(result);
if (resultMap == null) {
return;
}
final String stateMsg = resultMap['state_msg'];
String failedMsg = resultMap['failed_msg'];
final String? url = resultMap['url'];
final authBody = resultMap['auth_body'];
if (_stateMsg != stateMsg || _failedMsg != failedMsg) {
if (_url.isEmpty && url != null && url.isNotEmpty) {
launchUrl(Uri.parse(url));
_url = url;
}
if (authBody != null) {
_updateTimer?.cancel();
final String username = authBody['user']['name'];
widget.curOP.value = '';
widget.cbLogin(username);
}
setState(() {
_stateMsg = stateMsg;
_failedMsg = failedMsg;
if (failedMsg.isNotEmpty) {
widget.curOP.value = '';
_updateTimer?.cancel();
}
});
}
});
}
_resetState() {
_stateMsg = '';
_failedMsg = '';
_url = '';
}
@override
Widget build(BuildContext context) {
return Column(
children: [
ButtonOP(
op: widget.config.op,
curOP: widget.curOP,
iconWidth: widget.config.iconWidth,
primaryColor: str2color(widget.config.op, 0x7f),
height: 36,
onTap: () async {
_resetState();
widget.curOP.value = widget.config.op;
await bind.mainAccountAuth(op: widget.config.op);
_beginQueryState();
},
),
Obx(() {
if (widget.curOP.isNotEmpty &&
widget.curOP.value != widget.config.op) {
_failedMsg = '';
}
return Offstage(
offstage:
_failedMsg.isEmpty && widget.curOP.value != widget.config.op,
child: Row(
children: [
Text(
_stateMsg,
style: TextStyle(fontSize: 12),
),
SizedBox(width: 8),
Text(
_failedMsg,
style: TextStyle(
fontSize: 14,
color: Colors.red,
),
),
],
));
}),
Obx(
() => Offstage(
offstage: widget.curOP.value != widget.config.op,
child: const SizedBox(
height: 5.0,
),
),
),
Obx(
() => Offstage(
offstage: widget.curOP.value != widget.config.op,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 20),
child: ElevatedButton(
onPressed: () {
widget.curOP.value = '';
_updateTimer?.cancel();
_resetState();
bind.mainAccountAuthCancel();
},
child: Text(
translate('Cancel'),
style: TextStyle(fontSize: 15),
),
),
),
),
),
],
);
}
}
class LoginWidgetOP extends StatelessWidget {
final List<ConfigOP> ops;
final RxString curOP;
final Function(String) cbLogin;
LoginWidgetOP({
Key? key,
required this.ops,
required this.curOP,
required this.cbLogin,
}) : super(key: key);
@override
Widget build(BuildContext context) {
var children = ops
.map((op) => [
WidgetOP(
config: op,
curOP: curOP,
cbLogin: cbLogin,
),
const Divider(
indent: 5,
endIndent: 5,
)
])
.expand((i) => i)
.toList();
if (children.isNotEmpty) {
children.removeLast();
}
return SingleChildScrollView(
child: Container(
width: 200,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: children,
)));
}
}
class LoginWidgetUserPass extends StatelessWidget {
final TextEditingController username;
final TextEditingController pass;
final String? usernameMsg;
final String? passMsg;
final bool isInProgress;
final RxString curOP;
final RxBool autoLogin;
final Function() onLogin;
final FocusNode? userFocusNode;
const LoginWidgetUserPass({
Key? key,
this.userFocusNode,
required this.username,
required this.pass,
required this.usernameMsg,
required this.passMsg,
required this.isInProgress,
required this.curOP,
required this.autoLogin,
required this.onLogin,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 8.0),
DialogTextField(
title: '${translate("Username")}:',
controller: username,
focusNode: userFocusNode,
prefixIcon: Icon(Icons.account_circle_outlined),
errorText: usernameMsg),
DialogTextField(
title: '${translate("Password")}:',
obscureText: true,
controller: pass,
prefixIcon: Icon(Icons.lock_outline),
errorText: passMsg),
Obx(() => CheckboxListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
controlAffinity: ListTileControlAffinity.leading,
title: Text(
translate("Remember me"),
),
value: autoLogin.value,
onChanged: (v) {
if (v == null) return;
autoLogin.value = v;
},
)),
Offstage(
offstage: !isInProgress,
child: const LinearProgressIndicator()),
const SizedBox(height: 12.0),
FittedBox(
child:
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Container(
height: 38,
width: 200,
child: Obx(() => ElevatedButton(
child: Text(
translate('Login'),
style: TextStyle(fontSize: 16),
),
onPressed:
curOP.value.isEmpty || curOP.value == 'rustdesk'
? () {
onLogin();
}
: null,
)),
),
])),
],
));
}
}
class DialogTextField extends StatelessWidget {
final String title;
final bool obscureText;
final String? errorText;
final String? helperText;
final Widget? prefixIcon;
final TextEditingController controller;
final FocusNode? focusNode;
DialogTextField(
{Key? key,
this.focusNode,
this.obscureText = false,
this.errorText,
this.helperText,
this.prefixIcon,
required this.title,
required this.controller})
: super(key: key);
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: TextField(
decoration: InputDecoration(
labelText: title,
border: const OutlineInputBorder(),
prefixIcon: prefixIcon,
helperText: helperText,
helperMaxLines: 8,
errorText: errorText),
controller: controller,
focusNode: focusNode,
autofocus: true,
obscureText: obscureText,
),
),
],
).paddingSymmetric(vertical: 4.0);
}
}
/// common login dialog for desktop
/// call this directly
Future<bool?> loginDialog() async {
var username = TextEditingController();
var password = TextEditingController();
final userFocusNode = FocusNode()..requestFocus();
Timer(Duration(milliseconds: 100), () => userFocusNode..requestFocus());
String? usernameMsg;
String? passwordMsg;
var isInProgress = false;
final autoLogin = true.obs;
final RxString curOP = ''.obs;
final res = await gFFI.dialogManager.show<bool>((setState, close) {
username.addListener(() {
if (usernameMsg != null) {
setState(() => usernameMsg = null);
}
});
password.addListener(() {
if (passwordMsg != null) {
setState(() => passwordMsg = null);
}
});
onDialogCancel() {
isInProgress = false;
close(false);
}
onLogin() async {
// validate
if (username.text.isEmpty) {
setState(() => usernameMsg = translate('Username missed'));
return;
}
if (password.text.isEmpty) {
setState(() => passwordMsg = translate('Password missed'));
return;
}
curOP.value = 'rustdesk';
setState(() => isInProgress = true);
try {
final resp = await gFFI.userModel.login(LoginRequest(
username: username.text,
password: password.text,
id: await bind.mainGetMyId(),
uuid: await bind.mainGetUuid(),
autoLogin: autoLogin.value,
type: HttpType.kAuthReqTypeAccount));
switch (resp.type) {
case HttpType.kAuthResTypeToken:
if (resp.access_token != null) {
await bind.mainSetLocalOption(
key: 'access_token', value: resp.access_token!);
close(true);
return;
}
break;
case HttpType.kAuthResTypeEmailCheck:
setState(() => isInProgress = false);
final res = await verificationCodeDialog(resp.user);
if (res == true) {
close(true);
return;
}
break;
default:
passwordMsg = "Failed, bad response from server";
break;
}
} on RequestException catch (err) {
passwordMsg = translate(err.cause);
debugPrintStack(label: err.toString());
} catch (err) {
passwordMsg = "Unknown Error: $err";
debugPrintStack(label: err.toString());
}
curOP.value = '';
setState(() => isInProgress = false);
}
return CustomAlertDialog(
title: Text(translate('Login')),
contentBoxConstraints: BoxConstraints(minWidth: 400),
content: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(
height: 8.0,
),
LoginWidgetUserPass(
username: username,
pass: password,
usernameMsg: usernameMsg,
passMsg: passwordMsg,
isInProgress: isInProgress,
curOP: curOP,
autoLogin: autoLogin,
onLogin: onLogin,
userFocusNode: userFocusNode,
),
const SizedBox(
height: 8.0,
),
Center(
child: Text(
translate('or'),
style: TextStyle(fontSize: 16),
)),
const SizedBox(
height: 8.0,
),
LoginWidgetOP(
ops: [
ConfigOP(op: 'Github', iconWidth: 20),
ConfigOP(op: 'Google', iconWidth: 20),
ConfigOP(op: 'Okta', iconWidth: 38),
],
curOP: curOP,
cbLogin: (String username) {
gFFI.userModel.userName.value = username;
close(true);
},
),
],
),
actions: [msgBoxButton(translate('Close'), onDialogCancel)],
onCancel: onDialogCancel,
);
});
if (res != null) {
// update ab and group status
await gFFI.abModel.pullAb();
await gFFI.groupModel.pull();
}
return res;
}
Future<bool?> verificationCodeDialog(UserPayload? user) async {
var autoLogin = true;
var isInProgress = false;
String? errorText;
final code = TextEditingController();
final focusNode = FocusNode()..requestFocus();
Timer(Duration(milliseconds: 100), () => focusNode..requestFocus());
final res = await gFFI.dialogManager.show<bool>((setState, close) {
bool validate() {
return code.text.length >= 6;
}
code.addListener(() {
if (errorText != null) {
setState(() => errorText = null);
}
});
void onVerify() async {
if (!validate()) {
setState(
() => errorText = translate('Too short, at least 6 characters.'));
return;
}
setState(() => isInProgress = true);
try {
final resp = await gFFI.userModel.login(LoginRequest(
verificationCode: code.text,
username: user?.name,
id: await bind.mainGetMyId(),
uuid: await bind.mainGetUuid(),
autoLogin: autoLogin,
type: HttpType.kAuthReqTypeEmailCode));
switch (resp.type) {
case HttpType.kAuthResTypeToken:
if (resp.access_token != null) {
await bind.mainSetLocalOption(
key: 'access_token', value: resp.access_token!);
close(true);
return;
}
break;
default:
errorText = "Failed, bad response from server";
break;
}
} on RequestException catch (err) {
errorText = translate(err.cause);
debugPrintStack(label: err.toString());
} catch (err) {
errorText = "Unknown Error: $err";
debugPrintStack(label: err.toString());
}
setState(() => isInProgress = false);
}
return CustomAlertDialog(
title: Text(translate("Verification code")),
contentBoxConstraints: BoxConstraints(maxWidth: 300),
content: Column(
children: [
Offstage(
offstage: user?.email == null,
child: TextField(
decoration: InputDecoration(
labelText: "Email",
prefixIcon: Icon(Icons.email),
border: InputBorder.none),
readOnly: true,
controller: TextEditingController(text: user?.email),
)),
const SizedBox(height: 8),
DialogTextField(
title: '${translate("Verification code")}:',
controller: code,
errorText: errorText,
focusNode: focusNode,
helperText: translate('verification_tip'),
),
CheckboxListTile(
contentPadding: const EdgeInsets.all(0),
dense: true,
controlAffinity: ListTileControlAffinity.leading,
title: Row(children: [
Expanded(child: Text(translate("Trust this device")))
]),
value: autoLogin,
onChanged: (v) {
if (v == null) return;
setState(() => autoLogin = !autoLogin);
},
),
Offstage(
offstage: !isInProgress,
child: const LinearProgressIndicator()),
],
),
actions: [
TextButton(onPressed: close, child: Text(translate("Cancel"))),
TextButton(onPressed: onVerify, child: Text(translate("Verify"))),
]);
});
return res;
}

View File

@ -8,7 +8,6 @@ import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
import 'package:flutter_hbb/desktop/widgets/login.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/models/server_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@ -18,6 +17,7 @@ import 'package:url_launcher/url_launcher_string.dart';
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart'; import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
import '../../common/widgets/dialog.dart'; import '../../common/widgets/dialog.dart';
import '../../common/widgets/login.dart';
const double _kTabWidth = 235; const double _kTabWidth = 235;
const double _kTabHeight = 42; const double _kTabHeight = 42;

View File

@ -1,521 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../common.dart';
final _kMidButtonPadding = const EdgeInsets.fromLTRB(15, 0, 15, 0);
class _IconOP extends StatelessWidget {
final String icon;
final double iconWidth;
const _IconOP({Key? key, required this.icon, required this.iconWidth})
: super(key: key);
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 4.0),
child: SvgPicture.asset(
'assets/$icon.svg',
width: iconWidth,
),
);
}
}
class ButtonOP extends StatelessWidget {
final String op;
final RxString curOP;
final double iconWidth;
final Color primaryColor;
final double height;
final Function() onTap;
const ButtonOP({
Key? key,
required this.op,
required this.curOP,
required this.iconWidth,
required this.primaryColor,
required this.height,
required this.onTap,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(children: [
Expanded(
child: Container(
height: height,
padding: _kMidButtonPadding,
child: Obx(() => ElevatedButton(
style: ElevatedButton.styleFrom(
primary: curOP.value.isEmpty || curOP.value == op
? primaryColor
: Colors.grey,
).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)),
onPressed:
curOP.value.isEmpty || curOP.value == op ? onTap : null,
child: Stack(children: [
Center(child: Text('${translate("Continue with")} $op')),
Align(
alignment: Alignment.centerLeft,
child: SizedBox(
width: 120,
child: _IconOP(
icon: op,
iconWidth: iconWidth,
)),
),
]),
)),
),
)
]);
}
}
class ConfigOP {
final String op;
final double iconWidth;
ConfigOP({required this.op, required this.iconWidth});
}
class WidgetOP extends StatefulWidget {
final ConfigOP config;
final RxString curOP;
final Function(String) cbLogin;
const WidgetOP({
Key? key,
required this.config,
required this.curOP,
required this.cbLogin,
}) : super(key: key);
@override
State<StatefulWidget> createState() {
return _WidgetOPState();
}
}
class _WidgetOPState extends State<WidgetOP> {
Timer? _updateTimer;
String _stateMsg = '';
String _failedMsg = '';
String _url = '';
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
_updateTimer?.cancel();
}
_beginQueryState() {
_updateTimer = Timer.periodic(Duration(seconds: 1), (timer) {
_updateState();
});
}
_updateState() {
bind.mainAccountAuthResult().then((result) {
if (result.isEmpty) {
return;
}
final resultMap = jsonDecode(result);
if (resultMap == null) {
return;
}
final String stateMsg = resultMap['state_msg'];
String failedMsg = resultMap['failed_msg'];
final String? url = resultMap['url'];
final authBody = resultMap['auth_body'];
if (_stateMsg != stateMsg || _failedMsg != failedMsg) {
if (_url.isEmpty && url != null && url.isNotEmpty) {
launchUrl(Uri.parse(url));
_url = url;
}
if (authBody != null) {
_updateTimer?.cancel();
final String username = authBody['user']['name'];
widget.curOP.value = '';
widget.cbLogin(username);
}
setState(() {
_stateMsg = stateMsg;
_failedMsg = failedMsg;
if (failedMsg.isNotEmpty) {
widget.curOP.value = '';
_updateTimer?.cancel();
}
});
}
});
}
_resetState() {
_stateMsg = '';
_failedMsg = '';
_url = '';
}
@override
Widget build(BuildContext context) {
return Column(
children: [
ButtonOP(
op: widget.config.op,
curOP: widget.curOP,
iconWidth: widget.config.iconWidth,
primaryColor: str2color(widget.config.op, 0x7f),
height: 36,
onTap: () async {
_resetState();
widget.curOP.value = widget.config.op;
await bind.mainAccountAuth(op: widget.config.op);
_beginQueryState();
},
),
Obx(() {
if (widget.curOP.isNotEmpty &&
widget.curOP.value != widget.config.op) {
_failedMsg = '';
}
return Offstage(
offstage:
_failedMsg.isEmpty && widget.curOP.value != widget.config.op,
child: Row(
children: [
Text(
_stateMsg,
style: TextStyle(fontSize: 12),
),
SizedBox(width: 8),
Text(
_failedMsg,
style: TextStyle(
fontSize: 14,
color: Colors.red,
),
),
],
));
}),
Obx(
() => Offstage(
offstage: widget.curOP.value != widget.config.op,
child: const SizedBox(
height: 5.0,
),
),
),
Obx(
() => Offstage(
offstage: widget.curOP.value != widget.config.op,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 20),
child: ElevatedButton(
onPressed: () {
widget.curOP.value = '';
_updateTimer?.cancel();
_resetState();
bind.mainAccountAuthCancel();
},
child: Text(
translate('Cancel'),
style: TextStyle(fontSize: 15),
),
),
),
),
),
],
);
}
}
class LoginWidgetOP extends StatelessWidget {
final List<ConfigOP> ops;
final RxString curOP;
final Function(String) cbLogin;
LoginWidgetOP({
Key? key,
required this.ops,
required this.curOP,
required this.cbLogin,
}) : super(key: key);
@override
Widget build(BuildContext context) {
var children = ops
.map((op) => [
WidgetOP(
config: op,
curOP: curOP,
cbLogin: cbLogin,
),
const Divider(
indent: 5,
endIndent: 5,
)
])
.expand((i) => i)
.toList();
if (children.isNotEmpty) {
children.removeLast();
}
return SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: children,
));
}
}
class LoginWidgetUserPass extends StatelessWidget {
final String username;
final String pass;
final String usernameMsg;
final String passMsg;
final bool isInProgress;
final RxString curOP;
final Function(String, String) onLogin;
const LoginWidgetUserPass({
Key? key,
required this.username,
required this.pass,
required this.usernameMsg,
required this.passMsg,
required this.isInProgress,
required this.curOP,
required this.onLogin,
}) : super(key: key);
@override
Widget build(BuildContext context) {
var userController = TextEditingController(text: username);
var pwdController = TextEditingController(text: pass);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 8.0,
),
Container(
padding: _kMidButtonPadding,
child: Row(
children: [
ConstrainedBox(
constraints: const BoxConstraints(minWidth: 100),
child: Text(
'${translate("Username")}:',
textAlign: TextAlign.start,
).marginOnly(bottom: 16.0)),
const SizedBox(
width: 24.0,
),
Expanded(
child: TextField(
decoration: InputDecoration(
border: const OutlineInputBorder(),
errorText: usernameMsg.isNotEmpty ? usernameMsg : null),
controller: userController,
focusNode: FocusNode()..requestFocus(),
),
),
],
),
),
const SizedBox(
height: 8.0,
),
Container(
padding: _kMidButtonPadding,
child: Row(
children: [
ConstrainedBox(
constraints: const BoxConstraints(minWidth: 100),
child: Text('${translate("Password")}:')
.marginOnly(bottom: 16.0)),
const SizedBox(
width: 24.0,
),
Expanded(
child: TextField(
obscureText: true,
decoration: InputDecoration(
border: const OutlineInputBorder(),
errorText: passMsg.isNotEmpty ? passMsg : null),
controller: pwdController,
),
),
],
),
),
const SizedBox(
height: 4.0,
),
Offstage(
offstage: !isInProgress, child: const LinearProgressIndicator()),
const SizedBox(
height: 12.0,
),
Row(children: [
Expanded(
child: Container(
height: 38,
padding: _kMidButtonPadding,
child: Obx(() => ElevatedButton(
style: curOP.value.isEmpty || curOP.value == 'rustdesk'
? null
: ElevatedButton.styleFrom(
primary: Colors.grey,
),
child: Text(
translate('Login'),
style: TextStyle(fontSize: 16),
),
onPressed: curOP.value.isEmpty || curOP.value == 'rustdesk'
? () {
onLogin(userController.text, pwdController.text);
}
: null,
)),
),
),
]),
],
);
}
}
/// common login dialog for desktop
/// call this directly
Future<bool> loginDialog() async {
String username = '';
var usernameMsg = '';
String pass = '';
var passMsg = '';
var isInProgress = false;
var completer = Completer<bool>();
final RxString curOP = ''.obs;
gFFI.dialogManager.show((setState, close) {
cancel() {
isInProgress = false;
completer.complete(false);
close();
}
onLogin(String username0, String pass0) async {
setState(() {
usernameMsg = '';
passMsg = '';
isInProgress = true;
});
cancel() {
curOP.value = '';
if (isInProgress) {
setState(() {
isInProgress = false;
});
}
}
curOP.value = 'rustdesk';
username = username0;
pass = pass0;
if (username.isEmpty) {
usernameMsg = translate('Username missed');
cancel();
return;
}
if (pass.isEmpty) {
passMsg = translate('Password missed');
cancel();
return;
}
try {
final resp = await gFFI.userModel.login(username, pass);
if (resp.containsKey('error')) {
passMsg = resp['error'];
cancel();
return;
}
// {access_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJndWlkIjoiMDFkZjQ2ZjgtZjg3OS00MDE0LTk5Y2QtMGMwYzM2MmViZGJlIiwiZXhwIjoxNjYxNDg2NzYwfQ.GZpe1oI8TfM5yTYNrpcwbI599P4Z_-b2GmnwNl2Lr-w,
// token_type: Bearer, user: {id: , name: admin, email: null, note: null, status: null, grp: null, is_admin: true}}
debugPrint('$resp');
completer.complete(true);
} catch (err) {
debugPrintStack(label: err.toString());
cancel();
return;
}
close();
}
return CustomAlertDialog(
title: Text(translate('Login')),
content: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 500),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 8.0,
),
LoginWidgetUserPass(
username: username,
pass: pass,
usernameMsg: usernameMsg,
passMsg: passMsg,
isInProgress: isInProgress,
curOP: curOP,
onLogin: onLogin,
),
const SizedBox(
height: 8.0,
),
Center(
child: Text(
translate('or'),
style: TextStyle(fontSize: 16),
)),
const SizedBox(
height: 8.0,
),
LoginWidgetOP(
ops: [
ConfigOP(op: 'Github', iconWidth: 20),
ConfigOP(op: 'Google', iconWidth: 20),
ConfigOP(op: 'Okta', iconWidth: 38),
],
curOP: curOP,
cbLogin: (String username) {
gFFI.userModel.userName.value = username;
completer.complete(true);
close();
},
),
],
),
),
actions: [msgBoxButton(translate('Close'), cancel)],
onCancel: cancel,
);
});
return completer.future;
}

View File

@ -7,9 +7,8 @@ import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../../common.dart'; import '../../common.dart';
import '../../common/widgets/address_book.dart'; import '../../common/widgets/login.dart';
import '../../common/widgets/peer_tab_page.dart'; import '../../common/widgets/peer_tab_page.dart';
import '../../common/widgets/peers_view.dart';
import '../../consts.dart'; import '../../consts.dart';
import '../../models/model.dart'; import '../../models/model.dart';
import '../../models/platform_model.dart'; import '../../models/platform_model.dart';
@ -258,7 +257,7 @@ class _WebMenuState extends State<WebMenu> {
} }
if (value == 'login') { if (value == 'login') {
if (gFFI.userModel.userName.value.isEmpty) { if (gFFI.userModel.userName.value.isEmpty) {
showLogin(gFFI.dialogManager); loginDialog();
} else { } else {
gFFI.userModel.logOut(); gFFI.userModel.logOut();
} }

View File

@ -9,6 +9,7 @@ import 'package:url_launcher/url_launcher.dart';
import '../../common.dart'; import '../../common.dart';
import '../../common/widgets/dialog.dart'; import '../../common/widgets/dialog.dart';
import '../../common/widgets/login.dart';
import '../../models/model.dart'; import '../../models/model.dart';
import '../../models/platform_model.dart'; import '../../models/platform_model.dart';
import '../widgets/dialog.dart'; import '../widgets/dialog.dart';
@ -300,7 +301,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
leading: Icon(Icons.person), leading: Icon(Icons.person),
onPressed: (context) { onPressed: (context) {
if (gFFI.userModel.userName.value.isEmpty) { if (gFFI.userModel.userName.value.isEmpty) {
showLogin(gFFI.dialogManager); loginDialog();
} else { } else {
gFFI.userModel.logOut(); gFFI.userModel.logOut();
} }
@ -397,7 +398,7 @@ void showServerSettings(OverlayDialogManager dialogManager) async {
void showLanguageSettings(OverlayDialogManager dialogManager) async { void showLanguageSettings(OverlayDialogManager dialogManager) async {
try { try {
final langs = json.decode(await bind.mainGetLangs()) as List<dynamic>; final langs = json.decode(await bind.mainGetLangs()) as List<dynamic>;
var lang = await bind.mainGetLocalOption(key: "lang"); var lang = bind.mainGetLocalOption(key: "lang");
dialogManager.show((setState, close) { dialogManager.show((setState, close) {
setLang(v) { setLang(v) {
if (lang != v) { if (lang != v) {
@ -482,77 +483,6 @@ void showAbout(OverlayDialogManager dialogManager) {
}, clickMaskDismiss: true, backDismiss: true); }, clickMaskDismiss: true, backDismiss: true);
} }
void showLogin(OverlayDialogManager dialogManager) {
final passwordController = TextEditingController();
final nameController = TextEditingController();
var loading = false;
var error = '';
dialogManager.show((setState, close) {
return CustomAlertDialog(
title: Text(translate('Login')),
content: Column(mainAxisSize: MainAxisSize.min, children: [
TextField(
autofocus: true,
autocorrect: false,
enableSuggestions: false,
keyboardType: TextInputType.visiblePassword,
decoration: InputDecoration(
labelText: translate('Username'),
),
controller: nameController,
),
PasswordWidget(controller: passwordController, autoFocus: false),
]),
actions: (loading
? <Widget>[CircularProgressIndicator()]
: (error != ""
? <Widget>[
Text(translate(error),
style: TextStyle(color: Colors.red))
]
: <Widget>[])) +
<Widget>[
TextButton(
style: flatButtonStyle,
onPressed: loading
? null
: () {
close();
setState(() {
loading = false;
});
},
child: Text(translate('Cancel')),
),
TextButton(
style: flatButtonStyle,
onPressed: loading
? null
: () async {
final name = nameController.text.trim();
final pass = passwordController.text.trim();
if (name != "" && pass != "") {
setState(() {
loading = true;
});
final resp = await gFFI.userModel.login(name, pass);
setState(() {
loading = false;
});
if (resp.containsKey('error')) {
error = resp['error'];
return;
}
}
close();
},
child: Text(translate('OK')),
),
],
);
});
}
class ScanButton extends StatelessWidget { class ScanButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -27,8 +27,7 @@ class AbModel {
abError.value = ""; abError.value = "";
final api = "${await bind.mainGetApiServer()}/api/ab/get"; final api = "${await bind.mainGetApiServer()}/api/ab/get";
try { try {
final resp = final resp = await http.post(Uri.parse(api), headers: getHttpHeaders());
await http.post(Uri.parse(api), headers: await getHttpHeaders());
if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") { if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") {
Map<String, dynamic> json = jsonDecode(resp.body); Map<String, dynamic> json = jsonDecode(resp.body);
if (json.containsKey('error')) { if (json.containsKey('error')) {
@ -102,7 +101,7 @@ class AbModel {
Future<void> pushAb() async { Future<void> pushAb() async {
abLoading.value = true; abLoading.value = true;
final api = "${await bind.mainGetApiServer()}/api/ab"; final api = "${await bind.mainGetApiServer()}/api/ab";
var authHeaders = await getHttpHeaders(); var authHeaders = getHttpHeaders();
authHeaders['Content-Type'] = "application/json"; authHeaders['Content-Type'] = "application/json";
final peersJsonData = peers.map((e) => e.toJson()).toList(); final peersJsonData = peers.map((e) => e.toJson()).toList();
final body = jsonEncode({ final body = jsonEncode({

View File

@ -59,7 +59,7 @@ class GroupModel {
if (gFFI.userModel.isAdmin.isFalse) if (gFFI.userModel.isAdmin.isFalse)
'grp': gFFI.userModel.groupName.value, 'grp': gFFI.userModel.groupName.value,
}); });
final resp = await http.get(uri, headers: await getHttpHeaders()); final resp = await http.get(uri, headers: getHttpHeaders());
if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") { if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") {
Map<String, dynamic> json = jsonDecode(resp.body); Map<String, dynamic> json = jsonDecode(resp.body);
if (json.containsKey('error')) { if (json.containsKey('error')) {
@ -110,7 +110,7 @@ class GroupModel {
'grp': gFFI.userModel.groupName.value, 'grp': gFFI.userModel.groupName.value,
'target_user': username 'target_user': username
}); });
final resp = await http.get(uri, headers: await getHttpHeaders()); final resp = await http.get(uri, headers: getHttpHeaders());
if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") { if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") {
Map<String, dynamic> json = jsonDecode(resp.body); Map<String, dynamic> json = jsonDecode(resp.body);
if (json.containsKey('error')) { if (json.containsKey('error')) {

View File

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
import 'package:flutter_hbb/common/widgets/peer_tab_page.dart'; import 'package:flutter_hbb/common/widgets/peer_tab_page.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
@ -45,7 +46,9 @@ class UserModel {
if (error != null) { if (error != null) {
throw error; throw error;
} }
await _parseUserInfo(data);
final user = UserPayload.fromJson(data);
await _parseAndUpdateUser(user);
} catch (e) { } catch (e) {
print('Failed to refreshCurrentUser: $e'); print('Failed to refreshCurrentUser: $e');
} finally { } finally {
@ -55,7 +58,6 @@ class UserModel {
Future<void> reset() async { Future<void> reset() async {
await bind.mainSetLocalOption(key: 'access_token', value: ''); await bind.mainSetLocalOption(key: 'access_token', value: '');
await bind.mainSetLocalOption(key: 'user_info', value: '');
await gFFI.abModel.reset(); await gFFI.abModel.reset();
await gFFI.groupModel.reset(); await gFFI.groupModel.reset();
userName.value = ''; userName.value = '';
@ -63,11 +65,10 @@ class UserModel {
statePeerTab.check(); statePeerTab.check();
} }
Future<void> _parseUserInfo(dynamic userinfo) async { Future<void> _parseAndUpdateUser(UserPayload user) async {
bind.mainSetLocalOption(key: 'user_info', value: jsonEncode(userinfo)); userName.value = user.name;
userName.value = userinfo['name'] ?? ''; groupName.value = user.grp;
groupName.value = userinfo['grp'] ?? ''; isAdmin.value = user.isAdmin;
isAdmin.value = userinfo['is_admin'] == true;
} }
Future<void> _updateOtherModels() async { Future<void> _updateOtherModels() async {
@ -85,7 +86,7 @@ class UserModel {
'id': await bind.mainGetMyId(), 'id': await bind.mainGetMyId(),
'uuid': await bind.mainGetUuid(), 'uuid': await bind.mainGetUuid(),
}, },
headers: await getHttpHeaders()) headers: getHttpHeaders())
.timeout(Duration(seconds: 2)); .timeout(Duration(seconds: 2));
} catch (e) { } catch (e) {
print("request /api/logout failed: err=$e"); print("request /api/logout failed: err=$e");
@ -95,26 +96,37 @@ class UserModel {
} }
} }
Future<Map<String, dynamic>> login(String userName, String pass) async { /// throw [RequestException]
Future<LoginResponse> login(LoginRequest loginRequest) async {
final url = await bind.mainGetApiServer(); final url = await bind.mainGetApiServer();
try {
final resp = await http.post(Uri.parse('$url/api/login'), final resp = await http.post(Uri.parse('$url/api/login'),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
body: jsonEncode({ body: jsonEncode(loginRequest.toJson()));
'username': userName,
'password': pass, final Map<String, dynamic> body;
'id': await bind.mainGetMyId(), try {
'uuid': await bind.mainGetUuid() body = jsonDecode(resp.body);
})); } catch (e) {
final body = jsonDecode(resp.body); print("jsonDecode resp body failed: ${e.toString()}");
bind.mainSetLocalOption( rethrow;
key: 'access_token', value: body['access_token'] ?? ''); }
await _parseUserInfo(body['user']);
return body; if (resp.statusCode != 200) {
} catch (err) { throw RequestException(resp.statusCode, body['error'] ?? '');
return {'error': '$err'}; }
} finally {
await _updateOtherModels(); final LoginResponse loginResponse;
} try {
loginResponse = LoginResponse.fromJson(body);
} catch (e) {
print("jsonDecode LoginResponse failed: ${e.toString()}");
rethrow;
}
if (loginResponse.user != null) {
await _parseAndUpdateUser(loginResponse.user!);
}
return loginResponse;
} }
} }

View File

@ -210,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Always connect via relay", "Connecta sempre a través de relay"), ("Always connect via relay", "Connecta sempre a través de relay"),
("whitelist_tip", ""), ("whitelist_tip", ""),
("Login", "Inicia sessió"), ("Login", "Inicia sessió"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Sortir"), ("Logout", "Sortir"),
("Tags", ""), ("Tags", ""),
("Search ID", "Cerca ID"), ("Search ID", "Cerca ID"),

View File

@ -210,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Always connect via relay", "强制走中继连接"), ("Always connect via relay", "强制走中继连接"),
("whitelist_tip", "只有白名单里的ip才能访问我"), ("whitelist_tip", "只有白名单里的ip才能访问我"),
("Login", "登录"), ("Login", "登录"),
("Verify", "验证"),
("Remember me", "记住我"),
("Trust this device", "信任此设备"),
("Verification code", "验证码"),
("verification_tip", "检测到新设备登录,已向注册邮箱发送了登录验证码,输入验证码继续登录"),
("Logout", "登出"), ("Logout", "登出"),
("Tags", "标签"), ("Tags", "标签"),
("Search ID", "查找ID"), ("Search ID", "查找ID"),
@ -221,7 +226,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Network error", "网络错误"), ("Network error", "网络错误"),
("Username missed", "用户名没有填写"), ("Username missed", "用户名没有填写"),
("Password missed", "密码没有填写"), ("Password missed", "密码没有填写"),
("Wrong credentials", "用户名或者密码错误"), ("Wrong credentials", "提供的登入信息错误"),
("Edit Tag", "修改标签"), ("Edit Tag", "修改标签"),
("Unremember Password", "忘掉密码"), ("Unremember Password", "忘掉密码"),
("Favorites", "收藏"), ("Favorites", "收藏"),
@ -273,7 +278,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Do you accept?", "是否接受?"), ("Do you accept?", "是否接受?"),
("Open System Setting", "打开系统设置"), ("Open System Setting", "打开系统设置"),
("How to get Android input permission?", "如何获取安卓的输入权限?"), ("How to get Android input permission?", "如何获取安卓的输入权限?"),
("android_input_permission_tip1", "為了讓遠程設備通過鼠標或者觸屏控制您的安卓設備,你需要允許 RustDesk 使用\"無障礙\"服務"), ("android_input_permission_tip1", "为了让远程设备通过鼠标或触屏控制您的安卓设备你需要允許RustDesk使用\"无障碍\"服务"),
("android_input_permission_tip2", "请在接下来的系统设置页面里,找到并进入 [已安装的服务] 页面,将 [RustDesk Input] 服务开启。"), ("android_input_permission_tip2", "请在接下来的系统设置页面里,找到并进入 [已安装的服务] 页面,将 [RustDesk Input] 服务开启。"),
("android_new_connection_tip", "收到新的连接控制请求,对方想要控制你当前的设备。"), ("android_new_connection_tip", "收到新的连接控制请求,对方想要控制你当前的设备。"),
("android_service_will_start_tip", "开启录屏权限将自动开启服务,允许其他设备向此设备请求建立连接。"), ("android_service_will_start_tip", "开启录屏权限将自动开启服务,允许其他设备向此设备请求建立连接。"),

View File

@ -210,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Always connect via relay", "Vždy se spojovat prostřednictvím brány pro předávání (relay)"), ("Always connect via relay", "Vždy se spojovat prostřednictvím brány pro předávání (relay)"),
("whitelist_tip", "Přístup je umožněn pouze z IP adres, nacházejících se na seznamu povolených"), ("whitelist_tip", "Přístup je umožněn pouze z IP adres, nacházejících se na seznamu povolených"),
("Login", "Přihlásit se"), ("Login", "Přihlásit se"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Odhlásit se"), ("Logout", "Odhlásit se"),
("Tags", "Štítky"), ("Tags", "Štítky"),
("Search ID", "Hledat identifikátor"), ("Search ID", "Hledat identifikátor"),

View File

@ -210,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Always connect via relay", "Forbindelse via relæ-server"), ("Always connect via relay", "Forbindelse via relæ-server"),
("whitelist_tip", "Kun IP'er på udgivelseslisten kan få adgang til mig"), ("whitelist_tip", "Kun IP'er på udgivelseslisten kan få adgang til mig"),
("Login", "Login"), ("Login", "Login"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "logger af"), ("Logout", "logger af"),
("Tags", "Nøgleord"), ("Tags", "Nøgleord"),
("Search ID", "Søg ID"), ("Search ID", "Søg ID"),

View File

@ -210,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Always connect via relay", "Immer über Relay-Server verbinden"), ("Always connect via relay", "Immer über Relay-Server verbinden"),
("whitelist_tip", "Nur IPs auf der Whitelist können zugreifen."), ("whitelist_tip", "Nur IPs auf der Whitelist können zugreifen."),
("Login", "Anmelden"), ("Login", "Anmelden"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Abmelden"), ("Logout", "Abmelden"),
("Tags", "Schlagworte"), ("Tags", "Schlagworte"),
("Search ID", "Suche ID"), ("Search ID", "Suche ID"),

View File

@ -36,6 +36,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("hide_cm_tip", "Allow hiding only if accepting sessions via password and using permanent password"), ("hide_cm_tip", "Allow hiding only if accepting sessions via password and using permanent password"),
("wayland_experiment_tip", "Wayland support is in experimental stage, please use X11 if you require unattended access."), ("wayland_experiment_tip", "Wayland support is in experimental stage, please use X11 if you require unattended access."),
("Slogan_tip", "Made with heart in this chaotic world!"), ("Slogan_tip", "Made with heart in this chaotic world!"),
("verification_tip", "A new device has been detected, and a verification code has been sent to the registered email address, enter the verification code to continue logging in."),
("software_render_tip", "If you have an Nvidia graphics card and the remote window closes immediately after connecting, installing the nouveau driver and choosing to use software rendering may help. A software restart is required."), ("software_render_tip", "If you have an Nvidia graphics card and the remote window closes immediately after connecting, installing the nouveau driver and choosing to use software rendering may help. A software restart is required."),
("config_input", "In order to control remote desktop with keyboard, you need to grant RustDesk \"Input Monitoring\" permissions."), ("config_input", "In order to control remote desktop with keyboard, you need to grant RustDesk \"Input Monitoring\" permissions."),
].iter().cloned().collect(); ].iter().cloned().collect();

View File

@ -210,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Always connect via relay", "Ĉiam konekti per relajso"), ("Always connect via relay", "Ĉiam konekti per relajso"),
("whitelist_tip", "Nur la IP en la blanka listo povas kontroli mian komputilon"), ("whitelist_tip", "Nur la IP en la blanka listo povas kontroli mian komputilon"),
("Login", "Konekti"), ("Login", "Konekti"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Malkonekti"), ("Logout", "Malkonekti"),
("Tags", "Etikedi"), ("Tags", "Etikedi"),
("Search ID", "Serĉi ID"), ("Search ID", "Serĉi ID"),

View File

@ -210,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Always connect via relay", "Conéctese siempre a través de relay"), ("Always connect via relay", "Conéctese siempre a través de relay"),
("whitelist_tip", "Solo las direcciones IP autorizadas pueden conectarse a este escritorio"), ("whitelist_tip", "Solo las direcciones IP autorizadas pueden conectarse a este escritorio"),
("Login", "Iniciar sesión"), ("Login", "Iniciar sesión"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Salir"), ("Logout", "Salir"),
("Tags", "Tags"), ("Tags", "Tags"),
("Search ID", "Buscar ID"), ("Search ID", "Buscar ID"),

View File

@ -210,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Always connect via relay", "برای اتصال استفاده شود Relay از"), ("Always connect via relay", "برای اتصال استفاده شود Relay از"),
("whitelist_tip", "های مجاز می توانند به این دسکتاپ متصل شوند IP فقط"), ("whitelist_tip", "های مجاز می توانند به این دسکتاپ متصل شوند IP فقط"),
("Login", "ورود"), ("Login", "ورود"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "خروج"), ("Logout", "خروج"),
("Tags", "برچسب ها"), ("Tags", "برچسب ها"),
("Search ID", "جستجوی شناسه"), ("Search ID", "جستجوی شناسه"),

View File

@ -210,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Always connect via relay", "Forcer la connexion relais"), ("Always connect via relay", "Forcer la connexion relais"),
("whitelist_tip", "Seul l'IP dans la liste blanche peut accéder à mon appareil"), ("whitelist_tip", "Seul l'IP dans la liste blanche peut accéder à mon appareil"),
("Login", "Connexion"), ("Login", "Connexion"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Déconnexion"), ("Logout", "Déconnexion"),
("Tags", "Étiqueter"), ("Tags", "Étiqueter"),
("Search ID", "Rechercher un ID"), ("Search ID", "Rechercher un ID"),

View File

@ -210,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Always connect via relay", "Σύνδεση πάντα μέσω αναμετάδοσης"), ("Always connect via relay", "Σύνδεση πάντα μέσω αναμετάδοσης"),
("whitelist_tip", "Μόνο οι IP της λίστας επιτρεπόμενων έχουν πρόσβαση"), ("whitelist_tip", "Μόνο οι IP της λίστας επιτρεπόμενων έχουν πρόσβαση"),
("Login", "Σύνδεση"), ("Login", "Σύνδεση"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Αποσύνδεση"), ("Logout", "Αποσύνδεση"),
("Tags", "Ετικέτες"), ("Tags", "Ετικέτες"),
("Search ID", "Αναζήτηση ID"), ("Search ID", "Αναζήτηση ID"),

View File

@ -210,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Always connect via relay", "Mindig közvetítőn keresztüli csatlakozás"), ("Always connect via relay", "Mindig közvetítőn keresztüli csatlakozás"),
("whitelist_tip", "Csak az engedélyezési listán szereplő címek csatlakozhatnak"), ("whitelist_tip", "Csak az engedélyezési listán szereplő címek csatlakozhatnak"),
("Login", "Belépés"), ("Login", "Belépés"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Kilépés"), ("Logout", "Kilépés"),
("Tags", "Tagok"), ("Tags", "Tagok"),
("Search ID", "Azonosító keresése..."), ("Search ID", "Azonosító keresése..."),

View File

@ -210,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Always connect via relay", "Selalu terhubung melalui relai"), ("Always connect via relay", "Selalu terhubung melalui relai"),
("whitelist_tip", "Hanya whitelisted IP yang dapat mengakses saya"), ("whitelist_tip", "Hanya whitelisted IP yang dapat mengakses saya"),
("Login", "Masuk"), ("Login", "Masuk"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Keluar"), ("Logout", "Keluar"),
("Tags", "Tag"), ("Tags", "Tag"),
("Search ID", "Cari ID"), ("Search ID", "Cari ID"),

View File

@ -210,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Always connect via relay", "Connetti sempre tramite relay"), ("Always connect via relay", "Connetti sempre tramite relay"),
("whitelist_tip", "Solo gli indirizzi IP autorizzati possono connettersi a questo desktop"), ("whitelist_tip", "Solo gli indirizzi IP autorizzati possono connettersi a questo desktop"),
("Login", "Accedi"), ("Login", "Accedi"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Esci"), ("Logout", "Esci"),
("Tags", "Tag"), ("Tags", "Tag"),
("Search ID", "Cerca ID"), ("Search ID", "Cerca ID"),

View File

@ -210,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Always connect via relay", "常に中継サーバー経由で接続"), ("Always connect via relay", "常に中継サーバー経由で接続"),
("whitelist_tip", "ホワイトリストに登録されたIPからのみ接続を許可します"), ("whitelist_tip", "ホワイトリストに登録されたIPからのみ接続を許可します"),
("Login", "ログイン"), ("Login", "ログイン"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "ログアウト"), ("Logout", "ログアウト"),
("Tags", "タグ"), ("Tags", "タグ"),
("Search ID", "IDを検索"), ("Search ID", "IDを検索"),

View File

@ -210,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Always connect via relay", "항상 relay를 통해 접속하기"), ("Always connect via relay", "항상 relay를 통해 접속하기"),
("whitelist_tip", "화이트리스트에 있는 IP만 현 데스크탑에 접속 가능합니다"), ("whitelist_tip", "화이트리스트에 있는 IP만 현 데스크탑에 접속 가능합니다"),
("Login", "로그인"), ("Login", "로그인"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "로그아웃"), ("Logout", "로그아웃"),
("Tags", "태그"), ("Tags", "태그"),
("Search ID", "ID 검색"), ("Search ID", "ID 검색"),

View File

@ -210,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Always connect via relay", "Әрқашан да релай сербері арқылы қосылу"), ("Always connect via relay", "Әрқашан да релай сербері арқылы қосылу"),
("whitelist_tip", "Маған тек ақ-тізімделген IP қол жеткізе алады"), ("whitelist_tip", "Маған тек ақ-тізімделген IP қол жеткізе алады"),
("Login", "Кіру"), ("Login", "Кіру"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Шығу"), ("Logout", "Шығу"),
("Tags", "Тақтар"), ("Tags", "Тақтар"),
("Search ID", "ID Іздеу"), ("Search ID", "ID Іздеу"),

View File

@ -210,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Always connect via relay", "Zawsze łącz pośrednio"), ("Always connect via relay", "Zawsze łącz pośrednio"),
("whitelist_tip", "Zezwlaj na łączenie z tym komputerem tylko z adresów IP znajdujących się na białej liście"), ("whitelist_tip", "Zezwlaj na łączenie z tym komputerem tylko z adresów IP znajdujących się na białej liście"),
("Login", "Zaloguj"), ("Login", "Zaloguj"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Wyloguj"), ("Logout", "Wyloguj"),
("Tags", "Tagi"), ("Tags", "Tagi"),
("Search ID", "Szukaj ID"), ("Search ID", "Szukaj ID"),

View File

@ -210,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Always connect via relay", "Sempre conectar via relay"), ("Always connect via relay", "Sempre conectar via relay"),
("whitelist_tip", "Somente IPs na whitelist podem me acessar"), ("whitelist_tip", "Somente IPs na whitelist podem me acessar"),
("Login", "Login"), ("Login", "Login"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Sair"), ("Logout", "Sair"),
("Tags", "Tags"), ("Tags", "Tags"),
("Search ID", "Procurar ID"), ("Search ID", "Procurar ID"),

View File

@ -210,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Always connect via relay", "Sempre conectar via relay"), ("Always connect via relay", "Sempre conectar via relay"),
("whitelist_tip", "Somente IPs confiáveis podem me acessar"), ("whitelist_tip", "Somente IPs confiáveis podem me acessar"),
("Login", "Login"), ("Login", "Login"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Sair"), ("Logout", "Sair"),
("Tags", "Tags"), ("Tags", "Tags"),
("Search ID", "Pesquisar ID"), ("Search ID", "Pesquisar ID"),

View File

@ -210,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Always connect via relay", "Всегда подключаться через ретрансляционный сервер"), ("Always connect via relay", "Всегда подключаться через ретрансляционный сервер"),
("whitelist_tip", "Только IP-адреса из белого списка могут получить доступ ко мне"), ("whitelist_tip", "Только IP-адреса из белого списка могут получить доступ ко мне"),
("Login", "Войти"), ("Login", "Войти"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Выйти"), ("Logout", "Выйти"),
("Tags", "Метки"), ("Tags", "Метки"),
("Search ID", "Поиск по ID"), ("Search ID", "Поиск по ID"),

View File

@ -210,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Always connect via relay", "Vždy pripájať cez prepájací server"), ("Always connect via relay", "Vždy pripájať cez prepájací server"),
("whitelist_tip", "Len vymenované IP adresy majú oprávnenie sa pripojiť k vzdialenej správe"), ("whitelist_tip", "Len vymenované IP adresy majú oprávnenie sa pripojiť k vzdialenej správe"),
("Login", "Prihlásenie"), ("Login", "Prihlásenie"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Odhlásenie"), ("Logout", "Odhlásenie"),
("Tags", "Štítky"), ("Tags", "Štítky"),
("Search ID", "Hľadať ID"), ("Search ID", "Hľadať ID"),

View File

@ -210,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Always connect via relay", "Vedno poveži preko posrednika"), ("Always connect via relay", "Vedno poveži preko posrednika"),
("whitelist_tip", "Dostop je možen samo iz dovoljenih IPjev"), ("whitelist_tip", "Dostop je možen samo iz dovoljenih IPjev"),
("Login", "Prijavi"), ("Login", "Prijavi"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Odjavi"), ("Logout", "Odjavi"),
("Tags", "Oznake"), ("Tags", "Oznake"),
("Search ID", "Išči ID"), ("Search ID", "Išči ID"),

View File

@ -210,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Always connect via relay", "Gjithmonë lidheni me transmetues"), ("Always connect via relay", "Gjithmonë lidheni me transmetues"),
("whitelist_tip", "Vetëm IP e listës së bardhë mund të më aksesoj."), ("whitelist_tip", "Vetëm IP e listës së bardhë mund të më aksesoj."),
("Login", "Hyrje"), ("Login", "Hyrje"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Dalje"), ("Logout", "Dalje"),
("Tags", "Tage"), ("Tags", "Tage"),
("Search ID", "Kerko ID"), ("Search ID", "Kerko ID"),

View File

@ -210,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Always connect via relay", "Uvek se spoj preko posrednika"), ("Always connect via relay", "Uvek se spoj preko posrednika"),
("whitelist_tip", "Samo dozvoljene IP mi mogu pristupiti"), ("whitelist_tip", "Samo dozvoljene IP mi mogu pristupiti"),
("Login", "Prijava"), ("Login", "Prijava"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Odjava"), ("Logout", "Odjava"),
("Tags", "Oznake"), ("Tags", "Oznake"),
("Search ID", "Traži ID"), ("Search ID", "Traži ID"),

View File

@ -210,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Always connect via relay", "Anslut alltid via relay"), ("Always connect via relay", "Anslut alltid via relay"),
("whitelist_tip", "Bara vitlistade IPs kan koppla upp till mig"), ("whitelist_tip", "Bara vitlistade IPs kan koppla upp till mig"),
("Login", "Logga in"), ("Login", "Logga in"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Logga ut"), ("Logout", "Logga ut"),
("Tags", "Taggar"), ("Tags", "Taggar"),
("Search ID", "Sök ID"), ("Search ID", "Sök ID"),

View File

@ -210,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Always connect via relay", ""), ("Always connect via relay", ""),
("whitelist_tip", ""), ("whitelist_tip", ""),
("Login", ""), ("Login", ""),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", ""), ("Logout", ""),
("Tags", ""), ("Tags", ""),
("Search ID", ""), ("Search ID", ""),

View File

@ -210,6 +210,11 @@ lazy_static::lazy_static! {
("Always connect via relay", "เชื่อมต่อผ่านรีเลย์เสมอ"), ("Always connect via relay", "เชื่อมต่อผ่านรีเลย์เสมอ"),
("whitelist_tip", "อนุญาตเฉพาะการเชื่อมต่อจาก IP ที่ไวท์ลิสต์"), ("whitelist_tip", "อนุญาตเฉพาะการเชื่อมต่อจาก IP ที่ไวท์ลิสต์"),
("Login", "เข้าสู่ระบบ"), ("Login", "เข้าสู่ระบบ"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "ออกจากระบบ"), ("Logout", "ออกจากระบบ"),
("Tags", "แท็ก"), ("Tags", "แท็ก"),
("Search ID", "ค้นหา ID"), ("Search ID", "ค้นหา ID"),
@ -280,6 +285,7 @@ lazy_static::lazy_static! {
("android_stop_service_tip", "การปิดการใช้งานเซอร์วิสจะปิดการเชื่อมต่อทั้งหมดโดยอัตโนมัติ"), ("android_stop_service_tip", "การปิดการใช้งานเซอร์วิสจะปิดการเชื่อมต่อทั้งหมดโดยอัตโนมัติ"),
("android_version_audio_tip", "เวอร์ชั่นแอนดรอยด์ปัจจุบันของคุณไม่รองรับการบันทึกข้อมูลเสียง กรุณาอัปเกรดเป็นแอนดรอยด์เวอร์ชั่น 10 หรือสูงกว่า"), ("android_version_audio_tip", "เวอร์ชั่นแอนดรอยด์ปัจจุบันของคุณไม่รองรับการบันทึกข้อมูลเสียง กรุณาอัปเกรดเป็นแอนดรอยด์เวอร์ชั่น 10 หรือสูงกว่า"),
("android_start_service_tip", "แตะ [เริ่มต้นใช้งานเซอร์วิส] หรือเปิดสิทธิ์ [การบันทึกหน้าจอ] เพื่อเริ่มเซอร์วิสการแชร์หน้าจอ"), ("android_start_service_tip", "แตะ [เริ่มต้นใช้งานเซอร์วิส] หรือเปิดสิทธิ์ [การบันทึกหน้าจอ] เพื่อเริ่มเซอร์วิสการแชร์หน้าจอ"),
("Account", "บัญชี"),
("Overwrite", "เขียนทับ"), ("Overwrite", "เขียนทับ"),
("This file exists, skip or overwrite this file?", "พบไฟล์ที่มีอยู่แล้ว ต้องการเขียนทับหรือไม่?"), ("This file exists, skip or overwrite this file?", "พบไฟล์ที่มีอยู่แล้ว ต้องการเขียนทับหรือไม่?"),
("Quit", "ออก"), ("Quit", "ออก"),
@ -333,7 +339,6 @@ lazy_static::lazy_static! {
("Scale adaptive", "ขนาดยืดหยุ่น"), ("Scale adaptive", "ขนาดยืดหยุ่น"),
("General", "ทั่วไป"), ("General", "ทั่วไป"),
("Security", "ความปลอดภัย"), ("Security", "ความปลอดภัย"),
("Account", "บัญชี"),
("Theme", "ธีม"), ("Theme", "ธีม"),
("Dark Theme", "ธีมมืด"), ("Dark Theme", "ธีมมืด"),
("Dark", "มืด"), ("Dark", "มืด"),
@ -410,4 +415,3 @@ lazy_static::lazy_static! {
("config_input", ""), ("config_input", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -210,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Always connect via relay", "Always connect via relay"), ("Always connect via relay", "Always connect via relay"),
("whitelist_tip", "Bu masaüstüne yalnızca yetkili IP adresleri bağlanabilir"), ("whitelist_tip", "Bu masaüstüne yalnızca yetkili IP adresleri bağlanabilir"),
("Login", "Giriş yap"), ("Login", "Giriş yap"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Çıkış yap"), ("Logout", "Çıkış yap"),
("Tags", "Etiketler"), ("Tags", "Etiketler"),
("Search ID", "ID Arama"), ("Search ID", "ID Arama"),

View File

@ -210,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Always connect via relay", "一律透過轉送連線"), ("Always connect via relay", "一律透過轉送連線"),
("whitelist_tip", "只有白名單中的 IP 可以存取"), ("whitelist_tip", "只有白名單中的 IP 可以存取"),
("Login", "登入"), ("Login", "登入"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "登出"), ("Logout", "登出"),
("Tags", "標籤"), ("Tags", "標籤"),
("Search ID", "搜尋 ID"), ("Search ID", "搜尋 ID"),

View File

@ -210,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Always connect via relay", "Завжди підключатися через ретрансляційний сервер"), ("Always connect via relay", "Завжди підключатися через ретрансляційний сервер"),
("whitelist_tip", "Тільки IP-адреси з білого списку можуть отримати доступ до мене"), ("whitelist_tip", "Тільки IP-адреси з білого списку можуть отримати доступ до мене"),
("Login", "Увійти"), ("Login", "Увійти"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Вийти"), ("Logout", "Вийти"),
("Tags", "Ключові слова"), ("Tags", "Ключові слова"),
("Search ID", "Пошук за ID"), ("Search ID", "Пошук за ID"),

View File

@ -210,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Always connect via relay", "Luôn kết nối qua relay"), ("Always connect via relay", "Luôn kết nối qua relay"),
("whitelist_tip", "Chỉ có những IP đựoc cho phép mới có thể truy cập"), ("whitelist_tip", "Chỉ có những IP đựoc cho phép mới có thể truy cập"),
("Login", "Đăng nhập"), ("Login", "Đăng nhập"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Đăng xuất"), ("Logout", "Đăng xuất"),
("Tags", "Tags"), ("Tags", "Tags"),
("Search ID", "Tìm ID"), ("Search ID", "Tìm ID"),