2537 lines
		
	
	
		
			81 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			2537 lines
		
	
	
		
			81 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'dart:async';
 | |
| import 'dart:convert';
 | |
| import 'dart:io';
 | |
| 
 | |
| import 'package:file_picker/file_picker.dart';
 | |
| import 'package:flutter/material.dart';
 | |
| import 'package:flutter/services.dart';
 | |
| import 'package:flutter_hbb/common.dart';
 | |
| import 'package:flutter_hbb/common/widgets/audio_input.dart';
 | |
| import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
 | |
| import 'package:flutter_hbb/consts.dart';
 | |
| import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
 | |
| import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
 | |
| import 'package:flutter_hbb/models/platform_model.dart';
 | |
| import 'package:flutter_hbb/models/server_model.dart';
 | |
| import 'package:flutter_hbb/plugin/manager.dart';
 | |
| import 'package:flutter_hbb/plugin/widgets/desktop_settings.dart';
 | |
| import 'package:get/get.dart';
 | |
| import 'package:provider/provider.dart';
 | |
| import 'package:url_launcher/url_launcher.dart';
 | |
| import 'package:url_launcher/url_launcher_string.dart';
 | |
| import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
 | |
| 
 | |
| import '../../common/widgets/dialog.dart';
 | |
| import '../../common/widgets/login.dart';
 | |
| 
 | |
| const double _kTabWidth = 200;
 | |
| const double _kTabHeight = 42;
 | |
| const double _kCardFixedWidth = 540;
 | |
| const double _kCardLeftMargin = 15;
 | |
| const double _kContentHMargin = 15;
 | |
| const double _kContentHSubMargin = _kContentHMargin + 33;
 | |
| const double _kCheckBoxLeftMargin = 10;
 | |
| const double _kRadioLeftMargin = 10;
 | |
| const double _kListViewBottomMargin = 15;
 | |
| const double _kTitleFontSize = 20;
 | |
| const double _kContentFontSize = 15;
 | |
| const Color _accentColor = MyTheme.accent;
 | |
| const String _kSettingPageControllerTag = 'settingPageController';
 | |
| const String _kSettingPageTabKeyTag = 'settingPageTabKey';
 | |
| 
 | |
| class _TabInfo {
 | |
|   late final SettingsTabKey key;
 | |
|   late final String label;
 | |
|   late final IconData unselected;
 | |
|   late final IconData selected;
 | |
|   _TabInfo(this.key, this.label, this.unselected, this.selected);
 | |
| }
 | |
| 
 | |
| enum SettingsTabKey {
 | |
|   general,
 | |
|   safety,
 | |
|   network,
 | |
|   display,
 | |
|   plugin,
 | |
|   account,
 | |
|   about,
 | |
| }
 | |
| 
 | |
| class DesktopSettingPage extends StatefulWidget {
 | |
|   final SettingsTabKey initialTabkey;
 | |
|   static final List<SettingsTabKey> tabKeys = [
 | |
|     SettingsTabKey.general,
 | |
|     if (!isWeb &&
 | |
|         !bind.isOutgoingOnly() &&
 | |
|         !bind.isDisableSettings() &&
 | |
|         bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) != 'Y')
 | |
|       SettingsTabKey.safety,
 | |
|     if (!bind.isDisableSettings() &&
 | |
|         bind.mainGetBuildinOption(key: kOptionHideNetworkSetting) != 'Y')
 | |
|       SettingsTabKey.network,
 | |
|     if (!bind.isIncomingOnly()) SettingsTabKey.display,
 | |
|     if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled())
 | |
|       SettingsTabKey.plugin,
 | |
|     if (!bind.isDisableAccount()) SettingsTabKey.account,
 | |
|     SettingsTabKey.about,
 | |
|   ];
 | |
| 
 | |
|   DesktopSettingPage({Key? key, required this.initialTabkey}) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   State<DesktopSettingPage> createState() =>
 | |
|       _DesktopSettingPageState(initialTabkey);
 | |
| 
 | |
|   static void switch2page(SettingsTabKey page) {
 | |
|     try {
 | |
|       int index = tabKeys.indexOf(page);
 | |
|       if (index == -1) {
 | |
|         return;
 | |
|       }
 | |
|       if (Get.isRegistered<PageController>(tag: _kSettingPageControllerTag)) {
 | |
|         DesktopTabPage.onAddSetting(initialPage: page);
 | |
|         PageController controller =
 | |
|             Get.find<PageController>(tag: _kSettingPageControllerTag);
 | |
|         Rx<SettingsTabKey> selected =
 | |
|             Get.find<Rx<SettingsTabKey>>(tag: _kSettingPageTabKeyTag);
 | |
|         selected.value = page;
 | |
|         controller.jumpToPage(index);
 | |
|       } else {
 | |
|         DesktopTabPage.onAddSetting(initialPage: page);
 | |
|       }
 | |
|     } catch (e) {
 | |
|       debugPrintStack(label: '$e');
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _DesktopSettingPageState extends State<DesktopSettingPage>
 | |
|     with TickerProviderStateMixin, AutomaticKeepAliveClientMixin {
 | |
|   late PageController controller;
 | |
|   late Rx<SettingsTabKey> selectedTab;
 | |
| 
 | |
|   @override
 | |
|   bool get wantKeepAlive => true;
 | |
| 
 | |
|   _DesktopSettingPageState(SettingsTabKey initialTabkey) {
 | |
|     var initialIndex = DesktopSettingPage.tabKeys.indexOf(initialTabkey);
 | |
|     if (initialIndex == -1) {
 | |
|       initialIndex = 0;
 | |
|     }
 | |
|     selectedTab = DesktopSettingPage.tabKeys[initialIndex].obs;
 | |
|     Get.put<Rx<SettingsTabKey>>(selectedTab, tag: _kSettingPageTabKeyTag);
 | |
|     controller = PageController(initialPage: initialIndex);
 | |
|     Get.put<PageController>(controller, tag: _kSettingPageControllerTag);
 | |
|     controller.addListener(() {
 | |
|       if (controller.page != null) {
 | |
|         int page = controller.page!.toInt();
 | |
|         if (page < DesktopSettingPage.tabKeys.length) {
 | |
|           selectedTab.value = DesktopSettingPage.tabKeys[page];
 | |
|         }
 | |
|       }
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void dispose() {
 | |
|     super.dispose();
 | |
|     Get.delete<PageController>(tag: _kSettingPageControllerTag);
 | |
|     Get.delete<RxInt>(tag: _kSettingPageTabKeyTag);
 | |
|   }
 | |
| 
 | |
|   List<_TabInfo> _settingTabs() {
 | |
|     final List<_TabInfo> settingTabs = <_TabInfo>[];
 | |
|     for (final tab in DesktopSettingPage.tabKeys) {
 | |
|       switch (tab) {
 | |
|         case SettingsTabKey.general:
 | |
|           settingTabs.add(_TabInfo(
 | |
|               tab, 'General', Icons.settings_outlined, Icons.settings));
 | |
|           break;
 | |
|         case SettingsTabKey.safety:
 | |
|           settingTabs.add(_TabInfo(tab, 'Security',
 | |
|               Icons.enhanced_encryption_outlined, Icons.enhanced_encryption));
 | |
|           break;
 | |
|         case SettingsTabKey.network:
 | |
|           settingTabs
 | |
|               .add(_TabInfo(tab, 'Network', Icons.link_outlined, Icons.link));
 | |
|           break;
 | |
|         case SettingsTabKey.display:
 | |
|           settingTabs.add(_TabInfo(tab, 'Display',
 | |
|               Icons.desktop_windows_outlined, Icons.desktop_windows));
 | |
|           break;
 | |
|         case SettingsTabKey.plugin:
 | |
|           settingTabs.add(_TabInfo(
 | |
|               tab, 'Plugin', Icons.extension_outlined, Icons.extension));
 | |
|           break;
 | |
|         case SettingsTabKey.account:
 | |
|           settingTabs.add(
 | |
|               _TabInfo(tab, 'Account', Icons.person_outline, Icons.person));
 | |
|           break;
 | |
|         case SettingsTabKey.about:
 | |
|           settingTabs
 | |
|               .add(_TabInfo(tab, 'About', Icons.info_outline, Icons.info));
 | |
|           break;
 | |
|       }
 | |
|     }
 | |
|     return settingTabs;
 | |
|   }
 | |
| 
 | |
|   List<Widget> _children() {
 | |
|     final children = List<Widget>.empty(growable: true);
 | |
|     for (final tab in DesktopSettingPage.tabKeys) {
 | |
|       switch (tab) {
 | |
|         case SettingsTabKey.general:
 | |
|           children.add(const _General());
 | |
|           break;
 | |
|         case SettingsTabKey.safety:
 | |
|           children.add(const _Safety());
 | |
|           break;
 | |
|         case SettingsTabKey.network:
 | |
|           children.add(const _Network());
 | |
|           break;
 | |
|         case SettingsTabKey.display:
 | |
|           children.add(const _Display());
 | |
|           break;
 | |
|         case SettingsTabKey.plugin:
 | |
|           children.add(const _Plugin());
 | |
|           break;
 | |
|         case SettingsTabKey.account:
 | |
|           children.add(const _Account());
 | |
|           break;
 | |
|         case SettingsTabKey.about:
 | |
|           children.add(const _About());
 | |
|           break;
 | |
|       }
 | |
|     }
 | |
|     return children;
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     super.build(context);
 | |
|     return Scaffold(
 | |
|       backgroundColor: Theme.of(context).colorScheme.background,
 | |
|       body: Row(
 | |
|         children: <Widget>[
 | |
|           SizedBox(
 | |
|             width: _kTabWidth,
 | |
|             child: Column(
 | |
|               children: [
 | |
|                 _header(context),
 | |
|                 Flexible(child: _listView(tabs: _settingTabs())),
 | |
|               ],
 | |
|             ),
 | |
|           ),
 | |
|           const VerticalDivider(width: 1),
 | |
|           Expanded(
 | |
|             child: Container(
 | |
|               color: Theme.of(context).scaffoldBackgroundColor,
 | |
|               child: DesktopScrollWrapper(
 | |
|                   scrollController: controller,
 | |
|                   child: PageView(
 | |
|                     controller: controller,
 | |
|                     physics: NeverScrollableScrollPhysics(),
 | |
|                     children: _children(),
 | |
|                   )),
 | |
|             ),
 | |
|           )
 | |
|         ],
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   Widget _header(BuildContext context) {
 | |
|     final settingsText = Text(
 | |
|       translate('Settings'),
 | |
|       textAlign: TextAlign.left,
 | |
|       style: const TextStyle(
 | |
|         color: _accentColor,
 | |
|         fontSize: _kTitleFontSize,
 | |
|         fontWeight: FontWeight.w400,
 | |
|       ),
 | |
|     );
 | |
|     return Row(
 | |
|       children: [
 | |
|         if (isWeb)
 | |
|           IconButton(
 | |
|             onPressed: () {
 | |
|               if (Navigator.canPop(context)) {
 | |
|                 Navigator.pop(context);
 | |
|               }
 | |
|             },
 | |
|             icon: Icon(Icons.arrow_back),
 | |
|           ).marginOnly(left: 5),
 | |
|         if (isWeb)
 | |
|           SizedBox(
 | |
|             height: 62,
 | |
|             child: Align(
 | |
|               alignment: Alignment.center,
 | |
|               child: settingsText,
 | |
|             ),
 | |
|           ).marginOnly(left: 20),
 | |
|         if (!isWeb)
 | |
|           SizedBox(
 | |
|             height: 62,
 | |
|             child: settingsText,
 | |
|           ).marginOnly(left: 20, top: 10),
 | |
|         const Spacer(),
 | |
|       ],
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   Widget _listView({required List<_TabInfo> tabs}) {
 | |
|     final scrollController = ScrollController();
 | |
|     return DesktopScrollWrapper(
 | |
|         scrollController: scrollController,
 | |
|         child: ListView(
 | |
|           physics: DraggableNeverScrollableScrollPhysics(),
 | |
|           controller: scrollController,
 | |
|           children: tabs.map((tab) => _listItem(tab: tab)).toList(),
 | |
|         ));
 | |
|   }
 | |
| 
 | |
|   Widget _listItem({required _TabInfo tab}) {
 | |
|     return Obx(() {
 | |
|       bool selected = tab.key == selectedTab.value;
 | |
|       return SizedBox(
 | |
|         width: _kTabWidth,
 | |
|         height: _kTabHeight,
 | |
|         child: InkWell(
 | |
|           onTap: () {
 | |
|             if (selectedTab.value != tab.key) {
 | |
|               int index = DesktopSettingPage.tabKeys.indexOf(tab.key);
 | |
|               if (index == -1) {
 | |
|                 return;
 | |
|               }
 | |
|               controller.jumpToPage(index);
 | |
|             }
 | |
|             selectedTab.value = tab.key;
 | |
|           },
 | |
|           child: Row(children: [
 | |
|             Container(
 | |
|               width: 4,
 | |
|               height: _kTabHeight * 0.7,
 | |
|               color: selected ? _accentColor : null,
 | |
|             ),
 | |
|             Icon(
 | |
|               selected ? tab.selected : tab.unselected,
 | |
|               color: selected ? _accentColor : null,
 | |
|               size: 20,
 | |
|             ).marginOnly(left: 13, right: 10),
 | |
|             Text(
 | |
|               translate(tab.label),
 | |
|               style: TextStyle(
 | |
|                   color: selected ? _accentColor : null,
 | |
|                   fontWeight: FontWeight.w400,
 | |
|                   fontSize: _kContentFontSize),
 | |
|             ),
 | |
|           ]),
 | |
|         ),
 | |
|       );
 | |
|     });
 | |
|   }
 | |
| }
 | |
| 
 | |
| //#region pages
 | |
| 
 | |
| class _General extends StatefulWidget {
 | |
|   const _General({Key? key}) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   State<_General> createState() => _GeneralState();
 | |
| }
 | |
| 
 | |
| class _GeneralState extends State<_General> {
 | |
|   final RxBool serviceStop =
 | |
|       isWeb ? RxBool(false) : Get.find<RxBool>(tag: 'stop-service');
 | |
|   RxBool serviceBtnEnabled = true.obs;
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     final scrollController = ScrollController();
 | |
|     return DesktopScrollWrapper(
 | |
|         scrollController: scrollController,
 | |
|         child: ListView(
 | |
|           physics: DraggableNeverScrollableScrollPhysics(),
 | |
|           controller: scrollController,
 | |
|           children: [
 | |
|             if (!isWeb) service(),
 | |
|             theme(),
 | |
|             _Card(title: 'Language', children: [language()]),
 | |
|             if (!isWeb) hwcodec(),
 | |
|             if (!isWeb) audio(context),
 | |
|             if (!isWeb) record(context),
 | |
|             if (!isWeb) WaylandCard(),
 | |
|             other()
 | |
|           ],
 | |
|         ).marginOnly(bottom: _kListViewBottomMargin));
 | |
|   }
 | |
| 
 | |
|   Widget theme() {
 | |
|     final current = MyTheme.getThemeModePreference().toShortString();
 | |
|     onChanged(String value) {
 | |
|       MyTheme.changeDarkMode(MyTheme.themeModeFromString(value));
 | |
|       setState(() {});
 | |
|     }
 | |
| 
 | |
|     final isOptFixed = isOptionFixed(kCommConfKeyTheme);
 | |
|     return _Card(title: 'Theme', children: [
 | |
|       _Radio<String>(context,
 | |
|           value: 'light',
 | |
|           groupValue: current,
 | |
|           label: 'Light',
 | |
|           onChanged: isOptFixed ? null : onChanged),
 | |
|       _Radio<String>(context,
 | |
|           value: 'dark',
 | |
|           groupValue: current,
 | |
|           label: 'Dark',
 | |
|           onChanged: isOptFixed ? null : onChanged),
 | |
|       _Radio<String>(context,
 | |
|           value: 'system',
 | |
|           groupValue: current,
 | |
|           label: 'Follow System',
 | |
|           onChanged: isOptFixed ? null : onChanged),
 | |
|     ]);
 | |
|   }
 | |
| 
 | |
|   Widget service() {
 | |
|     if (bind.isOutgoingOnly()) {
 | |
|       return const Offstage();
 | |
|     }
 | |
| 
 | |
|     return _Card(title: 'Service', children: [
 | |
|       Obx(() => _Button(serviceStop.value ? 'Start' : 'Stop', () {
 | |
|             () async {
 | |
|               serviceBtnEnabled.value = false;
 | |
|               await start_service(serviceStop.value);
 | |
|               // enable the button after 1 second
 | |
|               Future.delayed(const Duration(seconds: 1), () {
 | |
|                 serviceBtnEnabled.value = true;
 | |
|               });
 | |
|             }();
 | |
|           }, enabled: serviceBtnEnabled.value))
 | |
|     ]);
 | |
|   }
 | |
| 
 | |
|   Widget other() {
 | |
|     final children = <Widget>[
 | |
|       if (!isWeb && !bind.isIncomingOnly())
 | |
|         _OptionCheckBox(context, 'Confirm before closing multiple tabs',
 | |
|             kOptionEnableConfirmClosingTabs,
 | |
|             isServer: false),
 | |
|       _OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr),
 | |
|       if (!isWeb) wallpaper(),
 | |
|       if (!isWeb && !bind.isIncomingOnly()) ...[
 | |
|         _OptionCheckBox(
 | |
|           context,
 | |
|           'Open connection in new tab',
 | |
|           kOptionOpenNewConnInTabs,
 | |
|           isServer: false,
 | |
|         ),
 | |
|         // though this is related to GUI, but opengl problem affects all users, so put in config rather than local
 | |
|         if (isLinux)
 | |
|           Tooltip(
 | |
|             message: translate('software_render_tip'),
 | |
|             child: _OptionCheckBox(
 | |
|               context,
 | |
|               "Always use software rendering",
 | |
|               kOptionAllowAlwaysSoftwareRender,
 | |
|             ),
 | |
|           ),
 | |
|         if (!isWeb)
 | |
|           Tooltip(
 | |
|             message: translate('texture_render_tip'),
 | |
|             child: _OptionCheckBox(
 | |
|               context,
 | |
|               "Use texture rendering",
 | |
|               kOptionTextureRender,
 | |
|               optGetter: bind.mainGetUseTextureRender,
 | |
|               optSetter: (k, v) async =>
 | |
|                   await bind.mainSetLocalOption(key: k, value: v ? 'Y' : 'N'),
 | |
|             ),
 | |
|           ),
 | |
|         if (!isWeb && !bind.isCustomClient())
 | |
|           _OptionCheckBox(
 | |
|             context,
 | |
|             'Check for software update on startup',
 | |
|             kOptionEnableCheckUpdate,
 | |
|             isServer: false,
 | |
|           ),
 | |
|         if (isWindows && !bind.isOutgoingOnly())
 | |
|           _OptionCheckBox(
 | |
|             context,
 | |
|             'Capture screen using DirectX',
 | |
|             kOptionDirectxCapture,
 | |
|           )
 | |
|       ],
 | |
|     ];
 | |
|     if (!isWeb && bind.mainShowOption(key: kOptionAllowLinuxHeadless)) {
 | |
|       children.add(_OptionCheckBox(
 | |
|           context, 'Allow linux headless', kOptionAllowLinuxHeadless));
 | |
|     }
 | |
|     return _Card(title: 'Other', children: children);
 | |
|   }
 | |
| 
 | |
|   Widget wallpaper() {
 | |
|     if (bind.isOutgoingOnly()) {
 | |
|       return const Offstage();
 | |
|     }
 | |
| 
 | |
|     return futureBuilder(future: () async {
 | |
|       final support = await bind.mainSupportRemoveWallpaper();
 | |
|       return support;
 | |
|     }(), hasData: (data) {
 | |
|       if (data is bool && data == true) {
 | |
|         bool value = mainGetBoolOptionSync(kOptionAllowRemoveWallpaper);
 | |
|         return Row(
 | |
|           children: [
 | |
|             Flexible(
 | |
|               child: _OptionCheckBox(
 | |
|                 context,
 | |
|                 'Remove wallpaper during incoming sessions',
 | |
|                 kOptionAllowRemoveWallpaper,
 | |
|                 update: (bool v) {
 | |
|                   setState(() {});
 | |
|                 },
 | |
|               ),
 | |
|             ),
 | |
|             if (value)
 | |
|               _CountDownButton(
 | |
|                 text: 'Test',
 | |
|                 second: 5,
 | |
|                 onPressed: () {
 | |
|                   bind.mainTestWallpaper(second: 5);
 | |
|                 },
 | |
|               )
 | |
|           ],
 | |
|         );
 | |
|       }
 | |
| 
 | |
|       return Offstage();
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   Widget hwcodec() {
 | |
|     final hwcodec = bind.mainHasHwcodec();
 | |
|     final vram = bind.mainHasVram();
 | |
|     return Offstage(
 | |
|       offstage: !(hwcodec || vram),
 | |
|       child: _Card(title: 'Hardware Codec', children: [
 | |
|         _OptionCheckBox(
 | |
|           context,
 | |
|           'Enable hardware codec',
 | |
|           kOptionEnableHwcodec,
 | |
|           update: (bool v) {
 | |
|             if (v) {
 | |
|               bind.mainCheckHwcodec();
 | |
|             }
 | |
|           },
 | |
|         )
 | |
|       ]),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   Widget audio(BuildContext context) {
 | |
|     if (bind.isOutgoingOnly()) {
 | |
|       return const Offstage();
 | |
|     }
 | |
| 
 | |
|     builder(devices, currentDevice, setDevice) {
 | |
|       final child = ComboBox(
 | |
|         keys: devices,
 | |
|         values: devices,
 | |
|         initialKey: currentDevice,
 | |
|         onChanged: (key) async {
 | |
|           setDevice(key);
 | |
|           setState(() {});
 | |
|         },
 | |
|       ).marginOnly(left: _kContentHMargin);
 | |
|       return _Card(title: 'Audio Input Device', children: [child]);
 | |
|     }
 | |
| 
 | |
|     return AudioInput(builder: builder, isCm: false, isVoiceCall: false);
 | |
|   }
 | |
| 
 | |
|   Widget record(BuildContext context) {
 | |
|     final showRootDir = isWindows && bind.mainIsInstalled();
 | |
|     return futureBuilder(future: () async {
 | |
|       String user_dir = bind.mainVideoSaveDirectory(root: false);
 | |
|       String root_dir =
 | |
|           showRootDir ? bind.mainVideoSaveDirectory(root: true) : '';
 | |
|       bool user_dir_exists = await Directory(user_dir).exists();
 | |
|       bool root_dir_exists =
 | |
|           showRootDir ? await Directory(root_dir).exists() : false;
 | |
|       // canLaunchUrl blocked on windows portable, user SYSTEM
 | |
|       return {
 | |
|         'user_dir': user_dir,
 | |
|         'root_dir': root_dir,
 | |
|         'user_dir_exists': user_dir_exists,
 | |
|         'root_dir_exists': root_dir_exists,
 | |
|       };
 | |
|     }(), hasData: (data) {
 | |
|       Map<String, dynamic> map = data as Map<String, dynamic>;
 | |
|       String user_dir = map['user_dir']!;
 | |
|       String root_dir = map['root_dir']!;
 | |
|       bool root_dir_exists = map['root_dir_exists']!;
 | |
|       bool user_dir_exists = map['user_dir_exists']!;
 | |
|       return _Card(title: 'Recording', children: [
 | |
|         _OptionCheckBox(context, 'Automatically record incoming sessions',
 | |
|             kOptionAllowAutoRecordIncoming),
 | |
|         if (showRootDir)
 | |
|           Row(
 | |
|             children: [
 | |
|               Text('${translate("Incoming")}:'),
 | |
|               Expanded(
 | |
|                 child: GestureDetector(
 | |
|                     onTap: root_dir_exists
 | |
|                         ? () => launchUrl(Uri.file(root_dir))
 | |
|                         : null,
 | |
|                     child: Text(
 | |
|                       root_dir,
 | |
|                       softWrap: true,
 | |
|                       style: root_dir_exists
 | |
|                           ? const TextStyle(
 | |
|                               decoration: TextDecoration.underline)
 | |
|                           : null,
 | |
|                     )).marginOnly(left: 10),
 | |
|               ),
 | |
|             ],
 | |
|           ).marginOnly(left: _kContentHMargin),
 | |
|         Row(
 | |
|           children: [
 | |
|             Text('${translate(showRootDir ? "Outgoing" : "Directory")}:'),
 | |
|             Expanded(
 | |
|               child: GestureDetector(
 | |
|                   onTap: user_dir_exists
 | |
|                       ? () => launchUrl(Uri.file(user_dir))
 | |
|                       : null,
 | |
|                   child: Text(
 | |
|                     user_dir,
 | |
|                     softWrap: true,
 | |
|                     style: user_dir_exists
 | |
|                         ? const TextStyle(decoration: TextDecoration.underline)
 | |
|                         : null,
 | |
|                   )).marginOnly(left: 10),
 | |
|             ),
 | |
|             ElevatedButton(
 | |
|                     onPressed: isOptionFixed(kOptionVideoSaveDirectory)
 | |
|                         ? null
 | |
|                         : () async {
 | |
|                             String? initialDirectory;
 | |
|                             if (await Directory.fromUri(Uri.directory(user_dir))
 | |
|                                 .exists()) {
 | |
|                               initialDirectory = user_dir;
 | |
|                             }
 | |
|                             String? selectedDirectory =
 | |
|                                 await FilePicker.platform.getDirectoryPath(
 | |
|                                     initialDirectory: initialDirectory);
 | |
|                             if (selectedDirectory != null) {
 | |
|                               await bind.mainSetOption(
 | |
|                                   key: kOptionVideoSaveDirectory,
 | |
|                                   value: selectedDirectory);
 | |
|                               setState(() {});
 | |
|                             }
 | |
|                           },
 | |
|                     child: Text(translate('Change')))
 | |
|                 .marginOnly(left: 5),
 | |
|           ],
 | |
|         ).marginOnly(left: _kContentHMargin),
 | |
|       ]);
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   Widget language() {
 | |
|     return futureBuilder(future: () async {
 | |
|       String langs = await bind.mainGetLangs();
 | |
|       return {'langs': langs};
 | |
|     }(), hasData: (res) {
 | |
|       Map<String, String> data = res as Map<String, String>;
 | |
|       List<dynamic> langsList = jsonDecode(data['langs']!);
 | |
|       Map<String, String> langsMap = {for (var v in langsList) v[0]: v[1]};
 | |
|       List<String> keys = langsMap.keys.toList();
 | |
|       List<String> values = langsMap.values.toList();
 | |
|       keys.insert(0, defaultOptionLang);
 | |
|       values.insert(0, translate('Default'));
 | |
|       String currentKey = bind.mainGetLocalOption(key: kCommConfKeyLang);
 | |
|       if (!keys.contains(currentKey)) {
 | |
|         currentKey = defaultOptionLang;
 | |
|       }
 | |
|       final isOptFixed = isOptionFixed(kCommConfKeyLang);
 | |
|       return ComboBox(
 | |
|         keys: keys,
 | |
|         values: values,
 | |
|         initialKey: currentKey,
 | |
|         onChanged: (key) async {
 | |
|           await bind.mainSetLocalOption(key: kCommConfKeyLang, value: key);
 | |
|           if (isWeb) reloadCurrentWindow();
 | |
|           if (!isWeb) reloadAllWindows();
 | |
|           if (!isWeb) bind.mainChangeLanguage(lang: key);
 | |
|         },
 | |
|         enabled: !isOptFixed,
 | |
|       ).marginOnly(left: _kContentHMargin);
 | |
|     });
 | |
|   }
 | |
| }
 | |
| 
 | |
| enum _AccessMode {
 | |
|   custom,
 | |
|   full,
 | |
|   view,
 | |
| }
 | |
| 
 | |
| class _Safety extends StatefulWidget {
 | |
|   const _Safety({Key? key}) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   State<_Safety> createState() => _SafetyState();
 | |
| }
 | |
| 
 | |
| class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
 | |
|   @override
 | |
|   bool get wantKeepAlive => true;
 | |
|   bool locked = bind.mainIsInstalled();
 | |
|   final scrollController = ScrollController();
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     super.build(context);
 | |
|     return DesktopScrollWrapper(
 | |
|         scrollController: scrollController,
 | |
|         child: SingleChildScrollView(
 | |
|             physics: DraggableNeverScrollableScrollPhysics(),
 | |
|             controller: scrollController,
 | |
|             child: Column(
 | |
|               children: [
 | |
|                 _lock(locked, 'Unlock Security Settings', () {
 | |
|                   locked = false;
 | |
|                   setState(() => {});
 | |
|                 }),
 | |
|                 AbsorbPointer(
 | |
|                   absorbing: locked,
 | |
|                   child: Column(children: [
 | |
|                     permissions(context),
 | |
|                     password(context),
 | |
|                     _Card(title: '2FA', children: [tfa()]),
 | |
|                     _Card(title: 'ID', children: [changeId()]),
 | |
|                     more(context),
 | |
|                   ]),
 | |
|                 ),
 | |
|               ],
 | |
|             )).marginOnly(bottom: _kListViewBottomMargin));
 | |
|   }
 | |
| 
 | |
|   Widget tfa() {
 | |
|     bool enabled = !locked;
 | |
|     // Simple temp wrapper for PR check
 | |
|     tmpWrapper() {
 | |
|       RxBool has2fa = bind.mainHasValid2FaSync().obs;
 | |
|       RxBool hasBot = bind.mainHasValidBotSync().obs;
 | |
|       update() async {
 | |
|         has2fa.value = bind.mainHasValid2FaSync();
 | |
|         setState(() {});
 | |
|       }
 | |
| 
 | |
|       onChanged(bool? checked) async {
 | |
|         if (checked == false) {
 | |
|           CommonConfirmDialog(
 | |
|               gFFI.dialogManager, translate('cancel-2fa-confirm-tip'), () {
 | |
|             change2fa(callback: update);
 | |
|           });
 | |
|         } else {
 | |
|           change2fa(callback: update);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       final tfa = GestureDetector(
 | |
|         child: InkWell(
 | |
|           child: Obx(() => Row(
 | |
|                 children: [
 | |
|                   Checkbox(
 | |
|                           value: has2fa.value,
 | |
|                           onChanged: enabled ? onChanged : null)
 | |
|                       .marginOnly(right: 5),
 | |
|                   Expanded(
 | |
|                       child: Text(
 | |
|                     translate('enable-2fa-title'),
 | |
|                     style:
 | |
|                         TextStyle(color: disabledTextColor(context, enabled)),
 | |
|                   ))
 | |
|                 ],
 | |
|               )),
 | |
|         ),
 | |
|         onTap: () {
 | |
|           onChanged(!has2fa.value);
 | |
|         },
 | |
|       ).marginOnly(left: _kCheckBoxLeftMargin);
 | |
|       if (!has2fa.value) {
 | |
|         return tfa;
 | |
|       }
 | |
|       updateBot() async {
 | |
|         hasBot.value = bind.mainHasValidBotSync();
 | |
|         setState(() {});
 | |
|       }
 | |
| 
 | |
|       onChangedBot(bool? checked) async {
 | |
|         if (checked == false) {
 | |
|           CommonConfirmDialog(
 | |
|               gFFI.dialogManager, translate('cancel-bot-confirm-tip'), () {
 | |
|             changeBot(callback: updateBot);
 | |
|           });
 | |
|         } else {
 | |
|           changeBot(callback: updateBot);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       final bot = GestureDetector(
 | |
|         child: Tooltip(
 | |
|           waitDuration: Duration(milliseconds: 300),
 | |
|           message: translate("enable-bot-tip"),
 | |
|           child: InkWell(
 | |
|               child: Obx(() => Row(
 | |
|                     children: [
 | |
|                       Checkbox(
 | |
|                               value: hasBot.value,
 | |
|                               onChanged: enabled ? onChangedBot : null)
 | |
|                           .marginOnly(right: 5),
 | |
|                       Expanded(
 | |
|                           child: Text(
 | |
|                         translate('Telegram bot'),
 | |
|                         style: TextStyle(
 | |
|                             color: disabledTextColor(context, enabled)),
 | |
|                       ))
 | |
|                     ],
 | |
|                   ))),
 | |
|         ),
 | |
|         onTap: () {
 | |
|           onChangedBot(!hasBot.value);
 | |
|         },
 | |
|       ).marginOnly(left: _kCheckBoxLeftMargin + 30);
 | |
| 
 | |
|       final trust = Row(
 | |
|         children: [
 | |
|           Flexible(
 | |
|             child: Tooltip(
 | |
|               waitDuration: Duration(milliseconds: 300),
 | |
|               message: translate("enable-trusted-devices-tip"),
 | |
|               child: _OptionCheckBox(context, "Enable trusted devices",
 | |
|                   kOptionEnableTrustedDevices,
 | |
|                   enabled: !locked, update: (v) {
 | |
|                 setState(() {});
 | |
|               }),
 | |
|             ),
 | |
|           ),
 | |
|           if (mainGetBoolOptionSync(kOptionEnableTrustedDevices))
 | |
|             ElevatedButton(
 | |
|                 onPressed: locked
 | |
|                     ? null
 | |
|                     : () {
 | |
|                         manageTrustedDeviceDialog();
 | |
|                       },
 | |
|                 child: Text(translate('Manage trusted devices')))
 | |
|         ],
 | |
|       ).marginOnly(left: 30);
 | |
| 
 | |
|       return Column(
 | |
|         children: [tfa, bot, trust],
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     return tmpWrapper();
 | |
|   }
 | |
| 
 | |
|   Widget changeId() {
 | |
|     return ChangeNotifierProvider.value(
 | |
|         value: gFFI.serverModel,
 | |
|         child: Consumer<ServerModel>(builder: ((context, model, child) {
 | |
|           return _Button('Change ID', changeIdDialog,
 | |
|               enabled: !locked && model.connectStatus > 0);
 | |
|         })));
 | |
|   }
 | |
| 
 | |
|   Widget permissions(context) {
 | |
|     bool enabled = !locked;
 | |
|     // Simple temp wrapper for PR check
 | |
|     tmpWrapper() {
 | |
|       String accessMode = bind.mainGetOptionSync(key: kOptionAccessMode);
 | |
|       _AccessMode mode;
 | |
|       if (accessMode == 'full') {
 | |
|         mode = _AccessMode.full;
 | |
|       } else if (accessMode == 'view') {
 | |
|         mode = _AccessMode.view;
 | |
|       } else {
 | |
|         mode = _AccessMode.custom;
 | |
|       }
 | |
|       String initialKey;
 | |
|       bool? fakeValue;
 | |
|       switch (mode) {
 | |
|         case _AccessMode.custom:
 | |
|           initialKey = '';
 | |
|           fakeValue = null;
 | |
|           break;
 | |
|         case _AccessMode.full:
 | |
|           initialKey = 'full';
 | |
|           fakeValue = true;
 | |
|           break;
 | |
|         case _AccessMode.view:
 | |
|           initialKey = 'view';
 | |
|           fakeValue = false;
 | |
|           break;
 | |
|       }
 | |
| 
 | |
|       return _Card(title: 'Permissions', children: [
 | |
|         ComboBox(
 | |
|             keys: [
 | |
|               defaultOptionAccessMode,
 | |
|               'full',
 | |
|               'view',
 | |
|             ],
 | |
|             values: [
 | |
|               translate('Custom'),
 | |
|               translate('Full Access'),
 | |
|               translate('Screen Share'),
 | |
|             ],
 | |
|             enabled: enabled && !isOptionFixed(kOptionAccessMode),
 | |
|             initialKey: initialKey,
 | |
|             onChanged: (mode) async {
 | |
|               await bind.mainSetOption(key: kOptionAccessMode, value: mode);
 | |
|               setState(() {});
 | |
|             }).marginOnly(left: _kContentHMargin),
 | |
|         Column(
 | |
|           children: [
 | |
|             _OptionCheckBox(
 | |
|                 context, 'Enable keyboard/mouse', kOptionEnableKeyboard,
 | |
|                 enabled: enabled, fakeValue: fakeValue),
 | |
|             _OptionCheckBox(context, 'Enable clipboard', kOptionEnableClipboard,
 | |
|                 enabled: enabled, fakeValue: fakeValue),
 | |
|             _OptionCheckBox(
 | |
|                 context, 'Enable file transfer', kOptionEnableFileTransfer,
 | |
|                 enabled: enabled, fakeValue: fakeValue),
 | |
|             _OptionCheckBox(context, 'Enable audio', kOptionEnableAudio,
 | |
|                 enabled: enabled, fakeValue: fakeValue),
 | |
|             _OptionCheckBox(
 | |
|                 context, 'Enable TCP tunneling', kOptionEnableTunnel,
 | |
|                 enabled: enabled, fakeValue: fakeValue),
 | |
|             _OptionCheckBox(
 | |
|                 context, 'Enable remote restart', kOptionEnableRemoteRestart,
 | |
|                 enabled: enabled, fakeValue: fakeValue),
 | |
|             _OptionCheckBox(
 | |
|                 context, 'Enable recording session', kOptionEnableRecordSession,
 | |
|                 enabled: enabled, fakeValue: fakeValue),
 | |
|             if (isWindows)
 | |
|               _OptionCheckBox(context, 'Enable blocking user input',
 | |
|                   kOptionEnableBlockInput,
 | |
|                   enabled: enabled, fakeValue: fakeValue),
 | |
|             _OptionCheckBox(context, 'Enable remote configuration modification',
 | |
|                 kOptionAllowRemoteConfigModification,
 | |
|                 enabled: enabled, fakeValue: fakeValue),
 | |
|           ],
 | |
|         ),
 | |
|       ]);
 | |
|     }
 | |
| 
 | |
|     return tmpWrapper();
 | |
|   }
 | |
| 
 | |
|   Widget password(BuildContext context) {
 | |
|     return ChangeNotifierProvider.value(
 | |
|         value: gFFI.serverModel,
 | |
|         child: Consumer<ServerModel>(builder: ((context, model, child) {
 | |
|           List<String> passwordKeys = [
 | |
|             kUseTemporaryPassword,
 | |
|             kUsePermanentPassword,
 | |
|             kUseBothPasswords,
 | |
|           ];
 | |
|           List<String> passwordValues = [
 | |
|             translate('Use one-time password'),
 | |
|             translate('Use permanent password'),
 | |
|             translate('Use both passwords'),
 | |
|           ];
 | |
|           bool tmpEnabled = model.verificationMethod != kUsePermanentPassword;
 | |
|           bool permEnabled = model.verificationMethod != kUseTemporaryPassword;
 | |
|           String currentValue =
 | |
|               passwordValues[passwordKeys.indexOf(model.verificationMethod)];
 | |
|           List<Widget> radios = passwordValues
 | |
|               .map((value) => _Radio<String>(
 | |
|                     context,
 | |
|                     value: value,
 | |
|                     groupValue: currentValue,
 | |
|                     label: value,
 | |
|                     onChanged: locked
 | |
|                         ? null
 | |
|                         : ((value) async {
 | |
|                             callback() async {
 | |
|                               await model.setVerificationMethod(
 | |
|                                   passwordKeys[passwordValues.indexOf(value)]);
 | |
|                               await model.updatePasswordModel();
 | |
|                             }
 | |
| 
 | |
|                             if (value ==
 | |
|                                     passwordValues[passwordKeys
 | |
|                                         .indexOf(kUsePermanentPassword)] &&
 | |
|                                 (await bind.mainGetPermanentPassword())
 | |
|                                     .isEmpty) {
 | |
|                               setPasswordDialog(notEmptyCallback: callback);
 | |
|                             } else {
 | |
|                               await callback();
 | |
|                             }
 | |
|                           }),
 | |
|                   ))
 | |
|               .toList();
 | |
| 
 | |
|           var onChanged = tmpEnabled && !locked
 | |
|               ? (value) {
 | |
|                   if (value != null) {
 | |
|                     () async {
 | |
|                       await model.setTemporaryPasswordLength(value.toString());
 | |
|                       await model.updatePasswordModel();
 | |
|                     }();
 | |
|                   }
 | |
|                 }
 | |
|               : null;
 | |
|           List<Widget> lengthRadios = ['6', '8', '10']
 | |
|               .map((value) => GestureDetector(
 | |
|                     child: Row(
 | |
|                       children: [
 | |
|                         Radio(
 | |
|                             value: value,
 | |
|                             groupValue: model.temporaryPasswordLength,
 | |
|                             onChanged: onChanged),
 | |
|                         Text(
 | |
|                           value,
 | |
|                           style: TextStyle(
 | |
|                               color: disabledTextColor(
 | |
|                                   context, onChanged != null)),
 | |
|                         ),
 | |
|                       ],
 | |
|                     ).paddingOnly(right: 10),
 | |
|                     onTap: () => onChanged?.call(value),
 | |
|                   ))
 | |
|               .toList();
 | |
| 
 | |
|           final modeKeys = <String>[
 | |
|             'password',
 | |
|             'click',
 | |
|             defaultOptionApproveMode
 | |
|           ];
 | |
|           final modeValues = [
 | |
|             translate('Accept sessions via password'),
 | |
|             translate('Accept sessions via click'),
 | |
|             translate('Accept sessions via both'),
 | |
|           ];
 | |
|           var modeInitialKey = model.approveMode;
 | |
|           if (!modeKeys.contains(modeInitialKey)) modeInitialKey = '';
 | |
|           final usePassword = model.approveMode != 'click';
 | |
| 
 | |
|           final isApproveModeFixed = isOptionFixed(kOptionApproveMode);
 | |
|           return _Card(title: 'Password', children: [
 | |
|             ComboBox(
 | |
|               enabled: !locked && !isApproveModeFixed,
 | |
|               keys: modeKeys,
 | |
|               values: modeValues,
 | |
|               initialKey: modeInitialKey,
 | |
|               onChanged: (key) => model.setApproveMode(key),
 | |
|             ).marginOnly(left: _kContentHMargin),
 | |
|             if (usePassword) radios[0],
 | |
|             if (usePassword)
 | |
|               _SubLabeledWidget(
 | |
|                   context,
 | |
|                   'One-time password length',
 | |
|                   Row(
 | |
|                     children: [
 | |
|                       ...lengthRadios,
 | |
|                     ],
 | |
|                   ),
 | |
|                   enabled: tmpEnabled && !locked),
 | |
|             if (usePassword) radios[1],
 | |
|             if (usePassword)
 | |
|               _SubButton('Set permanent password', setPasswordDialog,
 | |
|                   permEnabled && !locked),
 | |
|             // if (usePassword)
 | |
|             //   hide_cm(!locked).marginOnly(left: _kContentHSubMargin - 6),
 | |
|             if (usePassword) radios[2],
 | |
|           ]);
 | |
|         })));
 | |
|   }
 | |
| 
 | |
|   Widget more(BuildContext context) {
 | |
|     bool enabled = !locked;
 | |
|     return _Card(title: 'Security', children: [
 | |
|       shareRdp(context, enabled),
 | |
|       _OptionCheckBox(context, 'Deny LAN discovery', 'enable-lan-discovery',
 | |
|           reverse: true, enabled: enabled),
 | |
|       ...directIp(context),
 | |
|       whitelist(),
 | |
|       ...autoDisconnect(context),
 | |
|       if (bind.mainIsInstalled())
 | |
|         _OptionCheckBox(context, 'allow-only-conn-window-open-tip',
 | |
|             'allow-only-conn-window-open',
 | |
|             reverse: false, enabled: enabled),
 | |
|       if (bind.mainIsInstalled()) unlockPin()
 | |
|     ]);
 | |
|   }
 | |
| 
 | |
|   shareRdp(BuildContext context, bool enabled) {
 | |
|     onChanged(bool b) async {
 | |
|       await bind.mainSetShareRdp(enable: b);
 | |
|       setState(() {});
 | |
|     }
 | |
| 
 | |
|     bool value = bind.mainIsShareRdp();
 | |
|     return Offstage(
 | |
|       offstage: !(isWindows && bind.mainIsInstalled()),
 | |
|       child: GestureDetector(
 | |
|           child: Row(
 | |
|             children: [
 | |
|               Checkbox(
 | |
|                       value: value,
 | |
|                       onChanged: enabled ? (_) => onChanged(!value) : null)
 | |
|                   .marginOnly(right: 5),
 | |
|               Expanded(
 | |
|                 child: Text(translate('Enable RDP session sharing'),
 | |
|                     style:
 | |
|                         TextStyle(color: disabledTextColor(context, enabled))),
 | |
|               )
 | |
|             ],
 | |
|           ).marginOnly(left: _kCheckBoxLeftMargin),
 | |
|           onTap: enabled ? () => onChanged(!value) : null),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   List<Widget> directIp(BuildContext context) {
 | |
|     TextEditingController controller = TextEditingController();
 | |
|     update(bool v) => setState(() {});
 | |
|     RxBool applyEnabled = false.obs;
 | |
|     return [
 | |
|       _OptionCheckBox(context, 'Enable direct IP access', kOptionDirectServer,
 | |
|           update: update, enabled: !locked),
 | |
|       () {
 | |
|         // Simple temp wrapper for PR check
 | |
|         tmpWrapper() {
 | |
|           bool enabled = option2bool(kOptionDirectServer,
 | |
|               bind.mainGetOptionSync(key: kOptionDirectServer));
 | |
|           if (!enabled) applyEnabled.value = false;
 | |
|           controller.text =
 | |
|               bind.mainGetOptionSync(key: kOptionDirectAccessPort);
 | |
|           final isOptFixed = isOptionFixed(kOptionDirectAccessPort);
 | |
|           return Offstage(
 | |
|             offstage: !enabled,
 | |
|             child: _SubLabeledWidget(
 | |
|               context,
 | |
|               'Port',
 | |
|               Row(children: [
 | |
|                 SizedBox(
 | |
|                   width: 95,
 | |
|                   child: TextField(
 | |
|                     controller: controller,
 | |
|                     enabled: enabled && !locked && !isOptFixed,
 | |
|                     onChanged: (_) => applyEnabled.value = true,
 | |
|                     inputFormatters: [
 | |
|                       FilteringTextInputFormatter.allow(RegExp(
 | |
|                           r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')),
 | |
|                     ],
 | |
|                     decoration: const InputDecoration(
 | |
|                       hintText: '21118',
 | |
|                       contentPadding:
 | |
|                           EdgeInsets.symmetric(vertical: 12, horizontal: 12),
 | |
|                     ),
 | |
|                   ).marginOnly(right: 15),
 | |
|                 ),
 | |
|                 Obx(() => ElevatedButton(
 | |
|                       onPressed: applyEnabled.value &&
 | |
|                               enabled &&
 | |
|                               !locked &&
 | |
|                               !isOptFixed
 | |
|                           ? () async {
 | |
|                               applyEnabled.value = false;
 | |
|                               await bind.mainSetOption(
 | |
|                                   key: kOptionDirectAccessPort,
 | |
|                                   value: controller.text);
 | |
|                             }
 | |
|                           : null,
 | |
|                       child: Text(
 | |
|                         translate('Apply'),
 | |
|                       ),
 | |
|                     ))
 | |
|               ]),
 | |
|               enabled: enabled && !locked && !isOptFixed,
 | |
|             ),
 | |
|           );
 | |
|         }
 | |
| 
 | |
|         return tmpWrapper();
 | |
|       }(),
 | |
|     ];
 | |
|   }
 | |
| 
 | |
|   Widget whitelist() {
 | |
|     bool enabled = !locked;
 | |
|     // Simple temp wrapper for PR check
 | |
|     tmpWrapper() {
 | |
|       RxBool hasWhitelist = whitelistNotEmpty().obs;
 | |
|       update() async {
 | |
|         hasWhitelist.value = whitelistNotEmpty();
 | |
|       }
 | |
| 
 | |
|       onChanged(bool? checked) async {
 | |
|         changeWhiteList(callback: update);
 | |
|       }
 | |
| 
 | |
|       final isOptFixed = isOptionFixed(kOptionWhitelist);
 | |
|       return GestureDetector(
 | |
|         child: Tooltip(
 | |
|           message: translate('whitelist_tip'),
 | |
|           child: Obx(() => Row(
 | |
|                 children: [
 | |
|                   Checkbox(
 | |
|                           value: hasWhitelist.value,
 | |
|                           onChanged: enabled && !isOptFixed ? onChanged : null)
 | |
|                       .marginOnly(right: 5),
 | |
|                   Offstage(
 | |
|                     offstage: !hasWhitelist.value,
 | |
|                     child: MouseRegion(
 | |
|                       child: const Icon(Icons.warning_amber_rounded,
 | |
|                               color: Color.fromARGB(255, 255, 204, 0))
 | |
|                           .marginOnly(right: 5),
 | |
|                       cursor: SystemMouseCursors.click,
 | |
|                     ),
 | |
|                   ),
 | |
|                   Expanded(
 | |
|                       child: Text(
 | |
|                     translate('Use IP Whitelisting'),
 | |
|                     style:
 | |
|                         TextStyle(color: disabledTextColor(context, enabled)),
 | |
|                   ))
 | |
|                 ],
 | |
|               )),
 | |
|         ),
 | |
|         onTap: enabled
 | |
|             ? () {
 | |
|                 onChanged(!hasWhitelist.value);
 | |
|               }
 | |
|             : null,
 | |
|       ).marginOnly(left: _kCheckBoxLeftMargin);
 | |
|     }
 | |
| 
 | |
|     return tmpWrapper();
 | |
|   }
 | |
| 
 | |
|   Widget hide_cm(bool enabled) {
 | |
|     return ChangeNotifierProvider.value(
 | |
|         value: gFFI.serverModel,
 | |
|         child: Consumer<ServerModel>(builder: (context, model, child) {
 | |
|           final enableHideCm = model.approveMode == 'password' &&
 | |
|               model.verificationMethod == kUsePermanentPassword;
 | |
|           onHideCmChanged(bool? b) {
 | |
|             if (b != null) {
 | |
|               bind.mainSetOption(
 | |
|                   key: 'allow-hide-cm', value: bool2option('allow-hide-cm', b));
 | |
|             }
 | |
|           }
 | |
| 
 | |
|           return Tooltip(
 | |
|               message: enableHideCm ? "" : translate('hide_cm_tip'),
 | |
|               child: GestureDetector(
 | |
|                 onTap:
 | |
|                     enableHideCm ? () => onHideCmChanged(!model.hideCm) : null,
 | |
|                 child: Row(
 | |
|                   children: [
 | |
|                     Checkbox(
 | |
|                             value: model.hideCm,
 | |
|                             onChanged: enabled && enableHideCm
 | |
|                                 ? onHideCmChanged
 | |
|                                 : null)
 | |
|                         .marginOnly(right: 5),
 | |
|                     Expanded(
 | |
|                       child: Text(
 | |
|                         translate('Hide connection management window'),
 | |
|                         style: TextStyle(
 | |
|                             color: disabledTextColor(
 | |
|                                 context, enabled && enableHideCm)),
 | |
|                       ),
 | |
|                     ),
 | |
|                   ],
 | |
|                 ),
 | |
|               ));
 | |
|         }));
 | |
|   }
 | |
| 
 | |
|   List<Widget> autoDisconnect(BuildContext context) {
 | |
|     TextEditingController controller = TextEditingController();
 | |
|     update(bool v) => setState(() {});
 | |
|     RxBool applyEnabled = false.obs;
 | |
|     return [
 | |
|       _OptionCheckBox(
 | |
|           context, 'auto_disconnect_option_tip', kOptionAllowAutoDisconnect,
 | |
|           update: update, enabled: !locked),
 | |
|       () {
 | |
|         bool enabled = option2bool(kOptionAllowAutoDisconnect,
 | |
|             bind.mainGetOptionSync(key: kOptionAllowAutoDisconnect));
 | |
|         if (!enabled) applyEnabled.value = false;
 | |
|         controller.text =
 | |
|             bind.mainGetOptionSync(key: kOptionAutoDisconnectTimeout);
 | |
|         final isOptFixed = isOptionFixed(kOptionAutoDisconnectTimeout);
 | |
|         return Offstage(
 | |
|           offstage: !enabled,
 | |
|           child: _SubLabeledWidget(
 | |
|             context,
 | |
|             'Timeout in minutes',
 | |
|             Row(children: [
 | |
|               SizedBox(
 | |
|                 width: 95,
 | |
|                 child: TextField(
 | |
|                   controller: controller,
 | |
|                   enabled: enabled && !locked && !isOptFixed,
 | |
|                   onChanged: (_) => applyEnabled.value = true,
 | |
|                   inputFormatters: [
 | |
|                     FilteringTextInputFormatter.allow(RegExp(
 | |
|                         r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')),
 | |
|                   ],
 | |
|                   decoration: const InputDecoration(
 | |
|                     hintText: '10',
 | |
|                     contentPadding:
 | |
|                         EdgeInsets.symmetric(vertical: 12, horizontal: 12),
 | |
|                   ),
 | |
|                 ).marginOnly(right: 15),
 | |
|               ),
 | |
|               Obx(() => ElevatedButton(
 | |
|                     onPressed:
 | |
|                         applyEnabled.value && enabled && !locked && !isOptFixed
 | |
|                             ? () async {
 | |
|                                 applyEnabled.value = false;
 | |
|                                 await bind.mainSetOption(
 | |
|                                     key: kOptionAutoDisconnectTimeout,
 | |
|                                     value: controller.text);
 | |
|                               }
 | |
|                             : null,
 | |
|                     child: Text(
 | |
|                       translate('Apply'),
 | |
|                     ),
 | |
|                   ))
 | |
|             ]),
 | |
|             enabled: enabled && !locked && !isOptFixed,
 | |
|           ),
 | |
|         );
 | |
|       }(),
 | |
|     ];
 | |
|   }
 | |
| 
 | |
|   Widget unlockPin() {
 | |
|     bool enabled = !locked;
 | |
|     RxString unlockPin = bind.mainGetUnlockPin().obs;
 | |
|     update() async {
 | |
|       unlockPin.value = bind.mainGetUnlockPin();
 | |
|     }
 | |
| 
 | |
|     onChanged(bool? checked) async {
 | |
|       changeUnlockPinDialog(unlockPin.value, update);
 | |
|     }
 | |
| 
 | |
|     final isOptFixed = isOptionFixed(kOptionWhitelist);
 | |
|     return GestureDetector(
 | |
|       child: Obx(() => Row(
 | |
|             children: [
 | |
|               Checkbox(
 | |
|                       value: unlockPin.isNotEmpty,
 | |
|                       onChanged: enabled && !isOptFixed ? onChanged : null)
 | |
|                   .marginOnly(right: 5),
 | |
|               Expanded(
 | |
|                   child: Text(
 | |
|                 translate('Unlock with PIN'),
 | |
|                 style: TextStyle(color: disabledTextColor(context, enabled)),
 | |
|               ))
 | |
|             ],
 | |
|           )),
 | |
|       onTap: enabled
 | |
|           ? () {
 | |
|               onChanged(!unlockPin.isNotEmpty);
 | |
|             }
 | |
|           : null,
 | |
|     ).marginOnly(left: _kCheckBoxLeftMargin);
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _Network extends StatefulWidget {
 | |
|   const _Network({Key? key}) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   State<_Network> createState() => _NetworkState();
 | |
| }
 | |
| 
 | |
| class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
 | |
|   @override
 | |
|   bool get wantKeepAlive => true;
 | |
|   bool locked = !isWeb && bind.mainIsInstalled();
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     super.build(context);
 | |
|     bool enabled = !locked;
 | |
|     final scrollController = ScrollController();
 | |
|     final hideServer =
 | |
|         bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y';
 | |
|     // TODO: support web proxy
 | |
|     final hideProxy =
 | |
|         isWeb || bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
 | |
|     return DesktopScrollWrapper(
 | |
|         scrollController: scrollController,
 | |
|         child: ListView(
 | |
|             controller: scrollController,
 | |
|             physics: DraggableNeverScrollableScrollPhysics(),
 | |
|             children: [
 | |
|               _lock(locked, 'Unlock Network Settings', () {
 | |
|                 locked = false;
 | |
|                 setState(() => {});
 | |
|               }),
 | |
|               AbsorbPointer(
 | |
|                 absorbing: locked,
 | |
|                 child: Column(children: [
 | |
|                   if (!hideServer) server(enabled),
 | |
|                   if (!hideProxy)
 | |
|                     _Card(title: 'Proxy', children: [
 | |
|                       _Button('Socks5/Http(s) Proxy', changeSocks5Proxy,
 | |
|                           enabled: enabled),
 | |
|                     ]),
 | |
|                 ]),
 | |
|               ),
 | |
|             ]).marginOnly(bottom: _kListViewBottomMargin));
 | |
|   }
 | |
| 
 | |
|   server(bool enabled) {
 | |
|     // Simple temp wrapper for PR check
 | |
|     tmpWrapper() {
 | |
|       // Setting page is not modal, oldOptions should only be used when getting options, never when setting.
 | |
|       Map<String, dynamic> oldOptions = jsonDecode(bind.mainGetOptionsSync());
 | |
|       old(String key) {
 | |
|         return (oldOptions[key] ?? '').trim();
 | |
|       }
 | |
| 
 | |
|       RxString idErrMsg = ''.obs;
 | |
|       RxString relayErrMsg = ''.obs;
 | |
|       RxString apiErrMsg = ''.obs;
 | |
|       var idController =
 | |
|           TextEditingController(text: old('custom-rendezvous-server'));
 | |
|       var relayController = TextEditingController(text: old('relay-server'));
 | |
|       var apiController = TextEditingController(text: old('api-server'));
 | |
|       var keyController = TextEditingController(text: old('key'));
 | |
|       final controllers = [
 | |
|         idController,
 | |
|         relayController,
 | |
|         apiController,
 | |
|         keyController,
 | |
|       ];
 | |
|       final errMsgs = [
 | |
|         idErrMsg,
 | |
|         relayErrMsg,
 | |
|         apiErrMsg,
 | |
|       ];
 | |
| 
 | |
|       submit() async {
 | |
|         bool result = await setServerConfig(
 | |
|             null,
 | |
|             errMsgs,
 | |
|             ServerConfig(
 | |
|                 idServer: idController.text,
 | |
|                 relayServer: relayController.text,
 | |
|                 apiServer: apiController.text,
 | |
|                 key: keyController.text));
 | |
|         if (result) {
 | |
|           setState(() {});
 | |
|           showToast(translate('Successful'));
 | |
|         } else {
 | |
|           showToast(translate('Failed'));
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       bool secure = !enabled;
 | |
|       return _Card(
 | |
|           title: 'ID/Relay Server',
 | |
|           title_suffix: ServerConfigImportExportWidgets(controllers, errMsgs),
 | |
|           children: [
 | |
|             Column(
 | |
|               children: [
 | |
|                 Obx(() => _LabeledTextField(context, 'ID Server', idController,
 | |
|                     idErrMsg.value, enabled, secure)),
 | |
|                 Obx(() => _LabeledTextField(context, 'Relay Server',
 | |
|                     relayController, relayErrMsg.value, enabled, secure)),
 | |
|                 Obx(() => _LabeledTextField(context, 'API Server',
 | |
|                     apiController, apiErrMsg.value, enabled, secure)),
 | |
|                 _LabeledTextField(
 | |
|                     context, 'Key', keyController, '', enabled, secure),
 | |
|                 Row(
 | |
|                   mainAxisAlignment: MainAxisAlignment.end,
 | |
|                   children: [_Button('Apply', submit, enabled: enabled)],
 | |
|                 ).marginOnly(top: 10),
 | |
|               ],
 | |
|             )
 | |
|           ]);
 | |
|     }
 | |
| 
 | |
|     return tmpWrapper();
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _Display extends StatefulWidget {
 | |
|   const _Display({Key? key}) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   State<_Display> createState() => _DisplayState();
 | |
| }
 | |
| 
 | |
| class _DisplayState extends State<_Display> {
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     final scrollController = ScrollController();
 | |
|     return DesktopScrollWrapper(
 | |
|         scrollController: scrollController,
 | |
|         child: ListView(
 | |
|             controller: scrollController,
 | |
|             physics: DraggableNeverScrollableScrollPhysics(),
 | |
|             children: [
 | |
|               viewStyle(context),
 | |
|               scrollStyle(context),
 | |
|               imageQuality(context),
 | |
|               codec(context),
 | |
|               if (!isWeb) privacyModeImpl(context),
 | |
|               other(context),
 | |
|             ]).marginOnly(bottom: _kListViewBottomMargin));
 | |
|   }
 | |
| 
 | |
|   Widget viewStyle(BuildContext context) {
 | |
|     final isOptFixed = isOptionFixed(kOptionViewStyle);
 | |
|     onChanged(String value) async {
 | |
|       await bind.mainSetUserDefaultOption(key: kOptionViewStyle, value: value);
 | |
|       setState(() {});
 | |
|     }
 | |
| 
 | |
|     final groupValue = bind.mainGetUserDefaultOption(key: kOptionViewStyle);
 | |
|     return _Card(title: 'Default View Style', children: [
 | |
|       _Radio(context,
 | |
|           value: kRemoteViewStyleOriginal,
 | |
|           groupValue: groupValue,
 | |
|           label: 'Scale original',
 | |
|           onChanged: isOptFixed ? null : onChanged),
 | |
|       _Radio(context,
 | |
|           value: kRemoteViewStyleAdaptive,
 | |
|           groupValue: groupValue,
 | |
|           label: 'Scale adaptive',
 | |
|           onChanged: isOptFixed ? null : onChanged),
 | |
|     ]);
 | |
|   }
 | |
| 
 | |
|   Widget scrollStyle(BuildContext context) {
 | |
|     final isOptFixed = isOptionFixed(kOptionScrollStyle);
 | |
|     onChanged(String value) async {
 | |
|       await bind.mainSetUserDefaultOption(
 | |
|           key: kOptionScrollStyle, value: value);
 | |
|       setState(() {});
 | |
|     }
 | |
| 
 | |
|     final groupValue = bind.mainGetUserDefaultOption(key: kOptionScrollStyle);
 | |
|     return _Card(title: 'Default Scroll Style', children: [
 | |
|       _Radio(context,
 | |
|           value: kRemoteScrollStyleAuto,
 | |
|           groupValue: groupValue,
 | |
|           label: 'ScrollAuto',
 | |
|           onChanged: isOptFixed ? null : onChanged),
 | |
|       _Radio(context,
 | |
|           value: kRemoteScrollStyleBar,
 | |
|           groupValue: groupValue,
 | |
|           label: 'Scrollbar',
 | |
|           onChanged: isOptFixed ? null : onChanged),
 | |
|     ]);
 | |
|   }
 | |
| 
 | |
|   Widget imageQuality(BuildContext context) {
 | |
|     onChanged(String value) async {
 | |
|       await bind.mainSetUserDefaultOption(
 | |
|           key: kOptionImageQuality, value: value);
 | |
|       setState(() {});
 | |
|     }
 | |
| 
 | |
|     final isOptFixed = isOptionFixed(kOptionImageQuality);
 | |
|     final groupValue = bind.mainGetUserDefaultOption(key: kOptionImageQuality);
 | |
|     return _Card(title: 'Default Image Quality', children: [
 | |
|       _Radio(context,
 | |
|           value: kRemoteImageQualityBest,
 | |
|           groupValue: groupValue,
 | |
|           label: 'Good image quality',
 | |
|           onChanged: isOptFixed ? null : onChanged),
 | |
|       _Radio(context,
 | |
|           value: kRemoteImageQualityBalanced,
 | |
|           groupValue: groupValue,
 | |
|           label: 'Balanced',
 | |
|           onChanged: isOptFixed ? null : onChanged),
 | |
|       _Radio(context,
 | |
|           value: kRemoteImageQualityLow,
 | |
|           groupValue: groupValue,
 | |
|           label: 'Optimize reaction time',
 | |
|           onChanged: isOptFixed ? null : onChanged),
 | |
|       _Radio(context,
 | |
|           value: kRemoteImageQualityCustom,
 | |
|           groupValue: groupValue,
 | |
|           label: 'Custom',
 | |
|           onChanged: isOptFixed ? null : onChanged),
 | |
|       Offstage(
 | |
|         offstage: groupValue != kRemoteImageQualityCustom,
 | |
|         child: customImageQualitySetting(),
 | |
|       )
 | |
|     ]);
 | |
|   }
 | |
| 
 | |
|   Widget codec(BuildContext context) {
 | |
|     onChanged(String value) async {
 | |
|       await bind.mainSetUserDefaultOption(
 | |
|           key: kOptionCodecPreference, value: value);
 | |
|       setState(() {});
 | |
|     }
 | |
| 
 | |
|     final groupValue =
 | |
|         bind.mainGetUserDefaultOption(key: kOptionCodecPreference);
 | |
|     var hwRadios = [];
 | |
|     final isOptFixed = isOptionFixed(kOptionCodecPreference);
 | |
|     try {
 | |
|       final Map codecsJson = jsonDecode(bind.mainSupportedHwdecodings());
 | |
|       final h264 = codecsJson['h264'] ?? false;
 | |
|       final h265 = codecsJson['h265'] ?? false;
 | |
|       if (h264) {
 | |
|         hwRadios.add(_Radio(context,
 | |
|             value: 'h264',
 | |
|             groupValue: groupValue,
 | |
|             label: 'H264',
 | |
|             onChanged: isOptFixed ? null : onChanged));
 | |
|       }
 | |
|       if (h265) {
 | |
|         hwRadios.add(_Radio(context,
 | |
|             value: 'h265',
 | |
|             groupValue: groupValue,
 | |
|             label: 'H265',
 | |
|             onChanged: isOptFixed ? null : onChanged));
 | |
|       }
 | |
|     } catch (e) {
 | |
|       debugPrint("failed to parse supported hwdecodings, err=$e");
 | |
|     }
 | |
|     return _Card(title: 'Default Codec', children: [
 | |
|       _Radio(context,
 | |
|           value: 'auto',
 | |
|           groupValue: groupValue,
 | |
|           label: 'Auto',
 | |
|           onChanged: isOptFixed ? null : onChanged),
 | |
|       _Radio(context,
 | |
|           value: 'vp8',
 | |
|           groupValue: groupValue,
 | |
|           label: 'VP8',
 | |
|           onChanged: isOptFixed ? null : onChanged),
 | |
|       _Radio(context,
 | |
|           value: 'vp9',
 | |
|           groupValue: groupValue,
 | |
|           label: 'VP9',
 | |
|           onChanged: isOptFixed ? null : onChanged),
 | |
|       _Radio(context,
 | |
|           value: 'av1',
 | |
|           groupValue: groupValue,
 | |
|           label: 'AV1',
 | |
|           onChanged: isOptFixed ? null : onChanged),
 | |
|       ...hwRadios,
 | |
|     ]);
 | |
|   }
 | |
| 
 | |
|   Widget privacyModeImpl(BuildContext context) {
 | |
|     final supportedPrivacyModeImpls = bind.mainSupportedPrivacyModeImpls();
 | |
|     late final List<dynamic> privacyModeImpls;
 | |
|     try {
 | |
|       privacyModeImpls = jsonDecode(supportedPrivacyModeImpls);
 | |
|     } catch (e) {
 | |
|       debugPrint('failed to parse supported privacy mode impls, err=$e');
 | |
|       return Offstage();
 | |
|     }
 | |
|     if (privacyModeImpls.length < 2) {
 | |
|       return Offstage();
 | |
|     }
 | |
| 
 | |
|     final key = 'privacy-mode-impl-key';
 | |
|     onChanged(String value) async {
 | |
|       await bind.mainSetOption(key: key, value: value);
 | |
|       setState(() {});
 | |
|     }
 | |
| 
 | |
|     String groupValue = bind.mainGetOptionSync(key: key);
 | |
|     if (groupValue.isEmpty) {
 | |
|       groupValue = bind.mainDefaultPrivacyModeImpl();
 | |
|     }
 | |
|     return _Card(
 | |
|       title: 'Privacy mode',
 | |
|       children: privacyModeImpls.map((impl) {
 | |
|         final d = impl as List<dynamic>;
 | |
|         return _Radio(context,
 | |
|             value: d[0] as String,
 | |
|             groupValue: groupValue,
 | |
|             label: d[1] as String,
 | |
|             onChanged: onChanged);
 | |
|       }).toList(),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   Widget otherRow(String label, String key) {
 | |
|     final value = bind.mainGetUserDefaultOption(key: key) == 'Y';
 | |
|     final isOptFixed = isOptionFixed(key);
 | |
|     onChanged(bool b) async {
 | |
|       await bind.mainSetUserDefaultOption(
 | |
|           key: key,
 | |
|           value: b
 | |
|               ? 'Y'
 | |
|               : (key == kOptionEnableFileCopyPaste ? 'N' : defaultOptionNo));
 | |
|       setState(() {});
 | |
|     }
 | |
| 
 | |
|     return GestureDetector(
 | |
|         child: Row(
 | |
|           children: [
 | |
|             Checkbox(
 | |
|                     value: value,
 | |
|                     onChanged: isOptFixed ? null : (_) => onChanged(!value))
 | |
|                 .marginOnly(right: 5),
 | |
|             Expanded(
 | |
|               child: Text(translate(label)),
 | |
|             )
 | |
|           ],
 | |
|         ).marginOnly(left: _kCheckBoxLeftMargin),
 | |
|         onTap: isOptFixed ? null : () => onChanged(!value));
 | |
|   }
 | |
| 
 | |
|   Widget other(BuildContext context) {
 | |
|     final children =
 | |
|         otherDefaultSettings().map((e) => otherRow(e.$1, e.$2)).toList();
 | |
|     return _Card(title: 'Other Default Options', children: children);
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _Account extends StatefulWidget {
 | |
|   const _Account({Key? key}) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   State<_Account> createState() => _AccountState();
 | |
| }
 | |
| 
 | |
| class _AccountState extends State<_Account> {
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     final scrollController = ScrollController();
 | |
|     return DesktopScrollWrapper(
 | |
|         scrollController: scrollController,
 | |
|         child: ListView(
 | |
|           physics: DraggableNeverScrollableScrollPhysics(),
 | |
|           controller: scrollController,
 | |
|           children: [
 | |
|             _Card(title: 'Account', children: [accountAction(), useInfo()]),
 | |
|           ],
 | |
|         ).marginOnly(bottom: _kListViewBottomMargin));
 | |
|   }
 | |
| 
 | |
|   Widget accountAction() {
 | |
|     return Obx(() => _Button(
 | |
|         gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout',
 | |
|         () => {
 | |
|               gFFI.userModel.userName.value.isEmpty
 | |
|                   ? loginDialog()
 | |
|                   : logOutConfirmDialog()
 | |
|             }));
 | |
|   }
 | |
| 
 | |
|   Widget useInfo() {
 | |
|     text(String key, String value) {
 | |
|       return Align(
 | |
|         alignment: Alignment.centerLeft,
 | |
|         child: SelectionArea(child: Text('${translate(key)}: $value'))
 | |
|             .marginSymmetric(vertical: 4),
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     return Obx(() => Offstage(
 | |
|           offstage: gFFI.userModel.userName.value.isEmpty,
 | |
|           child: Column(
 | |
|             children: [
 | |
|               text('Username', gFFI.userModel.userName.value),
 | |
|               // text('Group', gFFI.groupModel.groupName.value),
 | |
|             ],
 | |
|           ),
 | |
|         )).marginOnly(left: 18, top: 16);
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _Checkbox extends StatefulWidget {
 | |
|   final String label;
 | |
|   final bool Function() getValue;
 | |
|   final Future<void> Function(bool) setValue;
 | |
| 
 | |
|   const _Checkbox(
 | |
|       {Key? key,
 | |
|       required this.label,
 | |
|       required this.getValue,
 | |
|       required this.setValue})
 | |
|       : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   State<_Checkbox> createState() => _CheckboxState();
 | |
| }
 | |
| 
 | |
| class _CheckboxState extends State<_Checkbox> {
 | |
|   var value = false;
 | |
| 
 | |
|   @override
 | |
|   initState() {
 | |
|     super.initState();
 | |
|     value = widget.getValue();
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     onChanged(bool b) async {
 | |
|       await widget.setValue(b);
 | |
|       setState(() {
 | |
|         value = widget.getValue();
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     return GestureDetector(
 | |
|       child: Row(
 | |
|         children: [
 | |
|           Checkbox(
 | |
|             value: value,
 | |
|             onChanged: (_) => onChanged(!value),
 | |
|           ).marginOnly(right: 5),
 | |
|           Expanded(
 | |
|             child: Text(translate(widget.label)),
 | |
|           )
 | |
|         ],
 | |
|       ).marginOnly(left: _kCheckBoxLeftMargin),
 | |
|       onTap: () => onChanged(!value),
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _Plugin extends StatefulWidget {
 | |
|   const _Plugin({Key? key}) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   State<_Plugin> createState() => _PluginState();
 | |
| }
 | |
| 
 | |
| class _PluginState extends State<_Plugin> {
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     bind.pluginListReload();
 | |
|     final scrollController = ScrollController();
 | |
|     return DesktopScrollWrapper(
 | |
|       scrollController: scrollController,
 | |
|       child: ChangeNotifierProvider.value(
 | |
|         value: pluginManager,
 | |
|         child: Consumer<PluginManager>(builder: (context, model, child) {
 | |
|           return ListView(
 | |
|             physics: DraggableNeverScrollableScrollPhysics(),
 | |
|             controller: scrollController,
 | |
|             children: model.plugins.map((entry) => pluginCard(entry)).toList(),
 | |
|           ).marginOnly(bottom: _kListViewBottomMargin);
 | |
|         }),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   Widget pluginCard(PluginInfo plugin) {
 | |
|     return ChangeNotifierProvider.value(
 | |
|       value: plugin,
 | |
|       child: Consumer<PluginInfo>(
 | |
|         builder: (context, model, child) => DesktopSettingsCard(plugin: model),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   Widget accountAction() {
 | |
|     return Obx(() => _Button(
 | |
|         gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout',
 | |
|         () => {
 | |
|               gFFI.userModel.userName.value.isEmpty
 | |
|                   ? loginDialog()
 | |
|                   : logOutConfirmDialog()
 | |
|             }));
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _About extends StatefulWidget {
 | |
|   const _About({Key? key}) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   State<_About> createState() => _AboutState();
 | |
| }
 | |
| 
 | |
| class _AboutState extends State<_About> {
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return futureBuilder(future: () async {
 | |
|       final license = await bind.mainGetLicense();
 | |
|       final version = await bind.mainGetVersion();
 | |
|       final buildDate = await bind.mainGetBuildDate();
 | |
|       final fingerprint = await bind.mainGetFingerprint();
 | |
|       return {
 | |
|         'license': license,
 | |
|         'version': version,
 | |
|         'buildDate': buildDate,
 | |
|         'fingerprint': fingerprint
 | |
|       };
 | |
|     }(), hasData: (data) {
 | |
|       final license = data['license'].toString();
 | |
|       final version = data['version'].toString();
 | |
|       final buildDate = data['buildDate'].toString();
 | |
|       final fingerprint = data['fingerprint'].toString();
 | |
|       const linkStyle = TextStyle(decoration: TextDecoration.underline);
 | |
|       final scrollController = ScrollController();
 | |
|       return DesktopScrollWrapper(
 | |
|           scrollController: scrollController,
 | |
|           child: SingleChildScrollView(
 | |
|             controller: scrollController,
 | |
|             physics: DraggableNeverScrollableScrollPhysics(),
 | |
|             child: _Card(title: translate('About RustDesk'), children: [
 | |
|               Column(
 | |
|                 crossAxisAlignment: CrossAxisAlignment.start,
 | |
|                 children: [
 | |
|                   const SizedBox(
 | |
|                     height: 8.0,
 | |
|                   ),
 | |
|                   SelectionArea(
 | |
|                       child: Text('${translate('Version')}: $version')
 | |
|                           .marginSymmetric(vertical: 4.0)),
 | |
|                   SelectionArea(
 | |
|                       child: Text('${translate('Build Date')}: $buildDate')
 | |
|                           .marginSymmetric(vertical: 4.0)),
 | |
|                   if (!isWeb)
 | |
|                     SelectionArea(
 | |
|                         child: Text('${translate('Fingerprint')}: $fingerprint')
 | |
|                             .marginSymmetric(vertical: 4.0)),
 | |
|                   InkWell(
 | |
|                       onTap: () {
 | |
|                         launchUrlString('https://rustdesk.com/privacy.html');
 | |
|                       },
 | |
|                       child: Text(
 | |
|                         translate('Privacy Statement'),
 | |
|                         style: linkStyle,
 | |
|                       ).marginSymmetric(vertical: 4.0)),
 | |
|                   InkWell(
 | |
|                       onTap: () {
 | |
|                         launchUrlString('https://rustdesk.com');
 | |
|                       },
 | |
|                       child: Text(
 | |
|                         translate('Website'),
 | |
|                         style: linkStyle,
 | |
|                       ).marginSymmetric(vertical: 4.0)),
 | |
|                   Container(
 | |
|                     decoration: const BoxDecoration(color: Color(0xFF2c8cff)),
 | |
|                     padding:
 | |
|                         const EdgeInsets.symmetric(vertical: 24, horizontal: 8),
 | |
|                     child: SelectionArea(
 | |
|                         child: Row(
 | |
|                       children: [
 | |
|                         Expanded(
 | |
|                           child: Column(
 | |
|                             crossAxisAlignment: CrossAxisAlignment.start,
 | |
|                             children: [
 | |
|                               Text(
 | |
|                                 'Copyright © ${DateTime.now().toString().substring(0, 4)} Purslane Ltd.\n$license',
 | |
|                                 style: const TextStyle(color: Colors.white),
 | |
|                               ),
 | |
|                               Text(
 | |
|                                 translate('Slogan_tip'),
 | |
|                                 style: TextStyle(
 | |
|                                     fontWeight: FontWeight.w800,
 | |
|                                     color: Colors.white),
 | |
|                               )
 | |
|                             ],
 | |
|                           ),
 | |
|                         ),
 | |
|                       ],
 | |
|                     )),
 | |
|                   ).marginSymmetric(vertical: 4.0)
 | |
|                 ],
 | |
|               ).marginOnly(left: _kContentHMargin)
 | |
|             ]),
 | |
|           ));
 | |
|     });
 | |
|   }
 | |
| }
 | |
| 
 | |
| //#endregion
 | |
| 
 | |
| //#region components
 | |
| 
 | |
| // ignore: non_constant_identifier_names
 | |
| Widget _Card(
 | |
|     {required String title,
 | |
|     required List<Widget> children,
 | |
|     List<Widget>? title_suffix}) {
 | |
|   return Row(
 | |
|     children: [
 | |
|       Flexible(
 | |
|         child: SizedBox(
 | |
|           width: _kCardFixedWidth,
 | |
|           child: Card(
 | |
|             child: Column(
 | |
|               children: [
 | |
|                 Row(
 | |
|                   children: [
 | |
|                     Expanded(
 | |
|                         child: Text(
 | |
|                       translate(title),
 | |
|                       textAlign: TextAlign.start,
 | |
|                       style: const TextStyle(
 | |
|                         fontSize: _kTitleFontSize,
 | |
|                       ),
 | |
|                     )),
 | |
|                     ...?title_suffix
 | |
|                   ],
 | |
|                 ).marginOnly(left: _kContentHMargin, top: 10, bottom: 10),
 | |
|                 ...children
 | |
|                     .map((e) => e.marginOnly(top: 4, right: _kContentHMargin)),
 | |
|               ],
 | |
|             ).marginOnly(bottom: 10),
 | |
|           ).marginOnly(left: _kCardLeftMargin, top: 15),
 | |
|         ),
 | |
|       ),
 | |
|     ],
 | |
|   );
 | |
| }
 | |
| 
 | |
| // ignore: non_constant_identifier_names
 | |
| Widget _OptionCheckBox(
 | |
|   BuildContext context,
 | |
|   String label,
 | |
|   String key, {
 | |
|   Function(bool)? update,
 | |
|   bool reverse = false,
 | |
|   bool enabled = true,
 | |
|   Icon? checkedIcon,
 | |
|   bool? fakeValue,
 | |
|   bool isServer = true,
 | |
|   bool Function()? optGetter,
 | |
|   Future<void> Function(String, bool)? optSetter,
 | |
| }) {
 | |
|   getOpt() => optGetter != null
 | |
|       ? optGetter()
 | |
|       : (isServer
 | |
|           ? mainGetBoolOptionSync(key)
 | |
|           : mainGetLocalBoolOptionSync(key));
 | |
|   bool value = getOpt();
 | |
|   final isOptFixed = isOptionFixed(key);
 | |
|   if (reverse) value = !value;
 | |
|   var ref = value.obs;
 | |
|   onChanged(option) async {
 | |
|     if (option != null) {
 | |
|       if (reverse) option = !option;
 | |
|       final setter =
 | |
|           optSetter ?? (isServer ? mainSetBoolOption : mainSetLocalBoolOption);
 | |
|       await setter(key, option);
 | |
|       final readOption = getOpt();
 | |
|       if (reverse) {
 | |
|         ref.value = !readOption;
 | |
|       } else {
 | |
|         ref.value = readOption;
 | |
|       }
 | |
|       update?.call(readOption);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   if (fakeValue != null) {
 | |
|     ref.value = fakeValue;
 | |
|     enabled = false;
 | |
|   }
 | |
| 
 | |
|   return GestureDetector(
 | |
|     child: Obx(
 | |
|       () => Row(
 | |
|         children: [
 | |
|           Checkbox(
 | |
|                   value: ref.value,
 | |
|                   onChanged: enabled && !isOptFixed ? onChanged : null)
 | |
|               .marginOnly(right: 5),
 | |
|           Offstage(
 | |
|             offstage: !ref.value || checkedIcon == null,
 | |
|             child: checkedIcon?.marginOnly(right: 5),
 | |
|           ),
 | |
|           Expanded(
 | |
|               child: Text(
 | |
|             translate(label),
 | |
|             style: TextStyle(color: disabledTextColor(context, enabled)),
 | |
|           ))
 | |
|         ],
 | |
|       ),
 | |
|     ).marginOnly(left: _kCheckBoxLeftMargin),
 | |
|     onTap: enabled && !isOptFixed
 | |
|         ? () {
 | |
|             onChanged(!ref.value);
 | |
|           }
 | |
|         : null,
 | |
|   );
 | |
| }
 | |
| 
 | |
| // ignore: non_constant_identifier_names
 | |
| Widget _Radio<T>(BuildContext context,
 | |
|     {required T value,
 | |
|     required T groupValue,
 | |
|     required String label,
 | |
|     required Function(T value)? onChanged,
 | |
|     bool autoNewLine = true}) {
 | |
|   final onChange2 = onChanged != null
 | |
|       ? (T? value) {
 | |
|           if (value != null) {
 | |
|             onChanged(value);
 | |
|           }
 | |
|         }
 | |
|       : null;
 | |
|   return GestureDetector(
 | |
|     child: Row(
 | |
|       children: [
 | |
|         Radio<T>(value: value, groupValue: groupValue, onChanged: onChange2),
 | |
|         Expanded(
 | |
|           child: Text(translate(label),
 | |
|                   overflow: autoNewLine ? null : TextOverflow.ellipsis,
 | |
|                   style: TextStyle(
 | |
|                       fontSize: _kContentFontSize,
 | |
|                       color: disabledTextColor(context, onChange2 != null)))
 | |
|               .marginOnly(left: 5),
 | |
|         ),
 | |
|       ],
 | |
|     ).marginOnly(left: _kRadioLeftMargin),
 | |
|     onTap: () => onChange2?.call(value),
 | |
|   );
 | |
| }
 | |
| 
 | |
| class WaylandCard extends StatefulWidget {
 | |
|   const WaylandCard({Key? key}) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   State<WaylandCard> createState() => _WaylandCardState();
 | |
| }
 | |
| 
 | |
| class _WaylandCardState extends State<WaylandCard> {
 | |
|   final restoreTokenKey = 'wayland-restore-token';
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return futureBuilder(
 | |
|       future: bind.mainHandleWaylandScreencastRestoreToken(
 | |
|           key: restoreTokenKey, value: "get"),
 | |
|       hasData: (restoreToken) {
 | |
|         final children = [
 | |
|           if (restoreToken.isNotEmpty)
 | |
|             _buildClearScreenSelection(context, restoreToken),
 | |
|         ];
 | |
|         return Offstage(
 | |
|           offstage: children.isEmpty,
 | |
|           child: _Card(title: 'Wayland', children: children),
 | |
|         );
 | |
|       },
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   Widget _buildClearScreenSelection(BuildContext context, String restoreToken) {
 | |
|     onConfirm() async {
 | |
|       final msg = await bind.mainHandleWaylandScreencastRestoreToken(
 | |
|           key: restoreTokenKey, value: "clear");
 | |
|       gFFI.dialogManager.dismissAll();
 | |
|       if (msg.isNotEmpty) {
 | |
|         msgBox(gFFI.sessionId, 'custom-nocancel', 'Error', msg, '',
 | |
|             gFFI.dialogManager);
 | |
|       } else {
 | |
|         setState(() {});
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     showConfirmMsgBox() => msgBoxCommon(
 | |
|             gFFI.dialogManager,
 | |
|             'Confirmation',
 | |
|             Text(
 | |
|               translate('confirm_clear_Wayland_screen_selection_tip'),
 | |
|             ),
 | |
|             [
 | |
|               dialogButton('OK', onPressed: onConfirm),
 | |
|               dialogButton('Cancel',
 | |
|                   onPressed: () => gFFI.dialogManager.dismissAll())
 | |
|             ]);
 | |
| 
 | |
|     return _Button(
 | |
|       'Clear Wayland screen selection',
 | |
|       showConfirmMsgBox,
 | |
|       tip: 'clear_Wayland_screen_selection_tip',
 | |
|       style: ButtonStyle(
 | |
|         backgroundColor: MaterialStateProperty.all<Color>(
 | |
|             Theme.of(context).colorScheme.error.withOpacity(0.75)),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| // ignore: non_constant_identifier_names
 | |
| Widget _Button(String label, Function() onPressed,
 | |
|     {bool enabled = true, String? tip, ButtonStyle? style}) {
 | |
|   var button = ElevatedButton(
 | |
|     onPressed: enabled ? onPressed : null,
 | |
|     child: Text(
 | |
|       translate(label),
 | |
|     ).marginSymmetric(horizontal: 15),
 | |
|     style: style,
 | |
|   );
 | |
|   StatefulWidget child;
 | |
|   if (tip == null) {
 | |
|     child = button;
 | |
|   } else {
 | |
|     child = Tooltip(message: translate(tip), child: button);
 | |
|   }
 | |
|   return Row(children: [
 | |
|     child,
 | |
|   ]).marginOnly(left: _kContentHMargin);
 | |
| }
 | |
| 
 | |
| // ignore: non_constant_identifier_names
 | |
| Widget _SubButton(String label, Function() onPressed, [bool enabled = true]) {
 | |
|   return Row(
 | |
|     children: [
 | |
|       ElevatedButton(
 | |
|         onPressed: enabled ? onPressed : null,
 | |
|         child: Text(
 | |
|           translate(label),
 | |
|         ).marginSymmetric(horizontal: 15),
 | |
|       ),
 | |
|     ],
 | |
|   ).marginOnly(left: _kContentHSubMargin);
 | |
| }
 | |
| 
 | |
| // ignore: non_constant_identifier_names
 | |
| Widget _SubLabeledWidget(BuildContext context, String label, Widget child,
 | |
|     {bool enabled = true}) {
 | |
|   return Row(
 | |
|     children: [
 | |
|       Text(
 | |
|         '${translate(label)}: ',
 | |
|         style: TextStyle(color: disabledTextColor(context, enabled)),
 | |
|       ),
 | |
|       SizedBox(
 | |
|         width: 10,
 | |
|       ),
 | |
|       child,
 | |
|     ],
 | |
|   ).marginOnly(left: _kContentHSubMargin);
 | |
| }
 | |
| 
 | |
| Widget _lock(
 | |
|   bool locked,
 | |
|   String label,
 | |
|   Function() onUnlock,
 | |
| ) {
 | |
|   return Offstage(
 | |
|       offstage: !locked,
 | |
|       child: Row(
 | |
|         children: [
 | |
|           Flexible(
 | |
|             child: SizedBox(
 | |
|               width: _kCardFixedWidth,
 | |
|               child: Card(
 | |
|                 child: ElevatedButton(
 | |
|                   child: SizedBox(
 | |
|                       height: 25,
 | |
|                       child: Row(
 | |
|                           mainAxisAlignment: MainAxisAlignment.center,
 | |
|                           children: [
 | |
|                             const Icon(
 | |
|                               Icons.security_sharp,
 | |
|                               size: 20,
 | |
|                             ),
 | |
|                             Text(translate(label)).marginOnly(left: 5),
 | |
|                           ]).marginSymmetric(vertical: 2)),
 | |
|                   onPressed: () async {
 | |
|                     final unlockPin = bind.mainGetUnlockPin();
 | |
|                     if (unlockPin.isEmpty) {
 | |
|                       bool checked = await callMainCheckSuperUserPermission();
 | |
|                       if (checked) {
 | |
|                         onUnlock();
 | |
|                       }
 | |
|                     } else {
 | |
|                       checkUnlockPinDialog(unlockPin, onUnlock);
 | |
|                     }
 | |
|                   },
 | |
|                 ).marginSymmetric(horizontal: 2, vertical: 4),
 | |
|               ).marginOnly(left: _kCardLeftMargin),
 | |
|             ).marginOnly(top: 10),
 | |
|           ),
 | |
|         ],
 | |
|       ));
 | |
| }
 | |
| 
 | |
| _LabeledTextField(
 | |
|     BuildContext context,
 | |
|     String label,
 | |
|     TextEditingController controller,
 | |
|     String errorText,
 | |
|     bool enabled,
 | |
|     bool secure) {
 | |
|   return Row(
 | |
|     children: [
 | |
|       ConstrainedBox(
 | |
|           constraints: const BoxConstraints(minWidth: 140),
 | |
|           child: Text(
 | |
|             '${translate(label)}:',
 | |
|             textAlign: TextAlign.right,
 | |
|             style: TextStyle(
 | |
|                 fontSize: 16, color: disabledTextColor(context, enabled)),
 | |
|           ).marginOnly(right: 10)),
 | |
|       Expanded(
 | |
|         child: TextField(
 | |
|             controller: controller,
 | |
|             enabled: enabled,
 | |
|             obscureText: secure,
 | |
|             decoration: InputDecoration(
 | |
|                 errorText: errorText.isNotEmpty ? errorText : null),
 | |
|             style: TextStyle(
 | |
|               color: disabledTextColor(context, enabled),
 | |
|             )),
 | |
|       ),
 | |
|     ],
 | |
|   ).marginOnly(bottom: 8);
 | |
| }
 | |
| 
 | |
| class _CountDownButton extends StatefulWidget {
 | |
|   _CountDownButton({
 | |
|     Key? key,
 | |
|     required this.text,
 | |
|     required this.second,
 | |
|     required this.onPressed,
 | |
|   }) : super(key: key);
 | |
|   final String text;
 | |
|   final VoidCallback? onPressed;
 | |
|   final int second;
 | |
| 
 | |
|   @override
 | |
|   State<_CountDownButton> createState() => _CountDownButtonState();
 | |
| }
 | |
| 
 | |
| class _CountDownButtonState extends State<_CountDownButton> {
 | |
|   bool _isButtonDisabled = false;
 | |
| 
 | |
|   late int _countdownSeconds = widget.second;
 | |
| 
 | |
|   Timer? _timer;
 | |
| 
 | |
|   @override
 | |
|   void dispose() {
 | |
|     _timer?.cancel();
 | |
|     super.dispose();
 | |
|   }
 | |
| 
 | |
|   void _startCountdownTimer() {
 | |
|     _timer = Timer.periodic(Duration(seconds: 1), (timer) {
 | |
|       if (_countdownSeconds <= 0) {
 | |
|         setState(() {
 | |
|           _isButtonDisabled = false;
 | |
|         });
 | |
|         timer.cancel();
 | |
|       } else {
 | |
|         setState(() {
 | |
|           _countdownSeconds--;
 | |
|         });
 | |
|       }
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return ElevatedButton(
 | |
|       onPressed: _isButtonDisabled
 | |
|           ? null
 | |
|           : () {
 | |
|               widget.onPressed?.call();
 | |
|               setState(() {
 | |
|                 _isButtonDisabled = true;
 | |
|                 _countdownSeconds = widget.second;
 | |
|               });
 | |
|               _startCountdownTimer();
 | |
|             },
 | |
|       child: Text(
 | |
|         _isButtonDisabled ? '$_countdownSeconds s' : translate(widget.text),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| //#endregion
 | |
| 
 | |
| //#region dialogs
 | |
| 
 | |
| void changeSocks5Proxy() async {
 | |
|   var socks = await bind.mainGetSocks();
 | |
| 
 | |
|   String proxy = '';
 | |
|   String proxyMsg = '';
 | |
|   String username = '';
 | |
|   String password = '';
 | |
|   if (socks.length == 3) {
 | |
|     proxy = socks[0];
 | |
|     username = socks[1];
 | |
|     password = socks[2];
 | |
|   }
 | |
|   var proxyController = TextEditingController(text: proxy);
 | |
|   var userController = TextEditingController(text: username);
 | |
|   var pwdController = TextEditingController(text: password);
 | |
|   RxBool obscure = true.obs;
 | |
| 
 | |
|   // proxy settings
 | |
|   // The following option is a not real key, it is just used for custom client advanced settings.
 | |
|   const String optionProxyUrl = "proxy-url";
 | |
|   final isOptFixed = isOptionFixed(optionProxyUrl);
 | |
| 
 | |
|   var isInProgress = false;
 | |
|   gFFI.dialogManager.show((setState, close, context) {
 | |
|     submit() async {
 | |
|       setState(() {
 | |
|         proxyMsg = '';
 | |
|         isInProgress = true;
 | |
|       });
 | |
|       cancel() {
 | |
|         setState(() {
 | |
|           isInProgress = false;
 | |
|         });
 | |
|       }
 | |
| 
 | |
|       proxy = proxyController.text.trim();
 | |
|       username = userController.text.trim();
 | |
|       password = pwdController.text.trim();
 | |
| 
 | |
|       if (proxy.isNotEmpty) {
 | |
|         String domainPort = proxy;
 | |
|         if (domainPort.contains('://')) {
 | |
|           domainPort = domainPort.split('://')[1];
 | |
|         }
 | |
|         proxyMsg = translate(await bind.mainTestIfValidServer(
 | |
|             server: domainPort, testWithProxy: false));
 | |
|         if (proxyMsg.isEmpty) {
 | |
|           // ignore
 | |
|         } else {
 | |
|           cancel();
 | |
|           return;
 | |
|         }
 | |
|       }
 | |
|       await bind.mainSetSocks(
 | |
|           proxy: proxy, username: username, password: password);
 | |
|       close();
 | |
|     }
 | |
| 
 | |
|     return CustomAlertDialog(
 | |
|       title: Text(translate('Socks5/Http(s) Proxy')),
 | |
|       content: ConstrainedBox(
 | |
|         constraints: const BoxConstraints(minWidth: 500),
 | |
|         child: Column(
 | |
|           crossAxisAlignment: CrossAxisAlignment.start,
 | |
|           children: [
 | |
|             Row(
 | |
|               children: [
 | |
|                 if (!isMobile)
 | |
|                   ConstrainedBox(
 | |
|                     constraints: const BoxConstraints(minWidth: 140),
 | |
|                     child: Align(
 | |
|                         alignment: Alignment.centerRight,
 | |
|                         child: Row(
 | |
|                           children: [
 | |
|                             Text(
 | |
|                               translate('Server'),
 | |
|                             ).marginOnly(right: 4),
 | |
|                             Tooltip(
 | |
|                               waitDuration: Duration(milliseconds: 0),
 | |
|                               message: translate("default_proxy_tip"),
 | |
|                               child: Icon(
 | |
|                                 Icons.help_outline_outlined,
 | |
|                                 size: 16,
 | |
|                                 color: Theme.of(context)
 | |
|                                     .textTheme
 | |
|                                     .titleLarge
 | |
|                                     ?.color
 | |
|                                     ?.withOpacity(0.5),
 | |
|                               ),
 | |
|                             ),
 | |
|                           ],
 | |
|                         )).marginOnly(right: 10),
 | |
|                   ),
 | |
|                 Expanded(
 | |
|                   child: TextField(
 | |
|                     decoration: InputDecoration(
 | |
|                       errorText: proxyMsg.isNotEmpty ? proxyMsg : null,
 | |
|                       labelText: isMobile ? translate('Server') : null,
 | |
|                       helperText:
 | |
|                           isMobile ? translate("default_proxy_tip") : null,
 | |
|                       helperMaxLines: isMobile ? 3 : null,
 | |
|                     ),
 | |
|                     controller: proxyController,
 | |
|                     autofocus: true,
 | |
|                     enabled: !isOptFixed,
 | |
|                   ),
 | |
|                 ),
 | |
|               ],
 | |
|             ).marginOnly(bottom: 8),
 | |
|             Row(
 | |
|               children: [
 | |
|                 if (!isMobile)
 | |
|                   ConstrainedBox(
 | |
|                       constraints: const BoxConstraints(minWidth: 140),
 | |
|                       child: Text(
 | |
|                         '${translate("Username")}:',
 | |
|                         textAlign: TextAlign.right,
 | |
|                       ).marginOnly(right: 10)),
 | |
|                 Expanded(
 | |
|                   child: TextField(
 | |
|                     controller: userController,
 | |
|                     decoration: InputDecoration(
 | |
|                       labelText: isMobile ? translate('Username') : null,
 | |
|                     ),
 | |
|                     enabled: !isOptFixed,
 | |
|                   ),
 | |
|                 ),
 | |
|               ],
 | |
|             ).marginOnly(bottom: 8),
 | |
|             Row(
 | |
|               children: [
 | |
|                 if (!isMobile)
 | |
|                   ConstrainedBox(
 | |
|                       constraints: const BoxConstraints(minWidth: 140),
 | |
|                       child: Text(
 | |
|                         '${translate("Password")}:',
 | |
|                         textAlign: TextAlign.right,
 | |
|                       ).marginOnly(right: 10)),
 | |
|                 Expanded(
 | |
|                   child: Obx(() => TextField(
 | |
|                         obscureText: obscure.value,
 | |
|                         decoration: InputDecoration(
 | |
|                             labelText: isMobile ? translate('Password') : null,
 | |
|                             suffixIcon: IconButton(
 | |
|                                 onPressed: () => obscure.value = !obscure.value,
 | |
|                                 icon: Icon(obscure.value
 | |
|                                     ? Icons.visibility_off
 | |
|                                     : Icons.visibility))),
 | |
|                         controller: pwdController,
 | |
|                         enabled: !isOptFixed,
 | |
|                         maxLength: bind.mainMaxEncryptLen(),
 | |
|                       )),
 | |
|                 ),
 | |
|               ],
 | |
|             ),
 | |
|             // NOT use Offstage to wrap LinearProgressIndicator
 | |
|             if (isInProgress)
 | |
|               const LinearProgressIndicator().marginOnly(top: 8),
 | |
|           ],
 | |
|         ),
 | |
|       ),
 | |
|       actions: [
 | |
|         dialogButton('Cancel', onPressed: close, isOutline: true),
 | |
|         if (!isOptFixed) dialogButton('OK', onPressed: submit),
 | |
|       ],
 | |
|       onSubmit: submit,
 | |
|       onCancel: close,
 | |
|     );
 | |
|   });
 | |
| }
 | |
| 
 | |
| //#endregion
 |