desktop_home_page.dart 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003
  1. import 'dart:async';
  2. import 'dart:io';
  3. import 'dart:convert';
  4. import 'package:auto_size_text/auto_size_text.dart';
  5. import 'package:flutter/material.dart';
  6. import 'package:flutter/services.dart';
  7. import 'package:flutter_hbb/common.dart';
  8. import 'package:flutter_hbb/common/widgets/animated_rotation_widget.dart';
  9. import 'package:flutter_hbb/common/widgets/custom_password.dart';
  10. import 'package:flutter_hbb/consts.dart';
  11. import 'package:flutter_hbb/desktop/pages/connection_page.dart';
  12. import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
  13. import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
  14. import 'package:flutter_hbb/models/platform_model.dart';
  15. import 'package:flutter_hbb/models/server_model.dart';
  16. import 'package:flutter_hbb/models/state_model.dart';
  17. import 'package:flutter_hbb/plugin/ui_manager.dart';
  18. import 'package:flutter_hbb/utils/multi_window_manager.dart';
  19. import 'package:get/get.dart';
  20. import 'package:provider/provider.dart';
  21. import 'package:url_launcher/url_launcher.dart';
  22. import 'package:window_manager/window_manager.dart';
  23. import 'package:window_size/window_size.dart' as window_size;
  24. import '../widgets/button.dart';
  25. class DesktopHomePage extends StatefulWidget {
  26. const DesktopHomePage({Key? key}) : super(key: key);
  27. @override
  28. State<DesktopHomePage> createState() => _DesktopHomePageState();
  29. }
  30. const borderColor = Color(0xFF2F65BA);
  31. class _DesktopHomePageState extends State<DesktopHomePage>
  32. with AutomaticKeepAliveClientMixin, WidgetsBindingObserver {
  33. final _leftPaneScrollController = ScrollController();
  34. @override
  35. bool get wantKeepAlive => true;
  36. var systemError = '';
  37. StreamSubscription? _uniLinksSubscription;
  38. var svcStopped = false.obs;
  39. var watchIsCanScreenRecording = false;
  40. var watchIsProcessTrust = false;
  41. var watchIsInputMonitoring = false;
  42. var watchIsCanRecordAudio = false;
  43. Timer? _updateTimer;
  44. bool isCardClosed = false;
  45. final RxBool _editHover = false.obs;
  46. final RxBool _block = false.obs;
  47. final GlobalKey _childKey = GlobalKey();
  48. @override
  49. Widget build(BuildContext context) {
  50. super.build(context);
  51. final isIncomingOnly = bind.isIncomingOnly();
  52. return _buildBlock(
  53. child: Row(
  54. crossAxisAlignment: CrossAxisAlignment.start,
  55. children: [
  56. buildLeftPane(context),
  57. if (!isIncomingOnly) const VerticalDivider(width: 1),
  58. if (!isIncomingOnly) Expanded(child: buildRightPane(context)),
  59. ],
  60. ));
  61. }
  62. Widget _buildBlock({required Widget child}) {
  63. return buildRemoteBlock(
  64. block: _block, mask: true, use: canBeBlocked, child: child);
  65. }
  66. Widget buildLeftPane(BuildContext context) {
  67. final isIncomingOnly = bind.isIncomingOnly();
  68. final isOutgoingOnly = bind.isOutgoingOnly();
  69. final children = <Widget>[
  70. if (!isOutgoingOnly) buildPresetPasswordWarning(),
  71. if (bind.isCustomClient())
  72. Align(
  73. alignment: Alignment.center,
  74. child: loadPowered(context),
  75. ),
  76. Align(
  77. alignment: Alignment.center,
  78. child: loadLogo(),
  79. ),
  80. buildTip(context),
  81. if (!isOutgoingOnly) buildIDBoard(context),
  82. if (!isOutgoingOnly) buildPasswordBoard(context),
  83. FutureBuilder<Widget>(
  84. future: Future.value(
  85. Obx(() => buildHelpCards(stateGlobal.updateUrl.value))),
  86. builder: (_, data) {
  87. if (data.hasData) {
  88. if (isIncomingOnly) {
  89. if (isInHomePage()) {
  90. Future.delayed(Duration(milliseconds: 300), () {
  91. _updateWindowSize();
  92. });
  93. }
  94. }
  95. return data.data!;
  96. } else {
  97. return const Offstage();
  98. }
  99. },
  100. ),
  101. buildPluginEntry(),
  102. ];
  103. if (isIncomingOnly) {
  104. children.addAll([
  105. Divider(),
  106. OnlineStatusWidget(
  107. onSvcStatusChanged: () {
  108. if (isInHomePage()) {
  109. Future.delayed(Duration(milliseconds: 300), () {
  110. _updateWindowSize();
  111. });
  112. }
  113. },
  114. ).marginOnly(bottom: 6, right: 6)
  115. ]);
  116. }
  117. final textColor = Theme.of(context).textTheme.titleLarge?.color;
  118. return ChangeNotifierProvider.value(
  119. value: gFFI.serverModel,
  120. child: Container(
  121. width: isIncomingOnly ? 280.0 : 200.0,
  122. color: Theme.of(context).colorScheme.background,
  123. child: Stack(
  124. children: [
  125. SingleChildScrollView(
  126. controller: _leftPaneScrollController,
  127. child: Column(
  128. key: _childKey,
  129. children: children,
  130. ),
  131. ),
  132. if (isOutgoingOnly)
  133. Positioned(
  134. bottom: 6,
  135. left: 12,
  136. child: Align(
  137. alignment: Alignment.centerLeft,
  138. child: InkWell(
  139. child: Obx(
  140. () => Icon(
  141. Icons.settings,
  142. color: _editHover.value
  143. ? textColor
  144. : Colors.grey.withOpacity(0.5),
  145. size: 22,
  146. ),
  147. ),
  148. onTap: () => {
  149. if (DesktopSettingPage.tabKeys.isNotEmpty)
  150. {
  151. DesktopSettingPage.switch2page(
  152. DesktopSettingPage.tabKeys[0])
  153. }
  154. },
  155. onHover: (value) => _editHover.value = value,
  156. ),
  157. ),
  158. )
  159. ],
  160. ),
  161. ),
  162. );
  163. }
  164. buildRightPane(BuildContext context) {
  165. return Container(
  166. color: Theme.of(context).scaffoldBackgroundColor,
  167. child: ConnectionPage(),
  168. );
  169. }
  170. buildIDBoard(BuildContext context) {
  171. final model = gFFI.serverModel;
  172. return Container(
  173. margin: const EdgeInsets.only(left: 20, right: 11),
  174. height: 57,
  175. child: Row(
  176. crossAxisAlignment: CrossAxisAlignment.baseline,
  177. textBaseline: TextBaseline.alphabetic,
  178. children: [
  179. Container(
  180. width: 2,
  181. decoration: const BoxDecoration(color: MyTheme.accent),
  182. ).marginOnly(top: 5),
  183. Expanded(
  184. child: Padding(
  185. padding: const EdgeInsets.only(left: 7),
  186. child: Column(
  187. crossAxisAlignment: CrossAxisAlignment.start,
  188. children: [
  189. Container(
  190. height: 25,
  191. child: Row(
  192. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  193. crossAxisAlignment: CrossAxisAlignment.start,
  194. children: [
  195. Text(
  196. translate("ID"),
  197. style: TextStyle(
  198. fontSize: 14,
  199. color: Theme.of(context)
  200. .textTheme
  201. .titleLarge
  202. ?.color
  203. ?.withOpacity(0.5)),
  204. ).marginOnly(top: 5),
  205. buildPopupMenu(context)
  206. ],
  207. ),
  208. ),
  209. Flexible(
  210. child: GestureDetector(
  211. onDoubleTap: () {
  212. Clipboard.setData(
  213. ClipboardData(text: model.serverId.text));
  214. showToast(translate("Copied"));
  215. },
  216. child: TextFormField(
  217. controller: model.serverId,
  218. readOnly: true,
  219. decoration: InputDecoration(
  220. border: InputBorder.none,
  221. contentPadding: EdgeInsets.only(top: 10, bottom: 10),
  222. ),
  223. style: TextStyle(
  224. fontSize: 22,
  225. ),
  226. ).workaroundFreezeLinuxMint(),
  227. ),
  228. )
  229. ],
  230. ),
  231. ),
  232. ),
  233. ],
  234. ),
  235. );
  236. }
  237. Widget buildPopupMenu(BuildContext context) {
  238. final textColor = Theme.of(context).textTheme.titleLarge?.color;
  239. RxBool hover = false.obs;
  240. return InkWell(
  241. onTap: DesktopTabPage.onAddSetting,
  242. child: Tooltip(
  243. message: translate('Settings'),
  244. child: Obx(
  245. () => CircleAvatar(
  246. radius: 15,
  247. backgroundColor: hover.value
  248. ? Theme.of(context).scaffoldBackgroundColor
  249. : Theme.of(context).colorScheme.background,
  250. child: Icon(
  251. Icons.more_vert_outlined,
  252. size: 20,
  253. color: hover.value ? textColor : textColor?.withOpacity(0.5),
  254. ),
  255. ),
  256. ),
  257. ),
  258. onHover: (value) => hover.value = value,
  259. );
  260. }
  261. buildPasswordBoard(BuildContext context) {
  262. return ChangeNotifierProvider.value(
  263. value: gFFI.serverModel,
  264. child: Consumer<ServerModel>(
  265. builder: (context, model, child) {
  266. return buildPasswordBoard2(context, model);
  267. },
  268. ));
  269. }
  270. buildPasswordBoard2(BuildContext context, ServerModel model) {
  271. RxBool refreshHover = false.obs;
  272. RxBool editHover = false.obs;
  273. final textColor = Theme.of(context).textTheme.titleLarge?.color;
  274. final showOneTime = model.approveMode != 'click' &&
  275. model.verificationMethod != kUsePermanentPassword;
  276. return Container(
  277. margin: EdgeInsets.only(left: 20.0, right: 16, top: 13, bottom: 13),
  278. child: Row(
  279. crossAxisAlignment: CrossAxisAlignment.baseline,
  280. textBaseline: TextBaseline.alphabetic,
  281. children: [
  282. Container(
  283. width: 2,
  284. height: 52,
  285. decoration: BoxDecoration(color: MyTheme.accent),
  286. ),
  287. Expanded(
  288. child: Padding(
  289. padding: const EdgeInsets.only(left: 7),
  290. child: Column(
  291. crossAxisAlignment: CrossAxisAlignment.start,
  292. children: [
  293. AutoSizeText(
  294. translate("One-time Password"),
  295. style: TextStyle(
  296. fontSize: 14, color: textColor?.withOpacity(0.5)),
  297. maxLines: 1,
  298. ),
  299. Row(
  300. children: [
  301. Expanded(
  302. child: GestureDetector(
  303. onDoubleTap: () {
  304. if (showOneTime) {
  305. Clipboard.setData(
  306. ClipboardData(text: model.serverPasswd.text));
  307. showToast(translate("Copied"));
  308. }
  309. },
  310. child: TextFormField(
  311. controller: model.serverPasswd,
  312. readOnly: true,
  313. decoration: InputDecoration(
  314. border: InputBorder.none,
  315. contentPadding:
  316. EdgeInsets.only(top: 14, bottom: 10),
  317. ),
  318. style: TextStyle(fontSize: 15),
  319. ).workaroundFreezeLinuxMint(),
  320. ),
  321. ),
  322. if (showOneTime)
  323. AnimatedRotationWidget(
  324. onPressed: () => bind.mainUpdateTemporaryPassword(),
  325. child: Tooltip(
  326. message: translate('Refresh Password'),
  327. child: Obx(() => RotatedBox(
  328. quarterTurns: 2,
  329. child: Icon(
  330. Icons.refresh,
  331. color: refreshHover.value
  332. ? textColor
  333. : Color(0xFFDDDDDD),
  334. size: 22,
  335. ))),
  336. ),
  337. onHover: (value) => refreshHover.value = value,
  338. ).marginOnly(right: 8, top: 4),
  339. if (!bind.isDisableSettings())
  340. InkWell(
  341. child: Tooltip(
  342. message: translate('Change Password'),
  343. child: Obx(
  344. () => Icon(
  345. Icons.edit,
  346. color: editHover.value
  347. ? textColor
  348. : Color(0xFFDDDDDD),
  349. size: 22,
  350. ).marginOnly(right: 8, top: 4),
  351. ),
  352. ),
  353. onTap: () => DesktopSettingPage.switch2page(
  354. SettingsTabKey.safety),
  355. onHover: (value) => editHover.value = value,
  356. ),
  357. ],
  358. ),
  359. ],
  360. ),
  361. ),
  362. ),
  363. ],
  364. ),
  365. );
  366. }
  367. buildTip(BuildContext context) {
  368. final isOutgoingOnly = bind.isOutgoingOnly();
  369. return Padding(
  370. padding:
  371. const EdgeInsets.only(left: 20.0, right: 16, top: 16.0, bottom: 5),
  372. child: Column(
  373. mainAxisAlignment: MainAxisAlignment.start,
  374. crossAxisAlignment: CrossAxisAlignment.start,
  375. children: [
  376. Column(
  377. children: [
  378. if (!isOutgoingOnly)
  379. Align(
  380. alignment: Alignment.centerLeft,
  381. child: Text(
  382. translate("Your Desktop"),
  383. style: Theme.of(context).textTheme.titleLarge,
  384. ),
  385. ),
  386. ],
  387. ),
  388. SizedBox(
  389. height: 10.0,
  390. ),
  391. if (!isOutgoingOnly)
  392. Text(
  393. translate("desk_tip"),
  394. overflow: TextOverflow.clip,
  395. style: Theme.of(context).textTheme.bodySmall,
  396. ),
  397. if (isOutgoingOnly)
  398. Text(
  399. translate("outgoing_only_desk_tip"),
  400. overflow: TextOverflow.clip,
  401. style: Theme.of(context).textTheme.bodySmall,
  402. ),
  403. ],
  404. ),
  405. );
  406. }
  407. Widget buildHelpCards(String updateUrl) {
  408. if (!bind.isCustomClient() &&
  409. updateUrl.isNotEmpty &&
  410. !isCardClosed &&
  411. bind.mainUriPrefixSync().contains('rustdesk')) {
  412. return buildInstallCard(
  413. "Status",
  414. "${translate("new-version-of-{${bind.mainGetAppNameSync()}}-tip")} (${bind.mainGetNewVersion()}).",
  415. "Click to download", () async {
  416. final Uri url = Uri.parse('https://rustdesk.com/download');
  417. await launchUrl(url);
  418. }, closeButton: true);
  419. }
  420. if (systemError.isNotEmpty) {
  421. return buildInstallCard("", systemError, "", () {});
  422. }
  423. if (isWindows && !bind.isDisableInstallation()) {
  424. if (!bind.mainIsInstalled()) {
  425. return buildInstallCard(
  426. "", bind.isOutgoingOnly() ? "" : "install_tip", "Install",
  427. () async {
  428. await rustDeskWinManager.closeAllSubWindows();
  429. bind.mainGotoInstall();
  430. });
  431. } else if (bind.mainIsInstalledLowerVersion()) {
  432. return buildInstallCard(
  433. "Status", "Your installation is lower version.", "Click to upgrade",
  434. () async {
  435. await rustDeskWinManager.closeAllSubWindows();
  436. bind.mainUpdateMe();
  437. });
  438. }
  439. } else if (isMacOS) {
  440. final isOutgoingOnly = bind.isOutgoingOnly();
  441. if (!(isOutgoingOnly || bind.mainIsCanScreenRecording(prompt: false))) {
  442. return buildInstallCard("Permissions", "config_screen", "Configure",
  443. () async {
  444. bind.mainIsCanScreenRecording(prompt: true);
  445. watchIsCanScreenRecording = true;
  446. }, help: 'Help', link: translate("doc_mac_permission"));
  447. } else if (!isOutgoingOnly && !bind.mainIsProcessTrusted(prompt: false)) {
  448. return buildInstallCard("Permissions", "config_acc", "Configure",
  449. () async {
  450. bind.mainIsProcessTrusted(prompt: true);
  451. watchIsProcessTrust = true;
  452. }, help: 'Help', link: translate("doc_mac_permission"));
  453. } else if (!bind.mainIsCanInputMonitoring(prompt: false)) {
  454. return buildInstallCard("Permissions", "config_input", "Configure",
  455. () async {
  456. bind.mainIsCanInputMonitoring(prompt: true);
  457. watchIsInputMonitoring = true;
  458. }, help: 'Help', link: translate("doc_mac_permission"));
  459. } else if (!isOutgoingOnly &&
  460. !svcStopped.value &&
  461. bind.mainIsInstalled() &&
  462. !bind.mainIsInstalledDaemon(prompt: false)) {
  463. return buildInstallCard("", "install_daemon_tip", "Install", () async {
  464. bind.mainIsInstalledDaemon(prompt: true);
  465. });
  466. }
  467. //// Disable microphone configuration for macOS. We will request the permission when needed.
  468. // else if ((await osxCanRecordAudio() !=
  469. // PermissionAuthorizeType.authorized)) {
  470. // return buildInstallCard("Permissions", "config_microphone", "Configure",
  471. // () async {
  472. // osxRequestAudio();
  473. // watchIsCanRecordAudio = true;
  474. // });
  475. // }
  476. } else if (isLinux) {
  477. if (bind.isOutgoingOnly()) {
  478. return Container();
  479. }
  480. final LinuxCards = <Widget>[];
  481. if (bind.isSelinuxEnforcing()) {
  482. // Check is SELinux enforcing, but show user a tip of is SELinux enabled for simple.
  483. final keyShowSelinuxHelpTip = "show-selinux-help-tip";
  484. if (bind.mainGetLocalOption(key: keyShowSelinuxHelpTip) != 'N') {
  485. LinuxCards.add(buildInstallCard(
  486. "Warning",
  487. "selinux_tip",
  488. "",
  489. () async {},
  490. marginTop: LinuxCards.isEmpty ? 20.0 : 5.0,
  491. help: 'Help',
  492. link:
  493. 'https://rustdesk.com/docs/en/client/linux/#permissions-issue',
  494. closeButton: true,
  495. closeOption: keyShowSelinuxHelpTip,
  496. ));
  497. }
  498. }
  499. if (bind.mainCurrentIsWayland()) {
  500. LinuxCards.add(buildInstallCard(
  501. "Warning", "wayland_experiment_tip", "", () async {},
  502. marginTop: LinuxCards.isEmpty ? 20.0 : 5.0,
  503. help: 'Help',
  504. link: 'https://rustdesk.com/docs/en/client/linux/#x11-required'));
  505. } else if (bind.mainIsLoginWayland()) {
  506. LinuxCards.add(buildInstallCard("Warning",
  507. "Login screen using Wayland is not supported", "", () async {},
  508. marginTop: LinuxCards.isEmpty ? 20.0 : 5.0,
  509. help: 'Help',
  510. link: 'https://rustdesk.com/docs/en/client/linux/#login-screen'));
  511. }
  512. if (LinuxCards.isNotEmpty) {
  513. return Column(
  514. children: LinuxCards,
  515. );
  516. }
  517. }
  518. if (bind.isIncomingOnly()) {
  519. return Align(
  520. alignment: Alignment.centerRight,
  521. child: OutlinedButton(
  522. onPressed: () {
  523. SystemNavigator.pop(); // Close the application
  524. // https://github.com/flutter/flutter/issues/66631
  525. if (isWindows) {
  526. exit(0);
  527. }
  528. },
  529. child: Text(translate('Quit')),
  530. ),
  531. ).marginAll(14);
  532. }
  533. return Container();
  534. }
  535. Widget buildInstallCard(String title, String content, String btnText,
  536. GestureTapCallback onPressed,
  537. {double marginTop = 20.0,
  538. String? help,
  539. String? link,
  540. bool? closeButton,
  541. String? closeOption}) {
  542. if (bind.mainGetBuildinOption(key: kOptionHideHelpCards) == 'Y' &&
  543. content != 'install_daemon_tip') {
  544. return const SizedBox();
  545. }
  546. void closeCard() async {
  547. if (closeOption != null) {
  548. await bind.mainSetLocalOption(key: closeOption, value: 'N');
  549. if (bind.mainGetLocalOption(key: closeOption) == 'N') {
  550. setState(() {
  551. isCardClosed = true;
  552. });
  553. }
  554. } else {
  555. setState(() {
  556. isCardClosed = true;
  557. });
  558. }
  559. }
  560. return Stack(
  561. children: [
  562. Container(
  563. margin: EdgeInsets.fromLTRB(
  564. 0, marginTop, 0, bind.isIncomingOnly() ? marginTop : 0),
  565. child: Container(
  566. decoration: BoxDecoration(
  567. gradient: LinearGradient(
  568. begin: Alignment.centerLeft,
  569. end: Alignment.centerRight,
  570. colors: [
  571. Color.fromARGB(255, 226, 66, 188),
  572. Color.fromARGB(255, 244, 114, 124),
  573. ],
  574. )),
  575. padding: EdgeInsets.all(20),
  576. child: Column(
  577. mainAxisAlignment: MainAxisAlignment.start,
  578. crossAxisAlignment: CrossAxisAlignment.start,
  579. children: (title.isNotEmpty
  580. ? <Widget>[
  581. Center(
  582. child: Text(
  583. translate(title),
  584. style: TextStyle(
  585. color: Colors.white,
  586. fontWeight: FontWeight.bold,
  587. fontSize: 15),
  588. ).marginOnly(bottom: 6)),
  589. ]
  590. : <Widget>[]) +
  591. <Widget>[
  592. if (content.isNotEmpty)
  593. Text(
  594. translate(content),
  595. style: TextStyle(
  596. height: 1.5,
  597. color: Colors.white,
  598. fontWeight: FontWeight.normal,
  599. fontSize: 13),
  600. ).marginOnly(bottom: 20)
  601. ] +
  602. (btnText.isNotEmpty
  603. ? <Widget>[
  604. Row(
  605. mainAxisAlignment: MainAxisAlignment.center,
  606. children: [
  607. FixedWidthButton(
  608. width: 150,
  609. padding: 8,
  610. isOutline: true,
  611. text: translate(btnText),
  612. textColor: Colors.white,
  613. borderColor: Colors.white,
  614. textSize: 20,
  615. radius: 10,
  616. onTap: onPressed,
  617. )
  618. ])
  619. ]
  620. : <Widget>[]) +
  621. (help != null
  622. ? <Widget>[
  623. Center(
  624. child: InkWell(
  625. onTap: () async =>
  626. await launchUrl(Uri.parse(link!)),
  627. child: Text(
  628. translate(help),
  629. style: TextStyle(
  630. decoration:
  631. TextDecoration.underline,
  632. color: Colors.white,
  633. fontSize: 12),
  634. )).marginOnly(top: 6)),
  635. ]
  636. : <Widget>[]))),
  637. ),
  638. if (closeButton != null && closeButton == true)
  639. Positioned(
  640. top: 18,
  641. right: 0,
  642. child: IconButton(
  643. icon: Icon(
  644. Icons.close,
  645. color: Colors.white,
  646. size: 20,
  647. ),
  648. onPressed: closeCard,
  649. ),
  650. ),
  651. ],
  652. );
  653. }
  654. @override
  655. void initState() {
  656. super.initState();
  657. _updateTimer = periodic_immediate(const Duration(seconds: 1), () async {
  658. await gFFI.serverModel.fetchID();
  659. final error = await bind.mainGetError();
  660. if (systemError != error) {
  661. systemError = error;
  662. setState(() {});
  663. }
  664. final v = await mainGetBoolOption(kOptionStopService);
  665. if (v != svcStopped.value) {
  666. svcStopped.value = v;
  667. setState(() {});
  668. }
  669. if (watchIsCanScreenRecording) {
  670. if (bind.mainIsCanScreenRecording(prompt: false)) {
  671. watchIsCanScreenRecording = false;
  672. setState(() {});
  673. }
  674. }
  675. if (watchIsProcessTrust) {
  676. if (bind.mainIsProcessTrusted(prompt: false)) {
  677. watchIsProcessTrust = false;
  678. setState(() {});
  679. }
  680. }
  681. if (watchIsInputMonitoring) {
  682. if (bind.mainIsCanInputMonitoring(prompt: false)) {
  683. watchIsInputMonitoring = false;
  684. // Do not notify for now.
  685. // Monitoring may not take effect until the process is restarted.
  686. // rustDeskWinManager.call(
  687. // WindowType.RemoteDesktop, kWindowDisableGrabKeyboard, '');
  688. setState(() {});
  689. }
  690. }
  691. if (watchIsCanRecordAudio) {
  692. if (isMacOS) {
  693. Future.microtask(() async {
  694. if ((await osxCanRecordAudio() ==
  695. PermissionAuthorizeType.authorized)) {
  696. watchIsCanRecordAudio = false;
  697. setState(() {});
  698. }
  699. });
  700. } else {
  701. watchIsCanRecordAudio = false;
  702. setState(() {});
  703. }
  704. }
  705. });
  706. Get.put<RxBool>(svcStopped, tag: 'stop-service');
  707. rustDeskWinManager.registerActiveWindowListener(onActiveWindowChanged);
  708. screenToMap(window_size.Screen screen) => {
  709. 'frame': {
  710. 'l': screen.frame.left,
  711. 't': screen.frame.top,
  712. 'r': screen.frame.right,
  713. 'b': screen.frame.bottom,
  714. },
  715. 'visibleFrame': {
  716. 'l': screen.visibleFrame.left,
  717. 't': screen.visibleFrame.top,
  718. 'r': screen.visibleFrame.right,
  719. 'b': screen.visibleFrame.bottom,
  720. },
  721. 'scaleFactor': screen.scaleFactor,
  722. };
  723. rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
  724. debugPrint(
  725. "[Main] call ${call.method} with args ${call.arguments} from window $fromWindowId");
  726. if (call.method == kWindowMainWindowOnTop) {
  727. windowOnTop(null);
  728. } else if (call.method == kWindowGetWindowInfo) {
  729. final screen = (await window_size.getWindowInfo()).screen;
  730. if (screen == null) {
  731. return '';
  732. } else {
  733. return jsonEncode(screenToMap(screen));
  734. }
  735. } else if (call.method == kWindowGetScreenList) {
  736. return jsonEncode(
  737. (await window_size.getScreenList()).map(screenToMap).toList());
  738. } else if (call.method == kWindowActionRebuild) {
  739. reloadCurrentWindow();
  740. } else if (call.method == kWindowEventShow) {
  741. await rustDeskWinManager.registerActiveWindow(call.arguments["id"]);
  742. } else if (call.method == kWindowEventHide) {
  743. await rustDeskWinManager.unregisterActiveWindow(call.arguments['id']);
  744. } else if (call.method == kWindowConnect) {
  745. await connectMainDesktop(
  746. call.arguments['id'],
  747. isFileTransfer: call.arguments['isFileTransfer'],
  748. isTcpTunneling: call.arguments['isTcpTunneling'],
  749. isRDP: call.arguments['isRDP'],
  750. password: call.arguments['password'],
  751. forceRelay: call.arguments['forceRelay'],
  752. connToken: call.arguments['connToken'],
  753. );
  754. } else if (call.method == kWindowEventMoveTabToNewWindow) {
  755. final args = call.arguments.split(',');
  756. int? windowId;
  757. try {
  758. windowId = int.parse(args[0]);
  759. } catch (e) {
  760. debugPrint("Failed to parse window id '${call.arguments}': $e");
  761. }
  762. if (windowId != null) {
  763. await rustDeskWinManager.moveTabToNewWindow(
  764. windowId, args[1], args[2]);
  765. }
  766. } else if (call.method == kWindowEventOpenMonitorSession) {
  767. final args = jsonDecode(call.arguments);
  768. final windowId = args['window_id'] as int;
  769. final peerId = args['peer_id'] as String;
  770. final display = args['display'] as int;
  771. final displayCount = args['display_count'] as int;
  772. final screenRect = parseParamScreenRect(args);
  773. await rustDeskWinManager.openMonitorSession(
  774. windowId, peerId, display, displayCount, screenRect);
  775. } else if (call.method == kWindowEventRemoteWindowCoords) {
  776. final windowId = int.tryParse(call.arguments);
  777. if (windowId != null) {
  778. return jsonEncode(
  779. await rustDeskWinManager.getOtherRemoteWindowCoords(windowId));
  780. }
  781. }
  782. });
  783. _uniLinksSubscription = listenUniLinks();
  784. if (bind.isIncomingOnly()) {
  785. WidgetsBinding.instance.addPostFrameCallback((_) {
  786. _updateWindowSize();
  787. });
  788. }
  789. WidgetsBinding.instance.addObserver(this);
  790. }
  791. _updateWindowSize() {
  792. RenderObject? renderObject = _childKey.currentContext?.findRenderObject();
  793. if (renderObject == null) {
  794. return;
  795. }
  796. if (renderObject is RenderBox) {
  797. final size = renderObject.size;
  798. if (size != imcomingOnlyHomeSize) {
  799. imcomingOnlyHomeSize = size;
  800. windowManager.setSize(getIncomingOnlyHomeSize());
  801. }
  802. }
  803. }
  804. @override
  805. void dispose() {
  806. _uniLinksSubscription?.cancel();
  807. Get.delete<RxBool>(tag: 'stop-service');
  808. _updateTimer?.cancel();
  809. WidgetsBinding.instance.removeObserver(this);
  810. super.dispose();
  811. }
  812. @override
  813. void didChangeAppLifecycleState(AppLifecycleState state) {
  814. super.didChangeAppLifecycleState(state);
  815. if (state == AppLifecycleState.resumed) {
  816. shouldBeBlocked(_block, canBeBlocked);
  817. }
  818. }
  819. Widget buildPluginEntry() {
  820. final entries = PluginUiManager.instance.entries.entries;
  821. return Offstage(
  822. offstage: entries.isEmpty,
  823. child: Column(
  824. crossAxisAlignment: CrossAxisAlignment.start,
  825. children: [
  826. ...entries.map((entry) {
  827. return entry.value;
  828. })
  829. ],
  830. ),
  831. );
  832. }
  833. }
  834. void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
  835. final pw = await bind.mainGetPermanentPassword();
  836. final p0 = TextEditingController(text: pw);
  837. final p1 = TextEditingController(text: pw);
  838. var errMsg0 = "";
  839. var errMsg1 = "";
  840. final RxString rxPass = pw.trim().obs;
  841. final rules = [
  842. DigitValidationRule(),
  843. UppercaseValidationRule(),
  844. LowercaseValidationRule(),
  845. // SpecialCharacterValidationRule(),
  846. MinCharactersValidationRule(8),
  847. ];
  848. final maxLength = bind.mainMaxEncryptLen();
  849. gFFI.dialogManager.show((setState, close, context) {
  850. submit() {
  851. setState(() {
  852. errMsg0 = "";
  853. errMsg1 = "";
  854. });
  855. final pass = p0.text.trim();
  856. if (pass.isNotEmpty) {
  857. final Iterable violations = rules.where((r) => !r.validate(pass));
  858. if (violations.isNotEmpty) {
  859. setState(() {
  860. errMsg0 =
  861. '${translate('Prompt')}: ${violations.map((r) => r.name).join(', ')}';
  862. });
  863. return;
  864. }
  865. }
  866. if (p1.text.trim() != pass) {
  867. setState(() {
  868. errMsg1 =
  869. '${translate('Prompt')}: ${translate("The confirmation is not identical.")}';
  870. });
  871. return;
  872. }
  873. bind.mainSetPermanentPassword(password: pass);
  874. if (pass.isNotEmpty) {
  875. notEmptyCallback?.call();
  876. }
  877. close();
  878. }
  879. return CustomAlertDialog(
  880. title: Text(translate("Set Password")),
  881. content: ConstrainedBox(
  882. constraints: const BoxConstraints(minWidth: 500),
  883. child: Column(
  884. crossAxisAlignment: CrossAxisAlignment.start,
  885. children: [
  886. const SizedBox(
  887. height: 8.0,
  888. ),
  889. Row(
  890. children: [
  891. Expanded(
  892. child: TextField(
  893. obscureText: true,
  894. decoration: InputDecoration(
  895. labelText: translate('Password'),
  896. errorText: errMsg0.isNotEmpty ? errMsg0 : null),
  897. controller: p0,
  898. autofocus: true,
  899. onChanged: (value) {
  900. rxPass.value = value.trim();
  901. setState(() {
  902. errMsg0 = '';
  903. });
  904. },
  905. maxLength: maxLength,
  906. ).workaroundFreezeLinuxMint(),
  907. ),
  908. ],
  909. ),
  910. Row(
  911. children: [
  912. Expanded(child: PasswordStrengthIndicator(password: rxPass)),
  913. ],
  914. ).marginSymmetric(vertical: 8),
  915. const SizedBox(
  916. height: 8.0,
  917. ),
  918. Row(
  919. children: [
  920. Expanded(
  921. child: TextField(
  922. obscureText: true,
  923. decoration: InputDecoration(
  924. labelText: translate('Confirmation'),
  925. errorText: errMsg1.isNotEmpty ? errMsg1 : null),
  926. controller: p1,
  927. onChanged: (value) {
  928. setState(() {
  929. errMsg1 = '';
  930. });
  931. },
  932. maxLength: maxLength,
  933. ).workaroundFreezeLinuxMint(),
  934. ),
  935. ],
  936. ),
  937. const SizedBox(
  938. height: 8.0,
  939. ),
  940. Obx(() => Wrap(
  941. runSpacing: 8,
  942. spacing: 4,
  943. children: rules.map((e) {
  944. var checked = e.validate(rxPass.value.trim());
  945. return Chip(
  946. label: Text(
  947. e.name,
  948. style: TextStyle(
  949. color: checked
  950. ? const Color(0xFF0A9471)
  951. : Color.fromARGB(255, 198, 86, 157)),
  952. ),
  953. backgroundColor: checked
  954. ? const Color(0xFFD0F7ED)
  955. : Color.fromARGB(255, 247, 205, 232));
  956. }).toList(),
  957. ))
  958. ],
  959. ),
  960. ),
  961. actions: [
  962. dialogButton("Cancel", onPressed: close, isOutline: true),
  963. dialogButton("OK", onPressed: submit),
  964. ],
  965. onSubmit: submit,
  966. onCancel: close,
  967. );
  968. });
  969. }