Initial commit: RustDesk 1.4.7 macOS desktop port
- Filled empty libs/hbb_common/ submodule (cloned from rustdesk/hbb_common) - Patched Flutter 3.44 / Dart 3.12 compatibility: * flutter/lib/generated_bridge.dart: asTypedList with cast<>, DartPort=Int64 * flutter/lib/common.dart: DialogTheme->DialogThemeData, TabBarTheme->TabBarThemeData * flutter/pubspec.yaml: extended_text 14.0.0->15.0.2, google_fonts override 5.0.0 * flutter/macos/Runner/Configs/Release.xcconfig: EXCLUDED_ARCHS=x86_64 - Build verified: cargo check + cargo build --features flutter + cargo build --release --features flutter - Verified flutter build macos --debug and --release both produce working .app - Verified .dmg installer (27MB arm64) created via hdiutil - Build deps: Xcode 26.5, CocoaPods 1.16.2, Flutter 3.44, VCPKG arm64-osx
This commit is contained in:
@@ -0,0 +1,790 @@
|
||||
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:flutter_hbb/models/user_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import './dialog.dart';
|
||||
|
||||
const kOpSvgList = [
|
||||
'github',
|
||||
'gitlab',
|
||||
'google',
|
||||
'apple',
|
||||
'okta',
|
||||
'facebook',
|
||||
'azure',
|
||||
'auth0',
|
||||
'microsoft'
|
||||
];
|
||||
|
||||
class _IconOP extends StatelessWidget {
|
||||
final String op;
|
||||
final String? icon;
|
||||
final EdgeInsets margin;
|
||||
const _IconOP(
|
||||
{Key? key,
|
||||
required this.op,
|
||||
required this.icon,
|
||||
this.margin = const EdgeInsets.symmetric(horizontal: 4.0)})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final svgFile =
|
||||
kOpSvgList.contains(op.toLowerCase()) ? op.toLowerCase() : 'default';
|
||||
return Container(
|
||||
margin: margin,
|
||||
child: icon == null
|
||||
? SvgPicture.asset(
|
||||
'assets/auth-$svgFile.svg',
|
||||
width: 20,
|
||||
)
|
||||
: SvgPicture.string(
|
||||
icon!,
|
||||
width: 20,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ButtonOP extends StatelessWidget {
|
||||
final String op;
|
||||
final RxString curOP;
|
||||
final String? icon;
|
||||
final Color primaryColor;
|
||||
final double height;
|
||||
final Function() onTap;
|
||||
|
||||
const ButtonOP({
|
||||
Key? key,
|
||||
required this.op,
|
||||
required this.curOP,
|
||||
required this.icon,
|
||||
required this.primaryColor,
|
||||
required this.height,
|
||||
required this.onTap,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final opLabel = {
|
||||
'github': 'GitHub',
|
||||
'gitlab': 'GitLab'
|
||||
}[op.toLowerCase()] ??
|
||||
toCapitalized(op);
|
||||
return Row(children: [
|
||||
Container(
|
||||
height: height,
|
||||
width: 200,
|
||||
child: Obx(() => ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: 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(
|
||||
op: op,
|
||||
icon: icon,
|
||||
margin: EdgeInsets.only(right: 5),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Center(
|
||||
child: Text(translate("Continue with {$opLabel}"))),
|
||||
),
|
||||
),
|
||||
],
|
||||
))),
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
class ConfigOP {
|
||||
final String op;
|
||||
final String? icon;
|
||||
ConfigOP({required this.op, required this.icon});
|
||||
}
|
||||
|
||||
class WidgetOP extends StatefulWidget {
|
||||
final ConfigOP config;
|
||||
final RxString curOP;
|
||||
final Function(Map<String, dynamic>) 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 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 bool urlLaunched = (resultMap['url_launched'] as bool?) ?? false;
|
||||
final authBody = resultMap['auth_body'];
|
||||
if (_stateMsg != stateMsg || _failedMsg != failedMsg) {
|
||||
if (_url.isEmpty && url != null && url.isNotEmpty) {
|
||||
if (!urlLaunched) {
|
||||
launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
|
||||
}
|
||||
_url = url;
|
||||
}
|
||||
if (authBody != null) {
|
||||
_updateTimer?.cancel();
|
||||
widget.curOP.value = '';
|
||||
widget.cbLogin(authBody as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
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,
|
||||
icon: widget.config.icon,
|
||||
primaryColor: str2color(widget.config.op, 0x7f),
|
||||
height: 36,
|
||||
onTap: () async {
|
||||
_resetState();
|
||||
widget.curOP.value = widget.config.op;
|
||||
await bind.mainAccountAuth(op: widget.config.op, rememberMe: true);
|
||||
_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: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (_stateMsg.isNotEmpty && _failedMsg.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: SelectableText(
|
||||
translate(_stateMsg),
|
||||
style: DefaultTextStyle.of(context)
|
||||
.style
|
||||
.copyWith(fontSize: 12),
|
||||
),
|
||||
),
|
||||
if (_failedMsg.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Builder(builder: (context) {
|
||||
final errorColor =
|
||||
Theme.of(context).colorScheme.error;
|
||||
final bgColor = Theme.of(context)
|
||||
.colorScheme
|
||||
.errorContainer
|
||||
.withOpacity(0.3);
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0, vertical: 6.0),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.error_outline,
|
||||
color: errorColor, size: 16),
|
||||
const SizedBox(width: 6),
|
||||
Flexible(
|
||||
child: SelectableText(
|
||||
translate(_failedMsg),
|
||||
style: DefaultTextStyle.of(context)
|
||||
.style
|
||||
.copyWith(
|
||||
fontSize: 13,
|
||||
color: errorColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
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(Map<String, dynamic>) 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 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.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(DialogTextField.kUsernameTitle),
|
||||
controller: username,
|
||||
focusNode: userFocusNode,
|
||||
prefixIcon: DialogTextField.kUsernameIcon,
|
||||
errorText: usernameMsg),
|
||||
PasswordWidget(
|
||||
controller: pass,
|
||||
autoFocus: false,
|
||||
reRequestFocus: true,
|
||||
errorText: passMsg,
|
||||
),
|
||||
// NOT use Offstage to wrap LinearProgressIndicator
|
||||
if (isInProgress) 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,
|
||||
)),
|
||||
),
|
||||
])),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
const kAuthReqTypeOidc = 'oidc/';
|
||||
|
||||
// call this directly
|
||||
Future<bool?> loginDialog() async {
|
||||
var username =
|
||||
TextEditingController(text: UserModel.getLocalUserInfo()?['name'] ?? '');
|
||||
var password = TextEditingController();
|
||||
final userFocusNode = FocusNode()..requestFocus();
|
||||
Timer(Duration(milliseconds: 100), () => userFocusNode..requestFocus());
|
||||
|
||||
String? usernameMsg;
|
||||
String? passwordMsg;
|
||||
var isInProgress = false;
|
||||
final RxString curOP = ''.obs;
|
||||
// Track hover state for the close icon
|
||||
bool isCloseHovered = false;
|
||||
|
||||
final loginOptions = [].obs;
|
||||
Future.delayed(Duration.zero, () async {
|
||||
loginOptions.value = await UserModel.queryOidcLoginOptions();
|
||||
});
|
||||
|
||||
final res = await gFFI.dialogManager.show<bool>((setState, close, context) {
|
||||
username.addListener(() {
|
||||
if (usernameMsg != null) {
|
||||
setState(() => usernameMsg = null);
|
||||
}
|
||||
});
|
||||
|
||||
password.addListener(() {
|
||||
if (passwordMsg != null) {
|
||||
setState(() => passwordMsg = null);
|
||||
}
|
||||
});
|
||||
|
||||
onDialogCancel() {
|
||||
isInProgress = false;
|
||||
close(false);
|
||||
}
|
||||
|
||||
handleLoginResponse(LoginResponse resp, bool storeIfAccessToken,
|
||||
void Function([dynamic])? close) async {
|
||||
switch (resp.type) {
|
||||
case HttpType.kAuthResTypeToken:
|
||||
if (resp.access_token != null) {
|
||||
if (storeIfAccessToken) {
|
||||
await bind.mainSetLocalOption(
|
||||
key: 'access_token', value: resp.access_token!);
|
||||
await bind.mainSetLocalOption(
|
||||
key: 'user_info', value: jsonEncode(resp.user ?? {}));
|
||||
}
|
||||
if (close != null) {
|
||||
close(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case HttpType.kAuthResTypeEmailCheck:
|
||||
bool? isEmailVerification;
|
||||
if (resp.tfa_type == null ||
|
||||
resp.tfa_type == HttpType.kAuthResTypeEmailCheck) {
|
||||
isEmailVerification = true;
|
||||
} else if (resp.tfa_type == HttpType.kAuthResTypeTfaCheck) {
|
||||
isEmailVerification = false;
|
||||
} else {
|
||||
passwordMsg = "Failed, bad tfa type from server";
|
||||
}
|
||||
if (isEmailVerification != null) {
|
||||
if (isMobile) {
|
||||
if (close != null) close(null);
|
||||
verificationCodeDialog(
|
||||
resp.user, resp.secret, isEmailVerification);
|
||||
} else {
|
||||
setState(() => isInProgress = false);
|
||||
// Workaround for web, close the dialog first, then show the verification code dialog.
|
||||
// Otherwise, the text field will keep selecting the text and we can't input the code.
|
||||
// Not sure why this happens.
|
||||
if (isWeb && close != null) close(null);
|
||||
final res = await verificationCodeDialog(
|
||||
resp.user, resp.secret, isEmailVerification);
|
||||
if (res == true) {
|
||||
if (!isWeb && close != null) close(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
passwordMsg = "Failed, bad response from server";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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: true,
|
||||
type: HttpType.kAuthReqTypeAccount));
|
||||
await handleLoginResponse(resp, true, close);
|
||||
} on RequestException catch (err) {
|
||||
passwordMsg = translate(err.cause);
|
||||
} catch (err) {
|
||||
passwordMsg = "Unknown Error: $err";
|
||||
}
|
||||
curOP.value = '';
|
||||
setState(() => isInProgress = false);
|
||||
}
|
||||
|
||||
thirdAuthWidget() => Obx(() {
|
||||
return Offstage(
|
||||
offstage: loginOptions.isEmpty,
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
Center(
|
||||
child: Text(
|
||||
translate('or'),
|
||||
style: TextStyle(fontSize: 16),
|
||||
)),
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
LoginWidgetOP(
|
||||
ops: loginOptions
|
||||
.map((e) => ConfigOP(op: e['name'], icon: e['icon']))
|
||||
.toList(),
|
||||
curOP: curOP,
|
||||
cbLogin: (Map<String, dynamic> authBody) async {
|
||||
LoginResponse? resp;
|
||||
try {
|
||||
// access_token is already stored in the rust side.
|
||||
resp =
|
||||
gFFI.userModel.getLoginResponseFromAuthBody(authBody);
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'Failed to parse oidc login body: "$authBody"');
|
||||
}
|
||||
close(true);
|
||||
|
||||
if (resp != null) {
|
||||
handleLoginResponse(resp, false, null);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
final title = Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
translate('Login'),
|
||||
).marginOnly(top: MyTheme.dialogPadding),
|
||||
MouseRegion(
|
||||
onEnter: (_) => setState(() => isCloseHovered = true),
|
||||
onExit: (_) => setState(() => isCloseHovered = false),
|
||||
child: InkWell(
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
size: 25,
|
||||
// No need to handle the branch of null.
|
||||
// Because we can ensure the color is not null when debug.
|
||||
color: isCloseHovered
|
||||
? Colors.white
|
||||
: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.color
|
||||
?.withOpacity(0.55),
|
||||
),
|
||||
onTap: onDialogCancel,
|
||||
hoverColor: Colors.red,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
).marginOnly(top: 10, right: 15),
|
||||
],
|
||||
);
|
||||
final titlePadding = EdgeInsets.fromLTRB(MyTheme.dialogPadding, 0, 0, 0);
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: title,
|
||||
titlePadding: titlePadding,
|
||||
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,
|
||||
onLogin: onLogin,
|
||||
userFocusNode: userFocusNode,
|
||||
),
|
||||
thirdAuthWidget(),
|
||||
],
|
||||
),
|
||||
onCancel: onDialogCancel,
|
||||
onSubmit: onLogin,
|
||||
);
|
||||
});
|
||||
|
||||
if (res != null) {
|
||||
await UserModel.updateOtherModels();
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
Future<bool?> verificationCodeDialog(
|
||||
UserPayload? user, String? secret, bool isEmailVerification) async {
|
||||
var autoLogin = true;
|
||||
var isInProgress = false;
|
||||
String? errorText;
|
||||
|
||||
final code = TextEditingController();
|
||||
|
||||
final res = await gFFI.dialogManager.show<bool>((setState, close, context) {
|
||||
void onVerify() async {
|
||||
setState(() => isInProgress = true);
|
||||
|
||||
try {
|
||||
final resp = await gFFI.userModel.login(LoginRequest(
|
||||
verificationCode: code.text,
|
||||
tfaCode: isEmailVerification ? null : code.text,
|
||||
secret: secret,
|
||||
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);
|
||||
} catch (err) {
|
||||
errorText = "Unknown Error: $err";
|
||||
}
|
||||
|
||||
setState(() => isInProgress = false);
|
||||
}
|
||||
|
||||
final codeField = isEmailVerification
|
||||
? DialogEmailCodeField(
|
||||
controller: code,
|
||||
errorText: errorText,
|
||||
readyCallback: onVerify,
|
||||
onChanged: () => errorText = null,
|
||||
)
|
||||
: Dialog2FaField(
|
||||
controller: code,
|
||||
errorText: errorText,
|
||||
readyCallback: onVerify,
|
||||
onChanged: () => errorText = null,
|
||||
);
|
||||
|
||||
getOnSubmit() => codeField.isReady ? onVerify : null;
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate("Verification code")),
|
||||
contentBoxConstraints: BoxConstraints(maxWidth: 300),
|
||||
content: Column(
|
||||
children: [
|
||||
Offstage(
|
||||
offstage: !isEmailVerification || user?.email == null,
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: "Email", prefixIcon: Icon(Icons.email)),
|
||||
readOnly: true,
|
||||
controller: TextEditingController(text: user?.email),
|
||||
).workaroundFreezeLinuxMint()),
|
||||
isEmailVerification ? const SizedBox(height: 8) : const Offstage(),
|
||||
codeField,
|
||||
/*
|
||||
CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: Row(children: [
|
||||
Expanded(child: Text(translate("Trust this device")))
|
||||
]),
|
||||
value: trustThisDevice,
|
||||
onChanged: (v) {
|
||||
if (v == null) return;
|
||||
setState(() => trustThisDevice = !trustThisDevice);
|
||||
},
|
||||
),
|
||||
*/
|
||||
// NOT use Offstage to wrap LinearProgressIndicator
|
||||
if (isInProgress) const LinearProgressIndicator(),
|
||||
],
|
||||
),
|
||||
onCancel: close,
|
||||
onSubmit: getOnSubmit(),
|
||||
actions: [
|
||||
dialogButton("Cancel", onPressed: close, isOutline: true),
|
||||
dialogButton("Verify", onPressed: getOnSubmit()),
|
||||
]);
|
||||
});
|
||||
// For verification code, desktop update other models in login dialog, mobile need to close login dialog first,
|
||||
// otherwise the soft keyboard will jump out on each key press, so mobile update in verification code dialog.
|
||||
if (isMobile && res == true) {
|
||||
await UserModel.updateOtherModels();
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
void logOutConfirmDialog() {
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
submit() {
|
||||
close();
|
||||
gFFI.userModel.logOut();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
content: Text(translate("logout_tip")),
|
||||
actions: [
|
||||
dialogButton(translate("Cancel"), onPressed: close, isOutline: true),
|
||||
dialogButton(translate("OK"), onPressed: submit),
|
||||
],
|
||||
onSubmit: submit,
|
||||
onCancel: close,
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user