971 lines
		
	
	
		
			33 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			971 lines
		
	
	
		
			33 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
import 'dart:async';
 | 
						|
import 'dart:io';
 | 
						|
import 'dart:convert';
 | 
						|
 | 
						|
import 'package:auto_size_text/auto_size_text.dart';
 | 
						|
import 'package:flutter/material.dart';
 | 
						|
import 'package:flutter/services.dart';
 | 
						|
import 'package:flutter_hbb/common.dart';
 | 
						|
import 'package:flutter_hbb/common/widgets/animated_rotation_widget.dart';
 | 
						|
import 'package:flutter_hbb/common/widgets/custom_password.dart';
 | 
						|
import 'package:flutter_hbb/consts.dart';
 | 
						|
import 'package:flutter_hbb/desktop/pages/connection_page.dart';
 | 
						|
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
 | 
						|
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
 | 
						|
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
 | 
						|
import 'package:flutter_hbb/models/platform_model.dart';
 | 
						|
import 'package:flutter_hbb/models/server_model.dart';
 | 
						|
import 'package:flutter_hbb/plugin/ui_manager.dart';
 | 
						|
import 'package:flutter_hbb/utils/multi_window_manager.dart';
 | 
						|
import 'package:get/get.dart';
 | 
						|
import 'package:provider/provider.dart';
 | 
						|
import 'package:url_launcher/url_launcher.dart';
 | 
						|
import 'package:window_manager/window_manager.dart';
 | 
						|
import 'package:window_size/window_size.dart' as window_size;
 | 
						|
 | 
						|
import '../widgets/button.dart';
 | 
						|
 | 
						|
class DesktopHomePage extends StatefulWidget {
 | 
						|
  const DesktopHomePage({Key? key}) : super(key: key);
 | 
						|
 | 
						|
  @override
 | 
						|
  State<DesktopHomePage> createState() => _DesktopHomePageState();
 | 
						|
}
 | 
						|
 | 
						|
const borderColor = Color(0xFF2F65BA);
 | 
						|
 | 
						|
class _DesktopHomePageState extends State<DesktopHomePage>
 | 
						|
    with AutomaticKeepAliveClientMixin {
 | 
						|
  final _leftPaneScrollController = ScrollController();
 | 
						|
 | 
						|
  @override
 | 
						|
  bool get wantKeepAlive => true;
 | 
						|
  var updateUrl = '';
 | 
						|
  var systemError = '';
 | 
						|
  StreamSubscription? _uniLinksSubscription;
 | 
						|
  var svcStopped = false.obs;
 | 
						|
  var watchIsCanScreenRecording = false;
 | 
						|
  var watchIsProcessTrust = false;
 | 
						|
  var watchIsInputMonitoring = false;
 | 
						|
  var watchIsCanRecordAudio = false;
 | 
						|
  Timer? _updateTimer;
 | 
						|
  bool isCardClosed = false;
 | 
						|
 | 
						|
  final RxBool _editHover = false.obs;
 | 
						|
 | 
						|
  final GlobalKey _childKey = GlobalKey();
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    super.build(context);
 | 
						|
    final isIncomingOnly = bind.isIncomingOnly();
 | 
						|
    return Row(
 | 
						|
      crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
      children: [
 | 
						|
        buildLeftPane(context),
 | 
						|
        if (!isIncomingOnly) const VerticalDivider(width: 1),
 | 
						|
        if (!isIncomingOnly) Expanded(child: buildRightPane(context)),
 | 
						|
      ],
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  Widget buildLeftPane(BuildContext context) {
 | 
						|
    final isIncomingOnly = bind.isIncomingOnly();
 | 
						|
    final isOutgoingOnly = bind.isOutgoingOnly();
 | 
						|
    final children = <Widget>[
 | 
						|
      if (!isOutgoingOnly) buildPresetPasswordWarning(),
 | 
						|
      if (bind.isCustomClient())
 | 
						|
        Align(
 | 
						|
          alignment: Alignment.center,
 | 
						|
          child: loadPowered(context),
 | 
						|
        ),
 | 
						|
      Align(
 | 
						|
        alignment: Alignment.center,
 | 
						|
        child: loadLogo(),
 | 
						|
      ),
 | 
						|
      buildTip(context),
 | 
						|
      if (!isOutgoingOnly) buildIDBoard(context),
 | 
						|
      if (!isOutgoingOnly) buildPasswordBoard(context),
 | 
						|
      FutureBuilder<Widget>(
 | 
						|
        future: buildHelpCards(),
 | 
						|
        builder: (_, data) {
 | 
						|
          if (data.hasData) {
 | 
						|
            if (isIncomingOnly) {
 | 
						|
              if (isInHomePage()) {
 | 
						|
                Future.delayed(Duration(milliseconds: 300), () {
 | 
						|
                  _updateWindowSize();
 | 
						|
                });
 | 
						|
              }
 | 
						|
            }
 | 
						|
            return data.data!;
 | 
						|
          } else {
 | 
						|
            return const Offstage();
 | 
						|
          }
 | 
						|
        },
 | 
						|
      ),
 | 
						|
      buildPluginEntry(),
 | 
						|
    ];
 | 
						|
    if (isIncomingOnly) {
 | 
						|
      children.addAll([
 | 
						|
        Divider(),
 | 
						|
        OnlineStatusWidget(
 | 
						|
          onSvcStatusChanged: () {
 | 
						|
            if (isInHomePage()) {
 | 
						|
              Future.delayed(Duration(milliseconds: 300), () {
 | 
						|
                _updateWindowSize();
 | 
						|
              });
 | 
						|
            }
 | 
						|
          },
 | 
						|
        ).marginOnly(bottom: 6, right: 6)
 | 
						|
      ]);
 | 
						|
    }
 | 
						|
    final textColor = Theme.of(context).textTheme.titleLarge?.color;
 | 
						|
    return ChangeNotifierProvider.value(
 | 
						|
      value: gFFI.serverModel,
 | 
						|
      child: Container(
 | 
						|
        width: isIncomingOnly ? 280.0 : 200.0,
 | 
						|
        color: Theme.of(context).colorScheme.background,
 | 
						|
        child: DesktopScrollWrapper(
 | 
						|
          scrollController: _leftPaneScrollController,
 | 
						|
          child: Stack(
 | 
						|
            children: [
 | 
						|
              SingleChildScrollView(
 | 
						|
                controller: _leftPaneScrollController,
 | 
						|
                physics: DraggableNeverScrollableScrollPhysics(),
 | 
						|
                child: Column(
 | 
						|
                  key: _childKey,
 | 
						|
                  children: children,
 | 
						|
                ),
 | 
						|
              ),
 | 
						|
              if (isOutgoingOnly)
 | 
						|
                Positioned(
 | 
						|
                  bottom: 6,
 | 
						|
                  left: 12,
 | 
						|
                  child: Align(
 | 
						|
                    alignment: Alignment.centerLeft,
 | 
						|
                    child: InkWell(
 | 
						|
                      child: Obx(
 | 
						|
                        () => Icon(
 | 
						|
                          Icons.settings,
 | 
						|
                          color: _editHover.value
 | 
						|
                              ? textColor
 | 
						|
                              : Colors.grey.withOpacity(0.5),
 | 
						|
                          size: 22,
 | 
						|
                        ),
 | 
						|
                      ),
 | 
						|
                      onTap: () => {
 | 
						|
                        if (DesktopSettingPage.tabKeys.isNotEmpty)
 | 
						|
                          {
 | 
						|
                            DesktopSettingPage.switch2page(
 | 
						|
                                DesktopSettingPage.tabKeys[0])
 | 
						|
                          }
 | 
						|
                      },
 | 
						|
                      onHover: (value) => _editHover.value = value,
 | 
						|
                    ),
 | 
						|
                  ),
 | 
						|
                )
 | 
						|
            ],
 | 
						|
          ),
 | 
						|
        ),
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  buildRightPane(BuildContext context) {
 | 
						|
    return Container(
 | 
						|
      color: Theme.of(context).scaffoldBackgroundColor,
 | 
						|
      child: ConnectionPage(),
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  buildIDBoard(BuildContext context) {
 | 
						|
    final model = gFFI.serverModel;
 | 
						|
    return Container(
 | 
						|
      margin: const EdgeInsets.only(left: 20, right: 11),
 | 
						|
      height: 57,
 | 
						|
      child: Row(
 | 
						|
        crossAxisAlignment: CrossAxisAlignment.baseline,
 | 
						|
        textBaseline: TextBaseline.alphabetic,
 | 
						|
        children: [
 | 
						|
          Container(
 | 
						|
            width: 2,
 | 
						|
            decoration: const BoxDecoration(color: MyTheme.accent),
 | 
						|
          ).marginOnly(top: 5),
 | 
						|
          Expanded(
 | 
						|
            child: Padding(
 | 
						|
              padding: const EdgeInsets.only(left: 7),
 | 
						|
              child: Column(
 | 
						|
                crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
                children: [
 | 
						|
                  Container(
 | 
						|
                    height: 25,
 | 
						|
                    child: Row(
 | 
						|
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
						|
                      crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
                      children: [
 | 
						|
                        Text(
 | 
						|
                          translate("ID"),
 | 
						|
                          style: TextStyle(
 | 
						|
                              fontSize: 14,
 | 
						|
                              color: Theme.of(context)
 | 
						|
                                  .textTheme
 | 
						|
                                  .titleLarge
 | 
						|
                                  ?.color
 | 
						|
                                  ?.withOpacity(0.5)),
 | 
						|
                        ).marginOnly(top: 5),
 | 
						|
                        buildPopupMenu(context)
 | 
						|
                      ],
 | 
						|
                    ),
 | 
						|
                  ),
 | 
						|
                  Flexible(
 | 
						|
                    child: GestureDetector(
 | 
						|
                      onDoubleTap: () {
 | 
						|
                        Clipboard.setData(
 | 
						|
                            ClipboardData(text: model.serverId.text));
 | 
						|
                        showToast(translate("Copied"));
 | 
						|
                      },
 | 
						|
                      child: TextFormField(
 | 
						|
                        controller: model.serverId,
 | 
						|
                        readOnly: true,
 | 
						|
                        decoration: InputDecoration(
 | 
						|
                          border: InputBorder.none,
 | 
						|
                          contentPadding: EdgeInsets.only(top: 10, bottom: 10),
 | 
						|
                        ),
 | 
						|
                        style: TextStyle(
 | 
						|
                          fontSize: 22,
 | 
						|
                        ),
 | 
						|
                      ),
 | 
						|
                    ),
 | 
						|
                  )
 | 
						|
                ],
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
        ],
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  Widget buildPopupMenu(BuildContext context) {
 | 
						|
    final textColor = Theme.of(context).textTheme.titleLarge?.color;
 | 
						|
    RxBool hover = false.obs;
 | 
						|
    return InkWell(
 | 
						|
      onTap: DesktopTabPage.onAddSetting,
 | 
						|
      child: Tooltip(
 | 
						|
        message: translate('Settings'),
 | 
						|
        child: Obx(
 | 
						|
          () => CircleAvatar(
 | 
						|
            radius: 15,
 | 
						|
            backgroundColor: hover.value
 | 
						|
                ? Theme.of(context).scaffoldBackgroundColor
 | 
						|
                : Theme.of(context).colorScheme.background,
 | 
						|
            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: Tooltip(
 | 
						|
                          message: translate('Refresh Password'),
 | 
						|
                          child: Obx(() => RotatedBox(
 | 
						|
                              quarterTurns: 2,
 | 
						|
                              child: Icon(
 | 
						|
                                Icons.refresh,
 | 
						|
                                color: refreshHover.value
 | 
						|
                                    ? textColor
 | 
						|
                                    : Color(0xFFDDDDDD),
 | 
						|
                                size: 22,
 | 
						|
                              ))),
 | 
						|
                        ),
 | 
						|
                        onHover: (value) => refreshHover.value = value,
 | 
						|
                      ).marginOnly(right: 8, top: 4),
 | 
						|
                      if (!bind.isDisableSettings())
 | 
						|
                        InkWell(
 | 
						|
                          child: Tooltip(
 | 
						|
                            message: translate('Change Password'),
 | 
						|
                            child: Obx(
 | 
						|
                              () => Icon(
 | 
						|
                                Icons.edit,
 | 
						|
                                color: editHover.value
 | 
						|
                                    ? textColor
 | 
						|
                                    : Color(0xFFDDDDDD),
 | 
						|
                                size: 22,
 | 
						|
                              ).marginOnly(right: 8, top: 4),
 | 
						|
                            ),
 | 
						|
                          ),
 | 
						|
                          onTap: () => DesktopSettingPage.switch2page(
 | 
						|
                              SettingsTabKey.safety),
 | 
						|
                          onHover: (value) => editHover.value = value,
 | 
						|
                        ),
 | 
						|
                    ],
 | 
						|
                  ),
 | 
						|
                ],
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
        ],
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  buildTip(BuildContext context) {
 | 
						|
    final isOutgoingOnly = bind.isOutgoingOnly();
 | 
						|
    return Padding(
 | 
						|
      padding:
 | 
						|
          const EdgeInsets.only(left: 20.0, right: 16, top: 16.0, bottom: 5),
 | 
						|
      child: Column(
 | 
						|
        mainAxisAlignment: MainAxisAlignment.start,
 | 
						|
        crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
        children: [
 | 
						|
          Column(
 | 
						|
            children: [
 | 
						|
              if (!isOutgoingOnly)
 | 
						|
                Align(
 | 
						|
                  alignment: Alignment.centerLeft,
 | 
						|
                  child: Text(
 | 
						|
                    translate("Your Desktop"),
 | 
						|
                    style: Theme.of(context).textTheme.titleLarge,
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
            ],
 | 
						|
          ),
 | 
						|
          SizedBox(
 | 
						|
            height: 10.0,
 | 
						|
          ),
 | 
						|
          if (!isOutgoingOnly)
 | 
						|
            Text(
 | 
						|
              translate("desk_tip"),
 | 
						|
              overflow: TextOverflow.clip,
 | 
						|
              style: Theme.of(context).textTheme.bodySmall,
 | 
						|
            ),
 | 
						|
          if (isOutgoingOnly)
 | 
						|
            Text(
 | 
						|
              translate("outgoing_only_desk_tip"),
 | 
						|
              overflow: TextOverflow.clip,
 | 
						|
              style: Theme.of(context).textTheme.bodySmall,
 | 
						|
            ),
 | 
						|
        ],
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  Future<Widget> buildHelpCards() async {
 | 
						|
    if (!bind.isCustomClient() &&
 | 
						|
        updateUrl.isNotEmpty &&
 | 
						|
        !isCardClosed &&
 | 
						|
        bind.mainUriPrefixSync().contains('rustdesk')) {
 | 
						|
      return buildInstallCard(
 | 
						|
          "Status",
 | 
						|
          "There is a newer version of ${bind.mainGetAppNameSync()} ${bind.mainGetNewVersion()} available.",
 | 
						|
          "Click to download", () async {
 | 
						|
        final Uri url = Uri.parse('https://rustdesk.com/download');
 | 
						|
        await launchUrl(url);
 | 
						|
      }, closeButton: true);
 | 
						|
    }
 | 
						|
    if (systemError.isNotEmpty) {
 | 
						|
      return buildInstallCard("", systemError, "", () {});
 | 
						|
    }
 | 
						|
 | 
						|
    if (isWindows && !bind.isDisableInstallation()) {
 | 
						|
      if (!bind.mainIsInstalled()) {
 | 
						|
        return buildInstallCard(
 | 
						|
            "", bind.isOutgoingOnly() ? "" : "install_tip", "Install",
 | 
						|
            () async {
 | 
						|
          await rustDeskWinManager.closeAllSubWindows();
 | 
						|
          bind.mainGotoInstall();
 | 
						|
        });
 | 
						|
      } else if (bind.mainIsInstalledLowerVersion()) {
 | 
						|
        return buildInstallCard(
 | 
						|
            "Status", "Your installation is lower version.", "Click to upgrade",
 | 
						|
            () async {
 | 
						|
          await rustDeskWinManager.closeAllSubWindows();
 | 
						|
          bind.mainUpdateMe();
 | 
						|
        });
 | 
						|
      }
 | 
						|
    } else if (isMacOS) {
 | 
						|
      if (!(bind.isOutgoingOnly() ||
 | 
						|
          bind.mainIsCanScreenRecording(prompt: false))) {
 | 
						|
        return buildInstallCard("Permissions", "config_screen", "Configure",
 | 
						|
            () async {
 | 
						|
          bind.mainIsCanScreenRecording(prompt: true);
 | 
						|
          watchIsCanScreenRecording = true;
 | 
						|
        }, help: 'Help', link: translate("doc_mac_permission"));
 | 
						|
      } else if (!bind.mainIsProcessTrusted(prompt: false)) {
 | 
						|
        return buildInstallCard("Permissions", "config_acc", "Configure",
 | 
						|
            () async {
 | 
						|
          bind.mainIsProcessTrusted(prompt: true);
 | 
						|
          watchIsProcessTrust = true;
 | 
						|
        }, help: 'Help', link: translate("doc_mac_permission"));
 | 
						|
      } else if (!bind.mainIsCanInputMonitoring(prompt: false)) {
 | 
						|
        return buildInstallCard("Permissions", "config_input", "Configure",
 | 
						|
            () async {
 | 
						|
          bind.mainIsCanInputMonitoring(prompt: true);
 | 
						|
          watchIsInputMonitoring = true;
 | 
						|
        }, help: 'Help', link: translate("doc_mac_permission"));
 | 
						|
      } else if (!svcStopped.value &&
 | 
						|
          bind.mainIsInstalled() &&
 | 
						|
          !bind.mainIsInstalledDaemon(prompt: false)) {
 | 
						|
        return buildInstallCard("", "install_daemon_tip", "Install", () async {
 | 
						|
          bind.mainIsInstalledDaemon(prompt: true);
 | 
						|
        });
 | 
						|
      }
 | 
						|
      //// Disable microphone configuration for macOS. We will request the permission when needed.
 | 
						|
      // else if ((await osxCanRecordAudio() !=
 | 
						|
      //     PermissionAuthorizeType.authorized)) {
 | 
						|
      //   return buildInstallCard("Permissions", "config_microphone", "Configure",
 | 
						|
      //       () async {
 | 
						|
      //     osxRequestAudio();
 | 
						|
      //     watchIsCanRecordAudio = true;
 | 
						|
      //   });
 | 
						|
      // }
 | 
						|
    } else if (isLinux) {
 | 
						|
      if (bind.isOutgoingOnly()) {
 | 
						|
        return Container();
 | 
						|
      }
 | 
						|
      final LinuxCards = <Widget>[];
 | 
						|
      if (bind.isSelinuxEnforcing()) {
 | 
						|
        // Check is SELinux enforcing, but show user a tip of is SELinux enabled for simple.
 | 
						|
        final keyShowSelinuxHelpTip = "show-selinux-help-tip";
 | 
						|
        if (bind.mainGetLocalOption(key: keyShowSelinuxHelpTip) != 'N') {
 | 
						|
          LinuxCards.add(buildInstallCard(
 | 
						|
            "Warning",
 | 
						|
            "selinux_tip",
 | 
						|
            "",
 | 
						|
            () async {},
 | 
						|
            marginTop: LinuxCards.isEmpty ? 20.0 : 5.0,
 | 
						|
            help: 'Help',
 | 
						|
            link:
 | 
						|
                'https://rustdesk.com/docs/en/client/linux/#permissions-issue',
 | 
						|
            closeButton: true,
 | 
						|
            closeOption: keyShowSelinuxHelpTip,
 | 
						|
          ));
 | 
						|
        }
 | 
						|
      }
 | 
						|
      if (bind.mainCurrentIsWayland()) {
 | 
						|
        LinuxCards.add(buildInstallCard(
 | 
						|
            "Warning", "wayland_experiment_tip", "", () async {},
 | 
						|
            marginTop: LinuxCards.isEmpty ? 20.0 : 5.0,
 | 
						|
            help: 'Help',
 | 
						|
            link: 'https://rustdesk.com/docs/en/client/linux/#x11-required'));
 | 
						|
      } else if (bind.mainIsLoginWayland()) {
 | 
						|
        LinuxCards.add(buildInstallCard("Warning",
 | 
						|
            "Login screen using Wayland is not supported", "", () async {},
 | 
						|
            marginTop: LinuxCards.isEmpty ? 20.0 : 5.0,
 | 
						|
            help: 'Help',
 | 
						|
            link: 'https://rustdesk.com/docs/en/client/linux/#login-screen'));
 | 
						|
      }
 | 
						|
      if (LinuxCards.isNotEmpty) {
 | 
						|
        return Column(
 | 
						|
          children: LinuxCards,
 | 
						|
        );
 | 
						|
      }
 | 
						|
    }
 | 
						|
    if (bind.isIncomingOnly()) {
 | 
						|
      return Align(
 | 
						|
        alignment: Alignment.centerRight,
 | 
						|
        child: OutlinedButton(
 | 
						|
          onPressed: () {
 | 
						|
            SystemNavigator.pop(); // Close the application
 | 
						|
            // https://github.com/flutter/flutter/issues/66631
 | 
						|
            if (isWindows) {
 | 
						|
              exit(0);
 | 
						|
            }
 | 
						|
          },
 | 
						|
          child: Text(translate('Quit')),
 | 
						|
        ),
 | 
						|
      ).marginAll(14);
 | 
						|
    }
 | 
						|
    return Container();
 | 
						|
  }
 | 
						|
 | 
						|
  Widget buildInstallCard(String title, String content, String btnText,
 | 
						|
      GestureTapCallback onPressed,
 | 
						|
      {double marginTop = 20.0,
 | 
						|
      String? help,
 | 
						|
      String? link,
 | 
						|
      bool? closeButton,
 | 
						|
      String? closeOption}) {
 | 
						|
    void closeCard() async {
 | 
						|
      if (closeOption != null) {
 | 
						|
        await bind.mainSetLocalOption(key: closeOption, value: 'N');
 | 
						|
        if (bind.mainGetLocalOption(key: closeOption) == 'N') {
 | 
						|
          setState(() {
 | 
						|
            isCardClosed = true;
 | 
						|
          });
 | 
						|
        }
 | 
						|
      } else {
 | 
						|
        setState(() {
 | 
						|
          isCardClosed = true;
 | 
						|
        });
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    return Stack(
 | 
						|
      children: [
 | 
						|
        Container(
 | 
						|
          margin: EdgeInsets.fromLTRB(
 | 
						|
              0, marginTop, 0, bind.isIncomingOnly() ? marginTop : 0),
 | 
						|
          child: Container(
 | 
						|
              decoration: BoxDecoration(
 | 
						|
                  gradient: LinearGradient(
 | 
						|
                begin: Alignment.centerLeft,
 | 
						|
                end: Alignment.centerRight,
 | 
						|
                colors: [
 | 
						|
                  Color.fromARGB(255, 226, 66, 188),
 | 
						|
                  Color.fromARGB(255, 244, 114, 124),
 | 
						|
                ],
 | 
						|
              )),
 | 
						|
              padding: EdgeInsets.all(20),
 | 
						|
              child: Column(
 | 
						|
                  mainAxisAlignment: MainAxisAlignment.start,
 | 
						|
                  crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
                  children: (title.isNotEmpty
 | 
						|
                          ? <Widget>[
 | 
						|
                              Center(
 | 
						|
                                  child: Text(
 | 
						|
                                translate(title),
 | 
						|
                                style: TextStyle(
 | 
						|
                                    color: Colors.white,
 | 
						|
                                    fontWeight: FontWeight.bold,
 | 
						|
                                    fontSize: 15),
 | 
						|
                              ).marginOnly(bottom: 6)),
 | 
						|
                            ]
 | 
						|
                          : <Widget>[]) +
 | 
						|
                      <Widget>[
 | 
						|
                        if (content.isNotEmpty)
 | 
						|
                          Text(
 | 
						|
                            translate(content),
 | 
						|
                            style: TextStyle(
 | 
						|
                                height: 1.5,
 | 
						|
                                color: Colors.white,
 | 
						|
                                fontWeight: FontWeight.normal,
 | 
						|
                                fontSize: 13),
 | 
						|
                          ).marginOnly(bottom: 20)
 | 
						|
                      ] +
 | 
						|
                      (btnText.isNotEmpty
 | 
						|
                          ? <Widget>[
 | 
						|
                              Row(
 | 
						|
                                  mainAxisAlignment: MainAxisAlignment.center,
 | 
						|
                                  children: [
 | 
						|
                                    FixedWidthButton(
 | 
						|
                                      width: 150,
 | 
						|
                                      padding: 8,
 | 
						|
                                      isOutline: true,
 | 
						|
                                      text: translate(btnText),
 | 
						|
                                      textColor: Colors.white,
 | 
						|
                                      borderColor: Colors.white,
 | 
						|
                                      textSize: 20,
 | 
						|
                                      radius: 10,
 | 
						|
                                      onTap: onPressed,
 | 
						|
                                    )
 | 
						|
                                  ])
 | 
						|
                            ]
 | 
						|
                          : <Widget>[]) +
 | 
						|
                      (help != null
 | 
						|
                          ? <Widget>[
 | 
						|
                              Center(
 | 
						|
                                  child: InkWell(
 | 
						|
                                      onTap: () async =>
 | 
						|
                                          await launchUrl(Uri.parse(link!)),
 | 
						|
                                      child: Text(
 | 
						|
                                        translate(help),
 | 
						|
                                        style: TextStyle(
 | 
						|
                                            decoration:
 | 
						|
                                                TextDecoration.underline,
 | 
						|
                                            color: Colors.white,
 | 
						|
                                            fontSize: 12),
 | 
						|
                                      )).marginOnly(top: 6)),
 | 
						|
                            ]
 | 
						|
                          : <Widget>[]))),
 | 
						|
        ),
 | 
						|
        if (closeButton != null && closeButton == true)
 | 
						|
          Positioned(
 | 
						|
            top: 18,
 | 
						|
            right: 0,
 | 
						|
            child: IconButton(
 | 
						|
              icon: Icon(
 | 
						|
                Icons.close,
 | 
						|
                color: Colors.white,
 | 
						|
                size: 20,
 | 
						|
              ),
 | 
						|
              onPressed: closeCard,
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
      ],
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  @override
 | 
						|
  void initState() {
 | 
						|
    super.initState();
 | 
						|
    Timer(const Duration(seconds: 1), () async {
 | 
						|
      updateUrl = await bind.mainGetSoftwareUpdateUrl();
 | 
						|
      if (updateUrl.isNotEmpty) setState(() {});
 | 
						|
    });
 | 
						|
    _updateTimer = periodic_immediate(const Duration(seconds: 1), () async {
 | 
						|
      await gFFI.serverModel.fetchID();
 | 
						|
      final error = await bind.mainGetError();
 | 
						|
      if (systemError != error) {
 | 
						|
        systemError = error;
 | 
						|
        setState(() {});
 | 
						|
      }
 | 
						|
      final v = await bind.mainGetOption(key: "stop-service") == "Y";
 | 
						|
      if (v != svcStopped.value) {
 | 
						|
        svcStopped.value = v;
 | 
						|
        setState(() {});
 | 
						|
      }
 | 
						|
      if (watchIsCanScreenRecording) {
 | 
						|
        if (bind.mainIsCanScreenRecording(prompt: false)) {
 | 
						|
          watchIsCanScreenRecording = false;
 | 
						|
          setState(() {});
 | 
						|
        }
 | 
						|
      }
 | 
						|
      if (watchIsProcessTrust) {
 | 
						|
        if (bind.mainIsProcessTrusted(prompt: false)) {
 | 
						|
          watchIsProcessTrust = false;
 | 
						|
          setState(() {});
 | 
						|
        }
 | 
						|
      }
 | 
						|
      if (watchIsInputMonitoring) {
 | 
						|
        if (bind.mainIsCanInputMonitoring(prompt: false)) {
 | 
						|
          watchIsInputMonitoring = false;
 | 
						|
          // Do not notify for now.
 | 
						|
          // Monitoring may not take effect until the process is restarted.
 | 
						|
          // rustDeskWinManager.call(
 | 
						|
          //     WindowType.RemoteDesktop, kWindowDisableGrabKeyboard, '');
 | 
						|
          setState(() {});
 | 
						|
        }
 | 
						|
      }
 | 
						|
      if (watchIsCanRecordAudio) {
 | 
						|
        if (isMacOS) {
 | 
						|
          Future.microtask(() async {
 | 
						|
            if ((await osxCanRecordAudio() ==
 | 
						|
                PermissionAuthorizeType.authorized)) {
 | 
						|
              watchIsCanRecordAudio = false;
 | 
						|
              setState(() {});
 | 
						|
            }
 | 
						|
          });
 | 
						|
        } else {
 | 
						|
          watchIsCanRecordAudio = false;
 | 
						|
          setState(() {});
 | 
						|
        }
 | 
						|
      }
 | 
						|
    });
 | 
						|
    Get.put<RxBool>(svcStopped, tag: 'stop-service');
 | 
						|
    rustDeskWinManager.registerActiveWindowListener(onActiveWindowChanged);
 | 
						|
 | 
						|
    screenToMap(window_size.Screen screen) => {
 | 
						|
          'frame': {
 | 
						|
            'l': screen.frame.left,
 | 
						|
            't': screen.frame.top,
 | 
						|
            'r': screen.frame.right,
 | 
						|
            'b': screen.frame.bottom,
 | 
						|
          },
 | 
						|
          'visibleFrame': {
 | 
						|
            'l': screen.visibleFrame.left,
 | 
						|
            't': screen.visibleFrame.top,
 | 
						|
            'r': screen.visibleFrame.right,
 | 
						|
            'b': screen.visibleFrame.bottom,
 | 
						|
          },
 | 
						|
          'scaleFactor': screen.scaleFactor,
 | 
						|
        };
 | 
						|
 | 
						|
    rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
 | 
						|
      debugPrint(
 | 
						|
          "[Main] call ${call.method} with args ${call.arguments} from window $fromWindowId");
 | 
						|
      if (call.method == kWindowMainWindowOnTop) {
 | 
						|
        windowOnTop(null);
 | 
						|
      } else if (call.method == kWindowGetWindowInfo) {
 | 
						|
        final screen = (await window_size.getWindowInfo()).screen;
 | 
						|
        if (screen == null) {
 | 
						|
          return '';
 | 
						|
        } else {
 | 
						|
          return jsonEncode(screenToMap(screen));
 | 
						|
        }
 | 
						|
      } else if (call.method == kWindowGetScreenList) {
 | 
						|
        return jsonEncode(
 | 
						|
            (await window_size.getScreenList()).map(screenToMap).toList());
 | 
						|
      } else if (call.method == kWindowActionRebuild) {
 | 
						|
        reloadCurrentWindow();
 | 
						|
      } else if (call.method == kWindowEventShow) {
 | 
						|
        await rustDeskWinManager.registerActiveWindow(call.arguments["id"]);
 | 
						|
      } else if (call.method == kWindowEventHide) {
 | 
						|
        await rustDeskWinManager.unregisterActiveWindow(call.arguments['id']);
 | 
						|
      } else if (call.method == kWindowConnect) {
 | 
						|
        await connectMainDesktop(
 | 
						|
          call.arguments['id'],
 | 
						|
          isFileTransfer: call.arguments['isFileTransfer'],
 | 
						|
          isTcpTunneling: call.arguments['isTcpTunneling'],
 | 
						|
          isRDP: call.arguments['isRDP'],
 | 
						|
          password: call.arguments['password'],
 | 
						|
          forceRelay: call.arguments['forceRelay'],
 | 
						|
        );
 | 
						|
      } else if (call.method == kWindowEventMoveTabToNewWindow) {
 | 
						|
        final args = call.arguments.split(',');
 | 
						|
        int? windowId;
 | 
						|
        try {
 | 
						|
          windowId = int.parse(args[0]);
 | 
						|
        } catch (e) {
 | 
						|
          debugPrint("Failed to parse window id '${call.arguments}': $e");
 | 
						|
        }
 | 
						|
        if (windowId != null) {
 | 
						|
          await rustDeskWinManager.moveTabToNewWindow(
 | 
						|
              windowId, args[1], args[2]);
 | 
						|
        }
 | 
						|
      } else if (call.method == kWindowEventOpenMonitorSession) {
 | 
						|
        final args = jsonDecode(call.arguments);
 | 
						|
        final windowId = args['window_id'] as int;
 | 
						|
        final peerId = args['peer_id'] as String;
 | 
						|
        final display = args['display'] as int;
 | 
						|
        final displayCount = args['display_count'] as int;
 | 
						|
        final screenRect = parseParamScreenRect(args);
 | 
						|
        await rustDeskWinManager.openMonitorSession(
 | 
						|
            windowId, peerId, display, displayCount, screenRect);
 | 
						|
      } else if (call.method == kWindowEventRemoteWindowCoords) {
 | 
						|
        final windowId = int.tryParse(call.arguments);
 | 
						|
        if (windowId != null) {
 | 
						|
          return jsonEncode(
 | 
						|
              await rustDeskWinManager.getOtherRemoteWindowCoords(windowId));
 | 
						|
        }
 | 
						|
      }
 | 
						|
    });
 | 
						|
    _uniLinksSubscription = listenUniLinks();
 | 
						|
 | 
						|
    if (bind.isIncomingOnly()) {
 | 
						|
      WidgetsBinding.instance.addPostFrameCallback((_) {
 | 
						|
        _updateWindowSize();
 | 
						|
      });
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  _updateWindowSize() {
 | 
						|
    RenderObject? renderObject = _childKey.currentContext?.findRenderObject();
 | 
						|
    if (renderObject == null) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    if (renderObject is RenderBox) {
 | 
						|
      final size = renderObject.size;
 | 
						|
      if (size != imcomingOnlyHomeSize) {
 | 
						|
        imcomingOnlyHomeSize = size;
 | 
						|
        windowManager.setSize(getIncomingOnlyHomeSize());
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  @override
 | 
						|
  void dispose() {
 | 
						|
    _uniLinksSubscription?.cancel();
 | 
						|
    Get.delete<RxBool>(tag: 'stop-service');
 | 
						|
    _updateTimer?.cancel();
 | 
						|
    super.dispose();
 | 
						|
  }
 | 
						|
 | 
						|
  Widget buildPluginEntry() {
 | 
						|
    final entries = PluginUiManager.instance.entries.entries;
 | 
						|
    return Offstage(
 | 
						|
      offstage: entries.isEmpty,
 | 
						|
      child: Column(
 | 
						|
        crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
        children: [
 | 
						|
          ...entries.map((entry) {
 | 
						|
            return entry.value;
 | 
						|
          })
 | 
						|
        ],
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
void setPasswordDialog() async {
 | 
						|
  final pw = await bind.mainGetPermanentPassword();
 | 
						|
  final p0 = TextEditingController(text: pw);
 | 
						|
  final p1 = TextEditingController(text: pw);
 | 
						|
  var errMsg0 = "";
 | 
						|
  var errMsg1 = "";
 | 
						|
  final RxString rxPass = pw.trim().obs;
 | 
						|
  final rules = [
 | 
						|
    DigitValidationRule(),
 | 
						|
    UppercaseValidationRule(),
 | 
						|
    LowercaseValidationRule(),
 | 
						|
    // SpecialCharacterValidationRule(),
 | 
						|
    MinCharactersValidationRule(8),
 | 
						|
  ];
 | 
						|
 | 
						|
  gFFI.dialogManager.show((setState, close, context) {
 | 
						|
    submit() {
 | 
						|
      setState(() {
 | 
						|
        errMsg0 = "";
 | 
						|
        errMsg1 = "";
 | 
						|
      });
 | 
						|
      final pass = p0.text.trim();
 | 
						|
      if (pass.isNotEmpty) {
 | 
						|
        final Iterable violations = rules.where((r) => !r.validate(pass));
 | 
						|
        if (violations.isNotEmpty) {
 | 
						|
          setState(() {
 | 
						|
            errMsg0 =
 | 
						|
                '${translate('Prompt')}: ${violations.map((r) => r.name).join(', ')}';
 | 
						|
          });
 | 
						|
          return;
 | 
						|
        }
 | 
						|
      }
 | 
						|
      if (p1.text.trim() != pass) {
 | 
						|
        setState(() {
 | 
						|
          errMsg1 =
 | 
						|
              '${translate('Prompt')}: ${translate("The confirmation is not identical.")}';
 | 
						|
        });
 | 
						|
        return;
 | 
						|
      }
 | 
						|
      bind.mainSetPermanentPassword(password: pass);
 | 
						|
      close();
 | 
						|
    }
 | 
						|
 | 
						|
    return CustomAlertDialog(
 | 
						|
      title: Text(translate("Set Password")),
 | 
						|
      content: ConstrainedBox(
 | 
						|
        constraints: const BoxConstraints(minWidth: 500),
 | 
						|
        child: Column(
 | 
						|
          crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
          children: [
 | 
						|
            const SizedBox(
 | 
						|
              height: 8.0,
 | 
						|
            ),
 | 
						|
            Row(
 | 
						|
              children: [
 | 
						|
                Expanded(
 | 
						|
                  child: TextField(
 | 
						|
                    obscureText: true,
 | 
						|
                    decoration: InputDecoration(
 | 
						|
                        labelText: translate('Password'),
 | 
						|
                        errorText: errMsg0.isNotEmpty ? errMsg0 : null),
 | 
						|
                    controller: p0,
 | 
						|
                    autofocus: true,
 | 
						|
                    onChanged: (value) {
 | 
						|
                      rxPass.value = value.trim();
 | 
						|
                      setState(() {
 | 
						|
                        errMsg0 = '';
 | 
						|
                      });
 | 
						|
                    },
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
              ],
 | 
						|
            ),
 | 
						|
            Row(
 | 
						|
              children: [
 | 
						|
                Expanded(child: PasswordStrengthIndicator(password: rxPass)),
 | 
						|
              ],
 | 
						|
            ).marginSymmetric(vertical: 8),
 | 
						|
            const SizedBox(
 | 
						|
              height: 8.0,
 | 
						|
            ),
 | 
						|
            Row(
 | 
						|
              children: [
 | 
						|
                Expanded(
 | 
						|
                  child: TextField(
 | 
						|
                    obscureText: true,
 | 
						|
                    decoration: InputDecoration(
 | 
						|
                        labelText: translate('Confirmation'),
 | 
						|
                        errorText: errMsg1.isNotEmpty ? errMsg1 : null),
 | 
						|
                    controller: p1,
 | 
						|
                    onChanged: (value) {
 | 
						|
                      setState(() {
 | 
						|
                        errMsg1 = '';
 | 
						|
                      });
 | 
						|
                    },
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
              ],
 | 
						|
            ),
 | 
						|
            const SizedBox(
 | 
						|
              height: 8.0,
 | 
						|
            ),
 | 
						|
            Obx(() => Wrap(
 | 
						|
                  runSpacing: 8,
 | 
						|
                  spacing: 4,
 | 
						|
                  children: rules.map((e) {
 | 
						|
                    var checked = e.validate(rxPass.value.trim());
 | 
						|
                    return Chip(
 | 
						|
                        label: Text(
 | 
						|
                          e.name,
 | 
						|
                          style: TextStyle(
 | 
						|
                              color: checked
 | 
						|
                                  ? const Color(0xFF0A9471)
 | 
						|
                                  : Color.fromARGB(255, 198, 86, 157)),
 | 
						|
                        ),
 | 
						|
                        backgroundColor: checked
 | 
						|
                            ? const Color(0xFFD0F7ED)
 | 
						|
                            : Color.fromARGB(255, 247, 205, 232));
 | 
						|
                  }).toList(),
 | 
						|
                ))
 | 
						|
          ],
 | 
						|
        ),
 | 
						|
      ),
 | 
						|
      actions: [
 | 
						|
        dialogButton("Cancel", onPressed: close, isOutline: true),
 | 
						|
        dialogButton("OK", onPressed: submit),
 | 
						|
      ],
 | 
						|
      onSubmit: submit,
 | 
						|
      onCancel: close,
 | 
						|
    );
 | 
						|
  });
 | 
						|
}
 |