831 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			831 lines
		
	
	
		
			29 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_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;
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     super.build(context);
 | |
|     return Row(
 | |
|       crossAxisAlignment: CrossAxisAlignment.start,
 | |
|       children: [
 | |
|         buildLeftPane(context),
 | |
|         const VerticalDivider(width: 1),
 | |
|         Expanded(
 | |
|           child: buildRightPane(context),
 | |
|         ),
 | |
|       ],
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   Widget buildLeftPane(BuildContext context) {
 | |
|     return ChangeNotifierProvider.value(
 | |
|       value: gFFI.serverModel,
 | |
|       child: Container(
 | |
|         width: 200,
 | |
|         color: Theme.of(context).colorScheme.background,
 | |
|         child: DesktopScrollWrapper(
 | |
|           scrollController: _leftPaneScrollController,
 | |
|           child: SingleChildScrollView(
 | |
|             controller: _leftPaneScrollController,
 | |
|             physics: DraggableNeverScrollableScrollPhysics(),
 | |
|             child: Column(
 | |
|               children: [
 | |
|                 buildTip(context),
 | |
|                 buildIDBoard(context),
 | |
|                 buildPasswordBoard(context),
 | |
|                 FutureBuilder<Widget>(
 | |
|                   future: buildHelpCards(),
 | |
|                   builder: (_, data) {
 | |
|                     if (data.hasData) {
 | |
|                       return data.data!;
 | |
|                     } else {
 | |
|                       return const Offstage();
 | |
|                     }
 | |
|                   },
 | |
|                 ),
 | |
|                 buildPluginEntry()
 | |
|               ],
 | |
|             ),
 | |
|           ),
 | |
|         ),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   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),
 | |
|                       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(1),
 | |
|                         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: Theme.of(context).textTheme.titleLarge,
 | |
|             // style: TextStyle(
 | |
|             //     // color: MyTheme.color(context).text,
 | |
|             //     fontWeight: FontWeight.normal,
 | |
|             //     fontSize: 19),
 | |
|           ),
 | |
|           SizedBox(
 | |
|             height: 10.0,
 | |
|           ),
 | |
|           Text(
 | |
|             translate("desk_tip"),
 | |
|             overflow: TextOverflow.clip,
 | |
|             style: Theme.of(context).textTheme.bodySmall,
 | |
|           )
 | |
|         ],
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   Future<Widget> buildHelpCards() async {
 | |
|     if (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 (Platform.isWindows) {
 | |
|       if (!bind.mainIsInstalled()) {
 | |
|         return buildInstallCard("", "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 (Platform.isMacOS) {
 | |
|       if (!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 (Platform.isLinux) {
 | |
|       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,
 | |
|         );
 | |
|       }
 | |
|     }
 | |
|     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.only(top: marginTop),
 | |
|           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>[
 | |
|                         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 (Platform.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'],
 | |
|           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();
 | |
|   }
 | |
| 
 | |
|   @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,
 | |
|     );
 | |
|   });
 | |
| }
 |