desktop_setting_page.dart 81 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560
  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'dart:io';
  4. import 'package:file_picker/file_picker.dart';
  5. import 'package:flutter/material.dart';
  6. import 'package:flutter/services.dart';
  7. import 'package:flutter_hbb/common.dart';
  8. import 'package:flutter_hbb/common/widgets/audio_input.dart';
  9. import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
  10. import 'package:flutter_hbb/consts.dart';
  11. import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
  12. import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
  13. import 'package:flutter_hbb/mobile/widgets/dialog.dart';
  14. import 'package:flutter_hbb/models/platform_model.dart';
  15. import 'package:flutter_hbb/models/server_model.dart';
  16. import 'package:flutter_hbb/models/state_model.dart';
  17. import 'package:flutter_hbb/plugin/manager.dart';
  18. import 'package:flutter_hbb/plugin/widgets/desktop_settings.dart';
  19. import 'package:get/get.dart';
  20. import 'package:provider/provider.dart';
  21. import 'package:url_launcher/url_launcher.dart';
  22. import 'package:url_launcher/url_launcher_string.dart';
  23. import '../../common/widgets/dialog.dart';
  24. import '../../common/widgets/login.dart';
  25. const double _kTabWidth = 200;
  26. const double _kTabHeight = 42;
  27. const double _kCardFixedWidth = 540;
  28. const double _kCardLeftMargin = 15;
  29. const double _kContentHMargin = 15;
  30. const double _kContentHSubMargin = _kContentHMargin + 33;
  31. const double _kCheckBoxLeftMargin = 10;
  32. const double _kRadioLeftMargin = 10;
  33. const double _kListViewBottomMargin = 15;
  34. const double _kTitleFontSize = 20;
  35. const double _kContentFontSize = 15;
  36. const Color _accentColor = MyTheme.accent;
  37. const String _kSettingPageControllerTag = 'settingPageController';
  38. const String _kSettingPageTabKeyTag = 'settingPageTabKey';
  39. class _TabInfo {
  40. late final SettingsTabKey key;
  41. late final String label;
  42. late final IconData unselected;
  43. late final IconData selected;
  44. _TabInfo(this.key, this.label, this.unselected, this.selected);
  45. }
  46. enum SettingsTabKey {
  47. general,
  48. safety,
  49. network,
  50. display,
  51. plugin,
  52. account,
  53. about,
  54. }
  55. class DesktopSettingPage extends StatefulWidget {
  56. final SettingsTabKey initialTabkey;
  57. static final List<SettingsTabKey> tabKeys = [
  58. SettingsTabKey.general,
  59. if (!isWeb &&
  60. !bind.isOutgoingOnly() &&
  61. !bind.isDisableSettings() &&
  62. bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) != 'Y')
  63. SettingsTabKey.safety,
  64. if (!bind.isDisableSettings() &&
  65. bind.mainGetBuildinOption(key: kOptionHideNetworkSetting) != 'Y')
  66. SettingsTabKey.network,
  67. if (!bind.isIncomingOnly()) SettingsTabKey.display,
  68. if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled())
  69. SettingsTabKey.plugin,
  70. if (!bind.isDisableAccount()) SettingsTabKey.account,
  71. SettingsTabKey.about,
  72. ];
  73. DesktopSettingPage({Key? key, required this.initialTabkey}) : super(key: key);
  74. @override
  75. State<DesktopSettingPage> createState() =>
  76. _DesktopSettingPageState(initialTabkey);
  77. static void switch2page(SettingsTabKey page) {
  78. try {
  79. int index = tabKeys.indexOf(page);
  80. if (index == -1) {
  81. return;
  82. }
  83. if (Get.isRegistered<PageController>(tag: _kSettingPageControllerTag)) {
  84. DesktopTabPage.onAddSetting(initialPage: page);
  85. PageController controller =
  86. Get.find<PageController>(tag: _kSettingPageControllerTag);
  87. Rx<SettingsTabKey> selected =
  88. Get.find<Rx<SettingsTabKey>>(tag: _kSettingPageTabKeyTag);
  89. selected.value = page;
  90. controller.jumpToPage(index);
  91. } else {
  92. DesktopTabPage.onAddSetting(initialPage: page);
  93. }
  94. } catch (e) {
  95. debugPrintStack(label: '$e');
  96. }
  97. }
  98. }
  99. class _DesktopSettingPageState extends State<DesktopSettingPage>
  100. with
  101. TickerProviderStateMixin,
  102. AutomaticKeepAliveClientMixin,
  103. WidgetsBindingObserver {
  104. late PageController controller;
  105. late Rx<SettingsTabKey> selectedTab;
  106. @override
  107. bool get wantKeepAlive => true;
  108. final RxBool _block = false.obs;
  109. final RxBool _canBeBlocked = false.obs;
  110. Timer? _videoConnTimer;
  111. _DesktopSettingPageState(SettingsTabKey initialTabkey) {
  112. var initialIndex = DesktopSettingPage.tabKeys.indexOf(initialTabkey);
  113. if (initialIndex == -1) {
  114. initialIndex = 0;
  115. }
  116. selectedTab = DesktopSettingPage.tabKeys[initialIndex].obs;
  117. Get.put<Rx<SettingsTabKey>>(selectedTab, tag: _kSettingPageTabKeyTag);
  118. controller = PageController(initialPage: initialIndex);
  119. Get.put<PageController>(controller, tag: _kSettingPageControllerTag);
  120. controller.addListener(() {
  121. if (controller.page != null) {
  122. int page = controller.page!.toInt();
  123. if (page < DesktopSettingPage.tabKeys.length) {
  124. selectedTab.value = DesktopSettingPage.tabKeys[page];
  125. }
  126. }
  127. });
  128. }
  129. @override
  130. void didChangeAppLifecycleState(AppLifecycleState state) {
  131. super.didChangeAppLifecycleState(state);
  132. if (state == AppLifecycleState.resumed) {
  133. shouldBeBlocked(_block, canBeBlocked);
  134. }
  135. }
  136. @override
  137. void initState() {
  138. super.initState();
  139. WidgetsBinding.instance.addObserver(this);
  140. _videoConnTimer =
  141. periodic_immediate(Duration(milliseconds: 1000), () async {
  142. if (!mounted) {
  143. return;
  144. }
  145. _canBeBlocked.value = await canBeBlocked();
  146. });
  147. }
  148. @override
  149. void dispose() {
  150. super.dispose();
  151. Get.delete<PageController>(tag: _kSettingPageControllerTag);
  152. Get.delete<RxInt>(tag: _kSettingPageTabKeyTag);
  153. WidgetsBinding.instance.removeObserver(this);
  154. _videoConnTimer?.cancel();
  155. }
  156. List<_TabInfo> _settingTabs() {
  157. final List<_TabInfo> settingTabs = <_TabInfo>[];
  158. for (final tab in DesktopSettingPage.tabKeys) {
  159. switch (tab) {
  160. case SettingsTabKey.general:
  161. settingTabs.add(_TabInfo(
  162. tab, 'General', Icons.settings_outlined, Icons.settings));
  163. break;
  164. case SettingsTabKey.safety:
  165. settingTabs.add(_TabInfo(tab, 'Security',
  166. Icons.enhanced_encryption_outlined, Icons.enhanced_encryption));
  167. break;
  168. case SettingsTabKey.network:
  169. settingTabs
  170. .add(_TabInfo(tab, 'Network', Icons.link_outlined, Icons.link));
  171. break;
  172. case SettingsTabKey.display:
  173. settingTabs.add(_TabInfo(tab, 'Display',
  174. Icons.desktop_windows_outlined, Icons.desktop_windows));
  175. break;
  176. case SettingsTabKey.plugin:
  177. settingTabs.add(_TabInfo(
  178. tab, 'Plugin', Icons.extension_outlined, Icons.extension));
  179. break;
  180. case SettingsTabKey.account:
  181. settingTabs.add(
  182. _TabInfo(tab, 'Account', Icons.person_outline, Icons.person));
  183. break;
  184. case SettingsTabKey.about:
  185. settingTabs
  186. .add(_TabInfo(tab, 'About', Icons.info_outline, Icons.info));
  187. break;
  188. }
  189. }
  190. return settingTabs;
  191. }
  192. List<Widget> _children() {
  193. final children = List<Widget>.empty(growable: true);
  194. for (final tab in DesktopSettingPage.tabKeys) {
  195. switch (tab) {
  196. case SettingsTabKey.general:
  197. children.add(const _General());
  198. break;
  199. case SettingsTabKey.safety:
  200. children.add(const _Safety());
  201. break;
  202. case SettingsTabKey.network:
  203. children.add(const _Network());
  204. break;
  205. case SettingsTabKey.display:
  206. children.add(const _Display());
  207. break;
  208. case SettingsTabKey.plugin:
  209. children.add(const _Plugin());
  210. break;
  211. case SettingsTabKey.account:
  212. children.add(const _Account());
  213. break;
  214. case SettingsTabKey.about:
  215. children.add(const _About());
  216. break;
  217. }
  218. }
  219. return children;
  220. }
  221. Widget _buildBlock({required List<Widget> children}) {
  222. // check both mouseMoveTime and videoConnCount
  223. return Obx(() {
  224. final videoConnBlock =
  225. _canBeBlocked.value && stateGlobal.videoConnCount > 0;
  226. return Stack(children: [
  227. buildRemoteBlock(
  228. block: _block,
  229. mask: false,
  230. use: canBeBlocked,
  231. child: preventMouseKeyBuilder(
  232. child: Row(children: children),
  233. block: videoConnBlock,
  234. ),
  235. ),
  236. if (videoConnBlock)
  237. Container(
  238. color: Colors.black.withOpacity(0.5),
  239. )
  240. ]);
  241. });
  242. }
  243. @override
  244. Widget build(BuildContext context) {
  245. super.build(context);
  246. return Scaffold(
  247. backgroundColor: Theme.of(context).colorScheme.background,
  248. body: _buildBlock(
  249. children: <Widget>[
  250. SizedBox(
  251. width: _kTabWidth,
  252. child: Column(
  253. children: [
  254. _header(context),
  255. Flexible(child: _listView(tabs: _settingTabs())),
  256. ],
  257. ),
  258. ),
  259. const VerticalDivider(width: 1),
  260. Expanded(
  261. child: Container(
  262. color: Theme.of(context).scaffoldBackgroundColor,
  263. child: PageView(
  264. controller: controller,
  265. physics: NeverScrollableScrollPhysics(),
  266. children: _children(),
  267. ),
  268. ),
  269. )
  270. ],
  271. ),
  272. );
  273. }
  274. Widget _header(BuildContext context) {
  275. final settingsText = Text(
  276. translate('Settings'),
  277. textAlign: TextAlign.left,
  278. style: const TextStyle(
  279. color: _accentColor,
  280. fontSize: _kTitleFontSize,
  281. fontWeight: FontWeight.w400,
  282. ),
  283. );
  284. return Row(
  285. children: [
  286. if (isWeb)
  287. IconButton(
  288. onPressed: () {
  289. if (Navigator.canPop(context)) {
  290. Navigator.pop(context);
  291. }
  292. },
  293. icon: Icon(Icons.arrow_back),
  294. ).marginOnly(left: 5),
  295. if (isWeb)
  296. SizedBox(
  297. height: 62,
  298. child: Align(
  299. alignment: Alignment.center,
  300. child: settingsText,
  301. ),
  302. ).marginOnly(left: 20),
  303. if (!isWeb)
  304. SizedBox(
  305. height: 62,
  306. child: settingsText,
  307. ).marginOnly(left: 20, top: 10),
  308. const Spacer(),
  309. ],
  310. );
  311. }
  312. Widget _listView({required List<_TabInfo> tabs}) {
  313. final scrollController = ScrollController();
  314. return ListView(
  315. controller: scrollController,
  316. children: tabs.map((tab) => _listItem(tab: tab)).toList(),
  317. );
  318. }
  319. Widget _listItem({required _TabInfo tab}) {
  320. return Obx(() {
  321. bool selected = tab.key == selectedTab.value;
  322. return SizedBox(
  323. width: _kTabWidth,
  324. height: _kTabHeight,
  325. child: InkWell(
  326. onTap: () {
  327. if (selectedTab.value != tab.key) {
  328. int index = DesktopSettingPage.tabKeys.indexOf(tab.key);
  329. if (index == -1) {
  330. return;
  331. }
  332. controller.jumpToPage(index);
  333. }
  334. selectedTab.value = tab.key;
  335. },
  336. child: Row(children: [
  337. Container(
  338. width: 4,
  339. height: _kTabHeight * 0.7,
  340. color: selected ? _accentColor : null,
  341. ),
  342. Icon(
  343. selected ? tab.selected : tab.unselected,
  344. color: selected ? _accentColor : null,
  345. size: 20,
  346. ).marginOnly(left: 13, right: 10),
  347. Text(
  348. translate(tab.label),
  349. style: TextStyle(
  350. color: selected ? _accentColor : null,
  351. fontWeight: FontWeight.w400,
  352. fontSize: _kContentFontSize),
  353. ),
  354. ]),
  355. ),
  356. );
  357. });
  358. }
  359. }
  360. //#region pages
  361. class _General extends StatefulWidget {
  362. const _General({Key? key}) : super(key: key);
  363. @override
  364. State<_General> createState() => _GeneralState();
  365. }
  366. class _GeneralState extends State<_General> {
  367. final RxBool serviceStop =
  368. isWeb ? RxBool(false) : Get.find<RxBool>(tag: 'stop-service');
  369. RxBool serviceBtnEnabled = true.obs;
  370. @override
  371. Widget build(BuildContext context) {
  372. final scrollController = ScrollController();
  373. return ListView(
  374. controller: scrollController,
  375. children: [
  376. if (!isWeb) service(),
  377. theme(),
  378. _Card(title: 'Language', children: [language()]),
  379. if (!isWeb) hwcodec(),
  380. if (!isWeb) audio(context),
  381. if (!isWeb) record(context),
  382. if (!isWeb) WaylandCard(),
  383. other()
  384. ],
  385. ).marginOnly(bottom: _kListViewBottomMargin);
  386. }
  387. Widget theme() {
  388. final current = MyTheme.getThemeModePreference().toShortString();
  389. onChanged(String value) async {
  390. await MyTheme.changeDarkMode(MyTheme.themeModeFromString(value));
  391. setState(() {});
  392. }
  393. final isOptFixed = isOptionFixed(kCommConfKeyTheme);
  394. return _Card(title: 'Theme', children: [
  395. _Radio<String>(context,
  396. value: 'light',
  397. groupValue: current,
  398. label: 'Light',
  399. onChanged: isOptFixed ? null : onChanged),
  400. _Radio<String>(context,
  401. value: 'dark',
  402. groupValue: current,
  403. label: 'Dark',
  404. onChanged: isOptFixed ? null : onChanged),
  405. _Radio<String>(context,
  406. value: 'system',
  407. groupValue: current,
  408. label: 'Follow System',
  409. onChanged: isOptFixed ? null : onChanged),
  410. ]);
  411. }
  412. Widget service() {
  413. if (bind.isOutgoingOnly()) {
  414. return const Offstage();
  415. }
  416. return _Card(title: 'Service', children: [
  417. Obx(() => _Button(serviceStop.value ? 'Start' : 'Stop', () {
  418. () async {
  419. serviceBtnEnabled.value = false;
  420. await start_service(serviceStop.value);
  421. // enable the button after 1 second
  422. Future.delayed(const Duration(seconds: 1), () {
  423. serviceBtnEnabled.value = true;
  424. });
  425. }();
  426. }, enabled: serviceBtnEnabled.value))
  427. ]);
  428. }
  429. Widget other() {
  430. final children = <Widget>[
  431. if (!isWeb && !bind.isIncomingOnly())
  432. _OptionCheckBox(context, 'Confirm before closing multiple tabs',
  433. kOptionEnableConfirmClosingTabs,
  434. isServer: false),
  435. _OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr),
  436. if (!isWeb) wallpaper(),
  437. if (!isWeb && !bind.isIncomingOnly()) ...[
  438. _OptionCheckBox(
  439. context,
  440. 'Open connection in new tab',
  441. kOptionOpenNewConnInTabs,
  442. isServer: false,
  443. ),
  444. // though this is related to GUI, but opengl problem affects all users, so put in config rather than local
  445. if (isLinux)
  446. Tooltip(
  447. message: translate('software_render_tip'),
  448. child: _OptionCheckBox(
  449. context,
  450. "Always use software rendering",
  451. kOptionAllowAlwaysSoftwareRender,
  452. ),
  453. ),
  454. if (!isWeb)
  455. Tooltip(
  456. message: translate('texture_render_tip'),
  457. child: _OptionCheckBox(
  458. context,
  459. "Use texture rendering",
  460. kOptionTextureRender,
  461. optGetter: bind.mainGetUseTextureRender,
  462. optSetter: (k, v) async =>
  463. await bind.mainSetLocalOption(key: k, value: v ? 'Y' : 'N'),
  464. ),
  465. ),
  466. if (!isWeb && !bind.isCustomClient())
  467. _OptionCheckBox(
  468. context,
  469. 'Check for software update on startup',
  470. kOptionEnableCheckUpdate,
  471. isServer: false,
  472. ),
  473. if (isWindows && !bind.isOutgoingOnly())
  474. _OptionCheckBox(
  475. context,
  476. 'Capture screen using DirectX',
  477. kOptionDirectxCapture,
  478. )
  479. ],
  480. ];
  481. if (!isWeb && bind.mainShowOption(key: kOptionAllowLinuxHeadless)) {
  482. children.add(_OptionCheckBox(
  483. context, 'Allow linux headless', kOptionAllowLinuxHeadless));
  484. }
  485. return _Card(title: 'Other', children: children);
  486. }
  487. Widget wallpaper() {
  488. if (bind.isOutgoingOnly()) {
  489. return const Offstage();
  490. }
  491. return futureBuilder(future: () async {
  492. final support = await bind.mainSupportRemoveWallpaper();
  493. return support;
  494. }(), hasData: (data) {
  495. if (data is bool && data == true) {
  496. bool value = mainGetBoolOptionSync(kOptionAllowRemoveWallpaper);
  497. return Row(
  498. children: [
  499. Flexible(
  500. child: _OptionCheckBox(
  501. context,
  502. 'Remove wallpaper during incoming sessions',
  503. kOptionAllowRemoveWallpaper,
  504. update: (bool v) {
  505. setState(() {});
  506. },
  507. ),
  508. ),
  509. if (value)
  510. _CountDownButton(
  511. text: 'Test',
  512. second: 5,
  513. onPressed: () {
  514. bind.mainTestWallpaper(second: 5);
  515. },
  516. )
  517. ],
  518. );
  519. }
  520. return Offstage();
  521. });
  522. }
  523. Widget hwcodec() {
  524. final hwcodec = bind.mainHasHwcodec();
  525. final vram = bind.mainHasVram();
  526. return Offstage(
  527. offstage: !(hwcodec || vram),
  528. child: _Card(title: 'Hardware Codec', children: [
  529. _OptionCheckBox(
  530. context,
  531. 'Enable hardware codec',
  532. kOptionEnableHwcodec,
  533. update: (bool v) {
  534. if (v) {
  535. bind.mainCheckHwcodec();
  536. }
  537. },
  538. )
  539. ]),
  540. );
  541. }
  542. Widget audio(BuildContext context) {
  543. if (bind.isOutgoingOnly()) {
  544. return const Offstage();
  545. }
  546. builder(devices, currentDevice, setDevice) {
  547. final child = ComboBox(
  548. keys: devices,
  549. values: devices,
  550. initialKey: currentDevice,
  551. onChanged: (key) async {
  552. setDevice(key);
  553. setState(() {});
  554. },
  555. ).marginOnly(left: _kContentHMargin);
  556. return _Card(title: 'Audio Input Device', children: [child]);
  557. }
  558. return AudioInput(builder: builder, isCm: false, isVoiceCall: false);
  559. }
  560. Widget record(BuildContext context) {
  561. final showRootDir = isWindows && bind.mainIsInstalled();
  562. return futureBuilder(future: () async {
  563. String user_dir = bind.mainVideoSaveDirectory(root: false);
  564. String root_dir =
  565. showRootDir ? bind.mainVideoSaveDirectory(root: true) : '';
  566. bool user_dir_exists = await Directory(user_dir).exists();
  567. bool root_dir_exists =
  568. showRootDir ? await Directory(root_dir).exists() : false;
  569. return {
  570. 'user_dir': user_dir,
  571. 'root_dir': root_dir,
  572. 'user_dir_exists': user_dir_exists,
  573. 'root_dir_exists': root_dir_exists,
  574. };
  575. }(), hasData: (data) {
  576. Map<String, dynamic> map = data as Map<String, dynamic>;
  577. String user_dir = map['user_dir']!;
  578. String root_dir = map['root_dir']!;
  579. bool root_dir_exists = map['root_dir_exists']!;
  580. bool user_dir_exists = map['user_dir_exists']!;
  581. return _Card(title: 'Recording', children: [
  582. if (!bind.isOutgoingOnly())
  583. _OptionCheckBox(context, 'Automatically record incoming sessions',
  584. kOptionAllowAutoRecordIncoming),
  585. if (!bind.isIncomingOnly())
  586. _OptionCheckBox(context, 'Automatically record outgoing sessions',
  587. kOptionAllowAutoRecordOutgoing,
  588. isServer: false),
  589. if (showRootDir && !bind.isOutgoingOnly())
  590. Row(
  591. children: [
  592. Text(
  593. '${translate(bind.isIncomingOnly() ? "Directory" : "Incoming")}:'),
  594. Expanded(
  595. child: GestureDetector(
  596. onTap: root_dir_exists
  597. ? () => launchUrl(Uri.file(root_dir))
  598. : null,
  599. child: Text(
  600. root_dir,
  601. softWrap: true,
  602. style: root_dir_exists
  603. ? const TextStyle(
  604. decoration: TextDecoration.underline)
  605. : null,
  606. )).marginOnly(left: 10),
  607. ),
  608. ],
  609. ).marginOnly(left: _kContentHMargin),
  610. if (!(showRootDir && bind.isIncomingOnly()))
  611. Row(
  612. children: [
  613. Text(
  614. '${translate((showRootDir && !bind.isOutgoingOnly()) ? "Outgoing" : "Directory")}:'),
  615. Expanded(
  616. child: GestureDetector(
  617. onTap: user_dir_exists
  618. ? () => launchUrl(Uri.file(user_dir))
  619. : null,
  620. child: Text(
  621. user_dir,
  622. softWrap: true,
  623. style: user_dir_exists
  624. ? const TextStyle(
  625. decoration: TextDecoration.underline)
  626. : null,
  627. )).marginOnly(left: 10),
  628. ),
  629. ElevatedButton(
  630. onPressed: isOptionFixed(kOptionVideoSaveDirectory)
  631. ? null
  632. : () async {
  633. String? initialDirectory;
  634. if (await Directory.fromUri(
  635. Uri.directory(user_dir))
  636. .exists()) {
  637. initialDirectory = user_dir;
  638. }
  639. String? selectedDirectory =
  640. await FilePicker.platform.getDirectoryPath(
  641. initialDirectory: initialDirectory);
  642. if (selectedDirectory != null) {
  643. await bind.mainSetLocalOption(
  644. key: kOptionVideoSaveDirectory,
  645. value: selectedDirectory);
  646. setState(() {});
  647. }
  648. },
  649. child: Text(translate('Change')))
  650. .marginOnly(left: 5),
  651. ],
  652. ).marginOnly(left: _kContentHMargin),
  653. ]);
  654. });
  655. }
  656. Widget language() {
  657. return futureBuilder(future: () async {
  658. String langs = await bind.mainGetLangs();
  659. return {'langs': langs};
  660. }(), hasData: (res) {
  661. Map<String, String> data = res as Map<String, String>;
  662. List<dynamic> langsList = jsonDecode(data['langs']!);
  663. Map<String, String> langsMap = {for (var v in langsList) v[0]: v[1]};
  664. List<String> keys = langsMap.keys.toList();
  665. List<String> values = langsMap.values.toList();
  666. keys.insert(0, defaultOptionLang);
  667. values.insert(0, translate('Default'));
  668. String currentKey = bind.mainGetLocalOption(key: kCommConfKeyLang);
  669. if (!keys.contains(currentKey)) {
  670. currentKey = defaultOptionLang;
  671. }
  672. final isOptFixed = isOptionFixed(kCommConfKeyLang);
  673. return ComboBox(
  674. keys: keys,
  675. values: values,
  676. initialKey: currentKey,
  677. onChanged: (key) async {
  678. await bind.mainSetLocalOption(key: kCommConfKeyLang, value: key);
  679. if (isWeb) reloadCurrentWindow();
  680. if (!isWeb) reloadAllWindows();
  681. if (!isWeb) bind.mainChangeLanguage(lang: key);
  682. },
  683. enabled: !isOptFixed,
  684. ).marginOnly(left: _kContentHMargin);
  685. });
  686. }
  687. }
  688. enum _AccessMode {
  689. custom,
  690. full,
  691. view,
  692. }
  693. class _Safety extends StatefulWidget {
  694. const _Safety({Key? key}) : super(key: key);
  695. @override
  696. State<_Safety> createState() => _SafetyState();
  697. }
  698. class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
  699. @override
  700. bool get wantKeepAlive => true;
  701. bool locked = bind.mainIsInstalled();
  702. final scrollController = ScrollController();
  703. @override
  704. Widget build(BuildContext context) {
  705. super.build(context);
  706. return SingleChildScrollView(
  707. controller: scrollController,
  708. child: Column(
  709. children: [
  710. _lock(locked, 'Unlock Security Settings', () {
  711. locked = false;
  712. setState(() => {});
  713. }),
  714. preventMouseKeyBuilder(
  715. block: locked,
  716. child: Column(children: [
  717. permissions(context),
  718. password(context),
  719. _Card(title: '2FA', children: [tfa()]),
  720. _Card(title: 'ID', children: [changeId()]),
  721. more(context),
  722. ]),
  723. ),
  724. ],
  725. )).marginOnly(bottom: _kListViewBottomMargin);
  726. }
  727. Widget tfa() {
  728. bool enabled = !locked;
  729. // Simple temp wrapper for PR check
  730. tmpWrapper() {
  731. RxBool has2fa = bind.mainHasValid2FaSync().obs;
  732. RxBool hasBot = bind.mainHasValidBotSync().obs;
  733. update() async {
  734. has2fa.value = bind.mainHasValid2FaSync();
  735. setState(() {});
  736. }
  737. onChanged(bool? checked) async {
  738. if (checked == false) {
  739. CommonConfirmDialog(
  740. gFFI.dialogManager, translate('cancel-2fa-confirm-tip'), () {
  741. change2fa(callback: update);
  742. });
  743. } else {
  744. change2fa(callback: update);
  745. }
  746. }
  747. final tfa = GestureDetector(
  748. child: InkWell(
  749. child: Obx(() => Row(
  750. children: [
  751. Checkbox(
  752. value: has2fa.value,
  753. onChanged: enabled ? onChanged : null)
  754. .marginOnly(right: 5),
  755. Expanded(
  756. child: Text(
  757. translate('enable-2fa-title'),
  758. style:
  759. TextStyle(color: disabledTextColor(context, enabled)),
  760. ))
  761. ],
  762. )),
  763. ),
  764. onTap: () {
  765. onChanged(!has2fa.value);
  766. },
  767. ).marginOnly(left: _kCheckBoxLeftMargin);
  768. if (!has2fa.value) {
  769. return tfa;
  770. }
  771. updateBot() async {
  772. hasBot.value = bind.mainHasValidBotSync();
  773. setState(() {});
  774. }
  775. onChangedBot(bool? checked) async {
  776. if (checked == false) {
  777. CommonConfirmDialog(
  778. gFFI.dialogManager, translate('cancel-bot-confirm-tip'), () {
  779. changeBot(callback: updateBot);
  780. });
  781. } else {
  782. changeBot(callback: updateBot);
  783. }
  784. }
  785. final bot = GestureDetector(
  786. child: Tooltip(
  787. waitDuration: Duration(milliseconds: 300),
  788. message: translate("enable-bot-tip"),
  789. child: InkWell(
  790. child: Obx(() => Row(
  791. children: [
  792. Checkbox(
  793. value: hasBot.value,
  794. onChanged: enabled ? onChangedBot : null)
  795. .marginOnly(right: 5),
  796. Expanded(
  797. child: Text(
  798. translate('Telegram bot'),
  799. style: TextStyle(
  800. color: disabledTextColor(context, enabled)),
  801. ))
  802. ],
  803. ))),
  804. ),
  805. onTap: () {
  806. onChangedBot(!hasBot.value);
  807. },
  808. ).marginOnly(left: _kCheckBoxLeftMargin + 30);
  809. final trust = Row(
  810. children: [
  811. Flexible(
  812. child: Tooltip(
  813. waitDuration: Duration(milliseconds: 300),
  814. message: translate("enable-trusted-devices-tip"),
  815. child: _OptionCheckBox(context, "Enable trusted devices",
  816. kOptionEnableTrustedDevices,
  817. enabled: !locked, update: (v) {
  818. setState(() {});
  819. }),
  820. ),
  821. ),
  822. if (mainGetBoolOptionSync(kOptionEnableTrustedDevices))
  823. ElevatedButton(
  824. onPressed: locked
  825. ? null
  826. : () {
  827. manageTrustedDeviceDialog();
  828. },
  829. child: Text(translate('Manage trusted devices')))
  830. ],
  831. ).marginOnly(left: 30);
  832. return Column(
  833. children: [tfa, bot, trust],
  834. );
  835. }
  836. return tmpWrapper();
  837. }
  838. Widget changeId() {
  839. return ChangeNotifierProvider.value(
  840. value: gFFI.serverModel,
  841. child: Consumer<ServerModel>(builder: ((context, model, child) {
  842. return _Button('Change ID', changeIdDialog,
  843. enabled: !locked && model.connectStatus > 0);
  844. })));
  845. }
  846. Widget permissions(context) {
  847. bool enabled = !locked;
  848. // Simple temp wrapper for PR check
  849. tmpWrapper() {
  850. String accessMode = bind.mainGetOptionSync(key: kOptionAccessMode);
  851. _AccessMode mode;
  852. if (accessMode == 'full') {
  853. mode = _AccessMode.full;
  854. } else if (accessMode == 'view') {
  855. mode = _AccessMode.view;
  856. } else {
  857. mode = _AccessMode.custom;
  858. }
  859. String initialKey;
  860. bool? fakeValue;
  861. switch (mode) {
  862. case _AccessMode.custom:
  863. initialKey = '';
  864. fakeValue = null;
  865. break;
  866. case _AccessMode.full:
  867. initialKey = 'full';
  868. fakeValue = true;
  869. break;
  870. case _AccessMode.view:
  871. initialKey = 'view';
  872. fakeValue = false;
  873. break;
  874. }
  875. return _Card(title: 'Permissions', children: [
  876. ComboBox(
  877. keys: [
  878. defaultOptionAccessMode,
  879. 'full',
  880. 'view',
  881. ],
  882. values: [
  883. translate('Custom'),
  884. translate('Full Access'),
  885. translate('Screen Share'),
  886. ],
  887. enabled: enabled && !isOptionFixed(kOptionAccessMode),
  888. initialKey: initialKey,
  889. onChanged: (mode) async {
  890. await bind.mainSetOption(key: kOptionAccessMode, value: mode);
  891. setState(() {});
  892. }).marginOnly(left: _kContentHMargin),
  893. Column(
  894. children: [
  895. _OptionCheckBox(
  896. context, 'Enable keyboard/mouse', kOptionEnableKeyboard,
  897. enabled: enabled, fakeValue: fakeValue),
  898. _OptionCheckBox(context, 'Enable clipboard', kOptionEnableClipboard,
  899. enabled: enabled, fakeValue: fakeValue),
  900. _OptionCheckBox(
  901. context, 'Enable file transfer', kOptionEnableFileTransfer,
  902. enabled: enabled, fakeValue: fakeValue),
  903. _OptionCheckBox(context, 'Enable audio', kOptionEnableAudio,
  904. enabled: enabled, fakeValue: fakeValue),
  905. _OptionCheckBox(
  906. context, 'Enable TCP tunneling', kOptionEnableTunnel,
  907. enabled: enabled, fakeValue: fakeValue),
  908. _OptionCheckBox(
  909. context, 'Enable remote restart', kOptionEnableRemoteRestart,
  910. enabled: enabled, fakeValue: fakeValue),
  911. _OptionCheckBox(
  912. context, 'Enable recording session', kOptionEnableRecordSession,
  913. enabled: enabled, fakeValue: fakeValue),
  914. if (isWindows)
  915. _OptionCheckBox(context, 'Enable blocking user input',
  916. kOptionEnableBlockInput,
  917. enabled: enabled, fakeValue: fakeValue),
  918. _OptionCheckBox(context, 'Enable remote configuration modification',
  919. kOptionAllowRemoteConfigModification,
  920. enabled: enabled, fakeValue: fakeValue),
  921. ],
  922. ),
  923. ]);
  924. }
  925. return tmpWrapper();
  926. }
  927. Widget password(BuildContext context) {
  928. return ChangeNotifierProvider.value(
  929. value: gFFI.serverModel,
  930. child: Consumer<ServerModel>(builder: ((context, model, child) {
  931. List<String> passwordKeys = [
  932. kUseTemporaryPassword,
  933. kUsePermanentPassword,
  934. kUseBothPasswords,
  935. ];
  936. List<String> passwordValues = [
  937. translate('Use one-time password'),
  938. translate('Use permanent password'),
  939. translate('Use both passwords'),
  940. ];
  941. bool tmpEnabled = model.verificationMethod != kUsePermanentPassword;
  942. bool permEnabled = model.verificationMethod != kUseTemporaryPassword;
  943. String currentValue =
  944. passwordValues[passwordKeys.indexOf(model.verificationMethod)];
  945. List<Widget> radios = passwordValues
  946. .map((value) => _Radio<String>(
  947. context,
  948. value: value,
  949. groupValue: currentValue,
  950. label: value,
  951. onChanged: locked
  952. ? null
  953. : ((value) async {
  954. callback() async {
  955. await model.setVerificationMethod(
  956. passwordKeys[passwordValues.indexOf(value)]);
  957. await model.updatePasswordModel();
  958. }
  959. if (value ==
  960. passwordValues[passwordKeys
  961. .indexOf(kUsePermanentPassword)] &&
  962. (await bind.mainGetPermanentPassword())
  963. .isEmpty) {
  964. setPasswordDialog(notEmptyCallback: callback);
  965. } else {
  966. await callback();
  967. }
  968. }),
  969. ))
  970. .toList();
  971. var onChanged = tmpEnabled && !locked
  972. ? (value) {
  973. if (value != null) {
  974. () async {
  975. await model.setTemporaryPasswordLength(value.toString());
  976. await model.updatePasswordModel();
  977. }();
  978. }
  979. }
  980. : null;
  981. List<Widget> lengthRadios = ['6', '8', '10']
  982. .map((value) => GestureDetector(
  983. child: Row(
  984. children: [
  985. Radio(
  986. value: value,
  987. groupValue: model.temporaryPasswordLength,
  988. onChanged: onChanged),
  989. Text(
  990. value,
  991. style: TextStyle(
  992. color: disabledTextColor(
  993. context, onChanged != null)),
  994. ),
  995. ],
  996. ).paddingOnly(right: 10),
  997. onTap: () => onChanged?.call(value),
  998. ))
  999. .toList();
  1000. final modeKeys = <String>[
  1001. 'password',
  1002. 'click',
  1003. defaultOptionApproveMode
  1004. ];
  1005. final modeValues = [
  1006. translate('Accept sessions via password'),
  1007. translate('Accept sessions via click'),
  1008. translate('Accept sessions via both'),
  1009. ];
  1010. var modeInitialKey = model.approveMode;
  1011. if (!modeKeys.contains(modeInitialKey)) {
  1012. modeInitialKey = defaultOptionApproveMode;
  1013. }
  1014. final usePassword = model.approveMode != 'click';
  1015. final isApproveModeFixed = isOptionFixed(kOptionApproveMode);
  1016. return _Card(title: 'Password', children: [
  1017. ComboBox(
  1018. enabled: !locked && !isApproveModeFixed,
  1019. keys: modeKeys,
  1020. values: modeValues,
  1021. initialKey: modeInitialKey,
  1022. onChanged: (key) => model.setApproveMode(key),
  1023. ).marginOnly(left: _kContentHMargin),
  1024. if (usePassword) radios[0],
  1025. if (usePassword)
  1026. _SubLabeledWidget(
  1027. context,
  1028. 'One-time password length',
  1029. Row(
  1030. children: [
  1031. ...lengthRadios,
  1032. ],
  1033. ),
  1034. enabled: tmpEnabled && !locked),
  1035. if (usePassword) radios[1],
  1036. if (usePassword)
  1037. _SubButton('Set permanent password', setPasswordDialog,
  1038. permEnabled && !locked),
  1039. // if (usePassword)
  1040. // hide_cm(!locked).marginOnly(left: _kContentHSubMargin - 6),
  1041. if (usePassword) radios[2],
  1042. ]);
  1043. })));
  1044. }
  1045. Widget more(BuildContext context) {
  1046. bool enabled = !locked;
  1047. return _Card(title: 'Security', children: [
  1048. shareRdp(context, enabled),
  1049. _OptionCheckBox(context, 'Deny LAN discovery', 'enable-lan-discovery',
  1050. reverse: true, enabled: enabled),
  1051. ...directIp(context),
  1052. whitelist(),
  1053. ...autoDisconnect(context),
  1054. if (bind.mainIsInstalled())
  1055. _OptionCheckBox(context, 'allow-only-conn-window-open-tip',
  1056. 'allow-only-conn-window-open',
  1057. reverse: false, enabled: enabled),
  1058. if (bind.mainIsInstalled()) unlockPin()
  1059. ]);
  1060. }
  1061. shareRdp(BuildContext context, bool enabled) {
  1062. onChanged(bool b) async {
  1063. await bind.mainSetShareRdp(enable: b);
  1064. setState(() {});
  1065. }
  1066. bool value = bind.mainIsShareRdp();
  1067. return Offstage(
  1068. offstage: !(isWindows && bind.mainIsInstalled()),
  1069. child: GestureDetector(
  1070. child: Row(
  1071. children: [
  1072. Checkbox(
  1073. value: value,
  1074. onChanged: enabled ? (_) => onChanged(!value) : null)
  1075. .marginOnly(right: 5),
  1076. Expanded(
  1077. child: Text(translate('Enable RDP session sharing'),
  1078. style:
  1079. TextStyle(color: disabledTextColor(context, enabled))),
  1080. )
  1081. ],
  1082. ).marginOnly(left: _kCheckBoxLeftMargin),
  1083. onTap: enabled ? () => onChanged(!value) : null),
  1084. );
  1085. }
  1086. List<Widget> directIp(BuildContext context) {
  1087. TextEditingController controller = TextEditingController();
  1088. update(bool v) => setState(() {});
  1089. RxBool applyEnabled = false.obs;
  1090. return [
  1091. _OptionCheckBox(context, 'Enable direct IP access', kOptionDirectServer,
  1092. update: update, enabled: !locked),
  1093. () {
  1094. // Simple temp wrapper for PR check
  1095. tmpWrapper() {
  1096. bool enabled = option2bool(kOptionDirectServer,
  1097. bind.mainGetOptionSync(key: kOptionDirectServer));
  1098. if (!enabled) applyEnabled.value = false;
  1099. controller.text =
  1100. bind.mainGetOptionSync(key: kOptionDirectAccessPort);
  1101. final isOptFixed = isOptionFixed(kOptionDirectAccessPort);
  1102. return Offstage(
  1103. offstage: !enabled,
  1104. child: _SubLabeledWidget(
  1105. context,
  1106. 'Port',
  1107. Row(children: [
  1108. SizedBox(
  1109. width: 95,
  1110. child: TextField(
  1111. controller: controller,
  1112. enabled: enabled && !locked && !isOptFixed,
  1113. onChanged: (_) => applyEnabled.value = true,
  1114. inputFormatters: [
  1115. FilteringTextInputFormatter.allow(RegExp(
  1116. r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')),
  1117. ],
  1118. decoration: const InputDecoration(
  1119. hintText: '21118',
  1120. contentPadding:
  1121. EdgeInsets.symmetric(vertical: 12, horizontal: 12),
  1122. ),
  1123. ).workaroundFreezeLinuxMint().marginOnly(right: 15),
  1124. ),
  1125. Obx(() => ElevatedButton(
  1126. onPressed: applyEnabled.value &&
  1127. enabled &&
  1128. !locked &&
  1129. !isOptFixed
  1130. ? () async {
  1131. applyEnabled.value = false;
  1132. await bind.mainSetOption(
  1133. key: kOptionDirectAccessPort,
  1134. value: controller.text);
  1135. }
  1136. : null,
  1137. child: Text(
  1138. translate('Apply'),
  1139. ),
  1140. ))
  1141. ]),
  1142. enabled: enabled && !locked && !isOptFixed,
  1143. ),
  1144. );
  1145. }
  1146. return tmpWrapper();
  1147. }(),
  1148. ];
  1149. }
  1150. Widget whitelist() {
  1151. bool enabled = !locked;
  1152. // Simple temp wrapper for PR check
  1153. tmpWrapper() {
  1154. RxBool hasWhitelist = whitelistNotEmpty().obs;
  1155. update() async {
  1156. hasWhitelist.value = whitelistNotEmpty();
  1157. }
  1158. onChanged(bool? checked) async {
  1159. changeWhiteList(callback: update);
  1160. }
  1161. final isOptFixed = isOptionFixed(kOptionWhitelist);
  1162. return GestureDetector(
  1163. child: Tooltip(
  1164. message: translate('whitelist_tip'),
  1165. child: Obx(() => Row(
  1166. children: [
  1167. Checkbox(
  1168. value: hasWhitelist.value,
  1169. onChanged: enabled && !isOptFixed ? onChanged : null)
  1170. .marginOnly(right: 5),
  1171. Offstage(
  1172. offstage: !hasWhitelist.value,
  1173. child: MouseRegion(
  1174. child: const Icon(Icons.warning_amber_rounded,
  1175. color: Color.fromARGB(255, 255, 204, 0))
  1176. .marginOnly(right: 5),
  1177. cursor: SystemMouseCursors.click,
  1178. ),
  1179. ),
  1180. Expanded(
  1181. child: Text(
  1182. translate('Use IP Whitelisting'),
  1183. style:
  1184. TextStyle(color: disabledTextColor(context, enabled)),
  1185. ))
  1186. ],
  1187. )),
  1188. ),
  1189. onTap: enabled
  1190. ? () {
  1191. onChanged(!hasWhitelist.value);
  1192. }
  1193. : null,
  1194. ).marginOnly(left: _kCheckBoxLeftMargin);
  1195. }
  1196. return tmpWrapper();
  1197. }
  1198. Widget hide_cm(bool enabled) {
  1199. return ChangeNotifierProvider.value(
  1200. value: gFFI.serverModel,
  1201. child: Consumer<ServerModel>(builder: (context, model, child) {
  1202. final enableHideCm = model.approveMode == 'password' &&
  1203. model.verificationMethod == kUsePermanentPassword;
  1204. onHideCmChanged(bool? b) {
  1205. if (b != null) {
  1206. bind.mainSetOption(
  1207. key: 'allow-hide-cm', value: bool2option('allow-hide-cm', b));
  1208. }
  1209. }
  1210. return Tooltip(
  1211. message: enableHideCm ? "" : translate('hide_cm_tip'),
  1212. child: GestureDetector(
  1213. onTap:
  1214. enableHideCm ? () => onHideCmChanged(!model.hideCm) : null,
  1215. child: Row(
  1216. children: [
  1217. Checkbox(
  1218. value: model.hideCm,
  1219. onChanged: enabled && enableHideCm
  1220. ? onHideCmChanged
  1221. : null)
  1222. .marginOnly(right: 5),
  1223. Expanded(
  1224. child: Text(
  1225. translate('Hide connection management window'),
  1226. style: TextStyle(
  1227. color: disabledTextColor(
  1228. context, enabled && enableHideCm)),
  1229. ),
  1230. ),
  1231. ],
  1232. ),
  1233. ));
  1234. }));
  1235. }
  1236. List<Widget> autoDisconnect(BuildContext context) {
  1237. TextEditingController controller = TextEditingController();
  1238. update(bool v) => setState(() {});
  1239. RxBool applyEnabled = false.obs;
  1240. return [
  1241. _OptionCheckBox(
  1242. context, 'auto_disconnect_option_tip', kOptionAllowAutoDisconnect,
  1243. update: update, enabled: !locked),
  1244. () {
  1245. bool enabled = option2bool(kOptionAllowAutoDisconnect,
  1246. bind.mainGetOptionSync(key: kOptionAllowAutoDisconnect));
  1247. if (!enabled) applyEnabled.value = false;
  1248. controller.text =
  1249. bind.mainGetOptionSync(key: kOptionAutoDisconnectTimeout);
  1250. final isOptFixed = isOptionFixed(kOptionAutoDisconnectTimeout);
  1251. return Offstage(
  1252. offstage: !enabled,
  1253. child: _SubLabeledWidget(
  1254. context,
  1255. 'Timeout in minutes',
  1256. Row(children: [
  1257. SizedBox(
  1258. width: 95,
  1259. child: TextField(
  1260. controller: controller,
  1261. enabled: enabled && !locked && !isOptFixed,
  1262. onChanged: (_) => applyEnabled.value = true,
  1263. inputFormatters: [
  1264. FilteringTextInputFormatter.allow(RegExp(
  1265. r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')),
  1266. ],
  1267. decoration: const InputDecoration(
  1268. hintText: '10',
  1269. contentPadding:
  1270. EdgeInsets.symmetric(vertical: 12, horizontal: 12),
  1271. ),
  1272. ).workaroundFreezeLinuxMint().marginOnly(right: 15),
  1273. ),
  1274. Obx(() => ElevatedButton(
  1275. onPressed:
  1276. applyEnabled.value && enabled && !locked && !isOptFixed
  1277. ? () async {
  1278. applyEnabled.value = false;
  1279. await bind.mainSetOption(
  1280. key: kOptionAutoDisconnectTimeout,
  1281. value: controller.text);
  1282. }
  1283. : null,
  1284. child: Text(
  1285. translate('Apply'),
  1286. ),
  1287. ))
  1288. ]),
  1289. enabled: enabled && !locked && !isOptFixed,
  1290. ),
  1291. );
  1292. }(),
  1293. ];
  1294. }
  1295. Widget unlockPin() {
  1296. bool enabled = !locked;
  1297. RxString unlockPin = bind.mainGetUnlockPin().obs;
  1298. update() async {
  1299. unlockPin.value = bind.mainGetUnlockPin();
  1300. }
  1301. onChanged(bool? checked) async {
  1302. changeUnlockPinDialog(unlockPin.value, update);
  1303. }
  1304. final isOptFixed = isOptionFixed(kOptionWhitelist);
  1305. return GestureDetector(
  1306. child: Obx(() => Row(
  1307. children: [
  1308. Checkbox(
  1309. value: unlockPin.isNotEmpty,
  1310. onChanged: enabled && !isOptFixed ? onChanged : null)
  1311. .marginOnly(right: 5),
  1312. Expanded(
  1313. child: Text(
  1314. translate('Unlock with PIN'),
  1315. style: TextStyle(color: disabledTextColor(context, enabled)),
  1316. ))
  1317. ],
  1318. )),
  1319. onTap: enabled
  1320. ? () {
  1321. onChanged(!unlockPin.isNotEmpty);
  1322. }
  1323. : null,
  1324. ).marginOnly(left: _kCheckBoxLeftMargin);
  1325. }
  1326. }
  1327. class _Network extends StatefulWidget {
  1328. const _Network({Key? key}) : super(key: key);
  1329. @override
  1330. State<_Network> createState() => _NetworkState();
  1331. }
  1332. class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
  1333. @override
  1334. bool get wantKeepAlive => true;
  1335. bool locked = !isWeb && bind.mainIsInstalled();
  1336. final scrollController = ScrollController();
  1337. @override
  1338. Widget build(BuildContext context) {
  1339. super.build(context);
  1340. return ListView(controller: scrollController, children: [
  1341. _lock(locked, 'Unlock Network Settings', () {
  1342. locked = false;
  1343. setState(() => {});
  1344. }),
  1345. preventMouseKeyBuilder(
  1346. block: locked,
  1347. child: Column(children: [
  1348. network(context),
  1349. ]),
  1350. ),
  1351. ]).marginOnly(bottom: _kListViewBottomMargin);
  1352. }
  1353. Widget network(BuildContext context) {
  1354. final hideServer =
  1355. bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y';
  1356. final hideProxy =
  1357. isWeb || bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y';
  1358. if (hideServer && hideProxy) {
  1359. return Offstage();
  1360. }
  1361. return _Card(
  1362. title: 'Network',
  1363. children: [
  1364. Container(
  1365. child: Column(
  1366. crossAxisAlignment: CrossAxisAlignment.start,
  1367. children: [
  1368. if (!hideServer)
  1369. ListTile(
  1370. leading: Icon(Icons.dns_outlined, color: _accentColor),
  1371. title: Text(
  1372. translate('ID/Relay Server'),
  1373. style: TextStyle(fontSize: _kContentFontSize),
  1374. ),
  1375. enabled: !locked,
  1376. onTap: () => showServerSettings(gFFI.dialogManager),
  1377. shape: RoundedRectangleBorder(
  1378. borderRadius: BorderRadius.circular(10),
  1379. ),
  1380. contentPadding: EdgeInsets.symmetric(horizontal: 16),
  1381. minLeadingWidth: 0,
  1382. horizontalTitleGap: 10,
  1383. ),
  1384. if (!hideServer && !hideProxy)
  1385. Divider(height: 1, indent: 16, endIndent: 16),
  1386. if (!hideProxy)
  1387. ListTile(
  1388. leading:
  1389. Icon(Icons.network_ping_outlined, color: _accentColor),
  1390. title: Text(
  1391. translate('Socks5/Http(s) Proxy'),
  1392. style: TextStyle(fontSize: _kContentFontSize),
  1393. ),
  1394. enabled: !locked,
  1395. onTap: changeSocks5Proxy,
  1396. shape: RoundedRectangleBorder(
  1397. borderRadius: BorderRadius.circular(10),
  1398. ),
  1399. contentPadding: EdgeInsets.symmetric(horizontal: 16),
  1400. minLeadingWidth: 0,
  1401. horizontalTitleGap: 10,
  1402. ),
  1403. ],
  1404. ),
  1405. ),
  1406. ],
  1407. );
  1408. }
  1409. }
  1410. class _Display extends StatefulWidget {
  1411. const _Display({Key? key}) : super(key: key);
  1412. @override
  1413. State<_Display> createState() => _DisplayState();
  1414. }
  1415. class _DisplayState extends State<_Display> {
  1416. @override
  1417. Widget build(BuildContext context) {
  1418. final scrollController = ScrollController();
  1419. return ListView(controller: scrollController, children: [
  1420. viewStyle(context),
  1421. scrollStyle(context),
  1422. imageQuality(context),
  1423. codec(context),
  1424. if (!isWeb) privacyModeImpl(context),
  1425. other(context),
  1426. ]).marginOnly(bottom: _kListViewBottomMargin);
  1427. }
  1428. Widget viewStyle(BuildContext context) {
  1429. final isOptFixed = isOptionFixed(kOptionViewStyle);
  1430. onChanged(String value) async {
  1431. await bind.mainSetUserDefaultOption(key: kOptionViewStyle, value: value);
  1432. setState(() {});
  1433. }
  1434. final groupValue = bind.mainGetUserDefaultOption(key: kOptionViewStyle);
  1435. return _Card(title: 'Default View Style', children: [
  1436. _Radio(context,
  1437. value: kRemoteViewStyleOriginal,
  1438. groupValue: groupValue,
  1439. label: 'Scale original',
  1440. onChanged: isOptFixed ? null : onChanged),
  1441. _Radio(context,
  1442. value: kRemoteViewStyleAdaptive,
  1443. groupValue: groupValue,
  1444. label: 'Scale adaptive',
  1445. onChanged: isOptFixed ? null : onChanged),
  1446. ]);
  1447. }
  1448. Widget scrollStyle(BuildContext context) {
  1449. final isOptFixed = isOptionFixed(kOptionScrollStyle);
  1450. onChanged(String value) async {
  1451. await bind.mainSetUserDefaultOption(
  1452. key: kOptionScrollStyle, value: value);
  1453. setState(() {});
  1454. }
  1455. final groupValue = bind.mainGetUserDefaultOption(key: kOptionScrollStyle);
  1456. return _Card(title: 'Default Scroll Style', children: [
  1457. _Radio(context,
  1458. value: kRemoteScrollStyleAuto,
  1459. groupValue: groupValue,
  1460. label: 'ScrollAuto',
  1461. onChanged: isOptFixed ? null : onChanged),
  1462. _Radio(context,
  1463. value: kRemoteScrollStyleBar,
  1464. groupValue: groupValue,
  1465. label: 'Scrollbar',
  1466. onChanged: isOptFixed ? null : onChanged),
  1467. ]);
  1468. }
  1469. Widget imageQuality(BuildContext context) {
  1470. onChanged(String value) async {
  1471. await bind.mainSetUserDefaultOption(
  1472. key: kOptionImageQuality, value: value);
  1473. setState(() {});
  1474. }
  1475. final isOptFixed = isOptionFixed(kOptionImageQuality);
  1476. final groupValue = bind.mainGetUserDefaultOption(key: kOptionImageQuality);
  1477. return _Card(title: 'Default Image Quality', children: [
  1478. _Radio(context,
  1479. value: kRemoteImageQualityBest,
  1480. groupValue: groupValue,
  1481. label: 'Good image quality',
  1482. onChanged: isOptFixed ? null : onChanged),
  1483. _Radio(context,
  1484. value: kRemoteImageQualityBalanced,
  1485. groupValue: groupValue,
  1486. label: 'Balanced',
  1487. onChanged: isOptFixed ? null : onChanged),
  1488. _Radio(context,
  1489. value: kRemoteImageQualityLow,
  1490. groupValue: groupValue,
  1491. label: 'Optimize reaction time',
  1492. onChanged: isOptFixed ? null : onChanged),
  1493. _Radio(context,
  1494. value: kRemoteImageQualityCustom,
  1495. groupValue: groupValue,
  1496. label: 'Custom',
  1497. onChanged: isOptFixed ? null : onChanged),
  1498. Offstage(
  1499. offstage: groupValue != kRemoteImageQualityCustom,
  1500. child: customImageQualitySetting(),
  1501. )
  1502. ]);
  1503. }
  1504. Widget codec(BuildContext context) {
  1505. onChanged(String value) async {
  1506. await bind.mainSetUserDefaultOption(
  1507. key: kOptionCodecPreference, value: value);
  1508. setState(() {});
  1509. }
  1510. final groupValue =
  1511. bind.mainGetUserDefaultOption(key: kOptionCodecPreference);
  1512. var hwRadios = [];
  1513. final isOptFixed = isOptionFixed(kOptionCodecPreference);
  1514. try {
  1515. final Map codecsJson = jsonDecode(bind.mainSupportedHwdecodings());
  1516. final h264 = codecsJson['h264'] ?? false;
  1517. final h265 = codecsJson['h265'] ?? false;
  1518. if (h264) {
  1519. hwRadios.add(_Radio(context,
  1520. value: 'h264',
  1521. groupValue: groupValue,
  1522. label: 'H264',
  1523. onChanged: isOptFixed ? null : onChanged));
  1524. }
  1525. if (h265) {
  1526. hwRadios.add(_Radio(context,
  1527. value: 'h265',
  1528. groupValue: groupValue,
  1529. label: 'H265',
  1530. onChanged: isOptFixed ? null : onChanged));
  1531. }
  1532. } catch (e) {
  1533. debugPrint("failed to parse supported hwdecodings, err=$e");
  1534. }
  1535. return _Card(title: 'Default Codec', children: [
  1536. _Radio(context,
  1537. value: 'auto',
  1538. groupValue: groupValue,
  1539. label: 'Auto',
  1540. onChanged: isOptFixed ? null : onChanged),
  1541. _Radio(context,
  1542. value: 'vp8',
  1543. groupValue: groupValue,
  1544. label: 'VP8',
  1545. onChanged: isOptFixed ? null : onChanged),
  1546. _Radio(context,
  1547. value: 'vp9',
  1548. groupValue: groupValue,
  1549. label: 'VP9',
  1550. onChanged: isOptFixed ? null : onChanged),
  1551. _Radio(context,
  1552. value: 'av1',
  1553. groupValue: groupValue,
  1554. label: 'AV1',
  1555. onChanged: isOptFixed ? null : onChanged),
  1556. ...hwRadios,
  1557. ]);
  1558. }
  1559. Widget privacyModeImpl(BuildContext context) {
  1560. final supportedPrivacyModeImpls = bind.mainSupportedPrivacyModeImpls();
  1561. late final List<dynamic> privacyModeImpls;
  1562. try {
  1563. privacyModeImpls = jsonDecode(supportedPrivacyModeImpls);
  1564. } catch (e) {
  1565. debugPrint('failed to parse supported privacy mode impls, err=$e');
  1566. return Offstage();
  1567. }
  1568. if (privacyModeImpls.length < 2) {
  1569. return Offstage();
  1570. }
  1571. final key = 'privacy-mode-impl-key';
  1572. onChanged(String value) async {
  1573. await bind.mainSetOption(key: key, value: value);
  1574. setState(() {});
  1575. }
  1576. String groupValue = bind.mainGetOptionSync(key: key);
  1577. if (groupValue.isEmpty) {
  1578. groupValue = bind.mainDefaultPrivacyModeImpl();
  1579. }
  1580. return _Card(
  1581. title: 'Privacy mode',
  1582. children: privacyModeImpls.map((impl) {
  1583. final d = impl as List<dynamic>;
  1584. return _Radio(context,
  1585. value: d[0] as String,
  1586. groupValue: groupValue,
  1587. label: d[1] as String,
  1588. onChanged: onChanged);
  1589. }).toList(),
  1590. );
  1591. }
  1592. Widget otherRow(String label, String key) {
  1593. final value = bind.mainGetUserDefaultOption(key: key) == 'Y';
  1594. final isOptFixed = isOptionFixed(key);
  1595. onChanged(bool b) async {
  1596. await bind.mainSetUserDefaultOption(
  1597. key: key,
  1598. value: b
  1599. ? 'Y'
  1600. : (key == kOptionEnableFileCopyPaste ? 'N' : defaultOptionNo));
  1601. setState(() {});
  1602. }
  1603. return GestureDetector(
  1604. child: Row(
  1605. children: [
  1606. Checkbox(
  1607. value: value,
  1608. onChanged: isOptFixed ? null : (_) => onChanged(!value))
  1609. .marginOnly(right: 5),
  1610. Expanded(
  1611. child: Text(translate(label)),
  1612. )
  1613. ],
  1614. ).marginOnly(left: _kCheckBoxLeftMargin),
  1615. onTap: isOptFixed ? null : () => onChanged(!value));
  1616. }
  1617. Widget other(BuildContext context) {
  1618. final children =
  1619. otherDefaultSettings().map((e) => otherRow(e.$1, e.$2)).toList();
  1620. return _Card(title: 'Other Default Options', children: children);
  1621. }
  1622. }
  1623. class _Account extends StatefulWidget {
  1624. const _Account({Key? key}) : super(key: key);
  1625. @override
  1626. State<_Account> createState() => _AccountState();
  1627. }
  1628. class _AccountState extends State<_Account> {
  1629. @override
  1630. Widget build(BuildContext context) {
  1631. final scrollController = ScrollController();
  1632. return ListView(
  1633. controller: scrollController,
  1634. children: [
  1635. _Card(title: 'Account', children: [accountAction(), useInfo()]),
  1636. ],
  1637. ).marginOnly(bottom: _kListViewBottomMargin);
  1638. }
  1639. Widget accountAction() {
  1640. return Obx(() => _Button(
  1641. gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout',
  1642. () => {
  1643. gFFI.userModel.userName.value.isEmpty
  1644. ? loginDialog()
  1645. : logOutConfirmDialog()
  1646. }));
  1647. }
  1648. Widget useInfo() {
  1649. text(String key, String value) {
  1650. return Align(
  1651. alignment: Alignment.centerLeft,
  1652. child: SelectionArea(child: Text('${translate(key)}: $value'))
  1653. .marginSymmetric(vertical: 4),
  1654. );
  1655. }
  1656. return Obx(() => Offstage(
  1657. offstage: gFFI.userModel.userName.value.isEmpty,
  1658. child: Column(
  1659. children: [
  1660. text('Username', gFFI.userModel.userName.value),
  1661. // text('Group', gFFI.groupModel.groupName.value),
  1662. ],
  1663. ),
  1664. )).marginOnly(left: 18, top: 16);
  1665. }
  1666. }
  1667. class _Checkbox extends StatefulWidget {
  1668. final String label;
  1669. final bool Function() getValue;
  1670. final Future<void> Function(bool) setValue;
  1671. const _Checkbox(
  1672. {Key? key,
  1673. required this.label,
  1674. required this.getValue,
  1675. required this.setValue})
  1676. : super(key: key);
  1677. @override
  1678. State<_Checkbox> createState() => _CheckboxState();
  1679. }
  1680. class _CheckboxState extends State<_Checkbox> {
  1681. var value = false;
  1682. @override
  1683. initState() {
  1684. super.initState();
  1685. value = widget.getValue();
  1686. }
  1687. @override
  1688. Widget build(BuildContext context) {
  1689. onChanged(bool b) async {
  1690. await widget.setValue(b);
  1691. setState(() {
  1692. value = widget.getValue();
  1693. });
  1694. }
  1695. return GestureDetector(
  1696. child: Row(
  1697. children: [
  1698. Checkbox(
  1699. value: value,
  1700. onChanged: (_) => onChanged(!value),
  1701. ).marginOnly(right: 5),
  1702. Expanded(
  1703. child: Text(translate(widget.label)),
  1704. )
  1705. ],
  1706. ).marginOnly(left: _kCheckBoxLeftMargin),
  1707. onTap: () => onChanged(!value),
  1708. );
  1709. }
  1710. }
  1711. class _Plugin extends StatefulWidget {
  1712. const _Plugin({Key? key}) : super(key: key);
  1713. @override
  1714. State<_Plugin> createState() => _PluginState();
  1715. }
  1716. class _PluginState extends State<_Plugin> {
  1717. @override
  1718. Widget build(BuildContext context) {
  1719. bind.pluginListReload();
  1720. final scrollController = ScrollController();
  1721. return ChangeNotifierProvider.value(
  1722. value: pluginManager,
  1723. child: Consumer<PluginManager>(builder: (context, model, child) {
  1724. return ListView(
  1725. controller: scrollController,
  1726. children: model.plugins.map((entry) => pluginCard(entry)).toList(),
  1727. ).marginOnly(bottom: _kListViewBottomMargin);
  1728. }),
  1729. );
  1730. }
  1731. Widget pluginCard(PluginInfo plugin) {
  1732. return ChangeNotifierProvider.value(
  1733. value: plugin,
  1734. child: Consumer<PluginInfo>(
  1735. builder: (context, model, child) => DesktopSettingsCard(plugin: model),
  1736. ),
  1737. );
  1738. }
  1739. Widget accountAction() {
  1740. return Obx(() => _Button(
  1741. gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout',
  1742. () => {
  1743. gFFI.userModel.userName.value.isEmpty
  1744. ? loginDialog()
  1745. : logOutConfirmDialog()
  1746. }));
  1747. }
  1748. }
  1749. class _About extends StatefulWidget {
  1750. const _About({Key? key}) : super(key: key);
  1751. @override
  1752. State<_About> createState() => _AboutState();
  1753. }
  1754. class _AboutState extends State<_About> {
  1755. @override
  1756. Widget build(BuildContext context) {
  1757. return futureBuilder(future: () async {
  1758. final license = await bind.mainGetLicense();
  1759. final version = await bind.mainGetVersion();
  1760. final buildDate = await bind.mainGetBuildDate();
  1761. final fingerprint = await bind.mainGetFingerprint();
  1762. return {
  1763. 'license': license,
  1764. 'version': version,
  1765. 'buildDate': buildDate,
  1766. 'fingerprint': fingerprint
  1767. };
  1768. }(), hasData: (data) {
  1769. final license = data['license'].toString();
  1770. final version = data['version'].toString();
  1771. final buildDate = data['buildDate'].toString();
  1772. final fingerprint = data['fingerprint'].toString();
  1773. const linkStyle = TextStyle(decoration: TextDecoration.underline);
  1774. final scrollController = ScrollController();
  1775. return SingleChildScrollView(
  1776. controller: scrollController,
  1777. child: _Card(title: translate('About RustDesk'), children: [
  1778. Column(
  1779. crossAxisAlignment: CrossAxisAlignment.start,
  1780. children: [
  1781. const SizedBox(
  1782. height: 8.0,
  1783. ),
  1784. SelectionArea(
  1785. child: Text('${translate('Version')}: $version')
  1786. .marginSymmetric(vertical: 4.0)),
  1787. SelectionArea(
  1788. child: Text('${translate('Build Date')}: $buildDate')
  1789. .marginSymmetric(vertical: 4.0)),
  1790. if (!isWeb)
  1791. SelectionArea(
  1792. child: Text('${translate('Fingerprint')}: $fingerprint')
  1793. .marginSymmetric(vertical: 4.0)),
  1794. InkWell(
  1795. onTap: () {
  1796. launchUrlString('https://rustdesk.com/privacy.html');
  1797. },
  1798. child: Text(
  1799. translate('Privacy Statement'),
  1800. style: linkStyle,
  1801. ).marginSymmetric(vertical: 4.0)),
  1802. InkWell(
  1803. onTap: () {
  1804. launchUrlString('https://rustdesk.com');
  1805. },
  1806. child: Text(
  1807. translate('Website'),
  1808. style: linkStyle,
  1809. ).marginSymmetric(vertical: 4.0)),
  1810. Container(
  1811. decoration: const BoxDecoration(color: Color(0xFF2c8cff)),
  1812. padding:
  1813. const EdgeInsets.symmetric(vertical: 24, horizontal: 8),
  1814. child: SelectionArea(
  1815. child: Row(
  1816. children: [
  1817. Expanded(
  1818. child: Column(
  1819. crossAxisAlignment: CrossAxisAlignment.start,
  1820. children: [
  1821. Text(
  1822. 'Copyright © ${DateTime.now().toString().substring(0, 4)} Purslane Ltd.\n$license',
  1823. style: const TextStyle(color: Colors.white),
  1824. ),
  1825. Text(
  1826. translate('Slogan_tip'),
  1827. style: TextStyle(
  1828. fontWeight: FontWeight.w800,
  1829. color: Colors.white),
  1830. )
  1831. ],
  1832. ),
  1833. ),
  1834. ],
  1835. )),
  1836. ).marginSymmetric(vertical: 4.0)
  1837. ],
  1838. ).marginOnly(left: _kContentHMargin)
  1839. ]),
  1840. );
  1841. });
  1842. }
  1843. }
  1844. //#endregion
  1845. //#region components
  1846. // ignore: non_constant_identifier_names
  1847. Widget _Card(
  1848. {required String title,
  1849. required List<Widget> children,
  1850. List<Widget>? title_suffix}) {
  1851. return Row(
  1852. children: [
  1853. Flexible(
  1854. child: SizedBox(
  1855. width: _kCardFixedWidth,
  1856. child: Card(
  1857. child: Column(
  1858. children: [
  1859. Row(
  1860. children: [
  1861. Expanded(
  1862. child: Text(
  1863. translate(title),
  1864. textAlign: TextAlign.start,
  1865. style: const TextStyle(
  1866. fontSize: _kTitleFontSize,
  1867. ),
  1868. )),
  1869. ...?title_suffix
  1870. ],
  1871. ).marginOnly(left: _kContentHMargin, top: 10, bottom: 10),
  1872. ...children
  1873. .map((e) => e.marginOnly(top: 4, right: _kContentHMargin)),
  1874. ],
  1875. ).marginOnly(bottom: 10),
  1876. ).marginOnly(left: _kCardLeftMargin, top: 15),
  1877. ),
  1878. ),
  1879. ],
  1880. );
  1881. }
  1882. // ignore: non_constant_identifier_names
  1883. Widget _OptionCheckBox(
  1884. BuildContext context,
  1885. String label,
  1886. String key, {
  1887. Function(bool)? update,
  1888. bool reverse = false,
  1889. bool enabled = true,
  1890. Icon? checkedIcon,
  1891. bool? fakeValue,
  1892. bool isServer = true,
  1893. bool Function()? optGetter,
  1894. Future<void> Function(String, bool)? optSetter,
  1895. }) {
  1896. getOpt() => optGetter != null
  1897. ? optGetter()
  1898. : (isServer
  1899. ? mainGetBoolOptionSync(key)
  1900. : mainGetLocalBoolOptionSync(key));
  1901. bool value = getOpt();
  1902. final isOptFixed = isOptionFixed(key);
  1903. if (reverse) value = !value;
  1904. var ref = value.obs;
  1905. onChanged(option) async {
  1906. if (option != null) {
  1907. if (reverse) option = !option;
  1908. final setter =
  1909. optSetter ?? (isServer ? mainSetBoolOption : mainSetLocalBoolOption);
  1910. await setter(key, option);
  1911. final readOption = getOpt();
  1912. if (reverse) {
  1913. ref.value = !readOption;
  1914. } else {
  1915. ref.value = readOption;
  1916. }
  1917. update?.call(readOption);
  1918. }
  1919. }
  1920. if (fakeValue != null) {
  1921. ref.value = fakeValue;
  1922. enabled = false;
  1923. }
  1924. return GestureDetector(
  1925. child: Obx(
  1926. () => Row(
  1927. children: [
  1928. Checkbox(
  1929. value: ref.value,
  1930. onChanged: enabled && !isOptFixed ? onChanged : null)
  1931. .marginOnly(right: 5),
  1932. Offstage(
  1933. offstage: !ref.value || checkedIcon == null,
  1934. child: checkedIcon?.marginOnly(right: 5),
  1935. ),
  1936. Expanded(
  1937. child: Text(
  1938. translate(label),
  1939. style: TextStyle(color: disabledTextColor(context, enabled)),
  1940. ))
  1941. ],
  1942. ),
  1943. ).marginOnly(left: _kCheckBoxLeftMargin),
  1944. onTap: enabled && !isOptFixed
  1945. ? () {
  1946. onChanged(!ref.value);
  1947. }
  1948. : null,
  1949. );
  1950. }
  1951. // ignore: non_constant_identifier_names
  1952. Widget _Radio<T>(BuildContext context,
  1953. {required T value,
  1954. required T groupValue,
  1955. required String label,
  1956. required Function(T value)? onChanged,
  1957. bool autoNewLine = true}) {
  1958. final onChange2 = onChanged != null
  1959. ? (T? value) {
  1960. if (value != null) {
  1961. onChanged(value);
  1962. }
  1963. }
  1964. : null;
  1965. return GestureDetector(
  1966. child: Row(
  1967. children: [
  1968. Radio<T>(value: value, groupValue: groupValue, onChanged: onChange2),
  1969. Expanded(
  1970. child: Text(translate(label),
  1971. overflow: autoNewLine ? null : TextOverflow.ellipsis,
  1972. style: TextStyle(
  1973. fontSize: _kContentFontSize,
  1974. color: disabledTextColor(context, onChange2 != null)))
  1975. .marginOnly(left: 5),
  1976. ),
  1977. ],
  1978. ).marginOnly(left: _kRadioLeftMargin),
  1979. onTap: () => onChange2?.call(value),
  1980. );
  1981. }
  1982. class WaylandCard extends StatefulWidget {
  1983. const WaylandCard({Key? key}) : super(key: key);
  1984. @override
  1985. State<WaylandCard> createState() => _WaylandCardState();
  1986. }
  1987. class _WaylandCardState extends State<WaylandCard> {
  1988. final restoreTokenKey = 'wayland-restore-token';
  1989. @override
  1990. Widget build(BuildContext context) {
  1991. return futureBuilder(
  1992. future: bind.mainHandleWaylandScreencastRestoreToken(
  1993. key: restoreTokenKey, value: "get"),
  1994. hasData: (restoreToken) {
  1995. final children = [
  1996. if (restoreToken.isNotEmpty)
  1997. _buildClearScreenSelection(context, restoreToken),
  1998. ];
  1999. return Offstage(
  2000. offstage: children.isEmpty,
  2001. child: _Card(title: 'Wayland', children: children),
  2002. );
  2003. },
  2004. );
  2005. }
  2006. Widget _buildClearScreenSelection(BuildContext context, String restoreToken) {
  2007. onConfirm() async {
  2008. final msg = await bind.mainHandleWaylandScreencastRestoreToken(
  2009. key: restoreTokenKey, value: "clear");
  2010. gFFI.dialogManager.dismissAll();
  2011. if (msg.isNotEmpty) {
  2012. msgBox(gFFI.sessionId, 'custom-nocancel', 'Error', msg, '',
  2013. gFFI.dialogManager);
  2014. } else {
  2015. setState(() {});
  2016. }
  2017. }
  2018. showConfirmMsgBox() => msgBoxCommon(
  2019. gFFI.dialogManager,
  2020. 'Confirmation',
  2021. Text(
  2022. translate('confirm_clear_Wayland_screen_selection_tip'),
  2023. ),
  2024. [
  2025. dialogButton('OK', onPressed: onConfirm),
  2026. dialogButton('Cancel',
  2027. onPressed: () => gFFI.dialogManager.dismissAll())
  2028. ]);
  2029. return _Button(
  2030. 'Clear Wayland screen selection',
  2031. showConfirmMsgBox,
  2032. tip: 'clear_Wayland_screen_selection_tip',
  2033. style: ButtonStyle(
  2034. backgroundColor: MaterialStateProperty.all<Color>(
  2035. Theme.of(context).colorScheme.error.withOpacity(0.75)),
  2036. ),
  2037. );
  2038. }
  2039. }
  2040. // ignore: non_constant_identifier_names
  2041. Widget _Button(String label, Function() onPressed,
  2042. {bool enabled = true, String? tip, ButtonStyle? style}) {
  2043. var button = ElevatedButton(
  2044. onPressed: enabled ? onPressed : null,
  2045. child: Text(
  2046. translate(label),
  2047. ).marginSymmetric(horizontal: 15),
  2048. style: style,
  2049. );
  2050. StatefulWidget child;
  2051. if (tip == null) {
  2052. child = button;
  2053. } else {
  2054. child = Tooltip(message: translate(tip), child: button);
  2055. }
  2056. return Row(children: [
  2057. child,
  2058. ]).marginOnly(left: _kContentHMargin);
  2059. }
  2060. // ignore: non_constant_identifier_names
  2061. Widget _SubButton(String label, Function() onPressed, [bool enabled = true]) {
  2062. return Row(
  2063. children: [
  2064. ElevatedButton(
  2065. onPressed: enabled ? onPressed : null,
  2066. child: Text(
  2067. translate(label),
  2068. ).marginSymmetric(horizontal: 15),
  2069. ),
  2070. ],
  2071. ).marginOnly(left: _kContentHSubMargin);
  2072. }
  2073. // ignore: non_constant_identifier_names
  2074. Widget _SubLabeledWidget(BuildContext context, String label, Widget child,
  2075. {bool enabled = true}) {
  2076. return Row(
  2077. children: [
  2078. Text(
  2079. '${translate(label)}: ',
  2080. style: TextStyle(color: disabledTextColor(context, enabled)),
  2081. ),
  2082. SizedBox(
  2083. width: 10,
  2084. ),
  2085. child,
  2086. ],
  2087. ).marginOnly(left: _kContentHSubMargin);
  2088. }
  2089. Widget _lock(
  2090. bool locked,
  2091. String label,
  2092. Function() onUnlock,
  2093. ) {
  2094. return Offstage(
  2095. offstage: !locked,
  2096. child: Row(
  2097. children: [
  2098. Flexible(
  2099. child: SizedBox(
  2100. width: _kCardFixedWidth,
  2101. child: Card(
  2102. child: ElevatedButton(
  2103. child: SizedBox(
  2104. height: 25,
  2105. child: Row(
  2106. mainAxisAlignment: MainAxisAlignment.center,
  2107. children: [
  2108. const Icon(
  2109. Icons.security_sharp,
  2110. size: 20,
  2111. ),
  2112. Text(translate(label)).marginOnly(left: 5),
  2113. ]).marginSymmetric(vertical: 2)),
  2114. onPressed: () async {
  2115. final unlockPin = bind.mainGetUnlockPin();
  2116. if (unlockPin.isEmpty) {
  2117. bool checked = await callMainCheckSuperUserPermission();
  2118. if (checked) {
  2119. onUnlock();
  2120. }
  2121. } else {
  2122. checkUnlockPinDialog(unlockPin, onUnlock);
  2123. }
  2124. },
  2125. ).marginSymmetric(horizontal: 2, vertical: 4),
  2126. ).marginOnly(left: _kCardLeftMargin),
  2127. ).marginOnly(top: 10),
  2128. ),
  2129. ],
  2130. ));
  2131. }
  2132. _LabeledTextField(
  2133. BuildContext context,
  2134. String label,
  2135. TextEditingController controller,
  2136. String errorText,
  2137. bool enabled,
  2138. bool secure) {
  2139. return Table(
  2140. columnWidths: const {
  2141. 0: FixedColumnWidth(150),
  2142. 1: FlexColumnWidth(),
  2143. },
  2144. defaultVerticalAlignment: TableCellVerticalAlignment.middle,
  2145. children: [
  2146. TableRow(
  2147. children: [
  2148. Padding(
  2149. padding: const EdgeInsets.only(right: 10),
  2150. child: Text(
  2151. '${translate(label)}:',
  2152. textAlign: TextAlign.right,
  2153. style: TextStyle(
  2154. fontSize: 16,
  2155. color: disabledTextColor(context, enabled),
  2156. ),
  2157. ),
  2158. ),
  2159. TextField(
  2160. controller: controller,
  2161. enabled: enabled,
  2162. obscureText: secure,
  2163. autocorrect: false,
  2164. decoration: InputDecoration(
  2165. errorText: errorText.isNotEmpty ? errorText : null,
  2166. ),
  2167. style: TextStyle(
  2168. color: disabledTextColor(context, enabled),
  2169. ),
  2170. ).workaroundFreezeLinuxMint(),
  2171. ],
  2172. ),
  2173. ],
  2174. ).marginOnly(bottom: 8);
  2175. }
  2176. class _CountDownButton extends StatefulWidget {
  2177. _CountDownButton({
  2178. Key? key,
  2179. required this.text,
  2180. required this.second,
  2181. required this.onPressed,
  2182. }) : super(key: key);
  2183. final String text;
  2184. final VoidCallback? onPressed;
  2185. final int second;
  2186. @override
  2187. State<_CountDownButton> createState() => _CountDownButtonState();
  2188. }
  2189. class _CountDownButtonState extends State<_CountDownButton> {
  2190. bool _isButtonDisabled = false;
  2191. late int _countdownSeconds = widget.second;
  2192. Timer? _timer;
  2193. @override
  2194. void dispose() {
  2195. _timer?.cancel();
  2196. super.dispose();
  2197. }
  2198. void _startCountdownTimer() {
  2199. _timer = Timer.periodic(Duration(seconds: 1), (timer) {
  2200. if (_countdownSeconds <= 0) {
  2201. setState(() {
  2202. _isButtonDisabled = false;
  2203. });
  2204. timer.cancel();
  2205. } else {
  2206. setState(() {
  2207. _countdownSeconds--;
  2208. });
  2209. }
  2210. });
  2211. }
  2212. @override
  2213. Widget build(BuildContext context) {
  2214. return ElevatedButton(
  2215. onPressed: _isButtonDisabled
  2216. ? null
  2217. : () {
  2218. widget.onPressed?.call();
  2219. setState(() {
  2220. _isButtonDisabled = true;
  2221. _countdownSeconds = widget.second;
  2222. });
  2223. _startCountdownTimer();
  2224. },
  2225. child: Text(
  2226. _isButtonDisabled ? '$_countdownSeconds s' : translate(widget.text),
  2227. ),
  2228. );
  2229. }
  2230. }
  2231. //#endregion
  2232. //#region dialogs
  2233. void changeSocks5Proxy() async {
  2234. var socks = await bind.mainGetSocks();
  2235. String proxy = '';
  2236. String proxyMsg = '';
  2237. String username = '';
  2238. String password = '';
  2239. if (socks.length == 3) {
  2240. proxy = socks[0];
  2241. username = socks[1];
  2242. password = socks[2];
  2243. }
  2244. var proxyController = TextEditingController(text: proxy);
  2245. var userController = TextEditingController(text: username);
  2246. var pwdController = TextEditingController(text: password);
  2247. RxBool obscure = true.obs;
  2248. // proxy settings
  2249. // The following option is a not real key, it is just used for custom client advanced settings.
  2250. const String optionProxyUrl = "proxy-url";
  2251. final isOptFixed = isOptionFixed(optionProxyUrl);
  2252. var isInProgress = false;
  2253. gFFI.dialogManager.show((setState, close, context) {
  2254. submit() async {
  2255. setState(() {
  2256. proxyMsg = '';
  2257. isInProgress = true;
  2258. });
  2259. cancel() {
  2260. setState(() {
  2261. isInProgress = false;
  2262. });
  2263. }
  2264. proxy = proxyController.text.trim();
  2265. username = userController.text.trim();
  2266. password = pwdController.text.trim();
  2267. if (proxy.isNotEmpty) {
  2268. String domainPort = proxy;
  2269. if (domainPort.contains('://')) {
  2270. domainPort = domainPort.split('://')[1];
  2271. }
  2272. proxyMsg = translate(await bind.mainTestIfValidServer(
  2273. server: domainPort, testWithProxy: false));
  2274. if (proxyMsg.isEmpty) {
  2275. // ignore
  2276. } else {
  2277. cancel();
  2278. return;
  2279. }
  2280. }
  2281. await bind.mainSetSocks(
  2282. proxy: proxy, username: username, password: password);
  2283. close();
  2284. }
  2285. return CustomAlertDialog(
  2286. title: Text(translate('Socks5/Http(s) Proxy')),
  2287. content: ConstrainedBox(
  2288. constraints: const BoxConstraints(minWidth: 500),
  2289. child: Column(
  2290. crossAxisAlignment: CrossAxisAlignment.start,
  2291. children: [
  2292. Row(
  2293. children: [
  2294. if (!isMobile)
  2295. ConstrainedBox(
  2296. constraints: const BoxConstraints(minWidth: 140),
  2297. child: Align(
  2298. alignment: Alignment.centerRight,
  2299. child: Row(
  2300. children: [
  2301. Text(
  2302. translate('Server'),
  2303. ).marginOnly(right: 4),
  2304. Tooltip(
  2305. waitDuration: Duration(milliseconds: 0),
  2306. message: translate("default_proxy_tip"),
  2307. child: Icon(
  2308. Icons.help_outline_outlined,
  2309. size: 16,
  2310. color: Theme.of(context)
  2311. .textTheme
  2312. .titleLarge
  2313. ?.color
  2314. ?.withOpacity(0.5),
  2315. ),
  2316. ),
  2317. ],
  2318. )).marginOnly(right: 10),
  2319. ),
  2320. Expanded(
  2321. child: TextField(
  2322. decoration: InputDecoration(
  2323. errorText: proxyMsg.isNotEmpty ? proxyMsg : null,
  2324. labelText: isMobile ? translate('Server') : null,
  2325. helperText:
  2326. isMobile ? translate("default_proxy_tip") : null,
  2327. helperMaxLines: isMobile ? 3 : null,
  2328. ),
  2329. controller: proxyController,
  2330. autofocus: true,
  2331. enabled: !isOptFixed,
  2332. ).workaroundFreezeLinuxMint(),
  2333. ),
  2334. ],
  2335. ).marginOnly(bottom: 8),
  2336. Row(
  2337. children: [
  2338. if (!isMobile)
  2339. ConstrainedBox(
  2340. constraints: const BoxConstraints(minWidth: 140),
  2341. child: Text(
  2342. '${translate("Username")}:',
  2343. textAlign: TextAlign.right,
  2344. ).marginOnly(right: 10)),
  2345. Expanded(
  2346. child: TextField(
  2347. controller: userController,
  2348. decoration: InputDecoration(
  2349. labelText: isMobile ? translate('Username') : null,
  2350. ),
  2351. enabled: !isOptFixed,
  2352. ).workaroundFreezeLinuxMint(),
  2353. ),
  2354. ],
  2355. ).marginOnly(bottom: 8),
  2356. Row(
  2357. children: [
  2358. if (!isMobile)
  2359. ConstrainedBox(
  2360. constraints: const BoxConstraints(minWidth: 140),
  2361. child: Text(
  2362. '${translate("Password")}:',
  2363. textAlign: TextAlign.right,
  2364. ).marginOnly(right: 10)),
  2365. Expanded(
  2366. child: Obx(() => TextField(
  2367. obscureText: obscure.value,
  2368. decoration: InputDecoration(
  2369. labelText: isMobile ? translate('Password') : null,
  2370. suffixIcon: IconButton(
  2371. onPressed: () => obscure.value = !obscure.value,
  2372. icon: Icon(obscure.value
  2373. ? Icons.visibility_off
  2374. : Icons.visibility))),
  2375. controller: pwdController,
  2376. enabled: !isOptFixed,
  2377. maxLength: bind.mainMaxEncryptLen(),
  2378. ).workaroundFreezeLinuxMint()),
  2379. ),
  2380. ],
  2381. ),
  2382. // NOT use Offstage to wrap LinearProgressIndicator
  2383. if (isInProgress)
  2384. const LinearProgressIndicator().marginOnly(top: 8),
  2385. ],
  2386. ),
  2387. ),
  2388. actions: [
  2389. dialogButton('Cancel', onPressed: close, isOutline: true),
  2390. if (!isOptFixed) dialogButton('OK', onPressed: submit),
  2391. ],
  2392. onSubmit: submit,
  2393. onCancel: close,
  2394. );
  2395. });
  2396. }
  2397. //#endregion