mirror of
https://github.com/rustdesk/rustdesk.git
synced 2024-11-28 23:59:05 +08:00
561 lines
17 KiB
Dart
561 lines
17 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/material.dart' hide MenuItem;
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_hbb/common.dart';
|
|
import 'package:flutter_hbb/desktop/pages/connection_page.dart';
|
|
import 'package:flutter_hbb/models/platform_model.dart';
|
|
import 'package:flutter_hbb/models/server_model.dart';
|
|
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:tray_manager/tray_manager.dart';
|
|
import 'package:window_manager/window_manager.dart';
|
|
|
|
class DesktopHomePage extends StatefulWidget {
|
|
const DesktopHomePage({Key? key}) : super(key: key);
|
|
|
|
@override
|
|
State<DesktopHomePage> createState() => _DesktopHomePageState();
|
|
}
|
|
|
|
const borderColor = Color(0xFF2F65BA);
|
|
|
|
class _DesktopHomePageState extends State<DesktopHomePage>
|
|
with TrayListener, WindowListener, AutomaticKeepAliveClientMixin {
|
|
@override
|
|
bool get wantKeepAlive => true;
|
|
|
|
@override
|
|
void onWindowClose() async {
|
|
super.onWindowClose();
|
|
// close all sub windows
|
|
if (await windowManager.isPreventClose()) {
|
|
try {
|
|
await Future.wait([
|
|
saveWindowPosition(WindowType.Main),
|
|
rustDeskWinManager.closeAllSubWindows()
|
|
]);
|
|
} catch (err) {
|
|
debugPrint("$err");
|
|
} finally {
|
|
await windowManager.setPreventClose(false);
|
|
await windowManager.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
super.build(context);
|
|
return Row(
|
|
children: [
|
|
buildLeftPane(context),
|
|
const VerticalDivider(
|
|
width: 1,
|
|
thickness: 1,
|
|
),
|
|
Expanded(
|
|
child: buildRightPane(context),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
buildLeftPane(BuildContext context) {
|
|
return ChangeNotifierProvider.value(
|
|
value: gFFI.serverModel,
|
|
child: Container(
|
|
width: 200,
|
|
color: MyTheme.color(context).bg,
|
|
child: Column(
|
|
children: [
|
|
buildTip(context),
|
|
buildIDBoard(context),
|
|
buildPasswordBoard(context),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
buildRightPane(BuildContext context) {
|
|
return Container(
|
|
color: MyTheme.color(context).grayBg,
|
|
child: ConnectionPage(),
|
|
);
|
|
}
|
|
|
|
buildIDBoard(BuildContext context) {
|
|
final model = gFFI.serverModel;
|
|
return Container(
|
|
margin: const EdgeInsets.only(left: 20, right: 11),
|
|
height: 57,
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.baseline,
|
|
textBaseline: TextBaseline.alphabetic,
|
|
children: [
|
|
Container(
|
|
width: 2,
|
|
decoration: const BoxDecoration(color: MyTheme.accent),
|
|
).marginOnly(top: 5),
|
|
Expanded(
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(left: 7),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
height: 25,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
translate("ID"),
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: MyTheme.color(context).lightText),
|
|
).marginOnly(top: 5),
|
|
buildPopupMenu(context)
|
|
],
|
|
),
|
|
),
|
|
Flexible(
|
|
child: GestureDetector(
|
|
onDoubleTap: () {
|
|
Clipboard.setData(
|
|
ClipboardData(text: model.serverId.text));
|
|
showToast(translate("Copied"));
|
|
},
|
|
child: TextFormField(
|
|
controller: model.serverId,
|
|
readOnly: true,
|
|
decoration: InputDecoration(
|
|
border: InputBorder.none,
|
|
contentPadding: EdgeInsets.only(bottom: 20),
|
|
),
|
|
style: TextStyle(
|
|
fontSize: 22,
|
|
),
|
|
),
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget buildPopupMenu(BuildContext context) {
|
|
RxBool hover = false.obs;
|
|
return InkWell(
|
|
onTap: () async {},
|
|
child: Obx(
|
|
() => CircleAvatar(
|
|
radius: 15,
|
|
backgroundColor: hover.value
|
|
? MyTheme.color(context).grayBg!
|
|
: MyTheme.color(context).bg!,
|
|
child: Icon(
|
|
Icons.more_vert_outlined,
|
|
size: 20,
|
|
color: hover.value
|
|
? MyTheme.color(context).text
|
|
: MyTheme.color(context).lightText,
|
|
),
|
|
),
|
|
),
|
|
onHover: (value) => hover.value = value,
|
|
);
|
|
}
|
|
|
|
buildPasswordBoard(BuildContext context) {
|
|
final model = gFFI.serverModel;
|
|
RxBool refreshHover = false.obs;
|
|
RxBool editHover = false.obs;
|
|
return Container(
|
|
margin: EdgeInsets.only(left: 20.0, right: 16, top: 13, bottom: 13),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.baseline,
|
|
textBaseline: TextBaseline.alphabetic,
|
|
children: [
|
|
Container(
|
|
width: 2,
|
|
height: 52,
|
|
decoration: BoxDecoration(color: MyTheme.accent),
|
|
),
|
|
Expanded(
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(left: 7),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
translate("Password"),
|
|
style: TextStyle(
|
|
fontSize: 14, color: MyTheme.color(context).lightText),
|
|
),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: GestureDetector(
|
|
onDoubleTap: () {
|
|
if (model.verificationMethod !=
|
|
kUsePermanentPassword) {
|
|
Clipboard.setData(
|
|
ClipboardData(text: model.serverPasswd.text));
|
|
showToast(translate("Copied"));
|
|
}
|
|
},
|
|
child: TextFormField(
|
|
controller: model.serverPasswd,
|
|
readOnly: true,
|
|
decoration: InputDecoration(
|
|
border: InputBorder.none,
|
|
contentPadding: EdgeInsets.only(bottom: 2),
|
|
),
|
|
style: TextStyle(fontSize: 15),
|
|
),
|
|
),
|
|
),
|
|
InkWell(
|
|
child: Obx(
|
|
() => Icon(
|
|
Icons.refresh,
|
|
color: refreshHover.value
|
|
? MyTheme.color(context).text
|
|
: Color(0xFFDDDDDD),
|
|
size: 22,
|
|
).marginOnly(right: 8, bottom: 2),
|
|
),
|
|
onTap: () => bind.mainUpdateTemporaryPassword(),
|
|
onHover: (value) => refreshHover.value = value,
|
|
),
|
|
InkWell(
|
|
child: Obx(
|
|
() => Icon(
|
|
Icons.edit,
|
|
color: editHover.value
|
|
? MyTheme.color(context).text
|
|
: Color(0xFFDDDDDD),
|
|
size: 22,
|
|
).marginOnly(right: 8, bottom: 2),
|
|
),
|
|
onTap: () => {},
|
|
onHover: (value) => editHover.value = value,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
buildTip(BuildContext context) {
|
|
return Padding(
|
|
padding:
|
|
const EdgeInsets.only(left: 20.0, right: 16, top: 16.0, bottom: 5),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
translate("Your Desktop"),
|
|
style: TextStyle(fontWeight: FontWeight.normal, fontSize: 19),
|
|
),
|
|
SizedBox(
|
|
height: 10.0,
|
|
),
|
|
Text(
|
|
translate("desk_tip"),
|
|
overflow: TextOverflow.clip,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: MyTheme.color(context).lighterText,
|
|
height: 1.25),
|
|
)
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void onTrayMenuItemClick(MenuItem menuItem) {
|
|
print('click ${menuItem.key}');
|
|
switch (menuItem.key) {
|
|
case "quit":
|
|
exit(0);
|
|
case "show":
|
|
// windowManager.show();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
trayManager.addListener(this);
|
|
windowManager.addListener(this);
|
|
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
|
print(
|
|
"call ${call.method} with args ${call.arguments} from window ${fromWindowId}");
|
|
if (call.method == "main_window_on_top") {
|
|
window_on_top(null);
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
trayManager.removeListener(this);
|
|
windowManager.removeListener(this);
|
|
super.dispose();
|
|
}
|
|
}
|
|
|
|
/// common login dialog for desktop
|
|
/// call this directly
|
|
Future<bool> loginDialog() async {
|
|
String userName = "";
|
|
var userNameMsg = "";
|
|
String pass = "";
|
|
var passMsg = "";
|
|
var userContontroller = TextEditingController(text: userName);
|
|
var pwdController = TextEditingController(text: pass);
|
|
|
|
var isInProgress = false;
|
|
var completer = Completer<bool>();
|
|
gFFI.dialogManager.show((setState, close) {
|
|
submit() async {
|
|
setState(() {
|
|
userNameMsg = "";
|
|
passMsg = "";
|
|
isInProgress = true;
|
|
});
|
|
cancel() {
|
|
setState(() {
|
|
isInProgress = false;
|
|
});
|
|
}
|
|
|
|
userName = userContontroller.text;
|
|
pass = pwdController.text;
|
|
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) {
|
|
// ignore: avoid_print
|
|
print(err.toString());
|
|
cancel();
|
|
return;
|
|
}
|
|
close();
|
|
}
|
|
|
|
cancel() {
|
|
completer.complete(false);
|
|
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,
|
|
),
|
|
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: userContontroller,
|
|
focusNode: FocusNode()..requestFocus(),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(
|
|
height: 8.0,
|
|
),
|
|
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())
|
|
],
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(onPressed: cancel, child: Text(translate("Cancel"))),
|
|
TextButton(onPressed: submit, child: Text(translate("OK"))),
|
|
],
|
|
onSubmit: submit,
|
|
onCancel: cancel,
|
|
);
|
|
});
|
|
return completer.future;
|
|
}
|
|
|
|
void setPasswordDialog() async {
|
|
final pw = await bind.mainGetPermanentPassword();
|
|
final p0 = TextEditingController(text: pw);
|
|
final p1 = TextEditingController(text: pw);
|
|
var errMsg0 = "";
|
|
var errMsg1 = "";
|
|
|
|
gFFI.dialogManager.show((setState, close) {
|
|
submit() {
|
|
setState(() {
|
|
errMsg0 = "";
|
|
errMsg1 = "";
|
|
});
|
|
final pass = p0.text.trim();
|
|
if (pass.length < 6) {
|
|
setState(() {
|
|
errMsg0 = translate("Too short, at least 6 characters.");
|
|
});
|
|
return;
|
|
}
|
|
if (p1.text.trim() != pass) {
|
|
setState(() {
|
|
errMsg1 = translate("The confirmation is not identical.");
|
|
});
|
|
return;
|
|
}
|
|
bind.mainSetPermanentPassword(password: pass);
|
|
close();
|
|
}
|
|
|
|
return CustomAlertDialog(
|
|
title: Text(translate("Set Password")),
|
|
content: ConstrainedBox(
|
|
constraints: const BoxConstraints(minWidth: 500),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const SizedBox(
|
|
height: 8.0,
|
|
),
|
|
Row(
|
|
children: [
|
|
ConstrainedBox(
|
|
constraints: const BoxConstraints(minWidth: 100),
|
|
child: Text(
|
|
"${translate('Password')}:",
|
|
textAlign: TextAlign.start,
|
|
).marginOnly(bottom: 16.0)),
|
|
const SizedBox(
|
|
width: 24.0,
|
|
),
|
|
Expanded(
|
|
child: TextField(
|
|
obscureText: true,
|
|
decoration: InputDecoration(
|
|
border: const OutlineInputBorder(),
|
|
errorText: errMsg0.isNotEmpty ? errMsg0 : null),
|
|
controller: p0,
|
|
focusNode: FocusNode()..requestFocus(),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(
|
|
height: 8.0,
|
|
),
|
|
Row(
|
|
children: [
|
|
ConstrainedBox(
|
|
constraints: const BoxConstraints(minWidth: 100),
|
|
child: Text("${translate('Confirmation')}:")
|
|
.marginOnly(bottom: 16.0)),
|
|
const SizedBox(
|
|
width: 24.0,
|
|
),
|
|
Expanded(
|
|
child: TextField(
|
|
obscureText: true,
|
|
decoration: InputDecoration(
|
|
border: const OutlineInputBorder(),
|
|
errorText: errMsg1.isNotEmpty ? errMsg1 : null),
|
|
controller: p1,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(onPressed: close, child: Text(translate("Cancel"))),
|
|
TextButton(onPressed: submit, child: Text(translate("OK"))),
|
|
],
|
|
onSubmit: submit,
|
|
onCancel: close,
|
|
);
|
|
});
|
|
}
|