server_page.dart 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925
  1. import 'dart:async';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter/services.dart';
  4. import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
  5. import 'package:flutter_hbb/mobile/widgets/dialog.dart';
  6. import 'package:flutter_hbb/models/chat_model.dart';
  7. import 'package:get/get.dart';
  8. import 'package:provider/provider.dart';
  9. import '../../common.dart';
  10. import '../../common/widgets/dialog.dart';
  11. import '../../consts.dart';
  12. import '../../models/platform_model.dart';
  13. import '../../models/server_model.dart';
  14. import 'home_page.dart';
  15. class ServerPage extends StatefulWidget implements PageShape {
  16. @override
  17. final title = translate("Share screen");
  18. @override
  19. final icon = const Icon(Icons.mobile_screen_share);
  20. @override
  21. final appBarActions = (!bind.isDisableSettings() &&
  22. bind.mainGetBuildinOption(key: kOptionHideSecuritySetting) != 'Y')
  23. ? [_DropDownAction()]
  24. : [];
  25. ServerPage({Key? key}) : super(key: key);
  26. @override
  27. State<StatefulWidget> createState() => _ServerPageState();
  28. }
  29. class _DropDownAction extends StatelessWidget {
  30. _DropDownAction();
  31. // should only have one action
  32. final actions = [
  33. PopupMenuButton<String>(
  34. tooltip: "",
  35. icon: const Icon(Icons.more_vert),
  36. itemBuilder: (context) {
  37. listTile(String text, bool checked) {
  38. return ListTile(
  39. title: Text(translate(text)),
  40. trailing: Icon(
  41. Icons.check,
  42. color: checked ? null : Colors.transparent,
  43. ));
  44. }
  45. final approveMode = gFFI.serverModel.approveMode;
  46. final verificationMethod = gFFI.serverModel.verificationMethod;
  47. final showPasswordOption = approveMode != 'click';
  48. final isApproveModeFixed = isOptionFixed(kOptionApproveMode);
  49. final isNumericOneTimePasswordFixed =
  50. isOptionFixed(kOptionAllowNumericOneTimePassword);
  51. final isAllowNumericOneTimePassword =
  52. gFFI.serverModel.allowNumericOneTimePassword;
  53. return [
  54. PopupMenuItem(
  55. enabled: gFFI.serverModel.connectStatus > 0,
  56. value: "changeID",
  57. child: Text(translate("Change ID")),
  58. ),
  59. const PopupMenuDivider(),
  60. PopupMenuItem(
  61. value: 'AcceptSessionsViaPassword',
  62. child: listTile(
  63. 'Accept sessions via password', approveMode == 'password'),
  64. enabled: !isApproveModeFixed,
  65. ),
  66. PopupMenuItem(
  67. value: 'AcceptSessionsViaClick',
  68. child:
  69. listTile('Accept sessions via click', approveMode == 'click'),
  70. enabled: !isApproveModeFixed,
  71. ),
  72. PopupMenuItem(
  73. value: "AcceptSessionsViaBoth",
  74. child: listTile("Accept sessions via both",
  75. approveMode != 'password' && approveMode != 'click'),
  76. enabled: !isApproveModeFixed,
  77. ),
  78. if (showPasswordOption) const PopupMenuDivider(),
  79. if (showPasswordOption &&
  80. verificationMethod != kUseTemporaryPassword)
  81. PopupMenuItem(
  82. value: "setPermanentPassword",
  83. child: Text(translate("Set permanent password")),
  84. ),
  85. if (showPasswordOption &&
  86. verificationMethod != kUsePermanentPassword)
  87. PopupMenuItem(
  88. value: "setTemporaryPasswordLength",
  89. child: Text(translate("One-time password length")),
  90. ),
  91. if (showPasswordOption &&
  92. verificationMethod != kUsePermanentPassword)
  93. PopupMenuItem(
  94. value: "allowNumericOneTimePassword",
  95. child: listTile(translate("Numeric one-time password"),
  96. isAllowNumericOneTimePassword),
  97. enabled: !isNumericOneTimePasswordFixed,
  98. ),
  99. if (showPasswordOption) const PopupMenuDivider(),
  100. if (showPasswordOption)
  101. PopupMenuItem(
  102. value: kUseTemporaryPassword,
  103. child: listTile('Use one-time password',
  104. verificationMethod == kUseTemporaryPassword),
  105. ),
  106. if (showPasswordOption)
  107. PopupMenuItem(
  108. value: kUsePermanentPassword,
  109. child: listTile('Use permanent password',
  110. verificationMethod == kUsePermanentPassword),
  111. ),
  112. if (showPasswordOption)
  113. PopupMenuItem(
  114. value: kUseBothPasswords,
  115. child: listTile(
  116. 'Use both passwords',
  117. verificationMethod != kUseTemporaryPassword &&
  118. verificationMethod != kUsePermanentPassword),
  119. ),
  120. ];
  121. },
  122. onSelected: (value) async {
  123. if (value == "changeID") {
  124. changeIdDialog();
  125. } else if (value == "setPermanentPassword") {
  126. setPasswordDialog();
  127. } else if (value == "setTemporaryPasswordLength") {
  128. setTemporaryPasswordLengthDialog(gFFI.dialogManager);
  129. } else if (value == "allowNumericOneTimePassword") {
  130. gFFI.serverModel.switchAllowNumericOneTimePassword();
  131. gFFI.serverModel.updatePasswordModel();
  132. } else if (value == kUsePermanentPassword ||
  133. value == kUseTemporaryPassword ||
  134. value == kUseBothPasswords) {
  135. callback() {
  136. bind.mainSetOption(key: kOptionVerificationMethod, value: value);
  137. gFFI.serverModel.updatePasswordModel();
  138. }
  139. if (value == kUsePermanentPassword &&
  140. (await bind.mainGetPermanentPassword()).isEmpty) {
  141. setPasswordDialog(notEmptyCallback: callback);
  142. } else {
  143. callback();
  144. }
  145. } else if (value.startsWith("AcceptSessionsVia")) {
  146. value = value.substring("AcceptSessionsVia".length);
  147. if (value == "Password") {
  148. gFFI.serverModel.setApproveMode('password');
  149. } else if (value == "Click") {
  150. gFFI.serverModel.setApproveMode('click');
  151. } else {
  152. gFFI.serverModel.setApproveMode(defaultOptionApproveMode);
  153. }
  154. }
  155. })
  156. ];
  157. @override
  158. Widget build(BuildContext context) {
  159. return actions[0];
  160. }
  161. }
  162. class _ServerPageState extends State<ServerPage> {
  163. Timer? _updateTimer;
  164. @override
  165. void initState() {
  166. super.initState();
  167. _updateTimer = periodic_immediate(const Duration(seconds: 3), () async {
  168. await gFFI.serverModel.fetchID();
  169. });
  170. gFFI.serverModel.checkAndroidPermission();
  171. }
  172. @override
  173. void dispose() {
  174. _updateTimer?.cancel();
  175. super.dispose();
  176. }
  177. @override
  178. Widget build(BuildContext context) {
  179. checkService();
  180. return ChangeNotifierProvider.value(
  181. value: gFFI.serverModel,
  182. child: Consumer<ServerModel>(
  183. builder: (context, serverModel, child) => SingleChildScrollView(
  184. controller: gFFI.serverModel.controller,
  185. child: Center(
  186. child: Column(
  187. mainAxisAlignment: MainAxisAlignment.start,
  188. children: [
  189. buildPresetPasswordWarningMobile(),
  190. gFFI.serverModel.isStart
  191. ? ServerInfo()
  192. : ServiceNotRunningNotification(),
  193. const ConnectionManager(),
  194. const PermissionChecker(),
  195. SizedBox.fromSize(size: const Size(0, 15.0)),
  196. ],
  197. ),
  198. ),
  199. )));
  200. }
  201. }
  202. void checkService() async {
  203. gFFI.invokeMethod("check_service");
  204. // for Android 10/11, request MANAGE_EXTERNAL_STORAGE permission from system setting page
  205. if (AndroidPermissionManager.isWaitingFile() && !gFFI.serverModel.fileOk) {
  206. AndroidPermissionManager.complete(kManageExternalStorage,
  207. await AndroidPermissionManager.check(kManageExternalStorage));
  208. debugPrint("file permission finished");
  209. }
  210. }
  211. class ServiceNotRunningNotification extends StatelessWidget {
  212. ServiceNotRunningNotification({Key? key}) : super(key: key);
  213. @override
  214. Widget build(BuildContext context) {
  215. final serverModel = Provider.of<ServerModel>(context);
  216. return PaddingCard(
  217. title: translate("Service is not running"),
  218. titleIcon:
  219. const Icon(Icons.warning_amber_sharp, color: Colors.redAccent),
  220. child: Column(
  221. crossAxisAlignment: CrossAxisAlignment.start,
  222. children: [
  223. Text(translate("android_start_service_tip"),
  224. style:
  225. const TextStyle(fontSize: 12, color: MyTheme.darkGray))
  226. .marginOnly(bottom: 8),
  227. ElevatedButton.icon(
  228. icon: const Icon(Icons.play_arrow),
  229. onPressed: () {
  230. if (gFFI.userModel.userName.value.isEmpty &&
  231. bind.mainGetLocalOption(key: "show-scam-warning") !=
  232. "N") {
  233. showScamWarning(context, serverModel);
  234. } else {
  235. serverModel.toggleService();
  236. }
  237. },
  238. label: Text(translate("Start service")))
  239. ],
  240. ));
  241. }
  242. }
  243. class ScamWarningDialog extends StatefulWidget {
  244. final ServerModel serverModel;
  245. ScamWarningDialog({required this.serverModel});
  246. @override
  247. ScamWarningDialogState createState() => ScamWarningDialogState();
  248. }
  249. class ScamWarningDialogState extends State<ScamWarningDialog> {
  250. int _countdown = bind.isCustomClient() ? 0 : 12;
  251. bool show_warning = false;
  252. late Timer _timer;
  253. late ServerModel _serverModel;
  254. @override
  255. void initState() {
  256. super.initState();
  257. _serverModel = widget.serverModel;
  258. startCountdown();
  259. }
  260. void startCountdown() {
  261. const oneSecond = Duration(seconds: 1);
  262. _timer = Timer.periodic(oneSecond, (timer) {
  263. setState(() {
  264. _countdown--;
  265. if (_countdown <= 0) {
  266. timer.cancel();
  267. }
  268. });
  269. });
  270. }
  271. @override
  272. void dispose() {
  273. _timer.cancel();
  274. super.dispose();
  275. }
  276. @override
  277. Widget build(BuildContext context) {
  278. final isButtonLocked = _countdown > 0;
  279. return AlertDialog(
  280. content: ClipRRect(
  281. borderRadius: BorderRadius.circular(20.0),
  282. child: SingleChildScrollView(
  283. child: Container(
  284. decoration: BoxDecoration(
  285. gradient: LinearGradient(
  286. begin: Alignment.topRight,
  287. end: Alignment.bottomLeft,
  288. colors: [
  289. Color(0xffe242bc),
  290. Color(0xfff4727c),
  291. ],
  292. ),
  293. ),
  294. padding: EdgeInsets.all(25.0),
  295. child: Column(
  296. mainAxisSize: MainAxisSize.min,
  297. crossAxisAlignment: CrossAxisAlignment.start,
  298. children: [
  299. Row(
  300. children: [
  301. Icon(
  302. Icons.warning_amber_sharp,
  303. color: Colors.white,
  304. ),
  305. SizedBox(width: 10),
  306. Text(
  307. translate("Warning"),
  308. style: TextStyle(
  309. color: Colors.white,
  310. fontWeight: FontWeight.bold,
  311. fontSize: 20.0,
  312. ),
  313. ),
  314. ],
  315. ),
  316. SizedBox(height: 20),
  317. Center(
  318. child: Image.asset(
  319. 'assets/scam.png',
  320. width: 180,
  321. ),
  322. ),
  323. SizedBox(height: 18),
  324. Text(
  325. translate("scam_title"),
  326. textAlign: TextAlign.center,
  327. style: TextStyle(
  328. color: Colors.white,
  329. fontWeight: FontWeight.bold,
  330. fontSize: 22.0,
  331. ),
  332. ),
  333. SizedBox(height: 18),
  334. Text(
  335. "${translate("scam_text1")}\n\n${translate("scam_text2")}\n",
  336. style: TextStyle(
  337. color: Colors.white,
  338. fontWeight: FontWeight.bold,
  339. fontSize: 16.0,
  340. ),
  341. ),
  342. Row(
  343. children: <Widget>[
  344. Checkbox(
  345. value: show_warning,
  346. onChanged: (value) {
  347. setState(() {
  348. show_warning = value!;
  349. });
  350. },
  351. ),
  352. Text(
  353. translate("Don't show again"),
  354. style: TextStyle(
  355. color: Colors.white,
  356. fontWeight: FontWeight.bold,
  357. fontSize: 15.0,
  358. ),
  359. ),
  360. ],
  361. ),
  362. Row(
  363. mainAxisAlignment: MainAxisAlignment.end,
  364. children: [
  365. Container(
  366. constraints: BoxConstraints(maxWidth: 150),
  367. child: ElevatedButton(
  368. onPressed: isButtonLocked
  369. ? null
  370. : () {
  371. Navigator.of(context).pop();
  372. _serverModel.toggleService();
  373. if (show_warning) {
  374. bind.mainSetLocalOption(
  375. key: "show-scam-warning", value: "N");
  376. }
  377. },
  378. style: ElevatedButton.styleFrom(
  379. backgroundColor: Colors.blueAccent,
  380. ),
  381. child: Text(
  382. isButtonLocked
  383. ? "${translate("I Agree")} (${_countdown}s)"
  384. : translate("I Agree"),
  385. style: TextStyle(
  386. fontWeight: FontWeight.bold,
  387. fontSize: 13.0,
  388. ),
  389. maxLines: 2,
  390. overflow: TextOverflow.ellipsis,
  391. ),
  392. ),
  393. ),
  394. SizedBox(width: 15),
  395. Container(
  396. constraints: BoxConstraints(maxWidth: 150),
  397. child: ElevatedButton(
  398. onPressed: () {
  399. Navigator.of(context).pop();
  400. },
  401. style: ElevatedButton.styleFrom(
  402. backgroundColor: Colors.blueAccent,
  403. ),
  404. child: Text(
  405. translate("Decline"),
  406. style: TextStyle(
  407. fontWeight: FontWeight.bold,
  408. fontSize: 13.0,
  409. ),
  410. maxLines: 2,
  411. overflow: TextOverflow.ellipsis,
  412. ),
  413. ),
  414. ),
  415. ],
  416. ),
  417. ],
  418. ),
  419. ),
  420. ),
  421. ),
  422. contentPadding: EdgeInsets.all(0.0),
  423. );
  424. }
  425. }
  426. class ServerInfo extends StatelessWidget {
  427. final model = gFFI.serverModel;
  428. final emptyController = TextEditingController(text: "-");
  429. ServerInfo({Key? key}) : super(key: key);
  430. @override
  431. Widget build(BuildContext context) {
  432. final serverModel = Provider.of<ServerModel>(context);
  433. const Color colorPositive = Colors.green;
  434. const Color colorNegative = Colors.red;
  435. const double iconMarginRight = 15;
  436. const double iconSize = 24;
  437. const TextStyle textStyleHeading = TextStyle(
  438. fontSize: 16.0, fontWeight: FontWeight.bold, color: Colors.grey);
  439. const TextStyle textStyleValue =
  440. TextStyle(fontSize: 25.0, fontWeight: FontWeight.bold);
  441. void copyToClipboard(String value) {
  442. Clipboard.setData(ClipboardData(text: value));
  443. showToast(translate('Copied'));
  444. }
  445. Widget ConnectionStateNotification() {
  446. if (serverModel.connectStatus == -1) {
  447. return Row(children: [
  448. const Icon(Icons.warning_amber_sharp,
  449. color: colorNegative, size: iconSize)
  450. .marginOnly(right: iconMarginRight),
  451. Expanded(child: Text(translate('not_ready_status')))
  452. ]);
  453. } else if (serverModel.connectStatus == 0) {
  454. return Row(children: [
  455. SizedBox(width: 20, height: 20, child: CircularProgressIndicator())
  456. .marginOnly(left: 4, right: iconMarginRight),
  457. Expanded(child: Text(translate('connecting_status')))
  458. ]);
  459. } else {
  460. return Row(children: [
  461. const Icon(Icons.check, color: colorPositive, size: iconSize)
  462. .marginOnly(right: iconMarginRight),
  463. Expanded(child: Text(translate('Ready')))
  464. ]);
  465. }
  466. }
  467. final showOneTime = serverModel.approveMode != 'click' &&
  468. serverModel.verificationMethod != kUsePermanentPassword;
  469. return PaddingCard(
  470. title: translate('Your Device'),
  471. child: Column(
  472. // ID
  473. children: [
  474. Row(children: [
  475. const Icon(Icons.perm_identity,
  476. color: Colors.grey, size: iconSize)
  477. .marginOnly(right: iconMarginRight),
  478. Text(
  479. translate('ID'),
  480. style: textStyleHeading,
  481. )
  482. ]),
  483. Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
  484. Text(
  485. model.serverId.value.text,
  486. style: textStyleValue,
  487. ),
  488. IconButton(
  489. visualDensity: VisualDensity.compact,
  490. icon: Icon(Icons.copy_outlined),
  491. onPressed: () {
  492. copyToClipboard(model.serverId.value.text.trim());
  493. })
  494. ]).marginOnly(left: 39, bottom: 10),
  495. // Password
  496. Row(children: [
  497. const Icon(Icons.lock_outline, color: Colors.grey, size: iconSize)
  498. .marginOnly(right: iconMarginRight),
  499. Text(
  500. translate('One-time Password'),
  501. style: textStyleHeading,
  502. )
  503. ]),
  504. Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
  505. Text(
  506. !showOneTime ? '-' : model.serverPasswd.value.text,
  507. style: textStyleValue,
  508. ),
  509. !showOneTime
  510. ? SizedBox.shrink()
  511. : Row(children: [
  512. IconButton(
  513. visualDensity: VisualDensity.compact,
  514. icon: const Icon(Icons.refresh),
  515. onPressed: () => bind.mainUpdateTemporaryPassword()),
  516. IconButton(
  517. visualDensity: VisualDensity.compact,
  518. icon: Icon(Icons.copy_outlined),
  519. onPressed: () {
  520. copyToClipboard(
  521. model.serverPasswd.value.text.trim());
  522. })
  523. ])
  524. ]).marginOnly(left: 40, bottom: 15),
  525. ConnectionStateNotification()
  526. ],
  527. ));
  528. }
  529. }
  530. class PermissionChecker extends StatefulWidget {
  531. const PermissionChecker({Key? key}) : super(key: key);
  532. @override
  533. State<PermissionChecker> createState() => _PermissionCheckerState();
  534. }
  535. class _PermissionCheckerState extends State<PermissionChecker> {
  536. @override
  537. Widget build(BuildContext context) {
  538. final serverModel = Provider.of<ServerModel>(context);
  539. final hasAudioPermission = androidVersion >= 30;
  540. return PaddingCard(
  541. title: translate("Permissions"),
  542. child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
  543. serverModel.mediaOk
  544. ? ElevatedButton.icon(
  545. style: ButtonStyle(
  546. backgroundColor:
  547. MaterialStateProperty.all(Colors.red)),
  548. icon: const Icon(Icons.stop),
  549. onPressed: serverModel.toggleService,
  550. label: Text(translate("Stop service")))
  551. .marginOnly(bottom: 8)
  552. : SizedBox.shrink(),
  553. PermissionRow(
  554. translate("Screen Capture"),
  555. serverModel.mediaOk,
  556. !serverModel.mediaOk &&
  557. gFFI.userModel.userName.value.isEmpty &&
  558. bind.mainGetLocalOption(key: "show-scam-warning") != "N"
  559. ? () => showScamWarning(context, serverModel)
  560. : serverModel.toggleService),
  561. PermissionRow(translate("Input Control"), serverModel.inputOk,
  562. serverModel.toggleInput),
  563. PermissionRow(translate("Transfer file"), serverModel.fileOk,
  564. serverModel.toggleFile),
  565. hasAudioPermission
  566. ? PermissionRow(translate("Audio Capture"), serverModel.audioOk,
  567. serverModel.toggleAudio)
  568. : Row(children: [
  569. Icon(Icons.info_outline).marginOnly(right: 15),
  570. Expanded(
  571. child: Text(
  572. translate("android_version_audio_tip"),
  573. style: const TextStyle(color: MyTheme.darkGray),
  574. ))
  575. ]),
  576. PermissionRow(translate("Enable clipboard"), serverModel.clipboardOk,
  577. serverModel.toggleClipboard),
  578. ]));
  579. }
  580. }
  581. class PermissionRow extends StatelessWidget {
  582. const PermissionRow(this.name, this.isOk, this.onPressed, {Key? key})
  583. : super(key: key);
  584. final String name;
  585. final bool isOk;
  586. final VoidCallback onPressed;
  587. @override
  588. Widget build(BuildContext context) {
  589. return SwitchListTile(
  590. visualDensity: VisualDensity.compact,
  591. contentPadding: EdgeInsets.all(0),
  592. title: Text(name),
  593. value: isOk,
  594. onChanged: (bool value) {
  595. onPressed();
  596. });
  597. }
  598. }
  599. class ConnectionManager extends StatelessWidget {
  600. const ConnectionManager({Key? key}) : super(key: key);
  601. @override
  602. Widget build(BuildContext context) {
  603. final serverModel = Provider.of<ServerModel>(context);
  604. return Column(
  605. children: serverModel.clients
  606. .map((client) => PaddingCard(
  607. title: translate(client.isFileTransfer
  608. ? "Transfer file"
  609. : "Share screen"),
  610. titleIcon: client.isFileTransfer
  611. ? Icon(Icons.folder_outlined)
  612. : Icon(Icons.mobile_screen_share),
  613. child: Column(children: [
  614. Row(
  615. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  616. children: [
  617. Expanded(child: ClientInfo(client)),
  618. Expanded(
  619. flex: -1,
  620. child: client.isFileTransfer || !client.authorized
  621. ? const SizedBox.shrink()
  622. : IconButton(
  623. onPressed: () {
  624. gFFI.chatModel.changeCurrentKey(
  625. MessageKey(client.peerId, client.id));
  626. final bar = navigationBarKey.currentWidget;
  627. if (bar != null) {
  628. bar as BottomNavigationBar;
  629. bar.onTap!(1);
  630. }
  631. },
  632. icon: unreadTopRightBuilder(
  633. client.unreadChatMessageCount)))
  634. ],
  635. ),
  636. client.authorized
  637. ? const SizedBox.shrink()
  638. : Text(
  639. translate("android_new_connection_tip"),
  640. style: Theme.of(context).textTheme.bodyMedium,
  641. ).marginOnly(bottom: 5),
  642. client.authorized
  643. ? _buildDisconnectButton(client)
  644. : _buildNewConnectionHint(serverModel, client),
  645. if (client.incomingVoiceCall && !client.inVoiceCall)
  646. ..._buildNewVoiceCallHint(context, serverModel, client),
  647. ])))
  648. .toList());
  649. }
  650. Widget _buildDisconnectButton(Client client) {
  651. final disconnectButton = ElevatedButton.icon(
  652. style: ButtonStyle(backgroundColor: MaterialStatePropertyAll(Colors.red)),
  653. icon: const Icon(Icons.close),
  654. onPressed: () {
  655. bind.cmCloseConnection(connId: client.id);
  656. gFFI.invokeMethod("cancel_notification", client.id);
  657. },
  658. label: Text(translate("Disconnect")),
  659. );
  660. final buttons = [disconnectButton];
  661. if (client.inVoiceCall) {
  662. buttons.insert(
  663. 0,
  664. ElevatedButton.icon(
  665. style: ButtonStyle(
  666. backgroundColor: MaterialStatePropertyAll(Colors.red)),
  667. icon: const Icon(Icons.phone),
  668. label: Text(translate("Stop")),
  669. onPressed: () {
  670. bind.cmCloseVoiceCall(id: client.id);
  671. gFFI.invokeMethod("cancel_notification", client.id);
  672. },
  673. ),
  674. );
  675. }
  676. if (buttons.length == 1) {
  677. return Container(
  678. alignment: Alignment.centerRight,
  679. child: disconnectButton,
  680. );
  681. } else {
  682. return Row(
  683. children: buttons,
  684. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  685. );
  686. }
  687. }
  688. Widget _buildNewConnectionHint(ServerModel serverModel, Client client) {
  689. return Row(mainAxisAlignment: MainAxisAlignment.end, children: [
  690. TextButton(
  691. child: Text(translate("Dismiss")),
  692. onPressed: () {
  693. serverModel.sendLoginResponse(client, false);
  694. }).marginOnly(right: 15),
  695. if (serverModel.approveMode != 'password')
  696. ElevatedButton.icon(
  697. icon: const Icon(Icons.check),
  698. label: Text(translate("Accept")),
  699. onPressed: () {
  700. serverModel.sendLoginResponse(client, true);
  701. }),
  702. ]);
  703. }
  704. List<Widget> _buildNewVoiceCallHint(
  705. BuildContext context, ServerModel serverModel, Client client) {
  706. return [
  707. Text(
  708. translate("android_new_voice_call_tip"),
  709. style: Theme.of(context).textTheme.bodyMedium,
  710. ).marginOnly(bottom: 5),
  711. Row(mainAxisAlignment: MainAxisAlignment.end, children: [
  712. TextButton(
  713. child: Text(translate("Dismiss")),
  714. onPressed: () {
  715. serverModel.handleVoiceCall(client, false);
  716. }).marginOnly(right: 15),
  717. if (serverModel.approveMode != 'password')
  718. ElevatedButton.icon(
  719. icon: const Icon(Icons.check),
  720. label: Text(translate("Accept")),
  721. onPressed: () {
  722. serverModel.handleVoiceCall(client, true);
  723. }),
  724. ])
  725. ];
  726. }
  727. }
  728. class PaddingCard extends StatelessWidget {
  729. const PaddingCard({Key? key, required this.child, this.title, this.titleIcon})
  730. : super(key: key);
  731. final String? title;
  732. final Icon? titleIcon;
  733. final Widget child;
  734. @override
  735. Widget build(BuildContext context) {
  736. final children = [child];
  737. if (title != null) {
  738. children.insert(
  739. 0,
  740. Padding(
  741. padding: const EdgeInsets.fromLTRB(0, 5, 0, 8),
  742. child: Row(
  743. children: [
  744. titleIcon?.marginOnly(right: 10) ?? const SizedBox.shrink(),
  745. Expanded(
  746. child: Text(title!,
  747. style: Theme.of(context)
  748. .textTheme
  749. .titleLarge
  750. ?.merge(TextStyle(fontWeight: FontWeight.bold))),
  751. )
  752. ],
  753. )));
  754. }
  755. return SizedBox(
  756. width: double.maxFinite,
  757. child: Card(
  758. shape: RoundedRectangleBorder(
  759. borderRadius: BorderRadius.circular(13),
  760. ),
  761. margin: const EdgeInsets.fromLTRB(12.0, 10.0, 12.0, 0),
  762. child: Padding(
  763. padding:
  764. const EdgeInsets.symmetric(vertical: 15.0, horizontal: 20.0),
  765. child: Column(
  766. children: children,
  767. ),
  768. ),
  769. ));
  770. }
  771. }
  772. class ClientInfo extends StatelessWidget {
  773. final Client client;
  774. ClientInfo(this.client);
  775. @override
  776. Widget build(BuildContext context) {
  777. return Padding(
  778. padding: const EdgeInsets.symmetric(vertical: 8),
  779. child: Column(children: [
  780. Row(
  781. children: [
  782. Expanded(
  783. flex: -1,
  784. child: Padding(
  785. padding: const EdgeInsets.only(right: 12),
  786. child: CircleAvatar(
  787. backgroundColor: str2color(
  788. client.name,
  789. Theme.of(context).brightness == Brightness.light
  790. ? 255
  791. : 150),
  792. child: Text(client.name[0])))),
  793. Expanded(
  794. child: Column(
  795. crossAxisAlignment: CrossAxisAlignment.start,
  796. children: [
  797. Text(client.name, style: const TextStyle(fontSize: 18)),
  798. const SizedBox(width: 8),
  799. Text(client.peerId, style: const TextStyle(fontSize: 10))
  800. ]))
  801. ],
  802. ),
  803. ]));
  804. }
  805. }
  806. void androidChannelInit() {
  807. gFFI.setMethodCallHandler((method, arguments) {
  808. debugPrint("flutter got android msg,$method,$arguments");
  809. try {
  810. switch (method) {
  811. case "start_capture":
  812. {
  813. gFFI.dialogManager.dismissAll();
  814. gFFI.serverModel.updateClientState();
  815. break;
  816. }
  817. case "on_state_changed":
  818. {
  819. var name = arguments["name"] as String;
  820. var value = arguments["value"] as String == "true";
  821. debugPrint("from jvm:on_state_changed,$name:$value");
  822. gFFI.serverModel.changeStatue(name, value);
  823. break;
  824. }
  825. case "on_android_permission_result":
  826. {
  827. var type = arguments["type"] as String;
  828. var result = arguments["result"] as bool;
  829. AndroidPermissionManager.complete(type, result);
  830. break;
  831. }
  832. case "on_media_projection_canceled":
  833. {
  834. gFFI.serverModel.stopService();
  835. break;
  836. }
  837. case "msgbox":
  838. {
  839. var type = arguments["type"] as String;
  840. var title = arguments["title"] as String;
  841. var text = arguments["text"] as String;
  842. var link = (arguments["link"] ?? '') as String;
  843. msgBox(gFFI.sessionId, type, title, text, link, gFFI.dialogManager);
  844. break;
  845. }
  846. case "stop_service":
  847. {
  848. print(
  849. "stop_service by kotlin, isStart:${gFFI.serverModel.isStart}");
  850. if (gFFI.serverModel.isStart) {
  851. gFFI.serverModel.stopService();
  852. }
  853. break;
  854. }
  855. }
  856. } catch (e) {
  857. debugPrintStack(label: "MethodCallHandler err:$e");
  858. }
  859. return "";
  860. });
  861. }
  862. void showScamWarning(BuildContext context, ServerModel serverModel) {
  863. showDialog(
  864. context: context,
  865. builder: (BuildContext context) {
  866. return ScamWarningDialog(serverModel: serverModel);
  867. },
  868. );
  869. }