rustdesk/flutter/lib/desktop/pages/desktop_home_page.dart

580 lines
19 KiB
Dart
Raw Normal View History

import 'dart:async';
import 'dart:io';
import 'dart:convert';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart' hide MenuItem;
2022-07-14 12:32:01 +08:00
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart';
2022-05-29 04:39:12 +08:00
import 'package:flutter_hbb/desktop/pages/connection_page.dart';
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
2022-09-28 11:20:57 +08:00
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.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:flutter_hbb/utils/tray_manager.dart';
2022-07-14 12:32:01 +08:00
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:tray_manager/tray_manager.dart';
2022-08-09 13:39:30 +08:00
import 'package:window_manager/window_manager.dart';
2022-09-25 21:45:37 +08:00
import 'package:url_launcher/url_launcher.dart';
import 'package:window_size/window_size.dart' as window_size;
2022-05-23 16:44:23 +08:00
2022-09-23 20:18:11 +08:00
import '../widgets/button.dart';
2022-05-23 16:44:23 +08:00
class DesktopHomePage extends StatefulWidget {
const DesktopHomePage({Key? key}) : super(key: key);
2022-05-23 16:44:23 +08:00
@override
State<DesktopHomePage> createState() => _DesktopHomePageState();
2022-05-23 16:44:23 +08:00
}
2022-05-29 19:55:50 +08:00
const borderColor = Color(0xFF2F65BA);
class _DesktopHomePageState extends State<DesktopHomePage>
with TrayListener, AutomaticKeepAliveClientMixin {
2022-09-28 11:20:57 +08:00
final _leftPaneScrollController = ScrollController();
@override
bool get wantKeepAlive => true;
2022-09-23 17:28:22 +08:00
var updateUrl = '';
2022-10-18 10:29:33 +08:00
StreamSubscription? _uniLinksSubscription;
2022-05-23 16:44:23 +08:00
@override
Widget build(BuildContext context) {
super.build(context);
return Row(
2022-09-28 11:20:57 +08:00
crossAxisAlignment: CrossAxisAlignment.start,
children: [
2022-09-19 18:38:19 +08:00
buildLeftPane(context),
const VerticalDivider(
width: 1,
thickness: 1,
),
Expanded(
2022-09-19 18:38:19 +08:00
child: buildRightPane(context),
),
],
);
}
2022-09-28 11:20:57 +08:00
Widget buildLeftPane(BuildContext context) {
return ChangeNotifierProvider.value(
value: gFFI.serverModel,
2022-05-29 10:25:36 +08:00
child: Container(
width: 200,
2022-09-23 16:31:50 +08:00
color: Theme.of(context).backgroundColor,
2022-09-28 11:20:57 +08:00
child: DesktopScrollWrapper(
scrollController: _leftPaneScrollController,
child: SingleChildScrollView(
controller: _leftPaneScrollController,
child: Column(
children: [
buildTip(context),
buildIDBoard(context),
buildPasswordBoard(context),
buildHelpCards(),
],
),
),
2022-05-29 10:25:36 +08:00
),
),
);
}
2022-09-19 18:38:19 +08:00
buildRightPane(BuildContext context) {
return Container(
2022-09-23 16:31:50 +08:00
color: Theme.of(context).scaffoldBackgroundColor,
child: ConnectionPage(),
);
}
buildIDBoard(BuildContext context) {
final model = gFFI.serverModel;
2022-05-29 10:25:36 +08:00
return Container(
margin: const EdgeInsets.only(left: 20, right: 11),
height: 57,
2022-05-29 10:25:36 +08:00
child: Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Container(
width: 2,
decoration: const BoxDecoration(color: MyTheme.accent),
).marginOnly(top: 5),
2022-05-29 10:25:36 +08:00
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 7),
2022-05-29 10:25:36 +08:00
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,
2022-09-23 16:31:50 +08:00
color: Theme.of(context)
.textTheme
.titleLarge
?.color
?.withOpacity(0.5)),
).marginOnly(top: 5),
buildPopupMenu(context)
],
),
2022-05-29 10:25:36 +08:00
),
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,
),
),
),
)
2022-05-29 10:25:36 +08:00
],
),
),
2022-05-29 10:25:36 +08:00
),
],
),
);
}
Widget buildPopupMenu(BuildContext context) {
2022-09-23 16:31:50 +08:00
final textColor = Theme.of(context).textTheme.titleLarge?.color;
RxBool hover = false.obs;
return InkWell(
onTap: DesktopTabPage.onAddSetting,
child: Obx(
() => CircleAvatar(
radius: 15,
backgroundColor: hover.value
2022-09-23 16:31:50 +08:00
? Theme.of(context).scaffoldBackgroundColor
: Theme.of(context).backgroundColor,
child: Icon(
Icons.more_vert_outlined,
size: 20,
2022-09-23 16:31:50 +08:00
color: hover.value ? textColor : textColor?.withOpacity(0.5),
),
),
),
onHover: (value) => hover.value = value,
);
}
2022-05-29 10:25:36 +08:00
buildPasswordBoard(BuildContext context) {
final model = gFFI.serverModel;
RxBool refreshHover = false.obs;
2022-09-19 18:38:19 +08:00
RxBool editHover = false.obs;
2022-09-23 16:31:50 +08:00
final textColor = Theme.of(context).textTheme.titleLarge?.color;
2022-05-29 10:25:36 +08:00
return Container(
margin: EdgeInsets.only(left: 20.0, right: 16, top: 13, bottom: 13),
2022-05-29 10:25:36 +08:00
child: Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Container(
width: 2,
height: 52,
2022-05-29 10:25:36 +08:00
decoration: BoxDecoration(color: MyTheme.accent),
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 7),
2022-05-29 10:25:36 +08:00
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AutoSizeText(
translate("One-time Password"),
style: TextStyle(
2022-09-23 16:31:50 +08:00
fontSize: 14, color: textColor?.withOpacity(0.5)),
maxLines: 1,
2022-05-29 10:25:36 +08:00
),
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
2022-09-23 16:31:50 +08:00
? textColor
: Color(0xFFDDDDDD), // TODO
size: 22,
).marginOnly(right: 8, bottom: 2),
),
onTap: () => bind.mainUpdateTemporaryPassword(),
onHover: (value) => refreshHover.value = value,
),
2022-09-19 18:38:19 +08:00
InkWell(
child: Obx(
() => Icon(
Icons.edit,
color: editHover.value
2022-09-23 16:31:50 +08:00
? textColor
: Color(0xFFDDDDDD), // TODO
2022-09-19 18:38:19 +08:00
size: 22,
).marginOnly(right: 8, bottom: 2),
),
onTap: () => DesktopSettingPage.switch2page(1),
2022-09-19 18:38:19 +08:00
onHover: (value) => editHover.value = value,
),
],
),
2022-05-29 10:25:36 +08:00
],
),
),
2022-05-29 10:25:36 +08:00
),
],
),
);
}
buildTip(BuildContext context) {
return Padding(
padding:
const EdgeInsets.only(left: 20.0, right: 16, top: 16.0, bottom: 5),
2022-05-29 10:25:36 +08:00
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
translate("Your Desktop"),
2022-09-23 16:31:50 +08:00
style: Theme.of(context).textTheme.titleLarge,
// style: TextStyle(
// // color: MyTheme.color(context).text,
// fontWeight: FontWeight.normal,
// fontSize: 19),
2022-05-29 10:25:36 +08:00
),
SizedBox(
height: 10.0,
2022-05-29 10:25:36 +08:00
),
Text(
translate("desk_tip"),
overflow: TextOverflow.clip,
2022-09-23 16:31:50 +08:00
style: Theme.of(context).textTheme.bodySmall,
2022-05-29 10:25:36 +08:00
)
],
),
);
2022-05-23 16:44:23 +08:00
}
2022-09-23 17:28:22 +08:00
Widget buildHelpCards() {
if (Platform.isWindows) {
if (!bind.mainIsInstalled()) {
2022-09-25 21:45:37 +08:00
return buildInstallCard(
"", "install_tip", "Install", bind.mainGotoInstall);
2022-09-23 17:28:22 +08:00
} else if (bind.mainIsInstalledLowerVersion()) {
2022-09-25 21:45:37 +08:00
return buildInstallCard("Status", "Your installation is lower version.",
"Click to upgrade", bind.mainUpdateMe);
2022-09-23 17:28:22 +08:00
}
}
if (updateUrl.isNotEmpty) {
2022-09-25 21:45:37 +08:00
return buildInstallCard(
"Status",
"There is a newer version of ${bind.mainGetAppNameSync()} ${bind.mainGetNewVersion()} available.",
"Click to download", () async {
final Uri url = Uri.parse('https://rustdesk.com');
await launchUrl(url);
});
2022-09-23 17:28:22 +08:00
}
if (Platform.isMacOS) {}
if (bind.mainIsInstalledLowerVersion()) {}
return Container();
}
2022-09-25 21:45:37 +08:00
Widget buildInstallCard(String title, String content, String btnText,
GestureTapCallback onPressed) {
2022-09-23 17:28:22 +08:00
return Container(
margin: EdgeInsets.only(top: 20),
2022-09-23 20:18:11 +08:00
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
Color.fromARGB(255, 226, 66, 188),
Color.fromARGB(255, 244, 114, 124),
],
)),
padding: EdgeInsets.all(20),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
2022-09-25 21:45:37 +08:00
children: (title.isNotEmpty
? <Widget>[
Center(
child: Text(
translate(title),
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 15),
).marginOnly(bottom: 6)),
]
: <Widget>[]) +
<Widget>[
Text(
translate(content),
style: TextStyle(
height: 1.5,
color: Colors.white,
fontWeight: FontWeight.normal,
fontSize: 13),
).marginOnly(bottom: 20),
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
FixedWidthButton(
width: 150,
2022-09-25 21:45:37 +08:00
padding: 8,
isOutline: true,
text: translate(btnText),
textColor: Colors.white,
borderColor: Colors.white,
textSize: 20,
radius: 10,
onTap: onPressed,
)
]),
],
2022-09-23 20:18:11 +08:00
)),
2022-09-23 17:28:22 +08:00
);
}
@override
void onTrayIconMouseDown() {
windowManager.show();
2022-11-02 11:10:28 +08:00
}
@override
void onTrayIconRightMouseDown() {
// linux does not support popup menu manually.
// linux will handle popup action ifself.
if (Platform.isMacOS || Platform.isWindows) {
trayManager.popUpContextMenu();
}
}
@override
void onTrayMenuItemClick(MenuItem menuItem) {
switch (menuItem.key) {
case kTrayItemQuitKey:
windowManager.close();
break;
case kTrayItemShowKey:
windowManager.show();
windowManager.focus();
break;
default:
break;
}
}
@override
void initState() {
super.initState();
bind.mainStartGrabKeyboard();
2022-09-23 17:28:22 +08:00
Timer(const Duration(seconds: 5), () async {
updateUrl = await bind.mainGetSoftwareUpdateUrl();
if (updateUrl.isNotEmpty) setState(() {});
});
2022-11-04 19:20:51 +08:00
// disable this tray because we use tray function provided by rust now
// initTray();
trayManager.addListener(this);
2022-11-05 23:41:22 +08:00
rustDeskWinManager.registerActiveWindowListener(onActiveWindowChanged);
// main window may be hidden because of the initial uni link or arguments.
// note that we must wrap this active window registration in future because
// we must ensure the execution is after `windowManager.hide/show()`.
Future.delayed(Duration.zero, () {
windowManager.isVisible().then((visibility) {
if (visibility) {
rustDeskWinManager.registerActiveWindow(kWindowMainId);
}
});
});
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
debugPrint(
2022-11-05 23:41:22 +08:00
"[Main] call ${call.method} with args ${call.arguments} from window $fromWindowId");
if (call.method == "main_window_on_top") {
2022-08-09 19:32:19 +08:00
window_on_top(null);
} else if (call.method == "get_window_info") {
final screen = (await window_size.getWindowInfo()).screen;
if (screen == null) {
return "";
} else {
return jsonEncode({
'frame': {
'l': screen.frame.left,
't': screen.frame.top,
'r': screen.frame.right,
'b': screen.frame.bottom,
},
'visibleFrame': {
'l': screen.visibleFrame.left,
't': screen.visibleFrame.top,
'r': screen.visibleFrame.right,
'b': screen.visibleFrame.bottom,
},
'scaleFactor': screen.scaleFactor,
});
}
} else if (call.method == kWindowActionRebuild) {
reloadCurrentWindow();
2022-11-05 23:41:22 +08:00
} else if (call.method == kWindowEventShow) {
rustDeskWinManager.registerActiveWindow(call.arguments["id"]);
} else if (call.method == kWindowEventHide) {
rustDeskWinManager.unregisterActiveWindow(call.arguments["id"]);
}
});
2022-10-18 10:29:33 +08:00
_uniLinksSubscription = listenUniLinks();
}
@override
void dispose() {
2022-11-05 23:41:22 +08:00
// destoryTray();
// fix: disable unregister to prevent from receiving events from other windows
// rustDeskWinManager.unregisterActiveWindowListener(onActiveWindowChanged);
trayManager.removeListener(this);
2022-10-18 10:29:33 +08:00
_uniLinksSubscription?.cancel();
super.dispose();
}
2022-05-23 16:44:23 +08:00
}
void setPasswordDialog() async {
final pw = await bind.mainGetPermanentPassword();
final p0 = TextEditingController(text: pw);
final p1 = TextEditingController(text: pw);
var errMsg0 = "";
var errMsg1 = "";
2022-08-12 18:42:02 +08:00
gFFI.dialogManager.show((setState, close) {
submit() {
setState(() {
errMsg0 = "";
errMsg1 = "";
});
final pass = p0.text.trim();
if (pass.length < 6 && pass.isNotEmpty) {
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,
);
});
}