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 tabKeys = [ SettingsTabKey.general, if (!bind.isOutgoingOnly() && !bind.isDisableSettings()) SettingsTabKey.safety, if (!bind.isDisableSettings()) 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 createState() => _DesktopSettingPageState(); static void switch2page(SettingsTabKey page) { try { int index = tabKeys.indexOf(page); if (index == -1) { return; } if (Get.isRegistered(tag: _kSettingPageControllerTag)) { DesktopTabPage.onAddSetting(initialPage: page); PageController controller = Get.find(tag: _kSettingPageControllerTag); Rx selected = Get.find(tag: _kSettingPageTabKeyTag); selected.value = page; controller.jumpToPage(index); } else { DesktopTabPage.onAddSetting(initialPage: page); } } catch (e) { debugPrintStack(label: '$e'); } } } class _DesktopSettingPageState extends State with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { late PageController controller; late Rx selectedTab; @override bool get wantKeepAlive => true; @override void initState() { super.initState(); var initialIndex = DesktopSettingPage.tabKeys.indexOf(widget.initialTabkey); if (initialIndex == -1) { initialIndex = 0; } selectedTab = DesktopSettingPage.tabKeys[initialIndex].obs; Get.put>(selectedTab, tag: _kSettingPageTabKeyTag); controller = PageController(initialPage: initialIndex); Get.put(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(tag: _kSettingPageControllerTag); Get.delete(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 _children() { final children = [ _General(), if (!bind.isOutgoingOnly() && !bind.isDisableSettings()) _Safety(), if (!bind.isDisableSettings()) _Network(), if (!bind.isIncomingOnly()) _Display(), if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled()) _Plugin(), if (!bind.isDisableAccount()) _Account(), _About(), ]; return children; } @override Widget build(BuildContext context) { super.build(context); return Scaffold( backgroundColor: Theme.of(context).colorScheme.background, body: Row( children: [ SizedBox( width: _kTabWidth, child: Column( children: [ _header(), 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() { return Row( children: [ SizedBox( height: 62, child: Text( translate('Settings'), textAlign: TextAlign.left, style: const TextStyle( color: _accentColor, fontSize: _kTitleFontSize, fontWeight: FontWeight.w400, ), ), ).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 = Get.find(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: [ service(), theme(), hwcodec(), audio(context), record(context), WaylandCard(), _Card(title: 'Language', children: [language()]), 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(context, value: 'light', groupValue: current, label: 'Light', onChanged: isOptFixed ? null : onChanged), _Radio(context, value: 'dark', groupValue: current, label: 'Dark', onChanged: isOptFixed ? null : onChanged), _Radio(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 = [ if (!bind.isIncomingOnly()) _OptionCheckBox(context, 'Confirm before closing multiple tabs', kOptionEnableConfirmClosingTabs, isServer: false), _OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr), wallpaper(), if (!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 Tooltip( message: translate('software_render_tip'), child: _OptionCheckBox(context, "Always use software rendering", kOptionAllowAlwaysSoftwareRender), ), if (!bind.isCustomClient()) _OptionCheckBox( context, 'Check for software update on startup', kOptionEnableCheckUpdate, isServer: false, ) ], ]; if (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: () { 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: () { if (mainGetBoolOptionSync(kOptionEnableHwcodec)) { bind.mainCheckHwcodec(); } }, ) ]), ); } Widget audio(BuildContext context) { if (bind.isOutgoingOnly()) { return const Offstage(); } return AudioInput(builder: (devices, currentDevice, setDevice) { return _Card(title: 'Audio Input Device', children: [ ...devices.map((device) => _Radio(context, value: device, groupValue: currentDevice, autoNewLine: false, label: device, onChanged: (value) { setDevice(value); setState(() {}); })) ]); }); } Widget record(BuildContext context) { final showRootDir = isWindows && bind.mainIsInstalled(); return futureBuilder(future: () async { String user_dir = await bind.mainVideoSaveDirectory(root: false); String root_dir = showRootDir ? await 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 map = data as Map; 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 data = res as Map; List langsList = jsonDecode(data['langs']!); Map langsMap = {for (var v in langsList) v[0]: v[1]}; List keys = langsMap.keys.toList(); List 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); reloadAllWindows(); 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; update() async { has2fa.value = bind.mainHasValid2FaSync(); } onChanged(bool? checked) async { change2fa(callback: update); } return 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); } return tmpWrapper(); } Widget changeId() { return ChangeNotifierProvider.value( value: gFFI.serverModel, child: Consumer(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(builder: ((context, model, child) { List passwordKeys = [ kUseTemporaryPassword, kUsePermanentPassword, kUseBothPasswords, ]; List 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 radios = passwordValues .map((value) => _Radio( context, value: value, groupValue: currentValue, label: value, onChanged: locked ? null : ((value) { () async { await model.setVerificationMethod( passwordKeys[passwordValues.indexOf(value)]); await model.updatePasswordModel(); }(); }), )) .toList(); var onChanged = tmpEnabled && !locked ? (value) { if (value != null) { () async { await model.setTemporaryPasswordLength(value.toString()); await model.updatePasswordModel(); }(); } } : null; List 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 = [ '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), ]); } 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 directIp(BuildContext context) { TextEditingController controller = TextEditingController(); update() => 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 = (bind.mainGetOptionSync(key: kOptionWhitelist) != defaultOptionWhitelist) .obs; update() async { hasWhitelist.value = bind.mainGetOptionSync(key: kOptionWhitelist) != defaultOptionWhitelist; } 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(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 autoDisconnect(BuildContext context) { TextEditingController controller = TextEditingController(); update() => 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, ), ); }(), ]; } } 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 = bind.mainIsInstalled(); @override Widget build(BuildContext context) { super.build(context); bool enabled = !locked; final scrollController = ScrollController(); 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: [ server(enabled), _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 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), 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 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; 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 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(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( 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)), 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 children, List? 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()? update, bool reverse = false, bool enabled = true, Icon? checkedIcon, bool? fakeValue, bool isServer = true}) { bool value = isServer ? mainGetBoolOptionSync(key) : mainGetLocalBoolOptionSync(key); final isOptFixed = isOptionFixed(key); if (reverse) value = !value; var ref = value.obs; onChanged(option) async { if (option != null) { if (reverse) option = !option; isServer ? await mainSetBoolOption(key, option) : await mainSetLocalBoolOption(key, option); final readOption = isServer ? mainGetBoolOptionSync(key) : mainGetLocalBoolOptionSync(key); if (reverse) { ref.value = !readOption; } else { ref.value = readOption; } update?.call(); } } 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(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(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 createState() => _WaylandCardState(); } class _WaylandCardState extends State { 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( 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 { bool checked = await callMainCheckSuperUserPermission(); if (checked) { 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: [ 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, ), controller: proxyController, autofocus: true, enabled: !isOptFixed, ), ), ], ).marginOnly(bottom: 8), Row( children: [ ConstrainedBox( constraints: const BoxConstraints(minWidth: 140), child: Text( '${translate("Username")}:', textAlign: TextAlign.right, ).marginOnly(right: 10)), Expanded( child: TextField( controller: userController, enabled: !isOptFixed, ), ), ], ).marginOnly(bottom: 8), Row( children: [ 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( suffixIcon: IconButton( onPressed: () => obscure.value = !obscure.value, icon: Icon(obscure.value ? Icons.visibility_off : Icons.visibility))), controller: pwdController, enabled: !isOptFixed, )), ), ], ), // 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