settings_page.dart 41 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174
  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'dart:typed_data';
  4. import 'package:flutter/material.dart';
  5. import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
  6. import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
  7. import 'package:get/get.dart';
  8. import 'package:provider/provider.dart';
  9. import 'package:settings_ui/settings_ui.dart';
  10. import 'package:url_launcher/url_launcher.dart';
  11. import 'package:url_launcher/url_launcher_string.dart';
  12. import '../../common.dart';
  13. import '../../common/widgets/dialog.dart';
  14. import '../../common/widgets/login.dart';
  15. import '../../consts.dart';
  16. import '../../models/model.dart';
  17. import '../../models/platform_model.dart';
  18. import '../widgets/dialog.dart';
  19. import 'home_page.dart';
  20. import 'scan_page.dart';
  21. class SettingsPage extends StatefulWidget implements PageShape {
  22. @override
  23. final title = translate("Settings");
  24. @override
  25. final icon = Icon(Icons.settings);
  26. @override
  27. final appBarActions = bind.isDisableSettings() ? [] : [ScanButton()];
  28. @override
  29. State<SettingsPage> createState() => _SettingsState();
  30. }
  31. const url = 'https://rustdesk.com/';
  32. enum KeepScreenOn {
  33. never,
  34. duringControlled,
  35. serviceOn,
  36. }
  37. String _keepScreenOnToOption(KeepScreenOn value) {
  38. switch (value) {
  39. case KeepScreenOn.never:
  40. return 'never';
  41. case KeepScreenOn.duringControlled:
  42. return 'during-controlled';
  43. case KeepScreenOn.serviceOn:
  44. return 'service-on';
  45. }
  46. }
  47. KeepScreenOn optionToKeepScreenOn(String value) {
  48. switch (value) {
  49. case 'never':
  50. return KeepScreenOn.never;
  51. case 'service-on':
  52. return KeepScreenOn.serviceOn;
  53. default:
  54. return KeepScreenOn.duringControlled;
  55. }
  56. }
  57. class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
  58. final _hasIgnoreBattery =
  59. false; //androidVersion >= 26; // remove because not work on every device
  60. var _ignoreBatteryOpt = false;
  61. var _enableStartOnBoot = false;
  62. var _floatingWindowDisabled = false;
  63. var _keepScreenOn = KeepScreenOn.duringControlled; // relay on floating window
  64. var _enableAbr = false;
  65. var _denyLANDiscovery = false;
  66. var _onlyWhiteList = false;
  67. var _enableDirectIPAccess = false;
  68. var _enableRecordSession = false;
  69. var _enableHardwareCodec = false;
  70. var _autoRecordIncomingSession = false;
  71. var _autoRecordOutgoingSession = false;
  72. var _allowAutoDisconnect = false;
  73. var _localIP = "";
  74. var _directAccessPort = "";
  75. var _fingerprint = "";
  76. var _buildDate = "";
  77. var _autoDisconnectTimeout = "";
  78. var _hideServer = false;
  79. var _hideProxy = false;
  80. var _hideNetwork = false;
  81. var _enableTrustedDevices = false;
  82. _SettingsState() {
  83. _enableAbr = option2bool(
  84. kOptionEnableAbr, bind.mainGetOptionSync(key: kOptionEnableAbr));
  85. _denyLANDiscovery = !option2bool(kOptionEnableLanDiscovery,
  86. bind.mainGetOptionSync(key: kOptionEnableLanDiscovery));
  87. _onlyWhiteList = whitelistNotEmpty();
  88. _enableDirectIPAccess = option2bool(
  89. kOptionDirectServer, bind.mainGetOptionSync(key: kOptionDirectServer));
  90. _enableRecordSession = option2bool(kOptionEnableRecordSession,
  91. bind.mainGetOptionSync(key: kOptionEnableRecordSession));
  92. _enableHardwareCodec = option2bool(kOptionEnableHwcodec,
  93. bind.mainGetOptionSync(key: kOptionEnableHwcodec));
  94. _autoRecordIncomingSession = option2bool(kOptionAllowAutoRecordIncoming,
  95. bind.mainGetOptionSync(key: kOptionAllowAutoRecordIncoming));
  96. _autoRecordOutgoingSession = option2bool(kOptionAllowAutoRecordOutgoing,
  97. bind.mainGetLocalOption(key: kOptionAllowAutoRecordOutgoing));
  98. _localIP = bind.mainGetOptionSync(key: 'local-ip-addr');
  99. _directAccessPort = bind.mainGetOptionSync(key: kOptionDirectAccessPort);
  100. _allowAutoDisconnect = option2bool(kOptionAllowAutoDisconnect,
  101. bind.mainGetOptionSync(key: kOptionAllowAutoDisconnect));
  102. _autoDisconnectTimeout =
  103. bind.mainGetOptionSync(key: kOptionAutoDisconnectTimeout);
  104. _hideServer =
  105. bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y';
  106. _hideProxy = bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
  107. _hideNetwork =
  108. bind.mainGetBuildinOption(key: kOptionHideNetworkSetting) == 'Y';
  109. _enableTrustedDevices = mainGetBoolOptionSync(kOptionEnableTrustedDevices);
  110. }
  111. @override
  112. void initState() {
  113. super.initState();
  114. WidgetsBinding.instance.addObserver(this);
  115. WidgetsBinding.instance.addPostFrameCallback((_) async {
  116. var update = false;
  117. if (_hasIgnoreBattery) {
  118. if (await checkAndUpdateIgnoreBatteryStatus()) {
  119. update = true;
  120. }
  121. }
  122. if (await checkAndUpdateStartOnBoot()) {
  123. update = true;
  124. }
  125. // start on boot depends on ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS and SYSTEM_ALERT_WINDOW
  126. var enableStartOnBoot =
  127. await gFFI.invokeMethod(AndroidChannel.kGetStartOnBootOpt);
  128. if (enableStartOnBoot) {
  129. if (!await canStartOnBoot()) {
  130. enableStartOnBoot = false;
  131. gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, false);
  132. }
  133. }
  134. if (enableStartOnBoot != _enableStartOnBoot) {
  135. update = true;
  136. _enableStartOnBoot = enableStartOnBoot;
  137. }
  138. var floatingWindowDisabled =
  139. bind.mainGetLocalOption(key: kOptionDisableFloatingWindow) == "Y" ||
  140. !await AndroidPermissionManager.check(kSystemAlertWindow);
  141. if (floatingWindowDisabled != _floatingWindowDisabled) {
  142. update = true;
  143. _floatingWindowDisabled = floatingWindowDisabled;
  144. }
  145. final keepScreenOn = _floatingWindowDisabled
  146. ? KeepScreenOn.never
  147. : optionToKeepScreenOn(
  148. bind.mainGetLocalOption(key: kOptionKeepScreenOn));
  149. if (keepScreenOn != _keepScreenOn) {
  150. update = true;
  151. _keepScreenOn = keepScreenOn;
  152. }
  153. final fingerprint = await bind.mainGetFingerprint();
  154. if (_fingerprint != fingerprint) {
  155. update = true;
  156. _fingerprint = fingerprint;
  157. }
  158. final buildDate = await bind.mainGetBuildDate();
  159. if (_buildDate != buildDate) {
  160. update = true;
  161. _buildDate = buildDate;
  162. }
  163. if (update) {
  164. setState(() {});
  165. }
  166. });
  167. }
  168. @override
  169. void dispose() {
  170. WidgetsBinding.instance.removeObserver(this);
  171. super.dispose();
  172. }
  173. @override
  174. void didChangeAppLifecycleState(AppLifecycleState state) {
  175. if (state == AppLifecycleState.resumed) {
  176. () async {
  177. final ibs = await checkAndUpdateIgnoreBatteryStatus();
  178. final sob = await checkAndUpdateStartOnBoot();
  179. if (ibs || sob) {
  180. setState(() {});
  181. }
  182. }();
  183. }
  184. }
  185. Future<bool> checkAndUpdateIgnoreBatteryStatus() async {
  186. final res = await AndroidPermissionManager.check(
  187. kRequestIgnoreBatteryOptimizations);
  188. if (_ignoreBatteryOpt != res) {
  189. _ignoreBatteryOpt = res;
  190. return true;
  191. } else {
  192. return false;
  193. }
  194. }
  195. Future<bool> checkAndUpdateStartOnBoot() async {
  196. if (!await canStartOnBoot() && _enableStartOnBoot) {
  197. _enableStartOnBoot = false;
  198. debugPrint(
  199. "checkAndUpdateStartOnBoot and set _enableStartOnBoot -> false");
  200. gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, false);
  201. return true;
  202. } else {
  203. return false;
  204. }
  205. }
  206. @override
  207. Widget build(BuildContext context) {
  208. Provider.of<FfiModel>(context);
  209. final outgoingOnly = bind.isOutgoingOnly();
  210. final incommingOnly = bind.isIncomingOnly();
  211. final customClientSection = CustomSettingsSection(
  212. child: Column(
  213. children: [
  214. if (bind.isCustomClient())
  215. Align(
  216. alignment: Alignment.center,
  217. child: loadPowered(context),
  218. ),
  219. Align(
  220. alignment: Alignment.center,
  221. child: loadLogo(),
  222. )
  223. ],
  224. ));
  225. final List<AbstractSettingsTile> enhancementsTiles = [];
  226. final enable2fa = bind.mainHasValid2FaSync();
  227. final List<AbstractSettingsTile> tfaTiles = [
  228. SettingsTile.switchTile(
  229. title: Text(translate('enable-2fa-title')),
  230. initialValue: enable2fa,
  231. onToggle: (v) async {
  232. update() async {
  233. setState(() {});
  234. }
  235. if (v == false) {
  236. CommonConfirmDialog(
  237. gFFI.dialogManager, translate('cancel-2fa-confirm-tip'), () {
  238. change2fa(callback: update);
  239. });
  240. } else {
  241. change2fa(callback: update);
  242. }
  243. },
  244. ),
  245. if (enable2fa)
  246. SettingsTile.switchTile(
  247. title: Text(translate('Telegram bot')),
  248. initialValue: bind.mainHasValidBotSync(),
  249. onToggle: (v) async {
  250. update() async {
  251. setState(() {});
  252. }
  253. if (v == false) {
  254. CommonConfirmDialog(
  255. gFFI.dialogManager, translate('cancel-bot-confirm-tip'), () {
  256. changeBot(callback: update);
  257. });
  258. } else {
  259. changeBot(callback: update);
  260. }
  261. },
  262. ),
  263. if (enable2fa)
  264. SettingsTile.switchTile(
  265. title: Column(
  266. crossAxisAlignment: CrossAxisAlignment.start,
  267. children: [
  268. Text(translate('Enable trusted devices')),
  269. Text('* ${translate('enable-trusted-devices-tip')}',
  270. style: Theme.of(context).textTheme.bodySmall),
  271. ],
  272. ),
  273. initialValue: _enableTrustedDevices,
  274. onToggle: isOptionFixed(kOptionEnableTrustedDevices)
  275. ? null
  276. : (v) async {
  277. mainSetBoolOption(kOptionEnableTrustedDevices, v);
  278. setState(() {
  279. _enableTrustedDevices = v;
  280. });
  281. },
  282. ),
  283. if (enable2fa && _enableTrustedDevices)
  284. SettingsTile(
  285. title: Text(translate('Manage trusted devices')),
  286. trailing: Icon(Icons.arrow_forward_ios),
  287. onPressed: (context) {
  288. Navigator.push(context, MaterialPageRoute(builder: (context) {
  289. return _ManageTrustedDevices();
  290. }));
  291. })
  292. ];
  293. final List<AbstractSettingsTile> shareScreenTiles = [
  294. SettingsTile.switchTile(
  295. title: Text(translate('Deny LAN discovery')),
  296. initialValue: _denyLANDiscovery,
  297. onToggle: isOptionFixed(kOptionEnableLanDiscovery)
  298. ? null
  299. : (v) async {
  300. await bind.mainSetOption(
  301. key: kOptionEnableLanDiscovery,
  302. value: bool2option(kOptionEnableLanDiscovery, !v));
  303. final newValue = !option2bool(kOptionEnableLanDiscovery,
  304. await bind.mainGetOption(key: kOptionEnableLanDiscovery));
  305. setState(() {
  306. _denyLANDiscovery = newValue;
  307. });
  308. },
  309. ),
  310. SettingsTile.switchTile(
  311. title: Row(children: [
  312. Expanded(child: Text(translate('Use IP Whitelisting'))),
  313. Offstage(
  314. offstage: !_onlyWhiteList,
  315. child: const Icon(Icons.warning_amber_rounded,
  316. color: Color.fromARGB(255, 255, 204, 0)))
  317. .marginOnly(left: 5)
  318. ]),
  319. initialValue: _onlyWhiteList,
  320. onToggle: (_) async {
  321. update() async {
  322. final onlyWhiteList = whitelistNotEmpty();
  323. if (onlyWhiteList != _onlyWhiteList) {
  324. setState(() {
  325. _onlyWhiteList = onlyWhiteList;
  326. });
  327. }
  328. }
  329. changeWhiteList(callback: update);
  330. },
  331. ),
  332. SettingsTile.switchTile(
  333. title: Text('${translate('Adaptive bitrate')} (beta)'),
  334. initialValue: _enableAbr,
  335. onToggle: isOptionFixed(kOptionEnableAbr)
  336. ? null
  337. : (v) async {
  338. await mainSetBoolOption(kOptionEnableAbr, v);
  339. final newValue = await mainGetBoolOption(kOptionEnableAbr);
  340. setState(() {
  341. _enableAbr = newValue;
  342. });
  343. },
  344. ),
  345. SettingsTile.switchTile(
  346. title: Text(translate('Enable recording session')),
  347. initialValue: _enableRecordSession,
  348. onToggle: isOptionFixed(kOptionEnableRecordSession)
  349. ? null
  350. : (v) async {
  351. await mainSetBoolOption(kOptionEnableRecordSession, v);
  352. final newValue =
  353. await mainGetBoolOption(kOptionEnableRecordSession);
  354. setState(() {
  355. _enableRecordSession = newValue;
  356. });
  357. },
  358. ),
  359. SettingsTile.switchTile(
  360. title: Row(
  361. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  362. crossAxisAlignment: CrossAxisAlignment.center,
  363. children: [
  364. Expanded(
  365. child: Column(
  366. crossAxisAlignment: CrossAxisAlignment.start,
  367. children: [
  368. Text(translate("Direct IP Access")),
  369. Offstage(
  370. offstage: !_enableDirectIPAccess,
  371. child: Text(
  372. '${translate("Local Address")}: $_localIP${_directAccessPort.isEmpty ? "" : ":$_directAccessPort"}',
  373. style: Theme.of(context).textTheme.bodySmall,
  374. )),
  375. ])),
  376. Offstage(
  377. offstage: !_enableDirectIPAccess,
  378. child: IconButton(
  379. padding: EdgeInsets.zero,
  380. icon: Icon(
  381. Icons.edit,
  382. size: 20,
  383. ),
  384. onPressed: isOptionFixed(kOptionDirectAccessPort)
  385. ? null
  386. : () async {
  387. final port = await changeDirectAccessPort(
  388. _localIP, _directAccessPort);
  389. setState(() {
  390. _directAccessPort = port;
  391. });
  392. }))
  393. ]),
  394. initialValue: _enableDirectIPAccess,
  395. onToggle: isOptionFixed(kOptionDirectServer)
  396. ? null
  397. : (_) async {
  398. _enableDirectIPAccess = !_enableDirectIPAccess;
  399. String value =
  400. bool2option(kOptionDirectServer, _enableDirectIPAccess);
  401. await bind.mainSetOption(
  402. key: kOptionDirectServer, value: value);
  403. setState(() {});
  404. },
  405. ),
  406. SettingsTile.switchTile(
  407. title: Row(
  408. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  409. crossAxisAlignment: CrossAxisAlignment.center,
  410. children: [
  411. Expanded(
  412. child: Column(
  413. crossAxisAlignment: CrossAxisAlignment.start,
  414. children: [
  415. Text(translate("auto_disconnect_option_tip")),
  416. Offstage(
  417. offstage: !_allowAutoDisconnect,
  418. child: Text(
  419. '${_autoDisconnectTimeout.isEmpty ? '10' : _autoDisconnectTimeout} min',
  420. style: Theme.of(context).textTheme.bodySmall,
  421. )),
  422. ])),
  423. Offstage(
  424. offstage: !_allowAutoDisconnect,
  425. child: IconButton(
  426. padding: EdgeInsets.zero,
  427. icon: Icon(
  428. Icons.edit,
  429. size: 20,
  430. ),
  431. onPressed: isOptionFixed(kOptionAutoDisconnectTimeout)
  432. ? null
  433. : () async {
  434. final timeout = await changeAutoDisconnectTimeout(
  435. _autoDisconnectTimeout);
  436. setState(() {
  437. _autoDisconnectTimeout = timeout;
  438. });
  439. }))
  440. ]),
  441. initialValue: _allowAutoDisconnect,
  442. onToggle: isOptionFixed(kOptionAllowAutoDisconnect)
  443. ? null
  444. : (_) async {
  445. _allowAutoDisconnect = !_allowAutoDisconnect;
  446. String value = bool2option(
  447. kOptionAllowAutoDisconnect, _allowAutoDisconnect);
  448. await bind.mainSetOption(
  449. key: kOptionAllowAutoDisconnect, value: value);
  450. setState(() {});
  451. },
  452. )
  453. ];
  454. if (_hasIgnoreBattery) {
  455. enhancementsTiles.insert(
  456. 0,
  457. SettingsTile.switchTile(
  458. initialValue: _ignoreBatteryOpt,
  459. title: Column(
  460. crossAxisAlignment: CrossAxisAlignment.start,
  461. children: [
  462. Text(translate('Keep RustDesk background service')),
  463. Text('* ${translate('Ignore Battery Optimizations')}',
  464. style: Theme.of(context).textTheme.bodySmall),
  465. ]),
  466. onToggle: (v) async {
  467. if (v) {
  468. await AndroidPermissionManager.request(
  469. kRequestIgnoreBatteryOptimizations);
  470. } else {
  471. final res = await gFFI.dialogManager.show<bool>(
  472. (setState, close, context) => CustomAlertDialog(
  473. title: Text(translate("Open System Setting")),
  474. content: Text(translate(
  475. "android_open_battery_optimizations_tip")),
  476. actions: [
  477. dialogButton("Cancel",
  478. onPressed: () => close(), isOutline: true),
  479. dialogButton(
  480. "Open System Setting",
  481. onPressed: () => close(true),
  482. ),
  483. ],
  484. ));
  485. if (res == true) {
  486. AndroidPermissionManager.startAction(
  487. kActionApplicationDetailsSettings);
  488. }
  489. }
  490. }));
  491. }
  492. enhancementsTiles.add(SettingsTile.switchTile(
  493. initialValue: _enableStartOnBoot,
  494. title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
  495. Text("${translate('Start on boot')} (beta)"),
  496. Text(
  497. '* ${translate('Start the screen sharing service on boot, requires special permissions')}',
  498. style: Theme.of(context).textTheme.bodySmall),
  499. ]),
  500. onToggle: (toValue) async {
  501. if (toValue) {
  502. // 1. request kIgnoreBatteryOptimizations
  503. if (!await AndroidPermissionManager.check(
  504. kRequestIgnoreBatteryOptimizations)) {
  505. if (!await AndroidPermissionManager.request(
  506. kRequestIgnoreBatteryOptimizations)) {
  507. return;
  508. }
  509. }
  510. // 2. request kSystemAlertWindow
  511. if (!await AndroidPermissionManager.check(kSystemAlertWindow)) {
  512. if (!await AndroidPermissionManager.request(kSystemAlertWindow)) {
  513. return;
  514. }
  515. }
  516. // (Optional) 3. request input permission
  517. }
  518. setState(() => _enableStartOnBoot = toValue);
  519. gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, toValue);
  520. }));
  521. onFloatingWindowChanged(bool toValue) async {
  522. if (toValue) {
  523. if (!await AndroidPermissionManager.check(kSystemAlertWindow)) {
  524. if (!await AndroidPermissionManager.request(kSystemAlertWindow)) {
  525. return;
  526. }
  527. }
  528. }
  529. final disable = !toValue;
  530. bind.mainSetLocalOption(
  531. key: kOptionDisableFloatingWindow,
  532. value: disable ? 'Y' : defaultOptionNo);
  533. setState(() => _floatingWindowDisabled = disable);
  534. gFFI.serverModel.androidUpdatekeepScreenOn();
  535. }
  536. enhancementsTiles.add(SettingsTile.switchTile(
  537. initialValue: !_floatingWindowDisabled,
  538. title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
  539. Text(translate('Floating window')),
  540. Text('* ${translate('floating_window_tip')}',
  541. style: Theme.of(context).textTheme.bodySmall),
  542. ]),
  543. onToggle: bind.mainIsOptionFixed(key: kOptionDisableFloatingWindow)
  544. ? null
  545. : onFloatingWindowChanged));
  546. enhancementsTiles.add(_getPopupDialogRadioEntry(
  547. title: 'Keep screen on',
  548. list: [
  549. _RadioEntry('Never', _keepScreenOnToOption(KeepScreenOn.never)),
  550. _RadioEntry('During controlled',
  551. _keepScreenOnToOption(KeepScreenOn.duringControlled)),
  552. _RadioEntry('During service is on',
  553. _keepScreenOnToOption(KeepScreenOn.serviceOn)),
  554. ],
  555. getter: () => _keepScreenOnToOption(_floatingWindowDisabled
  556. ? KeepScreenOn.never
  557. : optionToKeepScreenOn(
  558. bind.mainGetLocalOption(key: kOptionKeepScreenOn))),
  559. asyncSetter: isOptionFixed(kOptionKeepScreenOn) || _floatingWindowDisabled
  560. ? null
  561. : (value) async {
  562. await bind.mainSetLocalOption(
  563. key: kOptionKeepScreenOn, value: value);
  564. setState(() => _keepScreenOn = optionToKeepScreenOn(value));
  565. gFFI.serverModel.androidUpdatekeepScreenOn();
  566. },
  567. ));
  568. final disabledSettings = bind.isDisableSettings();
  569. final hideSecuritySettings =
  570. bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) == 'Y';
  571. final settings = SettingsList(
  572. sections: [
  573. customClientSection,
  574. if (!bind.isDisableAccount())
  575. SettingsSection(
  576. title: Text(translate('Account')),
  577. tiles: [
  578. SettingsTile(
  579. title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty
  580. ? translate('Login')
  581. : '${translate('Logout')} (${gFFI.userModel.userName.value})')),
  582. leading: Icon(Icons.person),
  583. onPressed: (context) {
  584. if (gFFI.userModel.userName.value.isEmpty) {
  585. loginDialog();
  586. } else {
  587. logOutConfirmDialog();
  588. }
  589. },
  590. ),
  591. ],
  592. ),
  593. SettingsSection(title: Text(translate("Settings")), tiles: [
  594. if (!disabledSettings && !_hideNetwork && !_hideServer)
  595. SettingsTile(
  596. title: Text(translate('ID/Relay Server')),
  597. leading: Icon(Icons.cloud),
  598. onPressed: (context) {
  599. showServerSettings(gFFI.dialogManager);
  600. }),
  601. if (!isIOS && !_hideNetwork && !_hideProxy)
  602. SettingsTile(
  603. title: Text(translate('Socks5/Http(s) Proxy')),
  604. leading: Icon(Icons.network_ping),
  605. onPressed: (context) {
  606. changeSocks5Proxy();
  607. }),
  608. SettingsTile(
  609. title: Text(translate('Language')),
  610. leading: Icon(Icons.translate),
  611. onPressed: (context) {
  612. showLanguageSettings(gFFI.dialogManager);
  613. }),
  614. SettingsTile(
  615. title: Text(translate(
  616. Theme.of(context).brightness == Brightness.light
  617. ? 'Light Theme'
  618. : 'Dark Theme')),
  619. leading: Icon(Theme.of(context).brightness == Brightness.light
  620. ? Icons.dark_mode
  621. : Icons.light_mode),
  622. onPressed: (context) {
  623. showThemeSettings(gFFI.dialogManager);
  624. },
  625. )
  626. ]),
  627. if (isAndroid)
  628. SettingsSection(title: Text(translate('Hardware Codec')), tiles: [
  629. SettingsTile.switchTile(
  630. title: Text(translate('Enable hardware codec')),
  631. initialValue: _enableHardwareCodec,
  632. onToggle: isOptionFixed(kOptionEnableHwcodec)
  633. ? null
  634. : (v) async {
  635. await mainSetBoolOption(kOptionEnableHwcodec, v);
  636. final newValue =
  637. await mainGetBoolOption(kOptionEnableHwcodec);
  638. setState(() {
  639. _enableHardwareCodec = newValue;
  640. });
  641. },
  642. ),
  643. ]),
  644. if (isAndroid)
  645. SettingsSection(
  646. title: Text(translate("Recording")),
  647. tiles: [
  648. if (!outgoingOnly)
  649. SettingsTile.switchTile(
  650. title:
  651. Text(translate('Automatically record incoming sessions')),
  652. initialValue: _autoRecordIncomingSession,
  653. onToggle: isOptionFixed(kOptionAllowAutoRecordIncoming)
  654. ? null
  655. : (v) async {
  656. await bind.mainSetOption(
  657. key: kOptionAllowAutoRecordIncoming,
  658. value: bool2option(
  659. kOptionAllowAutoRecordIncoming, v));
  660. final newValue = option2bool(
  661. kOptionAllowAutoRecordIncoming,
  662. await bind.mainGetOption(
  663. key: kOptionAllowAutoRecordIncoming));
  664. setState(() {
  665. _autoRecordIncomingSession = newValue;
  666. });
  667. },
  668. ),
  669. if (!incommingOnly)
  670. SettingsTile.switchTile(
  671. title:
  672. Text(translate('Automatically record outgoing sessions')),
  673. initialValue: _autoRecordOutgoingSession,
  674. onToggle: isOptionFixed(kOptionAllowAutoRecordOutgoing)
  675. ? null
  676. : (v) async {
  677. await bind.mainSetLocalOption(
  678. key: kOptionAllowAutoRecordOutgoing,
  679. value: bool2option(
  680. kOptionAllowAutoRecordOutgoing, v));
  681. final newValue = option2bool(
  682. kOptionAllowAutoRecordOutgoing,
  683. bind.mainGetLocalOption(
  684. key: kOptionAllowAutoRecordOutgoing));
  685. setState(() {
  686. _autoRecordOutgoingSession = newValue;
  687. });
  688. },
  689. ),
  690. SettingsTile(
  691. title: Text(translate("Directory")),
  692. description: Text(bind.mainVideoSaveDirectory(root: false)),
  693. ),
  694. ],
  695. ),
  696. if (isAndroid &&
  697. !disabledSettings &&
  698. !outgoingOnly &&
  699. !hideSecuritySettings)
  700. SettingsSection(title: Text('2FA'), tiles: tfaTiles),
  701. if (isAndroid &&
  702. !disabledSettings &&
  703. !outgoingOnly &&
  704. !hideSecuritySettings)
  705. SettingsSection(
  706. title: Text(translate("Share Screen")),
  707. tiles: shareScreenTiles,
  708. ),
  709. if (!bind.isIncomingOnly()) defaultDisplaySection(),
  710. if (isAndroid &&
  711. !disabledSettings &&
  712. !outgoingOnly &&
  713. !hideSecuritySettings)
  714. SettingsSection(
  715. title: Text(translate("Enhancements")),
  716. tiles: enhancementsTiles,
  717. ),
  718. SettingsSection(
  719. title: Text(translate("About")),
  720. tiles: [
  721. SettingsTile(
  722. onPressed: (context) async {
  723. if (await canLaunchUrl(Uri.parse(url))) {
  724. await launchUrl(Uri.parse(url));
  725. }
  726. },
  727. title: Text(translate("Version: ") + version),
  728. value: Padding(
  729. padding: EdgeInsets.symmetric(vertical: 8),
  730. child: Text('rustdesk.com',
  731. style: TextStyle(
  732. decoration: TextDecoration.underline,
  733. )),
  734. ),
  735. leading: Icon(Icons.info)),
  736. SettingsTile(
  737. title: Text(translate("Build Date")),
  738. value: Padding(
  739. padding: EdgeInsets.symmetric(vertical: 8),
  740. child: Text(_buildDate),
  741. ),
  742. leading: Icon(Icons.query_builder)),
  743. if (isAndroid)
  744. SettingsTile(
  745. onPressed: (context) => onCopyFingerprint(_fingerprint),
  746. title: Text(translate("Fingerprint")),
  747. value: Padding(
  748. padding: EdgeInsets.symmetric(vertical: 8),
  749. child: Text(_fingerprint),
  750. ),
  751. leading: Icon(Icons.fingerprint)),
  752. SettingsTile(
  753. title: Text(translate("Privacy Statement")),
  754. onPressed: (context) =>
  755. launchUrlString('https://rustdesk.com/privacy.html'),
  756. leading: Icon(Icons.privacy_tip),
  757. )
  758. ],
  759. ),
  760. ],
  761. );
  762. return settings;
  763. }
  764. Future<bool> canStartOnBoot() async {
  765. // start on boot depends on ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS and SYSTEM_ALERT_WINDOW
  766. if (_hasIgnoreBattery && !_ignoreBatteryOpt) {
  767. return false;
  768. }
  769. if (!await AndroidPermissionManager.check(kSystemAlertWindow)) {
  770. return false;
  771. }
  772. return true;
  773. }
  774. defaultDisplaySection() {
  775. return SettingsSection(
  776. title: Text(translate("Display Settings")),
  777. tiles: [
  778. SettingsTile(
  779. title: Text(translate('Display Settings')),
  780. leading: Icon(Icons.desktop_windows_outlined),
  781. trailing: Icon(Icons.arrow_forward_ios),
  782. onPressed: (context) {
  783. Navigator.push(context, MaterialPageRoute(builder: (context) {
  784. return _DisplayPage();
  785. }));
  786. })
  787. ],
  788. );
  789. }
  790. }
  791. void showServerSettings(OverlayDialogManager dialogManager) async {
  792. Map<String, dynamic> options = jsonDecode(await bind.mainGetOptions());
  793. showServerSettingsWithValue(ServerConfig.fromOptions(options), dialogManager);
  794. }
  795. void showLanguageSettings(OverlayDialogManager dialogManager) async {
  796. try {
  797. final langs = json.decode(await bind.mainGetLangs()) as List<dynamic>;
  798. var lang = bind.mainGetLocalOption(key: kCommConfKeyLang);
  799. dialogManager.show((setState, close, context) {
  800. setLang(v) async {
  801. if (lang != v) {
  802. setState(() {
  803. lang = v;
  804. });
  805. await bind.mainSetLocalOption(key: kCommConfKeyLang, value: v);
  806. HomePage.homeKey.currentState?.refreshPages();
  807. Future.delayed(Duration(milliseconds: 200), close);
  808. }
  809. }
  810. final isOptFixed = isOptionFixed(kCommConfKeyLang);
  811. return CustomAlertDialog(
  812. content: Column(
  813. children: [
  814. getRadio(Text(translate('Default')), defaultOptionLang, lang,
  815. isOptFixed ? null : setLang),
  816. Divider(color: MyTheme.border),
  817. ] +
  818. langs.map((e) {
  819. final key = e[0] as String;
  820. final name = e[1] as String;
  821. return getRadio(Text(translate(name)), key, lang,
  822. isOptFixed ? null : setLang);
  823. }).toList(),
  824. ),
  825. );
  826. }, backDismiss: true, clickMaskDismiss: true);
  827. } catch (e) {
  828. //
  829. }
  830. }
  831. void showThemeSettings(OverlayDialogManager dialogManager) async {
  832. var themeMode = MyTheme.getThemeModePreference();
  833. dialogManager.show((setState, close, context) {
  834. setTheme(v) {
  835. if (themeMode != v) {
  836. setState(() {
  837. themeMode = v;
  838. });
  839. MyTheme.changeDarkMode(themeMode);
  840. Future.delayed(Duration(milliseconds: 200), close);
  841. }
  842. }
  843. final isOptFixed = isOptionFixed(kCommConfKeyTheme);
  844. return CustomAlertDialog(
  845. content: Column(children: [
  846. getRadio(Text(translate('Light')), ThemeMode.light, themeMode,
  847. isOptFixed ? null : setTheme),
  848. getRadio(Text(translate('Dark')), ThemeMode.dark, themeMode,
  849. isOptFixed ? null : setTheme),
  850. getRadio(Text(translate('Follow System')), ThemeMode.system, themeMode,
  851. isOptFixed ? null : setTheme)
  852. ]),
  853. );
  854. }, backDismiss: true, clickMaskDismiss: true);
  855. }
  856. void showAbout(OverlayDialogManager dialogManager) {
  857. dialogManager.show((setState, close, context) {
  858. return CustomAlertDialog(
  859. title: Text(translate('About RustDesk')),
  860. content: Wrap(direction: Axis.vertical, spacing: 12, children: [
  861. Text('Version: $version'),
  862. InkWell(
  863. onTap: () async {
  864. const url = 'https://rustdesk.com/';
  865. if (await canLaunchUrl(Uri.parse(url))) {
  866. await launchUrl(Uri.parse(url));
  867. }
  868. },
  869. child: Padding(
  870. padding: EdgeInsets.symmetric(vertical: 8),
  871. child: Text('rustdesk.com',
  872. style: TextStyle(
  873. decoration: TextDecoration.underline,
  874. )),
  875. )),
  876. ]),
  877. actions: [],
  878. );
  879. }, clickMaskDismiss: true, backDismiss: true);
  880. }
  881. class ScanButton extends StatelessWidget {
  882. @override
  883. Widget build(BuildContext context) {
  884. return IconButton(
  885. icon: Icon(Icons.qr_code_scanner),
  886. onPressed: () {
  887. Navigator.push(
  888. context,
  889. MaterialPageRoute(
  890. builder: (BuildContext context) => ScanPage(),
  891. ),
  892. );
  893. },
  894. );
  895. }
  896. }
  897. class _DisplayPage extends StatefulWidget {
  898. const _DisplayPage();
  899. @override
  900. State<_DisplayPage> createState() => __DisplayPageState();
  901. }
  902. class __DisplayPageState extends State<_DisplayPage> {
  903. @override
  904. Widget build(BuildContext context) {
  905. final Map codecsJson = jsonDecode(bind.mainSupportedHwdecodings());
  906. final h264 = codecsJson['h264'] ?? false;
  907. final h265 = codecsJson['h265'] ?? false;
  908. var codecList = [
  909. _RadioEntry('Auto', 'auto'),
  910. _RadioEntry('VP8', 'vp8'),
  911. _RadioEntry('VP9', 'vp9'),
  912. _RadioEntry('AV1', 'av1'),
  913. if (h264) _RadioEntry('H264', 'h264'),
  914. if (h265) _RadioEntry('H265', 'h265')
  915. ];
  916. RxBool showCustomImageQuality = false.obs;
  917. return Scaffold(
  918. appBar: AppBar(
  919. leading: IconButton(
  920. onPressed: () => Navigator.pop(context),
  921. icon: Icon(Icons.arrow_back_ios)),
  922. title: Text(translate('Display Settings')),
  923. centerTitle: true,
  924. ),
  925. body: SettingsList(sections: [
  926. SettingsSection(
  927. tiles: [
  928. _getPopupDialogRadioEntry(
  929. title: 'Default View Style',
  930. list: [
  931. _RadioEntry('Scale original', kRemoteViewStyleOriginal),
  932. _RadioEntry('Scale adaptive', kRemoteViewStyleAdaptive)
  933. ],
  934. getter: () =>
  935. bind.mainGetUserDefaultOption(key: kOptionViewStyle),
  936. asyncSetter: isOptionFixed(kOptionViewStyle)
  937. ? null
  938. : (value) async {
  939. await bind.mainSetUserDefaultOption(
  940. key: kOptionViewStyle, value: value);
  941. },
  942. ),
  943. _getPopupDialogRadioEntry(
  944. title: 'Default Image Quality',
  945. list: [
  946. _RadioEntry('Good image quality', kRemoteImageQualityBest),
  947. _RadioEntry('Balanced', kRemoteImageQualityBalanced),
  948. _RadioEntry('Optimize reaction time', kRemoteImageQualityLow),
  949. _RadioEntry('Custom', kRemoteImageQualityCustom),
  950. ],
  951. getter: () {
  952. final v =
  953. bind.mainGetUserDefaultOption(key: kOptionImageQuality);
  954. showCustomImageQuality.value = v == kRemoteImageQualityCustom;
  955. return v;
  956. },
  957. asyncSetter: isOptionFixed(kOptionImageQuality)
  958. ? null
  959. : (value) async {
  960. await bind.mainSetUserDefaultOption(
  961. key: kOptionImageQuality, value: value);
  962. showCustomImageQuality.value =
  963. value == kRemoteImageQualityCustom;
  964. },
  965. tail: customImageQualitySetting(),
  966. showTail: showCustomImageQuality,
  967. notCloseValue: kRemoteImageQualityCustom,
  968. ),
  969. _getPopupDialogRadioEntry(
  970. title: 'Default Codec',
  971. list: codecList,
  972. getter: () =>
  973. bind.mainGetUserDefaultOption(key: kOptionCodecPreference),
  974. asyncSetter: isOptionFixed(kOptionCodecPreference)
  975. ? null
  976. : (value) async {
  977. await bind.mainSetUserDefaultOption(
  978. key: kOptionCodecPreference, value: value);
  979. },
  980. ),
  981. ],
  982. ),
  983. SettingsSection(
  984. title: Text(translate('Other Default Options')),
  985. tiles:
  986. otherDefaultSettings().map((e) => otherRow(e.$1, e.$2)).toList(),
  987. ),
  988. ]),
  989. );
  990. }
  991. SettingsTile otherRow(String label, String key) {
  992. final value = bind.mainGetUserDefaultOption(key: key) == 'Y';
  993. final isOptFixed = isOptionFixed(key);
  994. return SettingsTile.switchTile(
  995. initialValue: value,
  996. title: Text(translate(label)),
  997. onToggle: isOptFixed
  998. ? null
  999. : (b) async {
  1000. await bind.mainSetUserDefaultOption(
  1001. key: key, value: b ? 'Y' : defaultOptionNo);
  1002. setState(() {});
  1003. },
  1004. );
  1005. }
  1006. }
  1007. class _ManageTrustedDevices extends StatefulWidget {
  1008. const _ManageTrustedDevices();
  1009. @override
  1010. State<_ManageTrustedDevices> createState() => __ManageTrustedDevicesState();
  1011. }
  1012. class __ManageTrustedDevicesState extends State<_ManageTrustedDevices> {
  1013. RxList<TrustedDevice> trustedDevices = RxList.empty(growable: true);
  1014. RxList<Uint8List> selectedDevices = RxList.empty();
  1015. @override
  1016. Widget build(BuildContext context) {
  1017. return Scaffold(
  1018. appBar: AppBar(
  1019. title: Text(translate('Manage trusted devices')),
  1020. centerTitle: true,
  1021. actions: [
  1022. Obx(() => IconButton(
  1023. icon: Icon(Icons.delete, color: Colors.white),
  1024. onPressed: selectedDevices.isEmpty
  1025. ? null
  1026. : () {
  1027. confrimDeleteTrustedDevicesDialog(
  1028. trustedDevices, selectedDevices);
  1029. }))
  1030. ],
  1031. ),
  1032. body: FutureBuilder(
  1033. future: TrustedDevice.get(),
  1034. builder: (context, snapshot) {
  1035. if (snapshot.connectionState == ConnectionState.waiting) {
  1036. return Center(child: CircularProgressIndicator());
  1037. }
  1038. if (snapshot.hasError) {
  1039. return Center(child: Text('Error: ${snapshot.error}'));
  1040. }
  1041. final devices = snapshot.data as List<TrustedDevice>;
  1042. trustedDevices = devices.obs;
  1043. return trustedDevicesTable(trustedDevices, selectedDevices);
  1044. }),
  1045. );
  1046. }
  1047. }
  1048. class _RadioEntry {
  1049. final String label;
  1050. final String value;
  1051. _RadioEntry(this.label, this.value);
  1052. }
  1053. typedef _RadioEntryGetter = String Function();
  1054. typedef _RadioEntrySetter = Future<void> Function(String);
  1055. SettingsTile _getPopupDialogRadioEntry({
  1056. required String title,
  1057. required List<_RadioEntry> list,
  1058. required _RadioEntryGetter getter,
  1059. required _RadioEntrySetter? asyncSetter,
  1060. Widget? tail,
  1061. RxBool? showTail,
  1062. String? notCloseValue,
  1063. }) {
  1064. RxString groupValue = ''.obs;
  1065. RxString valueText = ''.obs;
  1066. init() {
  1067. groupValue.value = getter();
  1068. final e = list.firstWhereOrNull((e) => e.value == groupValue.value);
  1069. if (e != null) {
  1070. valueText.value = e.label;
  1071. }
  1072. }
  1073. init();
  1074. void showDialog() async {
  1075. gFFI.dialogManager.show((setState, close, context) {
  1076. final onChanged = asyncSetter == null
  1077. ? null
  1078. : (String? value) async {
  1079. if (value == null) return;
  1080. await asyncSetter(value);
  1081. init();
  1082. if (value != notCloseValue) {
  1083. close();
  1084. }
  1085. };
  1086. return CustomAlertDialog(
  1087. content: Obx(
  1088. () => Column(children: [
  1089. ...list
  1090. .map((e) => getRadio(Text(translate(e.label)), e.value,
  1091. groupValue.value, onChanged))
  1092. .toList(),
  1093. Offstage(
  1094. offstage:
  1095. !(tail != null && showTail != null && showTail.value == true),
  1096. child: tail,
  1097. ),
  1098. ]),
  1099. ));
  1100. }, backDismiss: true, clickMaskDismiss: true);
  1101. }
  1102. return SettingsTile(
  1103. title: Text(translate(title)),
  1104. onPressed: asyncSetter == null ? null : (context) => showDialog(),
  1105. value: Padding(
  1106. padding: EdgeInsets.symmetric(vertical: 8),
  1107. child: Obx(() => Text(translate(valueText.value))),
  1108. ),
  1109. );
  1110. }