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 {
'Authorization': 'Bearer ${bind.mainGetLocalOption(key: 'access_token')}'
};

View File

@ -1,12 +1,22 @@
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 {
String name = '';
String email = '';
String note = '';
int? status;
String grp = '';
bool is_admin = false;
bool isAdmin = false;
UserPayload.fromJson(Map<String, dynamic> json)
: name = json['name'] ?? '',
@ -14,7 +24,7 @@ class UserPayload {
note = json['note'] ?? '',
status = json['status'],
grp = json['grp'] ?? '',
is_admin = json['is_admin'] == true;
isAdmin = json['is_admin'] == true;
}
class PeerPayload {
@ -37,3 +47,73 @@ class PeerPayload {
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/peers_view.dart';
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
import 'package:flutter_hbb/desktop/widgets/login.dart';
import '../../consts.dart';
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
import 'package:get/get.dart';
import '../../common.dart';
import '../../desktop/pages/desktop_home_page.dart';
import '../../mobile/pages/settings_page.dart';
import 'login.dart';
class AddressBook extends StatefulWidget {
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 {
return Obx(() {
if (gFFI.userModel.userName.value.isEmpty) {
return Center(
child: InkWell(
onTap: handleLogin,
onTap: loginDialog,
child: Text(
translate("Login"),
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/desktop/pages/desktop_home_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/server_model.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 '../../common/widgets/dialog.dart';
import '../../common/widgets/login.dart';
const double _kTabWidth = 235;
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 '../../common.dart';
import '../../common/widgets/address_book.dart';
import '../../common/widgets/login.dart';
import '../../common/widgets/peer_tab_page.dart';
import '../../common/widgets/peers_view.dart';
import '../../consts.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
@ -258,7 +257,7 @@ class _WebMenuState extends State<WebMenu> {
}
if (value == 'login') {
if (gFFI.userModel.userName.value.isEmpty) {
showLogin(gFFI.dialogManager);
loginDialog();
} else {
gFFI.userModel.logOut();
}

View File

@ -9,6 +9,7 @@ import 'package:url_launcher/url_launcher.dart';
import '../../common.dart';
import '../../common/widgets/dialog.dart';
import '../../common/widgets/login.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
import '../widgets/dialog.dart';
@ -300,7 +301,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
leading: Icon(Icons.person),
onPressed: (context) {
if (gFFI.userModel.userName.value.isEmpty) {
showLogin(gFFI.dialogManager);
loginDialog();
} else {
gFFI.userModel.logOut();
}
@ -397,7 +398,7 @@ void showServerSettings(OverlayDialogManager dialogManager) async {
void showLanguageSettings(OverlayDialogManager dialogManager) async {
try {
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) {
setLang(v) {
if (lang != v) {
@ -482,77 +483,6 @@ void showAbout(OverlayDialogManager dialogManager) {
}, 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 {
@override
Widget build(BuildContext context) {

View File

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

View File

@ -59,7 +59,7 @@ class GroupModel {
if (gFFI.userModel.isAdmin.isFalse)
'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") {
Map<String, dynamic> json = jsonDecode(resp.body);
if (json.containsKey('error')) {
@ -110,7 +110,7 @@ class GroupModel {
'grp': gFFI.userModel.groupName.value,
'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") {
Map<String, dynamic> json = jsonDecode(resp.body);
if (json.containsKey('error')) {

View File

@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
import 'package:flutter_hbb/common/widgets/peer_tab_page.dart';
import 'package:get/get.dart';
import 'package:http/http.dart' as http;
@ -45,7 +46,9 @@ class UserModel {
if (error != null) {
throw error;
}
await _parseUserInfo(data);
final user = UserPayload.fromJson(data);
await _parseAndUpdateUser(user);
} catch (e) {
print('Failed to refreshCurrentUser: $e');
} finally {
@ -55,7 +58,6 @@ class UserModel {
Future<void> reset() async {
await bind.mainSetLocalOption(key: 'access_token', value: '');
await bind.mainSetLocalOption(key: 'user_info', value: '');
await gFFI.abModel.reset();
await gFFI.groupModel.reset();
userName.value = '';
@ -63,11 +65,10 @@ class UserModel {
statePeerTab.check();
}
Future<void> _parseUserInfo(dynamic userinfo) async {
bind.mainSetLocalOption(key: 'user_info', value: jsonEncode(userinfo));
userName.value = userinfo['name'] ?? '';
groupName.value = userinfo['grp'] ?? '';
isAdmin.value = userinfo['is_admin'] == true;
Future<void> _parseAndUpdateUser(UserPayload user) async {
userName.value = user.name;
groupName.value = user.grp;
isAdmin.value = user.isAdmin;
}
Future<void> _updateOtherModels() async {
@ -85,7 +86,7 @@ class UserModel {
'id': await bind.mainGetMyId(),
'uuid': await bind.mainGetUuid(),
},
headers: await getHttpHeaders())
headers: getHttpHeaders())
.timeout(Duration(seconds: 2));
} catch (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();
try {
final resp = await http.post(Uri.parse('$url/api/login'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'username': userName,
'password': pass,
'id': await bind.mainGetMyId(),
'uuid': await bind.mainGetUuid()
}));
final body = jsonDecode(resp.body);
bind.mainSetLocalOption(
key: 'access_token', value: body['access_token'] ?? '');
await _parseUserInfo(body['user']);
return body;
} catch (err) {
return {'error': '$err'};
} finally {
await _updateOtherModels();
}
body: jsonEncode(loginRequest.toJson()));
final Map<String, dynamic> body;
try {
body = jsonDecode(resp.body);
} catch (e) {
print("jsonDecode resp body failed: ${e.toString()}");
rethrow;
}
if (resp.statusCode != 200) {
throw RequestException(resp.statusCode, body['error'] ?? '');
}
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"),
("whitelist_tip", ""),
("Login", "Inicia sessió"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Sortir"),
("Tags", ""),
("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", "强制走中继连接"),
("whitelist_tip", "只有白名单里的ip才能访问我"),
("Login", "登录"),
("Verify", "验证"),
("Remember me", "记住我"),
("Trust this device", "信任此设备"),
("Verification code", "验证码"),
("verification_tip", "检测到新设备登录,已向注册邮箱发送了登录验证码,输入验证码继续登录"),
("Logout", "登出"),
("Tags", "标签"),
("Search ID", "查找ID"),
@ -221,7 +226,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Network error", "网络错误"),
("Username missed", "用户名没有填写"),
("Password missed", "密码没有填写"),
("Wrong credentials", "用户名或者密码错误"),
("Wrong credentials", "提供的登入信息错误"),
("Edit Tag", "修改标签"),
("Unremember Password", "忘掉密码"),
("Favorites", "收藏"),
@ -273,7 +278,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Do you accept?", "是否接受?"),
("Open System Setting", "打开系统设置"),
("How to get Android input permission?", "如何获取安卓的输入权限?"),
("android_input_permission_tip1", "為了讓遠程設備通過鼠標或者觸屏控制您的安卓設備,你需要允許 RustDesk 使用\"無障礙\"服務"),
("android_input_permission_tip1", "为了让远程设备通过鼠标或触屏控制您的安卓设备你需要允許RustDesk使用\"无障碍\"服务"),
("android_input_permission_tip2", "请在接下来的系统设置页面里,找到并进入 [已安装的服务] 页面,将 [RustDesk Input] 服务开启。"),
("android_new_connection_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)"),
("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"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Odhlásit se"),
("Tags", "Štítky"),
("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"),
("whitelist_tip", "Kun IP'er på udgivelseslisten kan få adgang til mig"),
("Login", "Login"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "logger af"),
("Tags", "Nøgleord"),
("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"),
("whitelist_tip", "Nur IPs auf der Whitelist können zugreifen."),
("Login", "Anmelden"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Abmelden"),
("Tags", "Schlagworte"),
("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"),
("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!"),
("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."),
("config_input", "In order to control remote desktop with keyboard, you need to grant RustDesk \"Input Monitoring\" permissions."),
].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"),
("whitelist_tip", "Nur la IP en la blanka listo povas kontroli mian komputilon"),
("Login", "Konekti"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Malkonekti"),
("Tags", "Etikedi"),
("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"),
("whitelist_tip", "Solo las direcciones IP autorizadas pueden conectarse a este escritorio"),
("Login", "Iniciar sesión"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Salir"),
("Tags", "Tags"),
("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 از"),
("whitelist_tip", "های مجاز می توانند به این دسکتاپ متصل شوند IP فقط"),
("Login", "ورود"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "خروج"),
("Tags", "برچسب ها"),
("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"),
("whitelist_tip", "Seul l'IP dans la liste blanche peut accéder à mon appareil"),
("Login", "Connexion"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Déconnexion"),
("Tags", "Étiqueter"),
("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", "Σύνδεση πάντα μέσω αναμετάδοσης"),
("whitelist_tip", "Μόνο οι IP της λίστας επιτρεπόμενων έχουν πρόσβαση"),
("Login", "Σύνδεση"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Αποσύνδεση"),
("Tags", "Ετικέτες"),
("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"),
("whitelist_tip", "Csak az engedélyezési listán szereplő címek csatlakozhatnak"),
("Login", "Belépés"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Kilépés"),
("Tags", "Tagok"),
("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"),
("whitelist_tip", "Hanya whitelisted IP yang dapat mengakses saya"),
("Login", "Masuk"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Keluar"),
("Tags", "Tag"),
("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"),
("whitelist_tip", "Solo gli indirizzi IP autorizzati possono connettersi a questo desktop"),
("Login", "Accedi"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Esci"),
("Tags", "Tag"),
("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", "常に中継サーバー経由で接続"),
("whitelist_tip", "ホワイトリストに登録されたIPからのみ接続を許可します"),
("Login", "ログイン"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "ログアウト"),
("Tags", "タグ"),
("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를 통해 접속하기"),
("whitelist_tip", "화이트리스트에 있는 IP만 현 데스크탑에 접속 가능합니다"),
("Login", "로그인"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "로그아웃"),
("Tags", "태그"),
("Search ID", "ID 검색"),

View File

@ -210,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Always connect via relay", "Әрқашан да релай сербері арқылы қосылу"),
("whitelist_tip", "Маған тек ақ-тізімделген IP қол жеткізе алады"),
("Login", "Кіру"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Шығу"),
("Tags", "Тақтар"),
("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"),
("whitelist_tip", "Zezwlaj na łączenie z tym komputerem tylko z adresów IP znajdujących się na białej liście"),
("Login", "Zaloguj"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Wyloguj"),
("Tags", "Tagi"),
("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"),
("whitelist_tip", "Somente IPs na whitelist podem me acessar"),
("Login", "Login"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Sair"),
("Tags", "Tags"),
("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"),
("whitelist_tip", "Somente IPs confiáveis podem me acessar"),
("Login", "Login"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Sair"),
("Tags", "Tags"),
("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", "Всегда подключаться через ретрансляционный сервер"),
("whitelist_tip", "Только IP-адреса из белого списка могут получить доступ ко мне"),
("Login", "Войти"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Выйти"),
("Tags", "Метки"),
("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"),
("whitelist_tip", "Len vymenované IP adresy majú oprávnenie sa pripojiť k vzdialenej správe"),
("Login", "Prihlásenie"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Odhlásenie"),
("Tags", "Štítky"),
("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"),
("whitelist_tip", "Dostop je možen samo iz dovoljenih IPjev"),
("Login", "Prijavi"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Odjavi"),
("Tags", "Oznake"),
("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"),
("whitelist_tip", "Vetëm IP e listës së bardhë mund të më aksesoj."),
("Login", "Hyrje"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Dalje"),
("Tags", "Tage"),
("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"),
("whitelist_tip", "Samo dozvoljene IP mi mogu pristupiti"),
("Login", "Prijava"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Odjava"),
("Tags", "Oznake"),
("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"),
("whitelist_tip", "Bara vitlistade IPs kan koppla upp till mig"),
("Login", "Logga in"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Logga ut"),
("Tags", "Taggar"),
("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", ""),
("whitelist_tip", ""),
("Login", ""),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", ""),
("Tags", ""),
("Search ID", ""),

View File

@ -210,6 +210,11 @@ lazy_static::lazy_static! {
("Always connect via relay", "เชื่อมต่อผ่านรีเลย์เสมอ"),
("whitelist_tip", "อนุญาตเฉพาะการเชื่อมต่อจาก IP ที่ไวท์ลิสต์"),
("Login", "เข้าสู่ระบบ"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "ออกจากระบบ"),
("Tags", "แท็ก"),
("Search ID", "ค้นหา ID"),
@ -280,6 +285,7 @@ lazy_static::lazy_static! {
("android_stop_service_tip", "การปิดการใช้งานเซอร์วิสจะปิดการเชื่อมต่อทั้งหมดโดยอัตโนมัติ"),
("android_version_audio_tip", "เวอร์ชั่นแอนดรอยด์ปัจจุบันของคุณไม่รองรับการบันทึกข้อมูลเสียง กรุณาอัปเกรดเป็นแอนดรอยด์เวอร์ชั่น 10 หรือสูงกว่า"),
("android_start_service_tip", "แตะ [เริ่มต้นใช้งานเซอร์วิส] หรือเปิดสิทธิ์ [การบันทึกหน้าจอ] เพื่อเริ่มเซอร์วิสการแชร์หน้าจอ"),
("Account", "บัญชี"),
("Overwrite", "เขียนทับ"),
("This file exists, skip or overwrite this file?", "พบไฟล์ที่มีอยู่แล้ว ต้องการเขียนทับหรือไม่?"),
("Quit", "ออก"),
@ -333,7 +339,6 @@ lazy_static::lazy_static! {
("Scale adaptive", "ขนาดยืดหยุ่น"),
("General", "ทั่วไป"),
("Security", "ความปลอดภัย"),
("Account", "บัญชี"),
("Theme", "ธีม"),
("Dark Theme", "ธีมมืด"),
("Dark", "มืด"),
@ -410,4 +415,3 @@ lazy_static::lazy_static! {
("config_input", ""),
].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"),
("whitelist_tip", "Bu masaüstüne yalnızca yetkili IP adresleri bağlanabilir"),
("Login", "Giriş yap"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Çıkış yap"),
("Tags", "Etiketler"),
("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", "一律透過轉送連線"),
("whitelist_tip", "只有白名單中的 IP 可以存取"),
("Login", "登入"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "登出"),
("Tags", "標籤"),
("Search ID", "搜尋 ID"),

View File

@ -210,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Always connect via relay", "Завжди підключатися через ретрансляційний сервер"),
("whitelist_tip", "Тільки IP-адреси з білого списку можуть отримати доступ до мене"),
("Login", "Увійти"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Вийти"),
("Tags", "Ключові слова"),
("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"),
("whitelist_tip", "Chỉ có những IP đựoc cho phép mới có thể truy cập"),
("Login", "Đăng nhập"),
("Verify", ""),
("Remember me", ""),
("Trust this device", ""),
("Verification code", ""),
("verification_tip", ""),
("Logout", "Đăng xuất"),
("Tags", "Tags"),
("Search ID", "Tìm ID"),