desktop_setting_page.dart 82 KB

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