dialog.dart 74 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538
  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'package:bot_toast/bot_toast.dart';
  4. import 'package:flutter/material.dart';
  5. import 'package:flutter/services.dart';
  6. import 'package:flutter_hbb/common/shared_state.dart';
  7. import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
  8. import 'package:flutter_hbb/consts.dart';
  9. import 'package:flutter_hbb/models/peer_model.dart';
  10. import 'package:flutter_hbb/models/peer_tab_model.dart';
  11. import 'package:flutter_hbb/models/state_model.dart';
  12. import 'package:get/get.dart';
  13. import 'package:qr_flutter/qr_flutter.dart';
  14. import '../../common.dart';
  15. import '../../models/model.dart';
  16. import '../../models/platform_model.dart';
  17. import 'address_book.dart';
  18. void clientClose(SessionID sessionId, OverlayDialogManager dialogManager) {
  19. msgBox(sessionId, 'info', 'Close', 'Are you sure to close the connection?',
  20. '', dialogManager);
  21. }
  22. abstract class ValidationRule {
  23. String get name;
  24. bool validate(String value);
  25. }
  26. class LengthRangeValidationRule extends ValidationRule {
  27. final int _min;
  28. final int _max;
  29. LengthRangeValidationRule(this._min, this._max);
  30. @override
  31. String get name => translate('length %min% to %max%')
  32. .replaceAll('%min%', _min.toString())
  33. .replaceAll('%max%', _max.toString());
  34. @override
  35. bool validate(String value) {
  36. return value.length >= _min && value.length <= _max;
  37. }
  38. }
  39. class RegexValidationRule extends ValidationRule {
  40. final String _name;
  41. final RegExp _regex;
  42. RegexValidationRule(this._name, this._regex);
  43. @override
  44. String get name => translate(_name);
  45. @override
  46. bool validate(String value) {
  47. return value.isNotEmpty ? value.contains(_regex) : false;
  48. }
  49. }
  50. void changeIdDialog() {
  51. var newId = "";
  52. var msg = "";
  53. var isInProgress = false;
  54. TextEditingController controller = TextEditingController();
  55. final RxString rxId = controller.text.trim().obs;
  56. final rules = [
  57. RegexValidationRule('starts with a letter', RegExp(r'^[a-zA-Z]')),
  58. LengthRangeValidationRule(6, 16),
  59. RegexValidationRule('allowed characters', RegExp(r'^[\w-]*$'))
  60. ];
  61. gFFI.dialogManager.show((setState, close, context) {
  62. submit() async {
  63. debugPrint("onSubmit");
  64. newId = controller.text.trim();
  65. final Iterable violations = rules.where((r) => !r.validate(newId));
  66. if (violations.isNotEmpty) {
  67. setState(() {
  68. msg = (isDesktop || isWebDesktop)
  69. ? '${translate('Prompt')}: ${violations.map((r) => r.name).join(', ')}'
  70. : violations.map((r) => r.name).join(', ');
  71. });
  72. return;
  73. }
  74. setState(() {
  75. msg = "";
  76. isInProgress = true;
  77. bind.mainChangeId(newId: newId);
  78. });
  79. var status = await bind.mainGetAsyncStatus();
  80. while (status == " ") {
  81. await Future.delayed(const Duration(milliseconds: 100));
  82. status = await bind.mainGetAsyncStatus();
  83. }
  84. if (status.isEmpty) {
  85. // ok
  86. close();
  87. return;
  88. }
  89. setState(() {
  90. isInProgress = false;
  91. msg = (isDesktop || isWebDesktop)
  92. ? '${translate('Prompt')}: ${translate(status)}'
  93. : translate(status);
  94. });
  95. }
  96. return CustomAlertDialog(
  97. title: Text(translate("Change ID")),
  98. content: Column(
  99. crossAxisAlignment: CrossAxisAlignment.start,
  100. children: [
  101. Text(translate("id_change_tip")),
  102. const SizedBox(
  103. height: 12.0,
  104. ),
  105. TextField(
  106. decoration: InputDecoration(
  107. labelText: translate('Your new ID'),
  108. errorText: msg.isEmpty ? null : translate(msg),
  109. suffixText: '${rxId.value.length}/16',
  110. suffixStyle: const TextStyle(fontSize: 12, color: Colors.grey)),
  111. inputFormatters: [
  112. LengthLimitingTextInputFormatter(16),
  113. // FilteringTextInputFormatter(RegExp(r"[a-zA-z][a-zA-z0-9\_]*"), allow: true)
  114. ],
  115. controller: controller,
  116. autofocus: true,
  117. onChanged: (value) {
  118. setState(() {
  119. rxId.value = value.trim();
  120. msg = '';
  121. });
  122. },
  123. ).workaroundFreezeLinuxMint(),
  124. const SizedBox(
  125. height: 8.0,
  126. ),
  127. (isDesktop || isWebDesktop)
  128. ? Obx(() => Wrap(
  129. runSpacing: 8,
  130. spacing: 4,
  131. children: rules.map((e) {
  132. var checked = e.validate(rxId.value);
  133. return Chip(
  134. label: Text(
  135. e.name,
  136. style: TextStyle(
  137. color: checked
  138. ? const Color(0xFF0A9471)
  139. : Color.fromARGB(255, 198, 86, 157)),
  140. ),
  141. backgroundColor: checked
  142. ? const Color(0xFFD0F7ED)
  143. : Color.fromARGB(255, 247, 205, 232));
  144. }).toList(),
  145. )).marginOnly(bottom: 8)
  146. : SizedBox.shrink(),
  147. // NOT use Offstage to wrap LinearProgressIndicator
  148. if (isInProgress) const LinearProgressIndicator(),
  149. ],
  150. ),
  151. actions: [
  152. dialogButton("Cancel", onPressed: close, isOutline: true),
  153. dialogButton("OK", onPressed: submit),
  154. ],
  155. onSubmit: submit,
  156. onCancel: close,
  157. );
  158. });
  159. }
  160. void changeWhiteList({Function()? callback}) async {
  161. final curWhiteList = await bind.mainGetOption(key: kOptionWhitelist);
  162. var newWhiteListField = curWhiteList == defaultOptionWhitelist
  163. ? ''
  164. : curWhiteList.split(',').join('\n');
  165. var controller = TextEditingController(text: newWhiteListField);
  166. var msg = "";
  167. var isInProgress = false;
  168. final isOptFixed = isOptionFixed(kOptionWhitelist);
  169. gFFI.dialogManager.show((setState, close, context) {
  170. return CustomAlertDialog(
  171. title: Text(translate("IP Whitelisting")),
  172. content: Column(
  173. crossAxisAlignment: CrossAxisAlignment.start,
  174. children: [
  175. Text(translate("whitelist_sep")),
  176. const SizedBox(
  177. height: 8.0,
  178. ),
  179. Row(
  180. children: [
  181. Expanded(
  182. child: TextField(
  183. maxLines: null,
  184. decoration: InputDecoration(
  185. errorText: msg.isEmpty ? null : translate(msg),
  186. ),
  187. controller: controller,
  188. enabled: !isOptFixed,
  189. autofocus: true)
  190. .workaroundFreezeLinuxMint(),
  191. ),
  192. ],
  193. ),
  194. const SizedBox(
  195. height: 4.0,
  196. ),
  197. // NOT use Offstage to wrap LinearProgressIndicator
  198. if (isInProgress) const LinearProgressIndicator(),
  199. ],
  200. ),
  201. actions: [
  202. dialogButton("Cancel", onPressed: close, isOutline: true),
  203. if (!isOptFixed)
  204. dialogButton("Clear", onPressed: () async {
  205. await bind.mainSetOption(
  206. key: kOptionWhitelist, value: defaultOptionWhitelist);
  207. callback?.call();
  208. close();
  209. }, isOutline: true),
  210. if (!isOptFixed)
  211. dialogButton(
  212. "OK",
  213. onPressed: () async {
  214. setState(() {
  215. msg = "";
  216. isInProgress = true;
  217. });
  218. newWhiteListField = controller.text.trim();
  219. var newWhiteList = "";
  220. if (newWhiteListField.isEmpty) {
  221. // pass
  222. } else {
  223. final ips =
  224. newWhiteListField.trim().split(RegExp(r"[\s,;\n]+"));
  225. // test ip
  226. final ipMatch = RegExp(
  227. r"^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)(\/([1-9]|[1-2][0-9]|3[0-2])){0,1}$");
  228. final ipv6Match = RegExp(
  229. r"^(((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7})(\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}$");
  230. for (final ip in ips) {
  231. if (!ipMatch.hasMatch(ip) && !ipv6Match.hasMatch(ip)) {
  232. msg = "${translate("Invalid IP")} $ip";
  233. setState(() {
  234. isInProgress = false;
  235. });
  236. return;
  237. }
  238. }
  239. newWhiteList = ips.join(',');
  240. }
  241. if (newWhiteList.trim().isEmpty) {
  242. newWhiteList = defaultOptionWhitelist;
  243. }
  244. await bind.mainSetOption(
  245. key: kOptionWhitelist, value: newWhiteList);
  246. callback?.call();
  247. close();
  248. },
  249. ),
  250. ],
  251. onCancel: close,
  252. );
  253. });
  254. }
  255. Future<String> changeDirectAccessPort(
  256. String currentIP, String currentPort) async {
  257. final controller = TextEditingController(text: currentPort);
  258. await gFFI.dialogManager.show((setState, close, context) {
  259. return CustomAlertDialog(
  260. title: Text(translate("Change Local Port")),
  261. content: Column(
  262. crossAxisAlignment: CrossAxisAlignment.start,
  263. children: [
  264. const SizedBox(height: 8.0),
  265. Row(
  266. children: [
  267. Expanded(
  268. child: TextField(
  269. maxLines: null,
  270. keyboardType: TextInputType.number,
  271. decoration: InputDecoration(
  272. hintText: '21118',
  273. isCollapsed: true,
  274. prefix: Text('$currentIP : '),
  275. suffix: IconButton(
  276. padding: EdgeInsets.zero,
  277. icon: const Icon(Icons.clear, size: 16),
  278. onPressed: () => controller.clear())),
  279. inputFormatters: [
  280. FilteringTextInputFormatter.allow(RegExp(
  281. 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])$')),
  282. ],
  283. controller: controller,
  284. autofocus: true)
  285. .workaroundFreezeLinuxMint(),
  286. ),
  287. ],
  288. ),
  289. ],
  290. ),
  291. actions: [
  292. dialogButton("Cancel", onPressed: close, isOutline: true),
  293. dialogButton("OK", onPressed: () async {
  294. await bind.mainSetOption(
  295. key: kOptionDirectAccessPort, value: controller.text);
  296. close();
  297. }),
  298. ],
  299. onCancel: close,
  300. );
  301. });
  302. return controller.text;
  303. }
  304. Future<String> changeAutoDisconnectTimeout(String old) async {
  305. final controller = TextEditingController(text: old);
  306. await gFFI.dialogManager.show((setState, close, context) {
  307. return CustomAlertDialog(
  308. title: Text(translate("Timeout in minutes")),
  309. content: Column(
  310. crossAxisAlignment: CrossAxisAlignment.start,
  311. children: [
  312. const SizedBox(height: 8.0),
  313. Row(
  314. children: [
  315. Expanded(
  316. child: TextField(
  317. maxLines: null,
  318. keyboardType: TextInputType.number,
  319. decoration: InputDecoration(
  320. hintText: '10',
  321. isCollapsed: true,
  322. suffix: IconButton(
  323. padding: EdgeInsets.zero,
  324. icon: const Icon(Icons.clear, size: 16),
  325. onPressed: () => controller.clear())),
  326. inputFormatters: [
  327. FilteringTextInputFormatter.allow(RegExp(
  328. 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])$')),
  329. ],
  330. controller: controller,
  331. autofocus: true)
  332. .workaroundFreezeLinuxMint(),
  333. ),
  334. ],
  335. ),
  336. ],
  337. ),
  338. actions: [
  339. dialogButton("Cancel", onPressed: close, isOutline: true),
  340. dialogButton("OK", onPressed: () async {
  341. await bind.mainSetOption(
  342. key: kOptionAutoDisconnectTimeout, value: controller.text);
  343. close();
  344. }),
  345. ],
  346. onCancel: close,
  347. );
  348. });
  349. return controller.text;
  350. }
  351. class DialogTextField extends StatelessWidget {
  352. final String title;
  353. final String? hintText;
  354. final bool obscureText;
  355. final String? errorText;
  356. final String? helperText;
  357. final Widget? prefixIcon;
  358. final Widget? suffixIcon;
  359. final TextEditingController controller;
  360. final FocusNode? focusNode;
  361. final TextInputType? keyboardType;
  362. final List<TextInputFormatter>? inputFormatters;
  363. final int? maxLength;
  364. static const kUsernameTitle = 'Username';
  365. static const kUsernameIcon = Icon(Icons.account_circle_outlined);
  366. static const kPasswordTitle = 'Password';
  367. static const kPasswordIcon = Icon(Icons.lock_outline);
  368. DialogTextField(
  369. {Key? key,
  370. this.focusNode,
  371. this.obscureText = false,
  372. this.errorText,
  373. this.helperText,
  374. this.prefixIcon,
  375. this.suffixIcon,
  376. this.hintText,
  377. this.keyboardType,
  378. this.inputFormatters,
  379. this.maxLength,
  380. required this.title,
  381. required this.controller})
  382. : super(key: key);
  383. @override
  384. Widget build(BuildContext context) {
  385. return Row(
  386. children: [
  387. Expanded(
  388. child: Column(
  389. children: [
  390. TextField(
  391. decoration: InputDecoration(
  392. labelText: title,
  393. hintText: hintText,
  394. prefixIcon: prefixIcon,
  395. suffixIcon: suffixIcon,
  396. helperText: helperText,
  397. helperMaxLines: 8,
  398. ),
  399. controller: controller,
  400. focusNode: focusNode,
  401. autofocus: true,
  402. obscureText: obscureText,
  403. keyboardType: keyboardType,
  404. inputFormatters: inputFormatters,
  405. maxLength: maxLength,
  406. ),
  407. if (errorText != null)
  408. Align(
  409. alignment: Alignment.centerLeft,
  410. child: SelectableText(
  411. errorText!,
  412. style: TextStyle(
  413. color: Theme.of(context).colorScheme.error,
  414. fontSize: 12,
  415. ),
  416. textAlign: TextAlign.left,
  417. ).paddingOnly(top: 8, left: 12),
  418. ),
  419. ],
  420. ).workaroundFreezeLinuxMint(),
  421. ),
  422. ],
  423. ).paddingSymmetric(vertical: 4.0);
  424. }
  425. }
  426. abstract class ValidationField extends StatelessWidget {
  427. ValidationField({Key? key}) : super(key: key);
  428. String? validate();
  429. bool get isReady;
  430. }
  431. class Dialog2FaField extends ValidationField {
  432. Dialog2FaField({
  433. Key? key,
  434. required this.controller,
  435. this.autoFocus = true,
  436. this.reRequestFocus = false,
  437. this.title,
  438. this.hintText,
  439. this.errorText,
  440. this.readyCallback,
  441. this.onChanged,
  442. }) : super(key: key);
  443. final TextEditingController controller;
  444. final bool autoFocus;
  445. final bool reRequestFocus;
  446. final String? title;
  447. final String? hintText;
  448. final String? errorText;
  449. final VoidCallback? readyCallback;
  450. final VoidCallback? onChanged;
  451. final errMsg = translate('2FA code must be 6 digits.');
  452. @override
  453. Widget build(BuildContext context) {
  454. return DialogVerificationCodeField(
  455. title: title ?? translate('2FA code'),
  456. controller: controller,
  457. errorText: errorText,
  458. autoFocus: autoFocus,
  459. reRequestFocus: reRequestFocus,
  460. hintText: hintText,
  461. readyCallback: readyCallback,
  462. onChanged: _onChanged,
  463. keyboardType: TextInputType.number,
  464. inputFormatters: [
  465. FilteringTextInputFormatter.allow(RegExp(r'[0-9]')),
  466. ],
  467. );
  468. }
  469. String get text => controller.text;
  470. bool get isAllDigits => text.codeUnits.every((e) => e >= 48 && e <= 57);
  471. @override
  472. bool get isReady => text.length == 6 && isAllDigits;
  473. @override
  474. String? validate() => isReady ? null : errMsg;
  475. _onChanged(StateSetter setState, SimpleWrapper<String?> errText) {
  476. onChanged?.call();
  477. if (text.length > 6) {
  478. setState(() => errText.value = errMsg);
  479. return;
  480. }
  481. if (!isAllDigits) {
  482. setState(() => errText.value = errMsg);
  483. return;
  484. }
  485. if (isReady) {
  486. readyCallback?.call();
  487. return;
  488. }
  489. if (errText.value != null) {
  490. setState(() => errText.value = null);
  491. }
  492. }
  493. }
  494. class DialogEmailCodeField extends ValidationField {
  495. DialogEmailCodeField({
  496. Key? key,
  497. required this.controller,
  498. this.autoFocus = true,
  499. this.reRequestFocus = false,
  500. this.hintText,
  501. this.errorText,
  502. this.readyCallback,
  503. this.onChanged,
  504. }) : super(key: key);
  505. final TextEditingController controller;
  506. final bool autoFocus;
  507. final bool reRequestFocus;
  508. final String? hintText;
  509. final String? errorText;
  510. final VoidCallback? readyCallback;
  511. final VoidCallback? onChanged;
  512. final errMsg = translate('Email verification code must be 6 characters.');
  513. @override
  514. Widget build(BuildContext context) {
  515. return DialogVerificationCodeField(
  516. title: translate('Verification code'),
  517. controller: controller,
  518. errorText: errorText,
  519. autoFocus: autoFocus,
  520. reRequestFocus: reRequestFocus,
  521. hintText: hintText,
  522. readyCallback: readyCallback,
  523. helperText: translate('verification_tip'),
  524. onChanged: _onChanged,
  525. keyboardType: TextInputType.visiblePassword,
  526. );
  527. }
  528. String get text => controller.text;
  529. @override
  530. bool get isReady => text.length == 6;
  531. @override
  532. String? validate() => isReady ? null : errMsg;
  533. _onChanged(StateSetter setState, SimpleWrapper<String?> errText) {
  534. onChanged?.call();
  535. if (text.length > 6) {
  536. setState(() => errText.value = errMsg);
  537. return;
  538. }
  539. if (isReady) {
  540. readyCallback?.call();
  541. return;
  542. }
  543. if (errText.value != null) {
  544. setState(() => errText.value = null);
  545. }
  546. }
  547. }
  548. class DialogVerificationCodeField extends StatefulWidget {
  549. DialogVerificationCodeField({
  550. Key? key,
  551. required this.controller,
  552. required this.title,
  553. this.autoFocus = true,
  554. this.reRequestFocus = false,
  555. this.helperText,
  556. this.hintText,
  557. this.errorText,
  558. this.textLength,
  559. this.readyCallback,
  560. this.onChanged,
  561. this.keyboardType,
  562. this.inputFormatters,
  563. }) : super(key: key);
  564. final TextEditingController controller;
  565. final bool autoFocus;
  566. final bool reRequestFocus;
  567. final String title;
  568. final String? helperText;
  569. final String? hintText;
  570. final String? errorText;
  571. final int? textLength;
  572. final VoidCallback? readyCallback;
  573. final Function(StateSetter setState, SimpleWrapper<String?> errText)?
  574. onChanged;
  575. final TextInputType? keyboardType;
  576. final List<TextInputFormatter>? inputFormatters;
  577. @override
  578. State<DialogVerificationCodeField> createState() =>
  579. _DialogVerificationCodeField();
  580. }
  581. class _DialogVerificationCodeField extends State<DialogVerificationCodeField> {
  582. final _focusNode = FocusNode();
  583. Timer? _timer;
  584. Timer? _timerReRequestFocus;
  585. SimpleWrapper<String?> errorText = SimpleWrapper(null);
  586. String _preText = '';
  587. @override
  588. void initState() {
  589. super.initState();
  590. if (widget.autoFocus) {
  591. _timer =
  592. Timer(Duration(milliseconds: 50), () => _focusNode.requestFocus());
  593. if (widget.onChanged != null) {
  594. widget.controller.addListener(() {
  595. final text = widget.controller.text.trim();
  596. if (text == _preText) return;
  597. widget.onChanged!(setState, errorText);
  598. _preText = text;
  599. });
  600. }
  601. }
  602. // software secure keyboard will take the focus since flutter 3.13
  603. // request focus again when android account password obtain focus
  604. if (isAndroid && widget.reRequestFocus) {
  605. _focusNode.addListener(() {
  606. if (_focusNode.hasFocus) {
  607. _timerReRequestFocus?.cancel();
  608. _timerReRequestFocus = Timer(
  609. Duration(milliseconds: 100), () => _focusNode.requestFocus());
  610. }
  611. });
  612. }
  613. }
  614. @override
  615. void dispose() {
  616. _timer?.cancel();
  617. _timerReRequestFocus?.cancel();
  618. _focusNode.unfocus();
  619. _focusNode.dispose();
  620. super.dispose();
  621. }
  622. @override
  623. Widget build(BuildContext context) {
  624. return DialogTextField(
  625. title: widget.title,
  626. controller: widget.controller,
  627. errorText: widget.errorText ?? errorText.value,
  628. focusNode: _focusNode,
  629. helperText: widget.helperText,
  630. keyboardType: widget.keyboardType,
  631. inputFormatters: widget.inputFormatters,
  632. );
  633. }
  634. }
  635. class PasswordWidget extends StatefulWidget {
  636. PasswordWidget({
  637. Key? key,
  638. required this.controller,
  639. this.autoFocus = true,
  640. this.reRequestFocus = false,
  641. this.hintText,
  642. this.errorText,
  643. this.title,
  644. this.maxLength,
  645. }) : super(key: key);
  646. final TextEditingController controller;
  647. final bool autoFocus;
  648. final bool reRequestFocus;
  649. final String? hintText;
  650. final String? errorText;
  651. final String? title;
  652. final int? maxLength;
  653. @override
  654. State<PasswordWidget> createState() => _PasswordWidgetState();
  655. }
  656. class _PasswordWidgetState extends State<PasswordWidget> {
  657. bool _passwordVisible = false;
  658. final _focusNode = FocusNode();
  659. Timer? _timer;
  660. Timer? _timerReRequestFocus;
  661. @override
  662. void initState() {
  663. super.initState();
  664. if (widget.autoFocus) {
  665. _timer =
  666. Timer(Duration(milliseconds: 50), () => _focusNode.requestFocus());
  667. }
  668. // software secure keyboard will take the focus since flutter 3.13
  669. // request focus again when android account password obtain focus
  670. if (isAndroid && widget.reRequestFocus) {
  671. _focusNode.addListener(() {
  672. if (_focusNode.hasFocus) {
  673. _timerReRequestFocus?.cancel();
  674. _timerReRequestFocus = Timer(
  675. Duration(milliseconds: 100), () => _focusNode.requestFocus());
  676. }
  677. });
  678. }
  679. }
  680. @override
  681. void dispose() {
  682. _timer?.cancel();
  683. _timerReRequestFocus?.cancel();
  684. _focusNode.unfocus();
  685. _focusNode.dispose();
  686. super.dispose();
  687. }
  688. @override
  689. Widget build(BuildContext context) {
  690. return DialogTextField(
  691. title: translate(widget.title ?? DialogTextField.kPasswordTitle),
  692. hintText: translate(widget.hintText ?? 'Enter your password'),
  693. controller: widget.controller,
  694. prefixIcon: DialogTextField.kPasswordIcon,
  695. suffixIcon: IconButton(
  696. icon: Icon(
  697. // Based on passwordVisible state choose the icon
  698. _passwordVisible ? Icons.visibility : Icons.visibility_off,
  699. color: MyTheme.lightTheme.primaryColor),
  700. onPressed: () {
  701. // Update the state i.e. toggle the state of passwordVisible variable
  702. setState(() {
  703. _passwordVisible = !_passwordVisible;
  704. });
  705. },
  706. ),
  707. obscureText: !_passwordVisible,
  708. errorText: widget.errorText,
  709. focusNode: _focusNode,
  710. maxLength: widget.maxLength,
  711. );
  712. }
  713. }
  714. void wrongPasswordDialog(SessionID sessionId,
  715. OverlayDialogManager dialogManager, type, title, text) {
  716. dialogManager.dismissAll();
  717. dialogManager.show((setState, close, context) {
  718. cancel() {
  719. close();
  720. closeConnection();
  721. }
  722. submit() {
  723. enterPasswordDialog(sessionId, dialogManager);
  724. }
  725. return CustomAlertDialog(
  726. title: null,
  727. content: msgboxContent(type, title, text),
  728. onSubmit: submit,
  729. onCancel: cancel,
  730. actions: [
  731. dialogButton(
  732. 'Cancel',
  733. onPressed: cancel,
  734. isOutline: true,
  735. ),
  736. dialogButton(
  737. 'Retry',
  738. onPressed: submit,
  739. ),
  740. ]);
  741. });
  742. }
  743. void enterPasswordDialog(
  744. SessionID sessionId, OverlayDialogManager dialogManager) async {
  745. await _connectDialog(
  746. sessionId,
  747. dialogManager,
  748. passwordController: TextEditingController(),
  749. );
  750. }
  751. void enterUserLoginDialog(
  752. SessionID sessionId, OverlayDialogManager dialogManager) async {
  753. await _connectDialog(
  754. sessionId,
  755. dialogManager,
  756. osUsernameController: TextEditingController(),
  757. osPasswordController: TextEditingController(),
  758. );
  759. }
  760. void enterUserLoginAndPasswordDialog(
  761. SessionID sessionId, OverlayDialogManager dialogManager) async {
  762. await _connectDialog(
  763. sessionId,
  764. dialogManager,
  765. osUsernameController: TextEditingController(),
  766. osPasswordController: TextEditingController(),
  767. passwordController: TextEditingController(),
  768. );
  769. }
  770. _connectDialog(
  771. SessionID sessionId,
  772. OverlayDialogManager dialogManager, {
  773. TextEditingController? osUsernameController,
  774. TextEditingController? osPasswordController,
  775. TextEditingController? passwordController,
  776. }) async {
  777. var rememberPassword = false;
  778. if (passwordController != null) {
  779. rememberPassword =
  780. await bind.sessionGetRemember(sessionId: sessionId) ?? false;
  781. }
  782. var rememberAccount = false;
  783. if (osUsernameController != null) {
  784. rememberAccount =
  785. await bind.sessionGetRemember(sessionId: sessionId) ?? false;
  786. }
  787. dialogManager.dismissAll();
  788. dialogManager.show((setState, close, context) {
  789. cancel() {
  790. close();
  791. closeConnection();
  792. }
  793. submit() {
  794. final osUsername = osUsernameController?.text.trim() ?? '';
  795. final osPassword = osPasswordController?.text.trim() ?? '';
  796. final password = passwordController?.text.trim() ?? '';
  797. if (passwordController != null && password.isEmpty) return;
  798. if (rememberAccount) {
  799. bind.sessionPeerOption(
  800. sessionId: sessionId, name: 'os-username', value: osUsername);
  801. bind.sessionPeerOption(
  802. sessionId: sessionId, name: 'os-password', value: osPassword);
  803. }
  804. gFFI.login(
  805. osUsername,
  806. osPassword,
  807. sessionId,
  808. password,
  809. rememberPassword,
  810. );
  811. close();
  812. dialogManager.showLoading(translate('Logging in...'),
  813. onCancel: closeConnection);
  814. }
  815. descWidget(String text) {
  816. return Column(
  817. children: [
  818. Align(
  819. alignment: Alignment.centerLeft,
  820. child: Text(
  821. text,
  822. maxLines: 3,
  823. softWrap: true,
  824. overflow: TextOverflow.ellipsis,
  825. style: TextStyle(fontSize: 16),
  826. ),
  827. ),
  828. Container(
  829. height: 8,
  830. ),
  831. ],
  832. );
  833. }
  834. rememberWidget(
  835. String desc,
  836. bool remember,
  837. ValueChanged<bool?>? onChanged,
  838. ) {
  839. return CheckboxListTile(
  840. contentPadding: const EdgeInsets.all(0),
  841. dense: true,
  842. controlAffinity: ListTileControlAffinity.leading,
  843. title: Text(desc),
  844. value: remember,
  845. onChanged: onChanged,
  846. );
  847. }
  848. osAccountWidget() {
  849. if (osUsernameController == null || osPasswordController == null) {
  850. return Offstage();
  851. }
  852. return Column(
  853. children: [
  854. descWidget(translate('login_linux_tip')),
  855. DialogTextField(
  856. title: translate(DialogTextField.kUsernameTitle),
  857. controller: osUsernameController,
  858. prefixIcon: DialogTextField.kUsernameIcon,
  859. errorText: null,
  860. ),
  861. PasswordWidget(
  862. controller: osPasswordController,
  863. autoFocus: false,
  864. ),
  865. rememberWidget(
  866. translate('remember_account_tip'),
  867. rememberAccount,
  868. (v) {
  869. if (v != null) {
  870. setState(() => rememberAccount = v);
  871. }
  872. },
  873. ),
  874. ],
  875. );
  876. }
  877. passwdWidget() {
  878. if (passwordController == null) {
  879. return Offstage();
  880. }
  881. return Column(
  882. children: [
  883. descWidget(translate('verify_rustdesk_password_tip')),
  884. PasswordWidget(
  885. controller: passwordController,
  886. autoFocus: osUsernameController == null,
  887. ),
  888. rememberWidget(
  889. translate('Remember password'),
  890. rememberPassword,
  891. (v) {
  892. if (v != null) {
  893. setState(() => rememberPassword = v);
  894. }
  895. },
  896. ),
  897. ],
  898. );
  899. }
  900. return CustomAlertDialog(
  901. title: Row(
  902. mainAxisAlignment: MainAxisAlignment.center,
  903. children: [
  904. Icon(Icons.password_rounded, color: MyTheme.accent),
  905. Text(translate('Password Required')).paddingOnly(left: 10),
  906. ],
  907. ),
  908. content: Column(mainAxisSize: MainAxisSize.min, children: [
  909. osAccountWidget(),
  910. osUsernameController == null || passwordController == null
  911. ? Offstage()
  912. : Container(height: 12),
  913. passwdWidget(),
  914. ]),
  915. actions: [
  916. dialogButton(
  917. 'Cancel',
  918. icon: Icon(Icons.close_rounded),
  919. onPressed: cancel,
  920. isOutline: true,
  921. ),
  922. dialogButton(
  923. 'OK',
  924. icon: Icon(Icons.done_rounded),
  925. onPressed: submit,
  926. ),
  927. ],
  928. onSubmit: submit,
  929. onCancel: cancel,
  930. );
  931. });
  932. }
  933. void showWaitUacDialog(
  934. SessionID sessionId, OverlayDialogManager dialogManager, String type) {
  935. dialogManager.dismissAll();
  936. dialogManager.show(
  937. tag: '$sessionId-wait-uac',
  938. (setState, close, context) => CustomAlertDialog(
  939. title: null,
  940. content: msgboxContent(type, 'Wait', 'wait_accept_uac_tip'),
  941. actions: [
  942. dialogButton(
  943. 'OK',
  944. icon: Icon(Icons.done_rounded),
  945. onPressed: close,
  946. ),
  947. ],
  948. ));
  949. }
  950. // Another username && password dialog?
  951. void showRequestElevationDialog(
  952. SessionID sessionId, OverlayDialogManager dialogManager) {
  953. RxString groupValue = ''.obs;
  954. RxString errUser = ''.obs;
  955. RxString errPwd = ''.obs;
  956. TextEditingController userController = TextEditingController();
  957. TextEditingController pwdController = TextEditingController();
  958. void onRadioChanged(String? value) {
  959. if (value != null) {
  960. groupValue.value = value;
  961. }
  962. }
  963. // TODO get from theme
  964. final double fontSizeNote = 13.00;
  965. Widget OptionRequestPermissions = Obx(
  966. () => Row(
  967. crossAxisAlignment: CrossAxisAlignment.start,
  968. children: [
  969. Radio(
  970. visualDensity: VisualDensity(horizontal: -4, vertical: -4),
  971. value: '',
  972. groupValue: groupValue.value,
  973. onChanged: onRadioChanged,
  974. ).marginOnly(right: 10),
  975. Expanded(
  976. child: Column(
  977. crossAxisAlignment: CrossAxisAlignment.start,
  978. children: [
  979. InkWell(
  980. hoverColor: Colors.transparent,
  981. onTap: () => groupValue.value = '',
  982. child: Text(
  983. translate('Ask the remote user for authentication'),
  984. ),
  985. ).marginOnly(bottom: 10),
  986. Text(
  987. translate('Choose this if the remote account is administrator'),
  988. style: TextStyle(fontSize: fontSizeNote),
  989. ),
  990. ],
  991. ).marginOnly(top: 3),
  992. ),
  993. ],
  994. ),
  995. );
  996. Widget OptionCredentials = Obx(
  997. () => Row(
  998. crossAxisAlignment: CrossAxisAlignment.start,
  999. children: [
  1000. Radio(
  1001. visualDensity: VisualDensity(horizontal: -4, vertical: -4),
  1002. value: 'logon',
  1003. groupValue: groupValue.value,
  1004. onChanged: onRadioChanged,
  1005. ).marginOnly(right: 10),
  1006. Expanded(
  1007. child: InkWell(
  1008. hoverColor: Colors.transparent,
  1009. onTap: () => onRadioChanged('logon'),
  1010. child: Text(
  1011. translate('Transmit the username and password of administrator'),
  1012. ),
  1013. ).marginOnly(top: 4),
  1014. ),
  1015. ],
  1016. ),
  1017. );
  1018. Widget UacNote = Container(
  1019. padding: EdgeInsets.fromLTRB(10, 8, 8, 8),
  1020. decoration: BoxDecoration(
  1021. color: MyTheme.currentThemeMode() == ThemeMode.dark
  1022. ? Color.fromARGB(135, 87, 87, 90)
  1023. : Colors.grey[100],
  1024. borderRadius: BorderRadius.circular(8),
  1025. border: Border.all(color: Colors.grey),
  1026. ),
  1027. child: Row(
  1028. children: [
  1029. Icon(Icons.info_outline_rounded, size: 20).marginOnly(right: 10),
  1030. Expanded(
  1031. child: Text(
  1032. translate('still_click_uac_tip'),
  1033. style: TextStyle(
  1034. fontSize: fontSizeNote, fontWeight: FontWeight.normal),
  1035. ),
  1036. )
  1037. ],
  1038. ),
  1039. );
  1040. var content = Obx(
  1041. () => Column(
  1042. children: [
  1043. OptionRequestPermissions.marginOnly(bottom: 15),
  1044. OptionCredentials,
  1045. Offstage(
  1046. offstage: 'logon' != groupValue.value,
  1047. child: Column(
  1048. children: [
  1049. UacNote.marginOnly(bottom: 10),
  1050. DialogTextField(
  1051. controller: userController,
  1052. title: translate('Username'),
  1053. hintText: translate('eg: admin'),
  1054. prefixIcon: DialogTextField.kUsernameIcon,
  1055. errorText: errUser.isEmpty ? null : errUser.value,
  1056. ),
  1057. PasswordWidget(
  1058. controller: pwdController,
  1059. autoFocus: false,
  1060. errorText: errPwd.isEmpty ? null : errPwd.value,
  1061. ),
  1062. ],
  1063. ).marginOnly(left: stateGlobal.isPortrait.isFalse ? 35 : 0),
  1064. ).marginOnly(top: 10),
  1065. ],
  1066. ),
  1067. );
  1068. dialogManager.dismissAll();
  1069. dialogManager.show(tag: '$sessionId-request-elevation',
  1070. (setState, close, context) {
  1071. void submit() {
  1072. if (groupValue.value == 'logon') {
  1073. if (userController.text.isEmpty) {
  1074. errUser.value = translate('Empty Username');
  1075. return;
  1076. }
  1077. if (pwdController.text.isEmpty) {
  1078. errPwd.value = translate('Empty Password');
  1079. return;
  1080. }
  1081. bind.sessionElevateWithLogon(
  1082. sessionId: sessionId,
  1083. username: userController.text,
  1084. password: pwdController.text);
  1085. } else {
  1086. bind.sessionElevateDirect(sessionId: sessionId);
  1087. }
  1088. close();
  1089. showWaitUacDialog(sessionId, dialogManager, "wait-uac");
  1090. }
  1091. return CustomAlertDialog(
  1092. title: Text(translate('Request Elevation')),
  1093. content: content,
  1094. actions: [
  1095. dialogButton(
  1096. 'Cancel',
  1097. icon: Icon(Icons.close_rounded),
  1098. onPressed: close,
  1099. isOutline: true,
  1100. ),
  1101. dialogButton(
  1102. 'OK',
  1103. icon: Icon(Icons.done_rounded),
  1104. onPressed: submit,
  1105. )
  1106. ],
  1107. onSubmit: submit,
  1108. onCancel: close,
  1109. );
  1110. });
  1111. }
  1112. void showOnBlockDialog(
  1113. SessionID sessionId,
  1114. String type,
  1115. String title,
  1116. String text,
  1117. OverlayDialogManager dialogManager,
  1118. ) {
  1119. if (dialogManager.existing('$sessionId-wait-uac') ||
  1120. dialogManager.existing('$sessionId-request-elevation')) {
  1121. return;
  1122. }
  1123. dialogManager.show(tag: '$sessionId-$type', (setState, close, context) {
  1124. void submit() {
  1125. close();
  1126. showRequestElevationDialog(sessionId, dialogManager);
  1127. }
  1128. return CustomAlertDialog(
  1129. title: null,
  1130. content: msgboxContent(type, title,
  1131. "${translate(text)}${type.contains('uac') ? '\n' : '\n\n'}${translate('request_elevation_tip')}"),
  1132. actions: [
  1133. dialogButton('Wait', onPressed: close, isOutline: true),
  1134. dialogButton('Request Elevation', onPressed: submit),
  1135. ],
  1136. onSubmit: submit,
  1137. onCancel: close,
  1138. );
  1139. });
  1140. }
  1141. void showElevationError(SessionID sessionId, String type, String title,
  1142. String text, OverlayDialogManager dialogManager) {
  1143. dialogManager.show(tag: '$sessionId-$type', (setState, close, context) {
  1144. void submit() {
  1145. close();
  1146. showRequestElevationDialog(sessionId, dialogManager);
  1147. }
  1148. return CustomAlertDialog(
  1149. title: null,
  1150. content: msgboxContent(type, title, text),
  1151. actions: [
  1152. dialogButton('Cancel', onPressed: () {
  1153. close();
  1154. }, isOutline: true),
  1155. if (text != 'No permission') dialogButton('Retry', onPressed: submit),
  1156. ],
  1157. onSubmit: submit,
  1158. onCancel: close,
  1159. );
  1160. });
  1161. }
  1162. void showWaitAcceptDialog(SessionID sessionId, String type, String title,
  1163. String text, OverlayDialogManager dialogManager) {
  1164. dialogManager.dismissAll();
  1165. dialogManager.show((setState, close, context) {
  1166. onCancel() {
  1167. closeConnection();
  1168. }
  1169. return CustomAlertDialog(
  1170. title: null,
  1171. content: msgboxContent(type, title, text),
  1172. actions: [
  1173. dialogButton('Cancel', onPressed: onCancel, isOutline: true),
  1174. ],
  1175. onCancel: onCancel,
  1176. );
  1177. });
  1178. }
  1179. void showRestartRemoteDevice(PeerInfo pi, String id, SessionID sessionId,
  1180. OverlayDialogManager dialogManager) async {
  1181. final res = await dialogManager
  1182. .show<bool>((setState, close, context) => CustomAlertDialog(
  1183. title: Row(children: [
  1184. Icon(Icons.warning_rounded, color: Colors.redAccent, size: 28),
  1185. Flexible(
  1186. child: Text(translate("Restart remote device"))
  1187. .paddingOnly(left: 10)),
  1188. ]),
  1189. content: Text(
  1190. "${translate('Are you sure you want to restart')} \n${pi.username}@${pi.hostname}($id) ?"),
  1191. actions: [
  1192. dialogButton(
  1193. "Cancel",
  1194. icon: Icon(Icons.close_rounded),
  1195. onPressed: close,
  1196. isOutline: true,
  1197. ),
  1198. dialogButton(
  1199. "OK",
  1200. icon: Icon(Icons.done_rounded),
  1201. onPressed: () => close(true),
  1202. ),
  1203. ],
  1204. onCancel: close,
  1205. onSubmit: () => close(true),
  1206. ));
  1207. if (res == true) bind.sessionRestartRemoteDevice(sessionId: sessionId);
  1208. }
  1209. showSetOSPassword(
  1210. SessionID sessionId,
  1211. bool login,
  1212. OverlayDialogManager dialogManager,
  1213. String? osPassword,
  1214. Function()? closeCallback,
  1215. ) async {
  1216. final controller = TextEditingController();
  1217. osPassword ??=
  1218. await bind.sessionGetOption(sessionId: sessionId, arg: 'os-password') ??
  1219. '';
  1220. var autoLogin =
  1221. await bind.sessionGetOption(sessionId: sessionId, arg: 'auto-login') !=
  1222. '';
  1223. controller.text = osPassword;
  1224. dialogManager.show((setState, close, context) {
  1225. closeWithCallback([dynamic]) {
  1226. close();
  1227. if (closeCallback != null) closeCallback();
  1228. }
  1229. submit() {
  1230. var text = controller.text.trim();
  1231. bind.sessionPeerOption(
  1232. sessionId: sessionId, name: 'os-password', value: text);
  1233. bind.sessionPeerOption(
  1234. sessionId: sessionId,
  1235. name: 'auto-login',
  1236. value: autoLogin ? 'Y' : '');
  1237. if (text != '' && login) {
  1238. bind.sessionInputOsPassword(sessionId: sessionId, value: text);
  1239. }
  1240. closeWithCallback();
  1241. }
  1242. return CustomAlertDialog(
  1243. title: Row(
  1244. mainAxisAlignment: MainAxisAlignment.center,
  1245. children: [
  1246. Icon(Icons.password_rounded, color: MyTheme.accent),
  1247. Text(translate('OS Password')).paddingOnly(left: 10),
  1248. ],
  1249. ),
  1250. content: Column(
  1251. mainAxisSize: MainAxisSize.min,
  1252. children: [
  1253. PasswordWidget(controller: controller),
  1254. CheckboxListTile(
  1255. contentPadding: const EdgeInsets.all(0),
  1256. dense: true,
  1257. controlAffinity: ListTileControlAffinity.leading,
  1258. title: Text(
  1259. translate('Auto Login'),
  1260. ),
  1261. value: autoLogin,
  1262. onChanged: (v) {
  1263. if (v == null) return;
  1264. setState(() => autoLogin = v);
  1265. },
  1266. ),
  1267. ],
  1268. ),
  1269. actions: [
  1270. dialogButton(
  1271. "Cancel",
  1272. icon: Icon(Icons.close_rounded),
  1273. onPressed: closeWithCallback,
  1274. isOutline: true,
  1275. ),
  1276. dialogButton(
  1277. "OK",
  1278. icon: Icon(Icons.done_rounded),
  1279. onPressed: submit,
  1280. ),
  1281. ],
  1282. onSubmit: submit,
  1283. onCancel: closeWithCallback,
  1284. );
  1285. });
  1286. }
  1287. showSetOSAccount(
  1288. SessionID sessionId,
  1289. OverlayDialogManager dialogManager,
  1290. ) async {
  1291. final usernameController = TextEditingController();
  1292. final passwdController = TextEditingController();
  1293. var username =
  1294. await bind.sessionGetOption(sessionId: sessionId, arg: 'os-username') ??
  1295. '';
  1296. var password =
  1297. await bind.sessionGetOption(sessionId: sessionId, arg: 'os-password') ??
  1298. '';
  1299. usernameController.text = username;
  1300. passwdController.text = password;
  1301. dialogManager.show((setState, close, context) {
  1302. submit() {
  1303. final username = usernameController.text.trim();
  1304. final password = usernameController.text.trim();
  1305. bind.sessionPeerOption(
  1306. sessionId: sessionId, name: 'os-username', value: username);
  1307. bind.sessionPeerOption(
  1308. sessionId: sessionId, name: 'os-password', value: password);
  1309. close();
  1310. }
  1311. descWidget(String text) {
  1312. return Column(
  1313. children: [
  1314. Align(
  1315. alignment: Alignment.centerLeft,
  1316. child: Text(
  1317. text,
  1318. maxLines: 3,
  1319. softWrap: true,
  1320. overflow: TextOverflow.ellipsis,
  1321. style: TextStyle(fontSize: 16),
  1322. ),
  1323. ),
  1324. Container(
  1325. height: 8,
  1326. ),
  1327. ],
  1328. );
  1329. }
  1330. return CustomAlertDialog(
  1331. title: Row(
  1332. mainAxisAlignment: MainAxisAlignment.center,
  1333. children: [
  1334. Icon(Icons.password_rounded, color: MyTheme.accent),
  1335. Text(translate('OS Account')).paddingOnly(left: 10),
  1336. ],
  1337. ),
  1338. content: Column(
  1339. mainAxisSize: MainAxisSize.min,
  1340. children: [
  1341. descWidget(translate("os_account_desk_tip")),
  1342. DialogTextField(
  1343. title: translate(DialogTextField.kUsernameTitle),
  1344. controller: usernameController,
  1345. prefixIcon: DialogTextField.kUsernameIcon,
  1346. errorText: null,
  1347. ),
  1348. PasswordWidget(controller: passwdController),
  1349. ],
  1350. ),
  1351. actions: [
  1352. dialogButton(
  1353. "Cancel",
  1354. icon: Icon(Icons.close_rounded),
  1355. onPressed: close,
  1356. isOutline: true,
  1357. ),
  1358. dialogButton(
  1359. "OK",
  1360. icon: Icon(Icons.done_rounded),
  1361. onPressed: submit,
  1362. ),
  1363. ],
  1364. onSubmit: submit,
  1365. onCancel: close,
  1366. );
  1367. });
  1368. }
  1369. showAuditDialog(FFI ffi) async {
  1370. final controller = TextEditingController(text: ffi.auditNote);
  1371. ffi.dialogManager.show((setState, close, context) {
  1372. submit() {
  1373. var text = controller.text;
  1374. bind.sessionSendNote(sessionId: ffi.sessionId, note: text);
  1375. ffi.auditNote = text;
  1376. close();
  1377. }
  1378. late final focusNode = FocusNode(
  1379. onKey: (FocusNode node, RawKeyEvent evt) {
  1380. if (evt.logicalKey.keyLabel == 'Enter') {
  1381. if (evt is RawKeyDownEvent) {
  1382. int pos = controller.selection.base.offset;
  1383. controller.text =
  1384. '${controller.text.substring(0, pos)}\n${controller.text.substring(pos)}';
  1385. controller.selection =
  1386. TextSelection.fromPosition(TextPosition(offset: pos + 1));
  1387. }
  1388. return KeyEventResult.handled;
  1389. }
  1390. if (evt.logicalKey.keyLabel == 'Esc') {
  1391. if (evt is RawKeyDownEvent) {
  1392. close();
  1393. }
  1394. return KeyEventResult.handled;
  1395. } else {
  1396. return KeyEventResult.ignored;
  1397. }
  1398. },
  1399. );
  1400. return CustomAlertDialog(
  1401. title: Text(translate('Note')),
  1402. content: SizedBox(
  1403. width: 250,
  1404. height: 120,
  1405. child: TextField(
  1406. autofocus: true,
  1407. keyboardType: TextInputType.multiline,
  1408. textInputAction: TextInputAction.newline,
  1409. decoration: const InputDecoration.collapsed(
  1410. hintText: 'input note here',
  1411. ),
  1412. maxLines: null,
  1413. maxLength: 256,
  1414. controller: controller,
  1415. focusNode: focusNode,
  1416. ).workaroundFreezeLinuxMint()),
  1417. actions: [
  1418. dialogButton('Cancel', onPressed: close, isOutline: true),
  1419. dialogButton('OK', onPressed: submit)
  1420. ],
  1421. onSubmit: submit,
  1422. onCancel: close,
  1423. );
  1424. });
  1425. }
  1426. void showConfirmSwitchSidesDialog(
  1427. SessionID sessionId, String id, OverlayDialogManager dialogManager) async {
  1428. dialogManager.show((setState, close, context) {
  1429. submit() async {
  1430. await bind.sessionSwitchSides(sessionId: sessionId);
  1431. closeConnection(id: id);
  1432. }
  1433. return CustomAlertDialog(
  1434. content: msgboxContent('info', 'Switch Sides',
  1435. 'Please confirm if you want to share your desktop?'),
  1436. actions: [
  1437. dialogButton('Cancel', onPressed: close, isOutline: true),
  1438. dialogButton('OK', onPressed: submit),
  1439. ],
  1440. onSubmit: submit,
  1441. onCancel: close,
  1442. );
  1443. });
  1444. }
  1445. customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async {
  1446. double initQuality = kDefaultQuality;
  1447. double initFps = kDefaultFps;
  1448. bool qualitySet = false;
  1449. bool fpsSet = false;
  1450. bool? direct;
  1451. try {
  1452. direct =
  1453. ConnectionTypeState.find(id).direct.value == ConnectionType.strDirect;
  1454. } catch (_) {}
  1455. bool hideFps = (await bind.mainIsUsingPublicServer() && direct != true) ||
  1456. versionCmp(ffi.ffiModel.pi.version, '1.2.0') < 0;
  1457. bool hideMoreQuality =
  1458. (await bind.mainIsUsingPublicServer() && direct != true) ||
  1459. versionCmp(ffi.ffiModel.pi.version, '1.2.2') < 0;
  1460. setCustomValues({double? quality, double? fps}) async {
  1461. debugPrint("setCustomValues quality:$quality, fps:$fps");
  1462. if (quality != null) {
  1463. qualitySet = true;
  1464. await bind.sessionSetCustomImageQuality(
  1465. sessionId: sessionId, value: quality.toInt());
  1466. }
  1467. if (fps != null) {
  1468. fpsSet = true;
  1469. await bind.sessionSetCustomFps(sessionId: sessionId, fps: fps.toInt());
  1470. }
  1471. if (!qualitySet) {
  1472. qualitySet = true;
  1473. await bind.sessionSetCustomImageQuality(
  1474. sessionId: sessionId, value: initQuality.toInt());
  1475. }
  1476. if (!hideFps && !fpsSet) {
  1477. fpsSet = true;
  1478. await bind.sessionSetCustomFps(
  1479. sessionId: sessionId, fps: initFps.toInt());
  1480. }
  1481. }
  1482. final btnClose = dialogButton('Close', onPressed: () async {
  1483. await setCustomValues();
  1484. ffi.dialogManager.dismissAll();
  1485. });
  1486. // quality
  1487. final quality = await bind.sessionGetCustomImageQuality(sessionId: sessionId);
  1488. initQuality = quality != null && quality.isNotEmpty
  1489. ? quality[0].toDouble()
  1490. : kDefaultQuality;
  1491. if (initQuality < kMinQuality ||
  1492. initQuality > (!hideMoreQuality ? kMaxMoreQuality : kMaxQuality)) {
  1493. initQuality = kDefaultQuality;
  1494. }
  1495. // fps
  1496. final fpsOption =
  1497. await bind.sessionGetOption(sessionId: sessionId, arg: 'custom-fps');
  1498. initFps = fpsOption == null
  1499. ? kDefaultFps
  1500. : double.tryParse(fpsOption) ?? kDefaultFps;
  1501. if (initFps < kMinFps || initFps > kMaxFps) {
  1502. initFps = kDefaultFps;
  1503. }
  1504. final content = customImageQualityWidget(
  1505. initQuality: initQuality,
  1506. initFps: initFps,
  1507. setQuality: (v) => setCustomValues(quality: v),
  1508. setFps: (v) => setCustomValues(fps: v),
  1509. showFps: !hideFps,
  1510. showMoreQuality: !hideMoreQuality);
  1511. msgBoxCommon(ffi.dialogManager, 'Custom Image Quality', content, [btnClose]);
  1512. }
  1513. trackpadSpeedDialog(SessionID sessionId, FFI ffi) async {
  1514. int initSpeed = ffi.inputModel.trackpadSpeed;
  1515. final curSpeed = SimpleWrapper(initSpeed);
  1516. final btnClose = dialogButton('Close', onPressed: () async {
  1517. if (curSpeed.value <= kMaxTrackpadSpeed &&
  1518. curSpeed.value >= kMinTrackpadSpeed &&
  1519. curSpeed.value != initSpeed) {
  1520. await bind.sessionSetTrackpadSpeed(
  1521. sessionId: sessionId, value: curSpeed.value);
  1522. await ffi.inputModel.updateTrackpadSpeed();
  1523. }
  1524. ffi.dialogManager.dismissAll();
  1525. });
  1526. msgBoxCommon(
  1527. ffi.dialogManager,
  1528. 'Trackpad speed',
  1529. TrackpadSpeedWidget(
  1530. value: curSpeed,
  1531. ),
  1532. [btnClose]);
  1533. }
  1534. void deleteConfirmDialog(Function onSubmit, String title) async {
  1535. gFFI.dialogManager.show(
  1536. (setState, close, context) {
  1537. submit() async {
  1538. await onSubmit();
  1539. close();
  1540. }
  1541. return CustomAlertDialog(
  1542. title: Row(
  1543. mainAxisAlignment: MainAxisAlignment.center,
  1544. children: [
  1545. Icon(
  1546. Icons.delete_rounded,
  1547. color: Colors.red,
  1548. ),
  1549. Expanded(
  1550. child: Text(title, overflow: TextOverflow.ellipsis).paddingOnly(
  1551. left: 10,
  1552. ),
  1553. ),
  1554. ],
  1555. ),
  1556. content: SizedBox.shrink(),
  1557. actions: [
  1558. dialogButton(
  1559. "Cancel",
  1560. icon: Icon(Icons.close_rounded),
  1561. onPressed: close,
  1562. isOutline: true,
  1563. ),
  1564. dialogButton(
  1565. "OK",
  1566. icon: Icon(Icons.done_rounded),
  1567. onPressed: submit,
  1568. ),
  1569. ],
  1570. onSubmit: submit,
  1571. onCancel: close,
  1572. );
  1573. },
  1574. );
  1575. }
  1576. void editAbTagDialog(
  1577. List<dynamic> currentTags, Function(List<dynamic>) onSubmit) {
  1578. var isInProgress = false;
  1579. final tags = List.of(gFFI.abModel.currentAbTags);
  1580. var selectedTag = currentTags.obs;
  1581. gFFI.dialogManager.show((setState, close, context) {
  1582. submit() async {
  1583. setState(() {
  1584. isInProgress = true;
  1585. });
  1586. await onSubmit(selectedTag);
  1587. close();
  1588. }
  1589. return CustomAlertDialog(
  1590. title: Text(translate("Edit Tag")),
  1591. content: Column(
  1592. crossAxisAlignment: CrossAxisAlignment.start,
  1593. children: [
  1594. Container(
  1595. padding: const EdgeInsets.symmetric(vertical: 8.0),
  1596. child: Wrap(
  1597. children: tags
  1598. .map((e) => AddressBookTag(
  1599. name: e,
  1600. tags: selectedTag,
  1601. onTap: () {
  1602. if (selectedTag.contains(e)) {
  1603. selectedTag.remove(e);
  1604. } else {
  1605. selectedTag.add(e);
  1606. }
  1607. },
  1608. showActionMenu: false))
  1609. .toList(growable: false),
  1610. ),
  1611. ),
  1612. // NOT use Offstage to wrap LinearProgressIndicator
  1613. if (isInProgress) const LinearProgressIndicator(),
  1614. ],
  1615. ),
  1616. actions: [
  1617. dialogButton("Cancel", onPressed: close, isOutline: true),
  1618. dialogButton("OK", onPressed: submit),
  1619. ],
  1620. onSubmit: submit,
  1621. onCancel: close,
  1622. );
  1623. });
  1624. }
  1625. void renameDialog(
  1626. {required String oldName,
  1627. FormFieldValidator<String>? validator,
  1628. required ValueChanged<String> onSubmit,
  1629. Function? onCancel}) async {
  1630. RxBool isInProgress = false.obs;
  1631. var controller = TextEditingController(text: oldName);
  1632. final formKey = GlobalKey<FormState>();
  1633. gFFI.dialogManager.show((setState, close, context) {
  1634. submit() async {
  1635. String text = controller.text.trim();
  1636. if (validator != null && formKey.currentState?.validate() == false) {
  1637. return;
  1638. }
  1639. isInProgress.value = true;
  1640. onSubmit(text);
  1641. close();
  1642. isInProgress.value = false;
  1643. }
  1644. cancel() {
  1645. onCancel?.call();
  1646. close();
  1647. }
  1648. return CustomAlertDialog(
  1649. title: Row(
  1650. mainAxisAlignment: MainAxisAlignment.center,
  1651. children: [
  1652. Icon(Icons.edit_rounded, color: MyTheme.accent),
  1653. Text(translate('Rename')).paddingOnly(left: 10),
  1654. ],
  1655. ),
  1656. content: Column(
  1657. crossAxisAlignment: CrossAxisAlignment.start,
  1658. children: [
  1659. Container(
  1660. child: Form(
  1661. key: formKey,
  1662. child: TextFormField(
  1663. controller: controller,
  1664. autofocus: true,
  1665. decoration: InputDecoration(labelText: translate('Name')),
  1666. validator: validator,
  1667. ).workaroundFreezeLinuxMint(),
  1668. ),
  1669. ),
  1670. // NOT use Offstage to wrap LinearProgressIndicator
  1671. Obx(() =>
  1672. isInProgress.value ? const LinearProgressIndicator() : Offstage())
  1673. ],
  1674. ),
  1675. actions: [
  1676. dialogButton(
  1677. "Cancel",
  1678. icon: Icon(Icons.close_rounded),
  1679. onPressed: cancel,
  1680. isOutline: true,
  1681. ),
  1682. dialogButton(
  1683. "OK",
  1684. icon: Icon(Icons.done_rounded),
  1685. onPressed: submit,
  1686. ),
  1687. ],
  1688. onSubmit: submit,
  1689. onCancel: cancel,
  1690. );
  1691. });
  1692. }
  1693. void changeBot({Function()? callback}) async {
  1694. if (bind.mainHasValidBotSync()) {
  1695. await bind.mainSetOption(key: "bot", value: "");
  1696. callback?.call();
  1697. return;
  1698. }
  1699. String errorText = '';
  1700. bool loading = false;
  1701. final controller = TextEditingController();
  1702. gFFI.dialogManager.show((setState, close, context) {
  1703. onVerify() async {
  1704. final token = controller.text.trim();
  1705. if (token == "") return;
  1706. loading = true;
  1707. errorText = '';
  1708. setState(() {});
  1709. final error = await bind.mainVerifyBot(token: token);
  1710. if (error == "") {
  1711. callback?.call();
  1712. close();
  1713. } else {
  1714. errorText = translate(error);
  1715. loading = false;
  1716. setState(() {});
  1717. }
  1718. }
  1719. final codeField = TextField(
  1720. autofocus: true,
  1721. controller: controller,
  1722. decoration: InputDecoration(
  1723. hintText: translate('Token'),
  1724. ),
  1725. ).workaroundFreezeLinuxMint();
  1726. return CustomAlertDialog(
  1727. title: Text(translate("Telegram bot")),
  1728. content: Column(
  1729. crossAxisAlignment: CrossAxisAlignment.start,
  1730. children: [
  1731. SelectableText(translate("enable-bot-desc"),
  1732. style: TextStyle(fontSize: 12))
  1733. .marginOnly(bottom: 12),
  1734. Row(children: [Expanded(child: codeField)]),
  1735. if (errorText != '')
  1736. Text(errorText, style: TextStyle(color: Colors.red))
  1737. .marginOnly(top: 12),
  1738. ],
  1739. ),
  1740. actions: [
  1741. dialogButton("Cancel", onPressed: close, isOutline: true),
  1742. loading
  1743. ? CircularProgressIndicator()
  1744. : dialogButton("OK", onPressed: onVerify),
  1745. ],
  1746. onCancel: close,
  1747. );
  1748. });
  1749. }
  1750. void change2fa({Function()? callback}) async {
  1751. if (bind.mainHasValid2FaSync()) {
  1752. await bind.mainSetOption(key: "2fa", value: "");
  1753. await bind.mainClearTrustedDevices();
  1754. callback?.call();
  1755. return;
  1756. }
  1757. var new2fa = (await bind.mainGenerate2Fa());
  1758. final secretRegex = RegExp(r'secret=([^&]+)');
  1759. final secret = secretRegex.firstMatch(new2fa)?.group(1);
  1760. String? errorText;
  1761. final controller = TextEditingController();
  1762. gFFI.dialogManager.show((setState, close, context) {
  1763. onVerify() async {
  1764. if (await bind.mainVerify2Fa(code: controller.text.trim())) {
  1765. callback?.call();
  1766. close();
  1767. } else {
  1768. errorText = translate('wrong-2fa-code');
  1769. }
  1770. }
  1771. final codeField = Dialog2FaField(
  1772. controller: controller,
  1773. errorText: errorText,
  1774. onChanged: () => setState(() => errorText = null),
  1775. title: translate('Verification code'),
  1776. readyCallback: () {
  1777. onVerify();
  1778. setState(() {});
  1779. },
  1780. );
  1781. getOnSubmit() => codeField.isReady ? onVerify : null;
  1782. return CustomAlertDialog(
  1783. title: Text(translate("enable-2fa-title")),
  1784. content: Column(
  1785. crossAxisAlignment: CrossAxisAlignment.start,
  1786. children: [
  1787. SelectableText(translate("enable-2fa-desc"),
  1788. style: TextStyle(fontSize: 12))
  1789. .marginOnly(bottom: 12),
  1790. SizedBox(
  1791. width: 160,
  1792. height: 160,
  1793. child: QrImageView(
  1794. backgroundColor: Colors.white,
  1795. data: new2fa,
  1796. version: QrVersions.auto,
  1797. size: 160,
  1798. gapless: false,
  1799. )).marginOnly(bottom: 6),
  1800. SelectableText(secret ?? '', style: TextStyle(fontSize: 12))
  1801. .marginOnly(bottom: 12),
  1802. Row(children: [Expanded(child: codeField)]),
  1803. ],
  1804. ),
  1805. actions: [
  1806. dialogButton("Cancel", onPressed: close, isOutline: true),
  1807. dialogButton("OK", onPressed: getOnSubmit()),
  1808. ],
  1809. onCancel: close,
  1810. );
  1811. });
  1812. }
  1813. void enter2FaDialog(
  1814. SessionID sessionId, OverlayDialogManager dialogManager) async {
  1815. final controller = TextEditingController();
  1816. final RxBool submitReady = false.obs;
  1817. final RxBool trustThisDevice = false.obs;
  1818. dialogManager.dismissAll();
  1819. dialogManager.show((setState, close, context) {
  1820. cancel() {
  1821. close();
  1822. closeConnection();
  1823. }
  1824. submit() {
  1825. gFFI.send2FA(sessionId, controller.text.trim(), trustThisDevice.value);
  1826. close();
  1827. dialogManager.showLoading(translate('Logging in...'),
  1828. onCancel: closeConnection);
  1829. }
  1830. late Dialog2FaField codeField;
  1831. codeField = Dialog2FaField(
  1832. controller: controller,
  1833. title: translate('Verification code'),
  1834. onChanged: () => submitReady.value = codeField.isReady,
  1835. );
  1836. final trustField = Obx(() => CheckboxListTile(
  1837. contentPadding: const EdgeInsets.all(0),
  1838. dense: true,
  1839. controlAffinity: ListTileControlAffinity.leading,
  1840. title: Text(translate("Trust this device")),
  1841. value: trustThisDevice.value,
  1842. onChanged: (value) {
  1843. if (value == null) return;
  1844. trustThisDevice.value = value;
  1845. },
  1846. ));
  1847. return CustomAlertDialog(
  1848. title: Text(translate('enter-2fa-title')),
  1849. content: Column(
  1850. children: [
  1851. codeField,
  1852. if (bind.sessionGetEnableTrustedDevices(sessionId: sessionId))
  1853. trustField,
  1854. ],
  1855. ),
  1856. actions: [
  1857. dialogButton('Cancel',
  1858. onPressed: cancel,
  1859. isOutline: true,
  1860. style: TextStyle(
  1861. color: Theme.of(context).textTheme.bodyMedium?.color)),
  1862. Obx(() => dialogButton(
  1863. 'OK',
  1864. onPressed: submitReady.isTrue ? submit : null,
  1865. )),
  1866. ],
  1867. onSubmit: submit,
  1868. onCancel: cancel);
  1869. });
  1870. }
  1871. // This dialog should not be dismissed, otherwise it will be black screen, have not reproduced this.
  1872. void showWindowsSessionsDialog(
  1873. String type,
  1874. String title,
  1875. String text,
  1876. OverlayDialogManager dialogManager,
  1877. SessionID sessionId,
  1878. String peerId,
  1879. String sessions) {
  1880. List<dynamic> sessionsList = [];
  1881. try {
  1882. sessionsList = json.decode(sessions);
  1883. } catch (e) {
  1884. print(e);
  1885. }
  1886. List<String> sids = [];
  1887. List<String> names = [];
  1888. for (var session in sessionsList) {
  1889. sids.add(session['sid']);
  1890. names.add(session['name']);
  1891. }
  1892. String selectedUserValue = sids.first;
  1893. dialogManager.dismissAll();
  1894. dialogManager.show((setState, close, context) {
  1895. submit() {
  1896. bind.sessionSendSelectedSessionId(
  1897. sessionId: sessionId, sid: selectedUserValue);
  1898. close();
  1899. }
  1900. return CustomAlertDialog(
  1901. title: null,
  1902. content: msgboxContent(type, title, text),
  1903. actions: [
  1904. ComboBox(
  1905. keys: sids,
  1906. values: names,
  1907. initialKey: selectedUserValue,
  1908. onChanged: (value) {
  1909. selectedUserValue = value;
  1910. }),
  1911. dialogButton('Connect', onPressed: submit, isOutline: false),
  1912. ],
  1913. );
  1914. });
  1915. }
  1916. void addPeersToAbDialog(
  1917. List<Peer> peers,
  1918. ) async {
  1919. Future<bool> addTo(String abname) async {
  1920. final mapList = peers.map((e) {
  1921. var json = e.toJson();
  1922. // remove password when add to another address book to avoid re-share
  1923. json.remove('password');
  1924. json.remove('hash');
  1925. return json;
  1926. }).toList();
  1927. final errMsg = await gFFI.abModel.addPeersTo(mapList, abname);
  1928. if (errMsg == null) {
  1929. showToast(translate('Successful'));
  1930. return true;
  1931. } else {
  1932. BotToast.showText(text: errMsg, contentColor: Colors.red);
  1933. return false;
  1934. }
  1935. }
  1936. // if only one address book and it is personal, add to it directly
  1937. if (gFFI.abModel.addressbooks.length == 1 &&
  1938. gFFI.abModel.current.isPersonal()) {
  1939. await addTo(gFFI.abModel.currentName.value);
  1940. return;
  1941. }
  1942. RxBool isInProgress = false.obs;
  1943. final names = gFFI.abModel.addressBooksCanWrite();
  1944. RxString currentName = gFFI.abModel.currentName.value.obs;
  1945. TextEditingController controller = TextEditingController();
  1946. if (gFFI.peerTabModel.currentTab == PeerTabIndex.ab.index) {
  1947. names.remove(currentName.value);
  1948. }
  1949. if (names.isEmpty) {
  1950. debugPrint('no address book to add peers to, should not happen');
  1951. return;
  1952. }
  1953. if (!names.contains(currentName.value)) {
  1954. currentName.value = names[0];
  1955. }
  1956. gFFI.dialogManager.show((setState, close, context) {
  1957. submit() async {
  1958. if (controller.text != gFFI.abModel.translatedName(currentName.value)) {
  1959. BotToast.showText(
  1960. text: 'illegal address book name: ${controller.text}',
  1961. contentColor: Colors.red);
  1962. return;
  1963. }
  1964. isInProgress.value = true;
  1965. if (await addTo(currentName.value)) {
  1966. close();
  1967. }
  1968. isInProgress.value = false;
  1969. }
  1970. cancel() {
  1971. close();
  1972. }
  1973. return CustomAlertDialog(
  1974. title: Row(
  1975. mainAxisAlignment: MainAxisAlignment.center,
  1976. children: [
  1977. Icon(IconFont.addressBook, color: MyTheme.accent),
  1978. Text(translate('Add to address book')).paddingOnly(left: 10),
  1979. ],
  1980. ),
  1981. content: Obx(() => Column(
  1982. crossAxisAlignment: CrossAxisAlignment.center,
  1983. children: [
  1984. // https://github.com/flutter/flutter/issues/145081
  1985. DropdownMenu(
  1986. initialSelection: currentName.value,
  1987. onSelected: (value) {
  1988. if (value != null) {
  1989. currentName.value = value;
  1990. }
  1991. },
  1992. dropdownMenuEntries: names
  1993. .map((e) => DropdownMenuEntry(
  1994. value: e, label: gFFI.abModel.translatedName(e)))
  1995. .toList(),
  1996. inputDecorationTheme: InputDecorationTheme(
  1997. isDense: true, border: UnderlineInputBorder()),
  1998. enableFilter: true,
  1999. controller: controller,
  2000. ),
  2001. // NOT use Offstage to wrap LinearProgressIndicator
  2002. isInProgress.value ? const LinearProgressIndicator() : Offstage()
  2003. ],
  2004. )),
  2005. actions: [
  2006. dialogButton(
  2007. "Cancel",
  2008. icon: Icon(Icons.close_rounded),
  2009. onPressed: cancel,
  2010. isOutline: true,
  2011. ),
  2012. dialogButton(
  2013. "OK",
  2014. icon: Icon(Icons.done_rounded),
  2015. onPressed: submit,
  2016. ),
  2017. ],
  2018. onSubmit: submit,
  2019. onCancel: cancel,
  2020. );
  2021. });
  2022. }
  2023. void setSharedAbPasswordDialog(String abName, Peer peer) {
  2024. TextEditingController controller = TextEditingController(text: '');
  2025. RxBool isInProgress = false.obs;
  2026. RxBool isInputEmpty = true.obs;
  2027. bool passwordVisible = false;
  2028. controller.addListener(() {
  2029. isInputEmpty.value = controller.text.isEmpty;
  2030. });
  2031. gFFI.dialogManager.show((setState, close, context) {
  2032. change(String password) async {
  2033. isInProgress.value = true;
  2034. bool res =
  2035. await gFFI.abModel.changeSharedPassword(abName, peer.id, password);
  2036. isInProgress.value = false;
  2037. if (res) {
  2038. showToast(translate('Successful'));
  2039. }
  2040. close();
  2041. }
  2042. cancel() {
  2043. close();
  2044. }
  2045. return CustomAlertDialog(
  2046. title: Row(
  2047. mainAxisAlignment: MainAxisAlignment.center,
  2048. children: [
  2049. Icon(Icons.key, color: MyTheme.accent),
  2050. Text(translate(peer.password.isEmpty
  2051. ? 'Set shared password'
  2052. : 'Change Password'))
  2053. .paddingOnly(left: 10),
  2054. ],
  2055. ),
  2056. content: Obx(() => Column(children: [
  2057. TextField(
  2058. controller: controller,
  2059. autofocus: true,
  2060. obscureText: !passwordVisible,
  2061. decoration: InputDecoration(
  2062. suffixIcon: IconButton(
  2063. icon: Icon(
  2064. passwordVisible ? Icons.visibility : Icons.visibility_off,
  2065. color: MyTheme.lightTheme.primaryColor),
  2066. onPressed: () {
  2067. setState(() {
  2068. passwordVisible = !passwordVisible;
  2069. });
  2070. },
  2071. ),
  2072. ),
  2073. ).workaroundFreezeLinuxMint(),
  2074. if (!gFFI.abModel.current.isPersonal())
  2075. Row(children: [
  2076. Icon(Icons.info, color: Colors.amber).marginOnly(right: 4),
  2077. Text(
  2078. translate('share_warning_tip'),
  2079. style: TextStyle(fontSize: 12),
  2080. )
  2081. ]).marginSymmetric(vertical: 10),
  2082. // NOT use Offstage to wrap LinearProgressIndicator
  2083. isInProgress.value ? const LinearProgressIndicator() : Offstage()
  2084. ])),
  2085. actions: [
  2086. dialogButton(
  2087. "Cancel",
  2088. icon: Icon(Icons.close_rounded),
  2089. onPressed: cancel,
  2090. isOutline: true,
  2091. ),
  2092. if (peer.password.isNotEmpty)
  2093. dialogButton(
  2094. "Remove",
  2095. icon: Icon(Icons.delete_outline_rounded),
  2096. onPressed: () => change(''),
  2097. buttonStyle: ButtonStyle(
  2098. backgroundColor: MaterialStatePropertyAll(Colors.red)),
  2099. ),
  2100. Obx(() => dialogButton(
  2101. "OK",
  2102. icon: Icon(Icons.done_rounded),
  2103. onPressed:
  2104. isInputEmpty.value ? null : () => change(controller.text),
  2105. )),
  2106. ],
  2107. onSubmit: isInputEmpty.value ? null : () => change(controller.text),
  2108. onCancel: cancel,
  2109. );
  2110. });
  2111. }
  2112. void CommonConfirmDialog(OverlayDialogManager dialogManager, String content,
  2113. VoidCallback onConfirm) {
  2114. dialogManager.show((setState, close, context) {
  2115. submit() {
  2116. close();
  2117. onConfirm.call();
  2118. }
  2119. return CustomAlertDialog(
  2120. content: Row(
  2121. children: [
  2122. Expanded(
  2123. child: Text(content,
  2124. style: const TextStyle(fontSize: 15),
  2125. textAlign: TextAlign.start),
  2126. ),
  2127. ],
  2128. ).marginOnly(bottom: 12),
  2129. actions: [
  2130. dialogButton(translate("Cancel"), onPressed: close, isOutline: true),
  2131. dialogButton(translate("OK"), onPressed: submit),
  2132. ],
  2133. onSubmit: submit,
  2134. onCancel: close,
  2135. );
  2136. });
  2137. }
  2138. void changeUnlockPinDialog(String oldPin, Function() callback) {
  2139. final pinController = TextEditingController(text: oldPin);
  2140. final confirmController = TextEditingController(text: oldPin);
  2141. String? pinErrorText;
  2142. String? confirmationErrorText;
  2143. final maxLength = bind.mainMaxEncryptLen();
  2144. gFFI.dialogManager.show((setState, close, context) {
  2145. submit() async {
  2146. pinErrorText = null;
  2147. confirmationErrorText = null;
  2148. final pin = pinController.text.trim();
  2149. final confirm = confirmController.text.trim();
  2150. if (pin != confirm) {
  2151. setState(() {
  2152. confirmationErrorText =
  2153. translate('The confirmation is not identical.');
  2154. });
  2155. return;
  2156. }
  2157. final errorMsg = bind.mainSetUnlockPin(pin: pin);
  2158. if (errorMsg != '') {
  2159. setState(() {
  2160. pinErrorText = translate(errorMsg);
  2161. });
  2162. return;
  2163. }
  2164. callback.call();
  2165. close();
  2166. }
  2167. return CustomAlertDialog(
  2168. title: Text(translate("Set PIN")),
  2169. content: Column(
  2170. children: [
  2171. DialogTextField(
  2172. title: 'PIN',
  2173. controller: pinController,
  2174. obscureText: true,
  2175. errorText: pinErrorText,
  2176. maxLength: maxLength,
  2177. ),
  2178. DialogTextField(
  2179. title: translate('Confirmation'),
  2180. controller: confirmController,
  2181. obscureText: true,
  2182. errorText: confirmationErrorText,
  2183. maxLength: maxLength,
  2184. )
  2185. ],
  2186. ).marginOnly(bottom: 12),
  2187. actions: [
  2188. dialogButton(translate("Cancel"), onPressed: close, isOutline: true),
  2189. dialogButton(translate("OK"), onPressed: submit),
  2190. ],
  2191. onSubmit: submit,
  2192. onCancel: close,
  2193. );
  2194. });
  2195. }
  2196. void checkUnlockPinDialog(String correctPin, Function() passCallback) {
  2197. final controller = TextEditingController();
  2198. String? errorText;
  2199. gFFI.dialogManager.show((setState, close, context) {
  2200. submit() async {
  2201. final pin = controller.text.trim();
  2202. if (correctPin != pin) {
  2203. setState(() {
  2204. errorText = translate('Wrong PIN');
  2205. });
  2206. return;
  2207. }
  2208. passCallback.call();
  2209. close();
  2210. }
  2211. return CustomAlertDialog(
  2212. content: Row(
  2213. children: [
  2214. Expanded(
  2215. child: PasswordWidget(
  2216. title: 'PIN',
  2217. controller: controller,
  2218. errorText: errorText,
  2219. hintText: '',
  2220. ))
  2221. ],
  2222. ).marginOnly(bottom: 12),
  2223. actions: [
  2224. dialogButton(translate("Cancel"), onPressed: close, isOutline: true),
  2225. dialogButton(translate("OK"), onPressed: submit),
  2226. ],
  2227. onSubmit: submit,
  2228. onCancel: close,
  2229. );
  2230. });
  2231. }
  2232. void confrimDeleteTrustedDevicesDialog(
  2233. RxList<TrustedDevice> trustedDevices, RxList<Uint8List> selectedDevices) {
  2234. CommonConfirmDialog(gFFI.dialogManager, '${translate('Confirm Delete')}?',
  2235. () async {
  2236. if (selectedDevices.isEmpty) return;
  2237. if (selectedDevices.length == trustedDevices.length) {
  2238. await bind.mainClearTrustedDevices();
  2239. trustedDevices.clear();
  2240. selectedDevices.clear();
  2241. } else {
  2242. final json = jsonEncode(selectedDevices.map((e) => e.toList()).toList());
  2243. await bind.mainRemoveTrustedDevices(json: json);
  2244. trustedDevices.removeWhere((element) {
  2245. return selectedDevices.contains(element.hwid);
  2246. });
  2247. selectedDevices.clear();
  2248. }
  2249. });
  2250. }
  2251. void manageTrustedDeviceDialog() async {
  2252. RxList<TrustedDevice> trustedDevices = (await TrustedDevice.get()).obs;
  2253. RxList<Uint8List> selectedDevices = RxList.empty();
  2254. gFFI.dialogManager.show((setState, close, context) {
  2255. return CustomAlertDialog(
  2256. title: Text(translate("Manage trusted devices")),
  2257. content: trustedDevicesTable(trustedDevices, selectedDevices),
  2258. actions: [
  2259. Obx(() => dialogButton(translate("Delete"),
  2260. onPressed: selectedDevices.isEmpty
  2261. ? null
  2262. : () {
  2263. confrimDeleteTrustedDevicesDialog(
  2264. trustedDevices,
  2265. selectedDevices,
  2266. );
  2267. },
  2268. isOutline: false)
  2269. .marginOnly(top: 12)),
  2270. dialogButton(translate("Close"), onPressed: close, isOutline: true)
  2271. .marginOnly(top: 12),
  2272. ],
  2273. onCancel: close,
  2274. );
  2275. });
  2276. }
  2277. class TrustedDevice {
  2278. late final Uint8List hwid;
  2279. late final int time;
  2280. late final String id;
  2281. late final String name;
  2282. late final String platform;
  2283. TrustedDevice.fromJson(Map<String, dynamic> json) {
  2284. final hwidList = json['hwid'] as List<dynamic>;
  2285. hwid = Uint8List.fromList(hwidList.cast<int>());
  2286. time = json['time'];
  2287. id = json['id'];
  2288. name = json['name'];
  2289. platform = json['platform'];
  2290. }
  2291. String daysRemaining() {
  2292. final expiry = time + 90 * 24 * 60 * 60 * 1000;
  2293. final remaining = expiry - DateTime.now().millisecondsSinceEpoch;
  2294. if (remaining < 0) {
  2295. return '0';
  2296. }
  2297. return (remaining / (24 * 60 * 60 * 1000)).toStringAsFixed(0);
  2298. }
  2299. static Future<List<TrustedDevice>> get() async {
  2300. final List<TrustedDevice> devices = List.empty(growable: true);
  2301. try {
  2302. final devicesJson = await bind.mainGetTrustedDevices();
  2303. if (devicesJson.isNotEmpty) {
  2304. final devicesList = json.decode(devicesJson);
  2305. if (devicesList is List) {
  2306. for (var device in devicesList) {
  2307. devices.add(TrustedDevice.fromJson(device));
  2308. }
  2309. }
  2310. }
  2311. } catch (e) {
  2312. print(e.toString());
  2313. }
  2314. devices.sort((a, b) => b.time.compareTo(a.time));
  2315. return devices;
  2316. }
  2317. }
  2318. Widget trustedDevicesTable(
  2319. RxList<TrustedDevice> devices, RxList<Uint8List> selectedDevices) {
  2320. RxBool selectAll = false.obs;
  2321. setSelectAll() {
  2322. if (selectedDevices.isNotEmpty &&
  2323. selectedDevices.length == devices.length) {
  2324. selectAll.value = true;
  2325. } else {
  2326. selectAll.value = false;
  2327. }
  2328. }
  2329. devices.listen((_) {
  2330. setSelectAll();
  2331. });
  2332. selectedDevices.listen((_) {
  2333. setSelectAll();
  2334. });
  2335. return FittedBox(
  2336. child: Obx(() => DataTable(
  2337. columns: [
  2338. DataColumn(
  2339. label: Checkbox(
  2340. value: selectAll.value,
  2341. onChanged: (value) {
  2342. if (value == true) {
  2343. selectedDevices.clear();
  2344. selectedDevices.addAll(devices.map((e) => e.hwid));
  2345. } else {
  2346. selectedDevices.clear();
  2347. }
  2348. },
  2349. )),
  2350. DataColumn(label: Text(translate('Platform'))),
  2351. DataColumn(label: Text(translate('ID'))),
  2352. DataColumn(label: Text(translate('Username'))),
  2353. DataColumn(label: Text(translate('Days remaining'))),
  2354. ],
  2355. rows: devices.map((device) {
  2356. return DataRow(cells: [
  2357. DataCell(Checkbox(
  2358. value: selectedDevices.contains(device.hwid),
  2359. onChanged: (value) {
  2360. if (value == null) return;
  2361. if (value) {
  2362. selectedDevices.remove(device.hwid);
  2363. selectedDevices.add(device.hwid);
  2364. } else {
  2365. selectedDevices.remove(device.hwid);
  2366. }
  2367. },
  2368. )),
  2369. DataCell(Text(device.platform)),
  2370. DataCell(Text(device.id)),
  2371. DataCell(Text(device.name)),
  2372. DataCell(Text(device.daysRemaining())),
  2373. ]);
  2374. }).toList(),
  2375. )),
  2376. );
  2377. }