mirror of
https://github.com/rustdesk/rustdesk.git
synced 2024-12-03 03:19:27 +08:00
85cafda168
Signed-off-by: fufesou <shuanglongchen@yeah.net>
1008 lines
35 KiB
Dart
1008 lines
35 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
import 'dart:convert';
|
|
|
|
import 'package:auto_size_text/auto_size_text.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_hbb/common.dart';
|
|
import 'package:flutter_hbb/common/widgets/animated_rotation_widget.dart';
|
|
import 'package:flutter_hbb/common/widgets/custom_password.dart';
|
|
import 'package:flutter_hbb/consts.dart';
|
|
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';
|
|
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/plugin/ui_manager.dart';
|
|
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
import 'package:window_manager/window_manager.dart';
|
|
import 'package:window_size/window_size.dart' as window_size;
|
|
|
|
import '../widgets/button.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 AutomaticKeepAliveClientMixin {
|
|
final _leftPaneScrollController = ScrollController();
|
|
|
|
@override
|
|
bool get wantKeepAlive => true;
|
|
var updateUrl = '';
|
|
var systemError = '';
|
|
StreamSubscription? _uniLinksSubscription;
|
|
var svcStopped = false.obs;
|
|
var watchIsCanScreenRecording = false;
|
|
var watchIsProcessTrust = false;
|
|
var watchIsInputMonitoring = false;
|
|
var watchIsCanRecordAudio = false;
|
|
Timer? _updateTimer;
|
|
bool isCardClosed = false;
|
|
|
|
final RxBool _editHover = false.obs;
|
|
|
|
final GlobalKey _childKey = GlobalKey();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
super.build(context);
|
|
final isIncomingOnly = bind.isIncomingOnly();
|
|
return Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
buildLeftPane(context),
|
|
if (!isIncomingOnly) const VerticalDivider(width: 1),
|
|
if (!isIncomingOnly) Expanded(child: buildRightPane(context)),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget buildPresetPasswordWarning() {
|
|
return FutureBuilder<bool>(
|
|
future: bind.isPresetPassword(),
|
|
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
|
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
return CircularProgressIndicator(); // Show a loading spinner while waiting for the Future to complete
|
|
} else if (snapshot.hasError) {
|
|
return Text(
|
|
'Error: ${snapshot.error}'); // Show an error message if the Future completed with an error
|
|
} else if (snapshot.hasData && snapshot.data == true) {
|
|
return Container(
|
|
color: Colors.yellow,
|
|
child: Column(
|
|
children: [
|
|
Align(
|
|
child: Text(
|
|
translate("Security Alert"),
|
|
style: TextStyle(
|
|
color: Colors.red,
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
)).paddingOnly(bottom: 8),
|
|
Text(
|
|
translate("preset_password_warning"),
|
|
style: TextStyle(color: Colors.red),
|
|
)
|
|
],
|
|
).paddingAll(8),
|
|
); // Show a warning message if the Future completed with true
|
|
} else {
|
|
return SizedBox
|
|
.shrink(); // Show nothing if the Future completed with false or null
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget buildLeftPane(BuildContext context) {
|
|
final isIncomingOnly = bind.isIncomingOnly();
|
|
final isOutgoingOnly = bind.isOutgoingOnly();
|
|
final children = <Widget>[
|
|
if (!isOutgoingOnly) buildPresetPasswordWarning(),
|
|
if (bind.isCustomClient())
|
|
Align(
|
|
alignment: Alignment.center,
|
|
child: MouseRegion(
|
|
cursor: SystemMouseCursors.click,
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
launchUrl(Uri.parse('https://rustdesk.com'));
|
|
},
|
|
child: Opacity(
|
|
opacity: 0.5,
|
|
child: Text(
|
|
translate("powered_by_me"),
|
|
overflow: TextOverflow.clip,
|
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
fontSize: 9, decoration: TextDecoration.underline),
|
|
)),
|
|
),
|
|
).marginOnly(top: 6),
|
|
),
|
|
Align(
|
|
alignment: Alignment.center,
|
|
child: loadLogo(),
|
|
),
|
|
buildTip(context),
|
|
if (!isOutgoingOnly) buildIDBoard(context),
|
|
if (!isOutgoingOnly) buildPasswordBoard(context),
|
|
FutureBuilder<Widget>(
|
|
future: buildHelpCards(),
|
|
builder: (_, data) {
|
|
if (data.hasData) {
|
|
if (isIncomingOnly) {
|
|
if (isInHomePage()) {
|
|
Future.delayed(Duration(milliseconds: 300), () {
|
|
_updateWindowSize();
|
|
});
|
|
}
|
|
}
|
|
return data.data!;
|
|
} else {
|
|
return const Offstage();
|
|
}
|
|
},
|
|
),
|
|
buildPluginEntry(),
|
|
];
|
|
if (isIncomingOnly) {
|
|
children.addAll([
|
|
Divider(),
|
|
OnlineStatusWidget(
|
|
onSvcStatusChanged: () {
|
|
if (isInHomePage()) {
|
|
Future.delayed(Duration(milliseconds: 300), () {
|
|
_updateWindowSize();
|
|
});
|
|
}
|
|
},
|
|
).marginOnly(bottom: 6, right: 6)
|
|
]);
|
|
}
|
|
final textColor = Theme.of(context).textTheme.titleLarge?.color;
|
|
return ChangeNotifierProvider.value(
|
|
value: gFFI.serverModel,
|
|
child: Container(
|
|
width: isIncomingOnly ? 280.0 : 200.0,
|
|
color: Theme.of(context).colorScheme.background,
|
|
child: DesktopScrollWrapper(
|
|
scrollController: _leftPaneScrollController,
|
|
child: Stack(
|
|
children: [
|
|
SingleChildScrollView(
|
|
controller: _leftPaneScrollController,
|
|
physics: DraggableNeverScrollableScrollPhysics(),
|
|
child: Column(
|
|
key: _childKey,
|
|
children: children,
|
|
),
|
|
),
|
|
if (isOutgoingOnly)
|
|
Positioned(
|
|
bottom: 6,
|
|
left: 12,
|
|
child: Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: InkWell(
|
|
child: Obx(
|
|
() => Icon(
|
|
Icons.settings,
|
|
color: _editHover.value
|
|
? textColor
|
|
: Colors.grey.withOpacity(0.5),
|
|
size: 22,
|
|
),
|
|
),
|
|
onTap: () => DesktopSettingPage.switch2page(0),
|
|
onHover: (value) => _editHover.value = value,
|
|
),
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
buildRightPane(BuildContext context) {
|
|
return Container(
|
|
color: Theme.of(context).scaffoldBackgroundColor,
|
|
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: Theme.of(context)
|
|
.textTheme
|
|
.titleLarge
|
|
?.color
|
|
?.withOpacity(0.5)),
|
|
).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(top: 10, bottom: 10),
|
|
),
|
|
style: TextStyle(
|
|
fontSize: 22,
|
|
),
|
|
),
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget buildPopupMenu(BuildContext context) {
|
|
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
|
|
? Theme.of(context).scaffoldBackgroundColor
|
|
: Theme.of(context).colorScheme.background,
|
|
child: Tooltip(
|
|
message: translate('Settings'),
|
|
child: Icon(
|
|
Icons.more_vert_outlined,
|
|
size: 20,
|
|
color: hover.value ? textColor : textColor?.withOpacity(0.5),
|
|
)),
|
|
),
|
|
),
|
|
onHover: (value) => hover.value = value,
|
|
);
|
|
}
|
|
|
|
buildPasswordBoard(BuildContext context) {
|
|
final model = gFFI.serverModel;
|
|
RxBool refreshHover = false.obs;
|
|
RxBool editHover = false.obs;
|
|
final textColor = Theme.of(context).textTheme.titleLarge?.color;
|
|
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: [
|
|
AutoSizeText(
|
|
translate("One-time Password"),
|
|
style: TextStyle(
|
|
fontSize: 14, color: textColor?.withOpacity(0.5)),
|
|
maxLines: 1,
|
|
),
|
|
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(top: 14, bottom: 10),
|
|
),
|
|
style: TextStyle(fontSize: 15),
|
|
),
|
|
),
|
|
),
|
|
AnimatedRotationWidget(
|
|
onPressed: () => bind.mainUpdateTemporaryPassword(),
|
|
child: Obx(() => RotatedBox(
|
|
quarterTurns: 2,
|
|
child: Tooltip(
|
|
message: translate('Refresh Password'),
|
|
child: Icon(
|
|
Icons.refresh,
|
|
color: refreshHover.value
|
|
? textColor
|
|
: Color(0xFFDDDDDD),
|
|
size: 22,
|
|
)))),
|
|
onHover: (value) => refreshHover.value = value,
|
|
).marginOnly(right: 8, top: 4),
|
|
if (!bind.isDisableSettings())
|
|
InkWell(
|
|
child: Obx(
|
|
() => Tooltip(
|
|
message: translate('Change Password'),
|
|
child: Icon(
|
|
Icons.edit,
|
|
color: editHover.value
|
|
? textColor
|
|
: Color(0xFFDDDDDD),
|
|
size: 22,
|
|
)).marginOnly(right: 8, top: 4),
|
|
),
|
|
onTap: () => DesktopSettingPage.switch2page(0),
|
|
onHover: (value) => editHover.value = value,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
buildTip(BuildContext context) {
|
|
final isOutgoingOnly = bind.isOutgoingOnly();
|
|
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: [
|
|
Column(
|
|
children: [
|
|
if (!isOutgoingOnly)
|
|
Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: Text(
|
|
translate("Your Desktop"),
|
|
style: Theme.of(context).textTheme.titleLarge,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
SizedBox(
|
|
height: 10.0,
|
|
),
|
|
if (!isOutgoingOnly)
|
|
Text(
|
|
translate("desk_tip"),
|
|
overflow: TextOverflow.clip,
|
|
style: Theme.of(context).textTheme.bodySmall,
|
|
),
|
|
if (isOutgoingOnly)
|
|
Text(
|
|
translate("outgoing_only_desk_tip"),
|
|
overflow: TextOverflow.clip,
|
|
style: Theme.of(context).textTheme.bodySmall,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<Widget> buildHelpCards() async {
|
|
if (!bind.isCustomClient() &&
|
|
updateUrl.isNotEmpty &&
|
|
!isCardClosed &&
|
|
bind.mainUriPrefixSync().contains('rustdesk')) {
|
|
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/download');
|
|
await launchUrl(url);
|
|
}, closeButton: true);
|
|
}
|
|
if (systemError.isNotEmpty) {
|
|
return buildInstallCard("", systemError, "", () {});
|
|
}
|
|
|
|
if (isWindows && !bind.isDisableInstallation()) {
|
|
if (!bind.mainIsInstalled()) {
|
|
return buildInstallCard(
|
|
"", bind.isOutgoingOnly() ? "" : "install_tip", "Install",
|
|
() async {
|
|
await rustDeskWinManager.closeAllSubWindows();
|
|
bind.mainGotoInstall();
|
|
});
|
|
} else if (bind.mainIsInstalledLowerVersion()) {
|
|
return buildInstallCard(
|
|
"Status", "Your installation is lower version.", "Click to upgrade",
|
|
() async {
|
|
await rustDeskWinManager.closeAllSubWindows();
|
|
bind.mainUpdateMe();
|
|
});
|
|
}
|
|
} else if (isMacOS) {
|
|
if (!(bind.isOutgoingOnly() ||
|
|
bind.mainIsCanScreenRecording(prompt: false))) {
|
|
return buildInstallCard("Permissions", "config_screen", "Configure",
|
|
() async {
|
|
bind.mainIsCanScreenRecording(prompt: true);
|
|
watchIsCanScreenRecording = true;
|
|
}, help: 'Help', link: translate("doc_mac_permission"));
|
|
} else if (!bind.mainIsProcessTrusted(prompt: false)) {
|
|
return buildInstallCard("Permissions", "config_acc", "Configure",
|
|
() async {
|
|
bind.mainIsProcessTrusted(prompt: true);
|
|
watchIsProcessTrust = true;
|
|
}, help: 'Help', link: translate("doc_mac_permission"));
|
|
} else if (!bind.mainIsCanInputMonitoring(prompt: false)) {
|
|
return buildInstallCard("Permissions", "config_input", "Configure",
|
|
() async {
|
|
bind.mainIsCanInputMonitoring(prompt: true);
|
|
watchIsInputMonitoring = true;
|
|
}, help: 'Help', link: translate("doc_mac_permission"));
|
|
} else if (!svcStopped.value &&
|
|
bind.mainIsInstalled() &&
|
|
!bind.mainIsInstalledDaemon(prompt: false)) {
|
|
return buildInstallCard("", "install_daemon_tip", "Install", () async {
|
|
bind.mainIsInstalledDaemon(prompt: true);
|
|
});
|
|
}
|
|
//// Disable microphone configuration for macOS. We will request the permission when needed.
|
|
// else if ((await osxCanRecordAudio() !=
|
|
// PermissionAuthorizeType.authorized)) {
|
|
// return buildInstallCard("Permissions", "config_microphone", "Configure",
|
|
// () async {
|
|
// osxRequestAudio();
|
|
// watchIsCanRecordAudio = true;
|
|
// });
|
|
// }
|
|
} else if (isLinux) {
|
|
if (bind.isOutgoingOnly()) {
|
|
return Container();
|
|
}
|
|
final LinuxCards = <Widget>[];
|
|
if (bind.isSelinuxEnforcing()) {
|
|
// Check is SELinux enforcing, but show user a tip of is SELinux enabled for simple.
|
|
final keyShowSelinuxHelpTip = "show-selinux-help-tip";
|
|
if (bind.mainGetLocalOption(key: keyShowSelinuxHelpTip) != 'N') {
|
|
LinuxCards.add(buildInstallCard(
|
|
"Warning",
|
|
"selinux_tip",
|
|
"",
|
|
() async {},
|
|
marginTop: LinuxCards.isEmpty ? 20.0 : 5.0,
|
|
help: 'Help',
|
|
link:
|
|
'https://rustdesk.com/docs/en/client/linux/#permissions-issue',
|
|
closeButton: true,
|
|
closeOption: keyShowSelinuxHelpTip,
|
|
));
|
|
}
|
|
}
|
|
if (bind.mainCurrentIsWayland()) {
|
|
LinuxCards.add(buildInstallCard(
|
|
"Warning", "wayland_experiment_tip", "", () async {},
|
|
marginTop: LinuxCards.isEmpty ? 20.0 : 5.0,
|
|
help: 'Help',
|
|
link: 'https://rustdesk.com/docs/en/client/linux/#x11-required'));
|
|
} else if (bind.mainIsLoginWayland()) {
|
|
LinuxCards.add(buildInstallCard("Warning",
|
|
"Login screen using Wayland is not supported", "", () async {},
|
|
marginTop: LinuxCards.isEmpty ? 20.0 : 5.0,
|
|
help: 'Help',
|
|
link: 'https://rustdesk.com/docs/en/client/linux/#login-screen'));
|
|
}
|
|
if (LinuxCards.isNotEmpty) {
|
|
return Column(
|
|
children: LinuxCards,
|
|
);
|
|
}
|
|
}
|
|
if (bind.isIncomingOnly()) {
|
|
return Align(
|
|
alignment: Alignment.centerRight,
|
|
child: OutlinedButton(
|
|
onPressed: () {
|
|
SystemNavigator.pop(); // Close the application
|
|
// https://github.com/flutter/flutter/issues/66631
|
|
if (isWindows) {
|
|
exit(0);
|
|
}
|
|
},
|
|
child: Text(translate('Quit')),
|
|
),
|
|
).marginAll(14);
|
|
}
|
|
return Container();
|
|
}
|
|
|
|
Widget buildInstallCard(String title, String content, String btnText,
|
|
GestureTapCallback onPressed,
|
|
{double marginTop = 20.0,
|
|
String? help,
|
|
String? link,
|
|
bool? closeButton,
|
|
String? closeOption}) {
|
|
void closeCard() async {
|
|
if (closeOption != null) {
|
|
await bind.mainSetLocalOption(key: closeOption, value: 'N');
|
|
if (bind.mainGetLocalOption(key: closeOption) == 'N') {
|
|
setState(() {
|
|
isCardClosed = true;
|
|
});
|
|
}
|
|
} else {
|
|
setState(() {
|
|
isCardClosed = true;
|
|
});
|
|
}
|
|
}
|
|
|
|
return Stack(
|
|
children: [
|
|
Container(
|
|
margin: EdgeInsets.fromLTRB(
|
|
0, marginTop, 0, bind.isIncomingOnly() ? marginTop : 0),
|
|
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,
|
|
children: (title.isNotEmpty
|
|
? <Widget>[
|
|
Center(
|
|
child: Text(
|
|
translate(title),
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 15),
|
|
).marginOnly(bottom: 6)),
|
|
]
|
|
: <Widget>[]) +
|
|
<Widget>[
|
|
if (content.isNotEmpty)
|
|
Text(
|
|
translate(content),
|
|
style: TextStyle(
|
|
height: 1.5,
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.normal,
|
|
fontSize: 13),
|
|
).marginOnly(bottom: 20)
|
|
] +
|
|
(btnText.isNotEmpty
|
|
? <Widget>[
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
FixedWidthButton(
|
|
width: 150,
|
|
padding: 8,
|
|
isOutline: true,
|
|
text: translate(btnText),
|
|
textColor: Colors.white,
|
|
borderColor: Colors.white,
|
|
textSize: 20,
|
|
radius: 10,
|
|
onTap: onPressed,
|
|
)
|
|
])
|
|
]
|
|
: <Widget>[]) +
|
|
(help != null
|
|
? <Widget>[
|
|
Center(
|
|
child: InkWell(
|
|
onTap: () async =>
|
|
await launchUrl(Uri.parse(link!)),
|
|
child: Text(
|
|
translate(help),
|
|
style: TextStyle(
|
|
decoration:
|
|
TextDecoration.underline,
|
|
color: Colors.white,
|
|
fontSize: 12),
|
|
)).marginOnly(top: 6)),
|
|
]
|
|
: <Widget>[]))),
|
|
),
|
|
if (closeButton != null && closeButton == true)
|
|
Positioned(
|
|
top: 18,
|
|
right: 0,
|
|
child: IconButton(
|
|
icon: Icon(
|
|
Icons.close,
|
|
color: Colors.white,
|
|
size: 20,
|
|
),
|
|
onPressed: closeCard,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
Timer(const Duration(seconds: 1), () async {
|
|
updateUrl = await bind.mainGetSoftwareUpdateUrl();
|
|
if (updateUrl.isNotEmpty) setState(() {});
|
|
});
|
|
_updateTimer = periodic_immediate(const Duration(seconds: 1), () async {
|
|
await gFFI.serverModel.fetchID();
|
|
final error = await bind.mainGetError();
|
|
if (systemError != error) {
|
|
systemError = error;
|
|
setState(() {});
|
|
}
|
|
final v = await bind.mainGetOption(key: "stop-service") == "Y";
|
|
if (v != svcStopped.value) {
|
|
svcStopped.value = v;
|
|
setState(() {});
|
|
}
|
|
if (watchIsCanScreenRecording) {
|
|
if (bind.mainIsCanScreenRecording(prompt: false)) {
|
|
watchIsCanScreenRecording = false;
|
|
setState(() {});
|
|
}
|
|
}
|
|
if (watchIsProcessTrust) {
|
|
if (bind.mainIsProcessTrusted(prompt: false)) {
|
|
watchIsProcessTrust = false;
|
|
setState(() {});
|
|
}
|
|
}
|
|
if (watchIsInputMonitoring) {
|
|
if (bind.mainIsCanInputMonitoring(prompt: false)) {
|
|
watchIsInputMonitoring = false;
|
|
// Do not notify for now.
|
|
// Monitoring may not take effect until the process is restarted.
|
|
// rustDeskWinManager.call(
|
|
// WindowType.RemoteDesktop, kWindowDisableGrabKeyboard, '');
|
|
setState(() {});
|
|
}
|
|
}
|
|
if (watchIsCanRecordAudio) {
|
|
if (isMacOS) {
|
|
Future.microtask(() async {
|
|
if ((await osxCanRecordAudio() ==
|
|
PermissionAuthorizeType.authorized)) {
|
|
watchIsCanRecordAudio = false;
|
|
setState(() {});
|
|
}
|
|
});
|
|
} else {
|
|
watchIsCanRecordAudio = false;
|
|
setState(() {});
|
|
}
|
|
}
|
|
});
|
|
Get.put<RxBool>(svcStopped, tag: 'stop-service');
|
|
rustDeskWinManager.registerActiveWindowListener(onActiveWindowChanged);
|
|
|
|
screenToMap(window_size.Screen screen) => {
|
|
'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,
|
|
};
|
|
|
|
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
|
debugPrint(
|
|
"[Main] call ${call.method} with args ${call.arguments} from window $fromWindowId");
|
|
if (call.method == kWindowMainWindowOnTop) {
|
|
windowOnTop(null);
|
|
} else if (call.method == kWindowGetWindowInfo) {
|
|
final screen = (await window_size.getWindowInfo()).screen;
|
|
if (screen == null) {
|
|
return '';
|
|
} else {
|
|
return jsonEncode(screenToMap(screen));
|
|
}
|
|
} else if (call.method == kWindowGetScreenList) {
|
|
return jsonEncode(
|
|
(await window_size.getScreenList()).map(screenToMap).toList());
|
|
} else if (call.method == kWindowActionRebuild) {
|
|
reloadCurrentWindow();
|
|
} else if (call.method == kWindowEventShow) {
|
|
await rustDeskWinManager.registerActiveWindow(call.arguments["id"]);
|
|
} else if (call.method == kWindowEventHide) {
|
|
await rustDeskWinManager.unregisterActiveWindow(call.arguments['id']);
|
|
} else if (call.method == kWindowConnect) {
|
|
await connectMainDesktop(
|
|
call.arguments['id'],
|
|
isFileTransfer: call.arguments['isFileTransfer'],
|
|
isTcpTunneling: call.arguments['isTcpTunneling'],
|
|
isRDP: call.arguments['isRDP'],
|
|
password: call.arguments['password'],
|
|
forceRelay: call.arguments['forceRelay'],
|
|
);
|
|
} else if (call.method == kWindowEventMoveTabToNewWindow) {
|
|
final args = call.arguments.split(',');
|
|
int? windowId;
|
|
try {
|
|
windowId = int.parse(args[0]);
|
|
} catch (e) {
|
|
debugPrint("Failed to parse window id '${call.arguments}': $e");
|
|
}
|
|
if (windowId != null) {
|
|
await rustDeskWinManager.moveTabToNewWindow(
|
|
windowId, args[1], args[2]);
|
|
}
|
|
} else if (call.method == kWindowEventOpenMonitorSession) {
|
|
final args = jsonDecode(call.arguments);
|
|
final windowId = args['window_id'] as int;
|
|
final peerId = args['peer_id'] as String;
|
|
final display = args['display'] as int;
|
|
final displayCount = args['display_count'] as int;
|
|
final screenRect = parseParamScreenRect(args);
|
|
await rustDeskWinManager.openMonitorSession(
|
|
windowId, peerId, display, displayCount, screenRect);
|
|
}
|
|
});
|
|
_uniLinksSubscription = listenUniLinks();
|
|
|
|
if (bind.isIncomingOnly()) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_updateWindowSize();
|
|
});
|
|
}
|
|
}
|
|
|
|
_updateWindowSize() {
|
|
RenderObject? renderObject = _childKey.currentContext?.findRenderObject();
|
|
if (renderObject == null) {
|
|
return;
|
|
}
|
|
if (renderObject is RenderBox) {
|
|
final size = renderObject.size;
|
|
if (size != imcomingOnlyHomeSize) {
|
|
imcomingOnlyHomeSize = size;
|
|
windowManager.setSize(getIncomingOnlyHomeSize());
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_uniLinksSubscription?.cancel();
|
|
Get.delete<RxBool>(tag: 'stop-service');
|
|
_updateTimer?.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
Widget buildPluginEntry() {
|
|
final entries = PluginUiManager.instance.entries.entries;
|
|
return Offstage(
|
|
offstage: entries.isEmpty,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
...entries.map((entry) {
|
|
return entry.value;
|
|
})
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
void setPasswordDialog() async {
|
|
final pw = await bind.mainGetPermanentPassword();
|
|
final p0 = TextEditingController(text: pw);
|
|
final p1 = TextEditingController(text: pw);
|
|
var errMsg0 = "";
|
|
var errMsg1 = "";
|
|
final RxString rxPass = pw.trim().obs;
|
|
final rules = [
|
|
DigitValidationRule(),
|
|
UppercaseValidationRule(),
|
|
LowercaseValidationRule(),
|
|
// SpecialCharacterValidationRule(),
|
|
MinCharactersValidationRule(8),
|
|
];
|
|
|
|
gFFI.dialogManager.show((setState, close, context) {
|
|
submit() {
|
|
setState(() {
|
|
errMsg0 = "";
|
|
errMsg1 = "";
|
|
});
|
|
final pass = p0.text.trim();
|
|
if (pass.isNotEmpty) {
|
|
final Iterable violations = rules.where((r) => !r.validate(pass));
|
|
if (violations.isNotEmpty) {
|
|
setState(() {
|
|
errMsg0 =
|
|
'${translate('Prompt')}: ${violations.map((r) => r.name).join(', ')}';
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
if (p1.text.trim() != pass) {
|
|
setState(() {
|
|
errMsg1 =
|
|
'${translate('Prompt')}: ${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: [
|
|
Expanded(
|
|
child: TextField(
|
|
obscureText: true,
|
|
decoration: InputDecoration(
|
|
labelText: translate('Password'),
|
|
errorText: errMsg0.isNotEmpty ? errMsg0 : null),
|
|
controller: p0,
|
|
autofocus: true,
|
|
onChanged: (value) {
|
|
rxPass.value = value.trim();
|
|
setState(() {
|
|
errMsg0 = '';
|
|
});
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
Row(
|
|
children: [
|
|
Expanded(child: PasswordStrengthIndicator(password: rxPass)),
|
|
],
|
|
).marginSymmetric(vertical: 8),
|
|
const SizedBox(
|
|
height: 8.0,
|
|
),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: TextField(
|
|
obscureText: true,
|
|
decoration: InputDecoration(
|
|
labelText: translate('Confirmation'),
|
|
errorText: errMsg1.isNotEmpty ? errMsg1 : null),
|
|
controller: p1,
|
|
onChanged: (value) {
|
|
setState(() {
|
|
errMsg1 = '';
|
|
});
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(
|
|
height: 8.0,
|
|
),
|
|
Obx(() => Wrap(
|
|
runSpacing: 8,
|
|
spacing: 4,
|
|
children: rules.map((e) {
|
|
var checked = e.validate(rxPass.value.trim());
|
|
return Chip(
|
|
label: Text(
|
|
e.name,
|
|
style: TextStyle(
|
|
color: checked
|
|
? const Color(0xFF0A9471)
|
|
: Color.fromARGB(255, 198, 86, 157)),
|
|
),
|
|
backgroundColor: checked
|
|
? const Color(0xFFD0F7ED)
|
|
: Color.fromARGB(255, 247, 205, 232));
|
|
}).toList(),
|
|
))
|
|
],
|
|
),
|
|
),
|
|
actions: [
|
|
dialogButton("Cancel", onPressed: close, isOutline: true),
|
|
dialogButton("OK", onPressed: submit),
|
|
],
|
|
onSubmit: submit,
|
|
onCancel: close,
|
|
);
|
|
});
|
|
}
|