settings_page.dart 41 KB

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