1013 lines
		
	
	
		
			35 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			1013 lines
		
	
	
		
			35 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'dart:async';
 | |
| import 'dart:convert';
 | |
| 
 | |
| import 'package:flutter/material.dart';
 | |
| import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
 | |
| import 'package:get/get.dart';
 | |
| import 'package:provider/provider.dart';
 | |
| import 'package:settings_ui/settings_ui.dart';
 | |
| import 'package:url_launcher/url_launcher.dart';
 | |
| import 'package:url_launcher/url_launcher_string.dart';
 | |
| 
 | |
| import '../../common.dart';
 | |
| import '../../common/widgets/dialog.dart';
 | |
| import '../../common/widgets/login.dart';
 | |
| import '../../consts.dart';
 | |
| import '../../models/model.dart';
 | |
| import '../../models/platform_model.dart';
 | |
| import '../widgets/dialog.dart';
 | |
| import 'home_page.dart';
 | |
| import 'scan_page.dart';
 | |
| 
 | |
| class SettingsPage extends StatefulWidget implements PageShape {
 | |
|   @override
 | |
|   final title = translate("Settings");
 | |
| 
 | |
|   @override
 | |
|   final icon = Icon(Icons.settings);
 | |
| 
 | |
|   @override
 | |
|   final appBarActions = bind.isDisableSettings() ? [] : [ScanButton()];
 | |
| 
 | |
|   @override
 | |
|   State<SettingsPage> createState() => _SettingsState();
 | |
| }
 | |
| 
 | |
| const url = 'https://rustdesk.com/';
 | |
| 
 | |
| enum KeepScreenOn {
 | |
|   never,
 | |
|   duringControlled,
 | |
|   serviceOn,
 | |
| }
 | |
| 
 | |
| String _keepScreenOnToOption(KeepScreenOn value) {
 | |
|   switch (value) {
 | |
|     case KeepScreenOn.never:
 | |
|       return 'never';
 | |
|     case KeepScreenOn.duringControlled:
 | |
|       return 'during-controlled';
 | |
|     case KeepScreenOn.serviceOn:
 | |
|       return 'service-on';
 | |
|   }
 | |
| }
 | |
| 
 | |
| KeepScreenOn optionToKeepScreenOn(String value) {
 | |
|   switch (value) {
 | |
|     case 'never':
 | |
|       return KeepScreenOn.never;
 | |
|     case 'service-on':
 | |
|       return KeepScreenOn.serviceOn;
 | |
|     default:
 | |
|       return KeepScreenOn.duringControlled;
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
 | |
|   final _hasIgnoreBattery =
 | |
|       false; //androidVersion >= 26; // remove because not work on every device
 | |
|   var _ignoreBatteryOpt = false;
 | |
|   var _enableStartOnBoot = false;
 | |
|   var _floatingWindowDisabled = false;
 | |
|   var _keepScreenOn = KeepScreenOn.duringControlled; // relay on floating window
 | |
|   var _enableAbr = false;
 | |
|   var _denyLANDiscovery = false;
 | |
|   var _onlyWhiteList = false;
 | |
|   var _enableDirectIPAccess = false;
 | |
|   var _enableRecordSession = false;
 | |
|   var _enableHardwareCodec = false;
 | |
|   var _autoRecordIncomingSession = false;
 | |
|   var _allowAutoDisconnect = false;
 | |
|   var _localIP = "";
 | |
|   var _directAccessPort = "";
 | |
|   var _fingerprint = "";
 | |
|   var _buildDate = "";
 | |
|   var _autoDisconnectTimeout = "";
 | |
| 
 | |
|   @override
 | |
|   void initState() {
 | |
|     super.initState();
 | |
|     WidgetsBinding.instance.addObserver(this);
 | |
| 
 | |
|     _enableAbr = option2bool(
 | |
|         kOptionEnableAbr, bind.mainGetOptionSync(key: kOptionEnableAbr));
 | |
|     _denyLANDiscovery = !option2bool(kOptionEnableLanDiscovery,
 | |
|         bind.mainGetOptionSync(key: kOptionEnableLanDiscovery));
 | |
|     _onlyWhiteList = (bind.mainGetOptionSync(key: kOptionWhitelist)) !=
 | |
|         defaultOptionWhitelist;
 | |
|     _enableDirectIPAccess = option2bool(
 | |
|         kOptionDirectServer, bind.mainGetOptionSync(key: kOptionDirectServer));
 | |
|     _enableRecordSession = option2bool(kOptionEnableRecordSession,
 | |
|         bind.mainGetOptionSync(key: kOptionEnableRecordSession));
 | |
|     _enableHardwareCodec = option2bool(kOptionEnableHwcodec,
 | |
|         bind.mainGetOptionSync(key: kOptionEnableHwcodec));
 | |
|     _autoRecordIncomingSession = option2bool(kOptionAllowAutoRecordIncoming,
 | |
|         bind.mainGetOptionSync(key: kOptionAllowAutoRecordIncoming));
 | |
|     _localIP = bind.mainGetOptionSync(key: 'local-ip-addr');
 | |
|     _directAccessPort = bind.mainGetOptionSync(key: kOptionDirectAccessPort);
 | |
|     _allowAutoDisconnect = option2bool(kOptionAllowAutoDisconnect,
 | |
|         bind.mainGetOptionSync(key: kOptionAllowAutoDisconnect));
 | |
|     _autoDisconnectTimeout =
 | |
|         bind.mainGetOptionSync(key: kOptionAutoDisconnectTimeout);
 | |
| 
 | |
|     () async {
 | |
|       var update = false;
 | |
| 
 | |
|       if (_hasIgnoreBattery) {
 | |
|         if (await checkAndUpdateIgnoreBatteryStatus()) {
 | |
|           update = true;
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       if (await checkAndUpdateStartOnBoot()) {
 | |
|         update = true;
 | |
|       }
 | |
| 
 | |
|       // start on boot depends on ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS and SYSTEM_ALERT_WINDOW
 | |
|       var enableStartOnBoot =
 | |
|           await gFFI.invokeMethod(AndroidChannel.kGetStartOnBootOpt);
 | |
|       if (enableStartOnBoot) {
 | |
|         if (!await canStartOnBoot()) {
 | |
|           enableStartOnBoot = false;
 | |
|           gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, false);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       if (enableStartOnBoot != _enableStartOnBoot) {
 | |
|         update = true;
 | |
|         _enableStartOnBoot = enableStartOnBoot;
 | |
|       }
 | |
| 
 | |
|       var floatingWindowDisabled =
 | |
|           bind.mainGetLocalOption(key: kOptionDisableFloatingWindow) == "Y" ||
 | |
|               !await AndroidPermissionManager.check(kSystemAlertWindow);
 | |
|       if (floatingWindowDisabled != _floatingWindowDisabled) {
 | |
|         update = true;
 | |
|         _floatingWindowDisabled = floatingWindowDisabled;
 | |
|       }
 | |
| 
 | |
|       final keepScreenOn = _floatingWindowDisabled
 | |
|           ? KeepScreenOn.never
 | |
|           : optionToKeepScreenOn(
 | |
|               bind.mainGetLocalOption(key: kOptionKeepScreenOn));
 | |
|       if (keepScreenOn != _keepScreenOn) {
 | |
|         update = true;
 | |
|         _keepScreenOn = keepScreenOn;
 | |
|       }
 | |
| 
 | |
|       final fingerprint = await bind.mainGetFingerprint();
 | |
|       if (_fingerprint != fingerprint) {
 | |
|         update = true;
 | |
|         _fingerprint = fingerprint;
 | |
|       }
 | |
| 
 | |
|       final buildDate = await bind.mainGetBuildDate();
 | |
|       if (_buildDate != buildDate) {
 | |
|         update = true;
 | |
|         _buildDate = buildDate;
 | |
|       }
 | |
|       if (update) {
 | |
|         setState(() {});
 | |
|       }
 | |
|     }();
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void dispose() {
 | |
|     WidgetsBinding.instance.removeObserver(this);
 | |
|     super.dispose();
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void didChangeAppLifecycleState(AppLifecycleState state) {
 | |
|     if (state == AppLifecycleState.resumed) {
 | |
|       () async {
 | |
|         final ibs = await checkAndUpdateIgnoreBatteryStatus();
 | |
|         final sob = await checkAndUpdateStartOnBoot();
 | |
|         if (ibs || sob) {
 | |
|           setState(() {});
 | |
|         }
 | |
|       }();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   Future<bool> checkAndUpdateIgnoreBatteryStatus() async {
 | |
|     final res = await AndroidPermissionManager.check(
 | |
|         kRequestIgnoreBatteryOptimizations);
 | |
|     if (_ignoreBatteryOpt != res) {
 | |
|       _ignoreBatteryOpt = res;
 | |
|       return true;
 | |
|     } else {
 | |
|       return false;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   Future<bool> checkAndUpdateStartOnBoot() async {
 | |
|     if (!await canStartOnBoot() && _enableStartOnBoot) {
 | |
|       _enableStartOnBoot = false;
 | |
|       debugPrint(
 | |
|           "checkAndUpdateStartOnBoot and set _enableStartOnBoot -> false");
 | |
|       gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, false);
 | |
|       return true;
 | |
|     } else {
 | |
|       return false;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     Provider.of<FfiModel>(context);
 | |
|     final outgoingOnly = bind.isOutgoingOnly();
 | |
|     final customClientSection = CustomSettingsSection(
 | |
|         child: Column(
 | |
|       children: [
 | |
|         if (bind.isCustomClient())
 | |
|           Align(
 | |
|             alignment: Alignment.center,
 | |
|             child: loadPowered(context),
 | |
|           ),
 | |
|         Align(
 | |
|           alignment: Alignment.center,
 | |
|           child: loadLogo(),
 | |
|         )
 | |
|       ],
 | |
|     ));
 | |
|     final List<AbstractSettingsTile> enhancementsTiles = [];
 | |
|     final List<AbstractSettingsTile> shareScreenTiles = [
 | |
|       SettingsTile.switchTile(
 | |
|         title: Text(translate('enable-2fa-title')),
 | |
|         initialValue: bind.mainHasValid2FaSync(),
 | |
|         onToggle: (_) async {
 | |
|           update() async {
 | |
|             setState(() {});
 | |
|           }
 | |
| 
 | |
|           change2fa(callback: update);
 | |
|         },
 | |
|       ),
 | |
|       SettingsTile.switchTile(
 | |
|         title: Text(translate('Deny LAN discovery')),
 | |
|         initialValue: _denyLANDiscovery,
 | |
|         onToggle: isOptionFixed(kOptionEnableLanDiscovery)
 | |
|             ? null
 | |
|             : (v) async {
 | |
|                 await bind.mainSetOption(
 | |
|                     key: kOptionEnableLanDiscovery,
 | |
|                     value: bool2option(kOptionEnableLanDiscovery, !v));
 | |
|                 final newValue = !option2bool(kOptionEnableLanDiscovery,
 | |
|                     await bind.mainGetOption(key: kOptionEnableLanDiscovery));
 | |
|                 setState(() {
 | |
|                   _denyLANDiscovery = newValue;
 | |
|                 });
 | |
|               },
 | |
|       ),
 | |
|       SettingsTile.switchTile(
 | |
|         title: Row(children: [
 | |
|           Expanded(child: Text(translate('Use IP Whitelisting'))),
 | |
|           Offstage(
 | |
|                   offstage: !_onlyWhiteList,
 | |
|                   child: const Icon(Icons.warning_amber_rounded,
 | |
|                       color: Color.fromARGB(255, 255, 204, 0)))
 | |
|               .marginOnly(left: 5)
 | |
|         ]),
 | |
|         initialValue: _onlyWhiteList,
 | |
|         onToggle: (_) async {
 | |
|           update() async {
 | |
|             final onlyWhiteList =
 | |
|                 (await bind.mainGetOption(key: kOptionWhitelist)) !=
 | |
|                     defaultOptionWhitelist;
 | |
|             if (onlyWhiteList != _onlyWhiteList) {
 | |
|               setState(() {
 | |
|                 _onlyWhiteList = onlyWhiteList;
 | |
|               });
 | |
|             }
 | |
|           }
 | |
| 
 | |
|           changeWhiteList(callback: update);
 | |
|         },
 | |
|       ),
 | |
|       SettingsTile.switchTile(
 | |
|         title: Text('${translate('Adaptive bitrate')} (beta)'),
 | |
|         initialValue: _enableAbr,
 | |
|         onToggle: isOptionFixed(kOptionEnableAbr)
 | |
|             ? null
 | |
|             : (v) async {
 | |
|                 await mainSetBoolOption(kOptionEnableAbr, v);
 | |
|                 final newValue = await mainGetBoolOption(kOptionEnableAbr);
 | |
|                 setState(() {
 | |
|                   _enableAbr = newValue;
 | |
|                 });
 | |
|               },
 | |
|       ),
 | |
|       SettingsTile.switchTile(
 | |
|         title: Text(translate('Enable recording session')),
 | |
|         initialValue: _enableRecordSession,
 | |
|         onToggle: isOptionFixed(kOptionEnableRecordSession)
 | |
|             ? null
 | |
|             : (v) async {
 | |
|                 await mainSetBoolOption(kOptionEnableRecordSession, v);
 | |
|                 final newValue =
 | |
|                     await mainGetBoolOption(kOptionEnableRecordSession);
 | |
|                 setState(() {
 | |
|                   _enableRecordSession = newValue;
 | |
|                 });
 | |
|               },
 | |
|       ),
 | |
|       SettingsTile.switchTile(
 | |
|         title: Row(
 | |
|             mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | |
|             crossAxisAlignment: CrossAxisAlignment.center,
 | |
|             children: [
 | |
|               Expanded(
 | |
|                   child: Column(
 | |
|                       crossAxisAlignment: CrossAxisAlignment.start,
 | |
|                       children: [
 | |
|                     Text(translate("Direct IP Access")),
 | |
|                     Offstage(
 | |
|                         offstage: !_enableDirectIPAccess,
 | |
|                         child: Text(
 | |
|                           '${translate("Local Address")}: $_localIP${_directAccessPort.isEmpty ? "" : ":$_directAccessPort"}',
 | |
|                           style: Theme.of(context).textTheme.bodySmall,
 | |
|                         )),
 | |
|                   ])),
 | |
|               Offstage(
 | |
|                   offstage: !_enableDirectIPAccess,
 | |
|                   child: IconButton(
 | |
|                       padding: EdgeInsets.zero,
 | |
|                       icon: Icon(
 | |
|                         Icons.edit,
 | |
|                         size: 20,
 | |
|                       ),
 | |
|                       onPressed: isOptionFixed(kOptionDirectAccessPort)
 | |
|                           ? null
 | |
|                           : () async {
 | |
|                               final port = await changeDirectAccessPort(
 | |
|                                   _localIP, _directAccessPort);
 | |
|                               setState(() {
 | |
|                                 _directAccessPort = port;
 | |
|                               });
 | |
|                             }))
 | |
|             ]),
 | |
|         initialValue: _enableDirectIPAccess,
 | |
|         onToggle: isOptionFixed(kOptionDirectServer)
 | |
|             ? null
 | |
|             : (_) async {
 | |
|                 _enableDirectIPAccess = !_enableDirectIPAccess;
 | |
|                 String value =
 | |
|                     bool2option(kOptionDirectServer, _enableDirectIPAccess);
 | |
|                 await bind.mainSetOption(
 | |
|                     key: kOptionDirectServer, value: value);
 | |
|                 setState(() {});
 | |
|               },
 | |
|       ),
 | |
|       SettingsTile.switchTile(
 | |
|         title: Row(
 | |
|             mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | |
|             crossAxisAlignment: CrossAxisAlignment.center,
 | |
|             children: [
 | |
|               Expanded(
 | |
|                   child: Column(
 | |
|                       crossAxisAlignment: CrossAxisAlignment.start,
 | |
|                       children: [
 | |
|                     Text(translate("auto_disconnect_option_tip")),
 | |
|                     Offstage(
 | |
|                         offstage: !_allowAutoDisconnect,
 | |
|                         child: Text(
 | |
|                           '${_autoDisconnectTimeout.isEmpty ? '10' : _autoDisconnectTimeout} min',
 | |
|                           style: Theme.of(context).textTheme.bodySmall,
 | |
|                         )),
 | |
|                   ])),
 | |
|               Offstage(
 | |
|                   offstage: !_allowAutoDisconnect,
 | |
|                   child: IconButton(
 | |
|                       padding: EdgeInsets.zero,
 | |
|                       icon: Icon(
 | |
|                         Icons.edit,
 | |
|                         size: 20,
 | |
|                       ),
 | |
|                       onPressed: isOptionFixed(kOptionAutoDisconnectTimeout)
 | |
|                           ? null
 | |
|                           : () async {
 | |
|                               final timeout = await changeAutoDisconnectTimeout(
 | |
|                                   _autoDisconnectTimeout);
 | |
|                               setState(() {
 | |
|                                 _autoDisconnectTimeout = timeout;
 | |
|                               });
 | |
|                             }))
 | |
|             ]),
 | |
|         initialValue: _allowAutoDisconnect,
 | |
|         onToggle: isOptionFixed(kOptionAllowAutoDisconnect)
 | |
|             ? null
 | |
|             : (_) async {
 | |
|                 _allowAutoDisconnect = !_allowAutoDisconnect;
 | |
|                 String value = bool2option(
 | |
|                     kOptionAllowAutoDisconnect, _allowAutoDisconnect);
 | |
|                 await bind.mainSetOption(
 | |
|                     key: kOptionAllowAutoDisconnect, value: value);
 | |
|                 setState(() {});
 | |
|               },
 | |
|       )
 | |
|     ];
 | |
|     if (_hasIgnoreBattery) {
 | |
|       enhancementsTiles.insert(
 | |
|           0,
 | |
|           SettingsTile.switchTile(
 | |
|               initialValue: _ignoreBatteryOpt,
 | |
|               title: Column(
 | |
|                   crossAxisAlignment: CrossAxisAlignment.start,
 | |
|                   children: [
 | |
|                     Text(translate('Keep RustDesk background service')),
 | |
|                     Text('* ${translate('Ignore Battery Optimizations')}',
 | |
|                         style: Theme.of(context).textTheme.bodySmall),
 | |
|                   ]),
 | |
|               onToggle: (v) async {
 | |
|                 if (v) {
 | |
|                   await AndroidPermissionManager.request(
 | |
|                       kRequestIgnoreBatteryOptimizations);
 | |
|                 } else {
 | |
|                   final res = await gFFI.dialogManager.show<bool>(
 | |
|                       (setState, close, context) => CustomAlertDialog(
 | |
|                             title: Text(translate("Open System Setting")),
 | |
|                             content: Text(translate(
 | |
|                                 "android_open_battery_optimizations_tip")),
 | |
|                             actions: [
 | |
|                               dialogButton("Cancel",
 | |
|                                   onPressed: () => close(), isOutline: true),
 | |
|                               dialogButton(
 | |
|                                 "Open System Setting",
 | |
|                                 onPressed: () => close(true),
 | |
|                               ),
 | |
|                             ],
 | |
|                           ));
 | |
|                   if (res == true) {
 | |
|                     AndroidPermissionManager.startAction(
 | |
|                         kActionApplicationDetailsSettings);
 | |
|                   }
 | |
|                 }
 | |
|               }));
 | |
|     }
 | |
|     enhancementsTiles.add(SettingsTile.switchTile(
 | |
|         initialValue: _enableStartOnBoot,
 | |
|         title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
 | |
|           Text("${translate('Start on boot')} (beta)"),
 | |
|           Text(
 | |
|               '* ${translate('Start the screen sharing service on boot, requires special permissions')}',
 | |
|               style: Theme.of(context).textTheme.bodySmall),
 | |
|         ]),
 | |
|         onToggle: (toValue) async {
 | |
|           if (toValue) {
 | |
|             // 1. request kIgnoreBatteryOptimizations
 | |
|             if (!await AndroidPermissionManager.check(
 | |
|                 kRequestIgnoreBatteryOptimizations)) {
 | |
|               if (!await AndroidPermissionManager.request(
 | |
|                   kRequestIgnoreBatteryOptimizations)) {
 | |
|                 return;
 | |
|               }
 | |
|             }
 | |
| 
 | |
|             // 2. request kSystemAlertWindow
 | |
|             if (!await AndroidPermissionManager.check(kSystemAlertWindow)) {
 | |
|               if (!await AndroidPermissionManager.request(kSystemAlertWindow)) {
 | |
|                 return;
 | |
|               }
 | |
|             }
 | |
| 
 | |
|             // (Optional) 3. request input permission
 | |
|           }
 | |
|           setState(() => _enableStartOnBoot = toValue);
 | |
| 
 | |
|           gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, toValue);
 | |
|         }));
 | |
| 
 | |
|     onFloatingWindowChanged(bool toValue) async {
 | |
|       if (toValue) {
 | |
|         if (!await AndroidPermissionManager.check(kSystemAlertWindow)) {
 | |
|           if (!await AndroidPermissionManager.request(kSystemAlertWindow)) {
 | |
|             return;
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|       final disable = !toValue;
 | |
|       bind.mainSetLocalOption(
 | |
|           key: kOptionDisableFloatingWindow,
 | |
|           value: disable ? 'Y' : defaultOptionNo);
 | |
|       setState(() => _floatingWindowDisabled = disable);
 | |
|       gFFI.serverModel.androidUpdatekeepScreenOn();
 | |
|     }
 | |
| 
 | |
|     enhancementsTiles.add(SettingsTile.switchTile(
 | |
|         initialValue: !_floatingWindowDisabled,
 | |
|         title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
 | |
|           Text(translate('Floating window')),
 | |
|           Text('* ${translate('floating_window_tip')}',
 | |
|               style: Theme.of(context).textTheme.bodySmall),
 | |
|         ]),
 | |
|         onToggle: bind.mainIsOptionFixed(key: kOptionDisableFloatingWindow)
 | |
|             ? null
 | |
|             : onFloatingWindowChanged));
 | |
| 
 | |
|     enhancementsTiles.add(_getPopupDialogRadioEntry(
 | |
|       title: 'Keep screen on',
 | |
|       list: [
 | |
|         _RadioEntry('Never', _keepScreenOnToOption(KeepScreenOn.never)),
 | |
|         _RadioEntry('During controlled',
 | |
|             _keepScreenOnToOption(KeepScreenOn.duringControlled)),
 | |
|         _RadioEntry('During service is on',
 | |
|             _keepScreenOnToOption(KeepScreenOn.serviceOn)),
 | |
|       ],
 | |
|       getter: () => _keepScreenOnToOption(_floatingWindowDisabled
 | |
|           ? KeepScreenOn.never
 | |
|           : optionToKeepScreenOn(
 | |
|               bind.mainGetLocalOption(key: kOptionKeepScreenOn))),
 | |
|       asyncSetter: isOptionFixed(kOptionKeepScreenOn) || _floatingWindowDisabled
 | |
|           ? null
 | |
|           : (value) async {
 | |
|               await bind.mainSetLocalOption(
 | |
|                   key: kOptionKeepScreenOn, value: value);
 | |
|               setState(() => _keepScreenOn = optionToKeepScreenOn(value));
 | |
|               gFFI.serverModel.androidUpdatekeepScreenOn();
 | |
|             },
 | |
|     ));
 | |
| 
 | |
|     final disabledSettings = bind.isDisableSettings();
 | |
|     final settings = SettingsList(
 | |
|       sections: [
 | |
|         customClientSection,
 | |
|         if (!bind.isDisableAccount())
 | |
|           SettingsSection(
 | |
|             title: Text(translate('Account')),
 | |
|             tiles: [
 | |
|               SettingsTile(
 | |
|                 title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty
 | |
|                     ? translate('Login')
 | |
|                     : '${translate('Logout')} (${gFFI.userModel.userName.value})')),
 | |
|                 leading: Icon(Icons.person),
 | |
|                 onPressed: (context) {
 | |
|                   if (gFFI.userModel.userName.value.isEmpty) {
 | |
|                     loginDialog();
 | |
|                   } else {
 | |
|                     logOutConfirmDialog();
 | |
|                   }
 | |
|                 },
 | |
|               ),
 | |
|             ],
 | |
|           ),
 | |
|         SettingsSection(title: Text(translate("Settings")), tiles: [
 | |
|           if (!disabledSettings)
 | |
|             SettingsTile(
 | |
|                 title: Text(translate('ID/Relay Server')),
 | |
|                 leading: Icon(Icons.cloud),
 | |
|                 onPressed: (context) {
 | |
|                   showServerSettings(gFFI.dialogManager);
 | |
|                 }),
 | |
|           SettingsTile(
 | |
|               title: Text(translate('Language')),
 | |
|               leading: Icon(Icons.translate),
 | |
|               onPressed: (context) {
 | |
|                 showLanguageSettings(gFFI.dialogManager);
 | |
|               }),
 | |
|           SettingsTile(
 | |
|             title: Text(translate(
 | |
|                 Theme.of(context).brightness == Brightness.light
 | |
|                     ? 'Light Theme'
 | |
|                     : 'Dark Theme')),
 | |
|             leading: Icon(Theme.of(context).brightness == Brightness.light
 | |
|                 ? Icons.dark_mode
 | |
|                 : Icons.light_mode),
 | |
|             onPressed: (context) {
 | |
|               showThemeSettings(gFFI.dialogManager);
 | |
|             },
 | |
|           )
 | |
|         ]),
 | |
|         if (isAndroid)
 | |
|           SettingsSection(title: Text(translate('Hardware Codec')), tiles: [
 | |
|             SettingsTile.switchTile(
 | |
|               title: Text(translate('Enable hardware codec')),
 | |
|               initialValue: _enableHardwareCodec,
 | |
|               onToggle: isOptionFixed(kOptionEnableHwcodec)
 | |
|                   ? null
 | |
|                   : (v) async {
 | |
|                       await mainSetBoolOption(kOptionEnableHwcodec, v);
 | |
|                       final newValue =
 | |
|                           await mainGetBoolOption(kOptionEnableHwcodec);
 | |
|                       setState(() {
 | |
|                         _enableHardwareCodec = newValue;
 | |
|                       });
 | |
|                     },
 | |
|             ),
 | |
|           ]),
 | |
|         if (isAndroid && !outgoingOnly)
 | |
|           SettingsSection(
 | |
|             title: Text(translate("Recording")),
 | |
|             tiles: [
 | |
|               SettingsTile.switchTile(
 | |
|                 title:
 | |
|                     Text(translate('Automatically record incoming sessions')),
 | |
|                 leading: Icon(Icons.videocam),
 | |
|                 description: Text(
 | |
|                     "${translate("Directory")}: ${bind.mainVideoSaveDirectory(root: false)}"),
 | |
|                 initialValue: _autoRecordIncomingSession,
 | |
|                 onToggle: isOptionFixed(kOptionAllowAutoRecordIncoming)
 | |
|                     ? null
 | |
|                     : (v) async {
 | |
|                         await bind.mainSetOption(
 | |
|                             key: kOptionAllowAutoRecordIncoming,
 | |
|                             value:
 | |
|                                 bool2option(kOptionAllowAutoRecordIncoming, v));
 | |
|                         final newValue = option2bool(
 | |
|                             kOptionAllowAutoRecordIncoming,
 | |
|                             await bind.mainGetOption(
 | |
|                                 key: kOptionAllowAutoRecordIncoming));
 | |
|                         setState(() {
 | |
|                           _autoRecordIncomingSession = newValue;
 | |
|                         });
 | |
|                       },
 | |
|               ),
 | |
|             ],
 | |
|           ),
 | |
|         if (isAndroid && !disabledSettings && !outgoingOnly)
 | |
|           SettingsSection(
 | |
|             title: Text(translate("Share Screen")),
 | |
|             tiles: shareScreenTiles,
 | |
|           ),
 | |
|         if (!bind.isIncomingOnly()) defaultDisplaySection(),
 | |
|         if (isAndroid && !disabledSettings && !outgoingOnly)
 | |
|           SettingsSection(
 | |
|             title: Text(translate("Enhancements")),
 | |
|             tiles: enhancementsTiles,
 | |
|           ),
 | |
|         SettingsSection(
 | |
|           title: Text(translate("About")),
 | |
|           tiles: [
 | |
|             SettingsTile(
 | |
|                 onPressed: (context) async {
 | |
|                   if (await canLaunchUrl(Uri.parse(url))) {
 | |
|                     await launchUrl(Uri.parse(url));
 | |
|                   }
 | |
|                 },
 | |
|                 title: Text(translate("Version: ") + version),
 | |
|                 value: Padding(
 | |
|                   padding: EdgeInsets.symmetric(vertical: 8),
 | |
|                   child: Text('rustdesk.com',
 | |
|                       style: TextStyle(
 | |
|                         decoration: TextDecoration.underline,
 | |
|                       )),
 | |
|                 ),
 | |
|                 leading: Icon(Icons.info)),
 | |
|             SettingsTile(
 | |
|                 title: Text(translate("Build Date")),
 | |
|                 value: Padding(
 | |
|                   padding: EdgeInsets.symmetric(vertical: 8),
 | |
|                   child: Text(_buildDate),
 | |
|                 ),
 | |
|                 leading: Icon(Icons.query_builder)),
 | |
|             if (isAndroid)
 | |
|               SettingsTile(
 | |
|                   onPressed: (context) => onCopyFingerprint(_fingerprint),
 | |
|                   title: Text(translate("Fingerprint")),
 | |
|                   value: Padding(
 | |
|                     padding: EdgeInsets.symmetric(vertical: 8),
 | |
|                     child: Text(_fingerprint),
 | |
|                   ),
 | |
|                   leading: Icon(Icons.fingerprint)),
 | |
|             SettingsTile(
 | |
|               title: Text(translate("Privacy Statement")),
 | |
|               onPressed: (context) =>
 | |
|                   launchUrlString('https://rustdesk.com/privacy.html'),
 | |
|               leading: Icon(Icons.privacy_tip),
 | |
|             )
 | |
|           ],
 | |
|         ),
 | |
|       ],
 | |
|     );
 | |
|     return settings;
 | |
|   }
 | |
| 
 | |
|   Future<bool> canStartOnBoot() async {
 | |
|     // start on boot depends on ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS and SYSTEM_ALERT_WINDOW
 | |
|     if (_hasIgnoreBattery && !_ignoreBatteryOpt) {
 | |
|       return false;
 | |
|     }
 | |
|     if (!await AndroidPermissionManager.check(kSystemAlertWindow)) {
 | |
|       return false;
 | |
|     }
 | |
|     return true;
 | |
|   }
 | |
| 
 | |
|   defaultDisplaySection() {
 | |
|     return SettingsSection(
 | |
|       title: Text(translate("Display Settings")),
 | |
|       tiles: [
 | |
|         SettingsTile(
 | |
|             title: Text(translate('Display Settings')),
 | |
|             leading: Icon(Icons.desktop_windows_outlined),
 | |
|             trailing: Icon(Icons.arrow_forward_ios),
 | |
|             onPressed: (context) {
 | |
|               Navigator.push(context, MaterialPageRoute(builder: (context) {
 | |
|                 return _DisplayPage();
 | |
|               }));
 | |
|             })
 | |
|       ],
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| void showServerSettings(OverlayDialogManager dialogManager) async {
 | |
|   Map<String, dynamic> options = jsonDecode(await bind.mainGetOptions());
 | |
|   showServerSettingsWithValue(ServerConfig.fromOptions(options), dialogManager);
 | |
| }
 | |
| 
 | |
| void showLanguageSettings(OverlayDialogManager dialogManager) async {
 | |
|   try {
 | |
|     final langs = json.decode(await bind.mainGetLangs()) as List<dynamic>;
 | |
|     var lang = bind.mainGetLocalOption(key: kCommConfKeyLang);
 | |
|     dialogManager.show((setState, close, context) {
 | |
|       setLang(v) async {
 | |
|         if (lang != v) {
 | |
|           setState(() {
 | |
|             lang = v;
 | |
|           });
 | |
|           await bind.mainSetLocalOption(key: kCommConfKeyLang, value: v);
 | |
|           HomePage.homeKey.currentState?.refreshPages();
 | |
|           Future.delayed(Duration(milliseconds: 200), close);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       final isOptFixed = isOptionFixed(kCommConfKeyLang);
 | |
|       return CustomAlertDialog(
 | |
|         content: Column(
 | |
|           children: [
 | |
|                 getRadio(Text(translate('Default')), defaultOptionLang, lang,
 | |
|                     isOptFixed ? null : setLang),
 | |
|                 Divider(color: MyTheme.border),
 | |
|               ] +
 | |
|               langs.map((e) {
 | |
|                 final key = e[0] as String;
 | |
|                 final name = e[1] as String;
 | |
|                 return getRadio(Text(translate(name)), key, lang,
 | |
|                     isOptFixed ? null : setLang);
 | |
|               }).toList(),
 | |
|         ),
 | |
|       );
 | |
|     }, backDismiss: true, clickMaskDismiss: true);
 | |
|   } catch (e) {
 | |
|     //
 | |
|   }
 | |
| }
 | |
| 
 | |
| void showThemeSettings(OverlayDialogManager dialogManager) async {
 | |
|   var themeMode = MyTheme.getThemeModePreference();
 | |
| 
 | |
|   dialogManager.show((setState, close, context) {
 | |
|     setTheme(v) {
 | |
|       if (themeMode != v) {
 | |
|         setState(() {
 | |
|           themeMode = v;
 | |
|         });
 | |
|         MyTheme.changeDarkMode(themeMode);
 | |
|         Future.delayed(Duration(milliseconds: 200), close);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     final isOptFixed = isOptionFixed(kCommConfKeyTheme);
 | |
|     return CustomAlertDialog(
 | |
|       content: Column(children: [
 | |
|         getRadio(Text(translate('Light')), ThemeMode.light, themeMode,
 | |
|             isOptFixed ? null : setTheme),
 | |
|         getRadio(Text(translate('Dark')), ThemeMode.dark, themeMode,
 | |
|             isOptFixed ? null : setTheme),
 | |
|         getRadio(Text(translate('Follow System')), ThemeMode.system, themeMode,
 | |
|             isOptFixed ? null : setTheme)
 | |
|       ]),
 | |
|     );
 | |
|   }, backDismiss: true, clickMaskDismiss: true);
 | |
| }
 | |
| 
 | |
| void showAbout(OverlayDialogManager dialogManager) {
 | |
|   dialogManager.show((setState, close, context) {
 | |
|     return CustomAlertDialog(
 | |
|       title: Text('${translate('About')} RustDesk'),
 | |
|       content: Wrap(direction: Axis.vertical, spacing: 12, children: [
 | |
|         Text('Version: $version'),
 | |
|         InkWell(
 | |
|             onTap: () async {
 | |
|               const url = 'https://rustdesk.com/';
 | |
|               if (await canLaunchUrl(Uri.parse(url))) {
 | |
|                 await launchUrl(Uri.parse(url));
 | |
|               }
 | |
|             },
 | |
|             child: Padding(
 | |
|               padding: EdgeInsets.symmetric(vertical: 8),
 | |
|               child: Text('rustdesk.com',
 | |
|                   style: TextStyle(
 | |
|                     decoration: TextDecoration.underline,
 | |
|                   )),
 | |
|             )),
 | |
|       ]),
 | |
|       actions: [],
 | |
|     );
 | |
|   }, clickMaskDismiss: true, backDismiss: true);
 | |
| }
 | |
| 
 | |
| class ScanButton extends StatelessWidget {
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return IconButton(
 | |
|       icon: Icon(Icons.qr_code_scanner),
 | |
|       onPressed: () {
 | |
|         Navigator.push(
 | |
|           context,
 | |
|           MaterialPageRoute(
 | |
|             builder: (BuildContext context) => ScanPage(),
 | |
|           ),
 | |
|         );
 | |
|       },
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _DisplayPage extends StatefulWidget {
 | |
|   const _DisplayPage();
 | |
| 
 | |
|   @override
 | |
|   State<_DisplayPage> createState() => __DisplayPageState();
 | |
| }
 | |
| 
 | |
| class __DisplayPageState extends State<_DisplayPage> {
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     final Map codecsJson = jsonDecode(bind.mainSupportedHwdecodings());
 | |
|     final h264 = codecsJson['h264'] ?? false;
 | |
|     final h265 = codecsJson['h265'] ?? false;
 | |
|     var codecList = [
 | |
|       _RadioEntry('Auto', 'auto'),
 | |
|       _RadioEntry('VP8', 'vp8'),
 | |
|       _RadioEntry('VP9', 'vp9'),
 | |
|       _RadioEntry('AV1', 'av1'),
 | |
|       if (h264) _RadioEntry('H264', 'h264'),
 | |
|       if (h265) _RadioEntry('H265', 'h265')
 | |
|     ];
 | |
|     RxBool showCustomImageQuality = false.obs;
 | |
|     return Scaffold(
 | |
|       appBar: AppBar(
 | |
|         leading: IconButton(
 | |
|             onPressed: () => Navigator.pop(context),
 | |
|             icon: Icon(Icons.arrow_back_ios)),
 | |
|         title: Text(translate('Display Settings')),
 | |
|         centerTitle: true,
 | |
|       ),
 | |
|       body: SettingsList(sections: [
 | |
|         SettingsSection(
 | |
|           tiles: [
 | |
|             _getPopupDialogRadioEntry(
 | |
|               title: 'Default View Style',
 | |
|               list: [
 | |
|                 _RadioEntry('Scale original', kRemoteViewStyleOriginal),
 | |
|                 _RadioEntry('Scale adaptive', kRemoteViewStyleAdaptive)
 | |
|               ],
 | |
|               getter: () =>
 | |
|                   bind.mainGetUserDefaultOption(key: kOptionViewStyle),
 | |
|               asyncSetter: isOptionFixed(kOptionViewStyle)
 | |
|                   ? null
 | |
|                   : (value) async {
 | |
|                       await bind.mainSetUserDefaultOption(
 | |
|                           key: kOptionViewStyle, value: value);
 | |
|                     },
 | |
|             ),
 | |
|             _getPopupDialogRadioEntry(
 | |
|               title: 'Default Image Quality',
 | |
|               list: [
 | |
|                 _RadioEntry('Good image quality', kRemoteImageQualityBest),
 | |
|                 _RadioEntry('Balanced', kRemoteImageQualityBalanced),
 | |
|                 _RadioEntry('Optimize reaction time', kRemoteImageQualityLow),
 | |
|                 _RadioEntry('Custom', kRemoteImageQualityCustom),
 | |
|               ],
 | |
|               getter: () {
 | |
|                 final v =
 | |
|                     bind.mainGetUserDefaultOption(key: kOptionImageQuality);
 | |
|                 showCustomImageQuality.value = v == kRemoteImageQualityCustom;
 | |
|                 return v;
 | |
|               },
 | |
|               asyncSetter: isOptionFixed(kOptionImageQuality)
 | |
|                   ? null
 | |
|                   : (value) async {
 | |
|                       await bind.mainSetUserDefaultOption(
 | |
|                           key: kOptionImageQuality, value: value);
 | |
|                       showCustomImageQuality.value =
 | |
|                           value == kRemoteImageQualityCustom;
 | |
|                     },
 | |
|               tail: customImageQualitySetting(),
 | |
|               showTail: showCustomImageQuality,
 | |
|               notCloseValue: kRemoteImageQualityCustom,
 | |
|             ),
 | |
|             _getPopupDialogRadioEntry(
 | |
|               title: 'Default Codec',
 | |
|               list: codecList,
 | |
|               getter: () =>
 | |
|                   bind.mainGetUserDefaultOption(key: kOptionCodecPreference),
 | |
|               asyncSetter: isOptionFixed(kOptionCodecPreference)
 | |
|                   ? null
 | |
|                   : (value) async {
 | |
|                       await bind.mainSetUserDefaultOption(
 | |
|                           key: kOptionCodecPreference, value: value);
 | |
|                     },
 | |
|             ),
 | |
|           ],
 | |
|         ),
 | |
|         SettingsSection(
 | |
|           title: Text(translate('Other Default Options')),
 | |
|           tiles:
 | |
|               otherDefaultSettings().map((e) => otherRow(e.$1, e.$2)).toList(),
 | |
|         ),
 | |
|       ]),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   SettingsTile otherRow(String label, String key) {
 | |
|     final value = bind.mainGetUserDefaultOption(key: key) == 'Y';
 | |
|     final isOptFixed = isOptionFixed(key);
 | |
|     return SettingsTile.switchTile(
 | |
|       initialValue: value,
 | |
|       title: Text(translate(label)),
 | |
|       onToggle: isOptFixed
 | |
|           ? null
 | |
|           : (b) async {
 | |
|               await bind.mainSetUserDefaultOption(
 | |
|                   key: key, value: b ? 'Y' : defaultOptionNo);
 | |
|               setState(() {});
 | |
|             },
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _RadioEntry {
 | |
|   final String label;
 | |
|   final String value;
 | |
|   _RadioEntry(this.label, this.value);
 | |
| }
 | |
| 
 | |
| typedef _RadioEntryGetter = String Function();
 | |
| typedef _RadioEntrySetter = Future<void> Function(String);
 | |
| 
 | |
| SettingsTile _getPopupDialogRadioEntry({
 | |
|   required String title,
 | |
|   required List<_RadioEntry> list,
 | |
|   required _RadioEntryGetter getter,
 | |
|   required _RadioEntrySetter? asyncSetter,
 | |
|   Widget? tail,
 | |
|   RxBool? showTail,
 | |
|   String? notCloseValue,
 | |
| }) {
 | |
|   RxString groupValue = ''.obs;
 | |
|   RxString valueText = ''.obs;
 | |
| 
 | |
|   init() {
 | |
|     groupValue.value = getter();
 | |
|     final e = list.firstWhereOrNull((e) => e.value == groupValue.value);
 | |
|     if (e != null) {
 | |
|       valueText.value = e.label;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   init();
 | |
| 
 | |
|   void showDialog() async {
 | |
|     gFFI.dialogManager.show((setState, close, context) {
 | |
|       final onChanged = asyncSetter == null
 | |
|           ? null
 | |
|           : (String? value) async {
 | |
|               if (value == null) return;
 | |
|               await asyncSetter(value);
 | |
|               init();
 | |
|               if (value != notCloseValue) {
 | |
|                 close();
 | |
|               }
 | |
|             };
 | |
| 
 | |
|       return CustomAlertDialog(
 | |
|           content: Obx(
 | |
|         () => Column(children: [
 | |
|           ...list
 | |
|               .map((e) => getRadio(Text(translate(e.label)), e.value,
 | |
|                   groupValue.value, onChanged))
 | |
|               .toList(),
 | |
|           Offstage(
 | |
|             offstage:
 | |
|                 !(tail != null && showTail != null && showTail.value == true),
 | |
|             child: tail,
 | |
|           ),
 | |
|         ]),
 | |
|       ));
 | |
|     }, backDismiss: true, clickMaskDismiss: true);
 | |
|   }
 | |
| 
 | |
|   return SettingsTile(
 | |
|     title: Text(translate(title)),
 | |
|     onPressed: asyncSetter == null ? null : (context) => showDialog(),
 | |
|     value: Padding(
 | |
|       padding: EdgeInsets.symmetric(vertical: 8),
 | |
|       child: Obx(() => Text(translate(valueText.value))),
 | |
|     ),
 | |
|   );
 | |
| }
 |