server_page.dart 31 KB

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