desktop_home_page.dart 35 KB

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