server_page.dart 47 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402
  1. // original cm window in Sciter version.
  2. import 'dart:async';
  3. import 'dart:math';
  4. import 'package:flutter/material.dart';
  5. import 'package:flutter_hbb/common/widgets/audio_input.dart';
  6. import 'package:flutter_hbb/consts.dart';
  7. import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
  8. import 'package:flutter_hbb/models/chat_model.dart';
  9. import 'package:flutter_hbb/models/cm_file_model.dart';
  10. import 'package:flutter_hbb/utils/platform_channel.dart';
  11. import 'package:get/get.dart';
  12. import 'package:percent_indicator/linear_percent_indicator.dart';
  13. import 'package:provider/provider.dart';
  14. import 'package:window_manager/window_manager.dart';
  15. import 'package:flutter_svg/flutter_svg.dart';
  16. import '../../common.dart';
  17. import '../../common/widgets/chat_page.dart';
  18. import '../../models/file_model.dart';
  19. import '../../models/platform_model.dart';
  20. import '../../models/server_model.dart';
  21. class DesktopServerPage extends StatefulWidget {
  22. const DesktopServerPage({Key? key}) : super(key: key);
  23. @override
  24. State<DesktopServerPage> createState() => _DesktopServerPageState();
  25. }
  26. class _DesktopServerPageState extends State<DesktopServerPage>
  27. with WindowListener, AutomaticKeepAliveClientMixin {
  28. final tabController = gFFI.serverModel.tabController;
  29. _DesktopServerPageState() {
  30. gFFI.ffiModel.updateEventListener(gFFI.sessionId, "");
  31. Get.put<DesktopTabController>(tabController);
  32. tabController.onRemoved = (_, id) {
  33. onRemoveId(id);
  34. };
  35. }
  36. @override
  37. void initState() {
  38. windowManager.addListener(this);
  39. super.initState();
  40. }
  41. @override
  42. void dispose() {
  43. windowManager.removeListener(this);
  44. super.dispose();
  45. }
  46. @override
  47. void onWindowClose() {
  48. Future.wait([gFFI.serverModel.closeAll(), gFFI.close()]).then((_) {
  49. if (isMacOS) {
  50. RdPlatformChannel.instance.terminate();
  51. } else {
  52. windowManager.setPreventClose(false);
  53. windowManager.close();
  54. }
  55. });
  56. super.onWindowClose();
  57. }
  58. void onRemoveId(String id) {
  59. if (tabController.state.value.tabs.isEmpty) {
  60. windowManager.close();
  61. }
  62. }
  63. @override
  64. Widget build(BuildContext context) {
  65. super.build(context);
  66. return MultiProvider(
  67. providers: [
  68. ChangeNotifierProvider.value(value: gFFI.serverModel),
  69. ChangeNotifierProvider.value(value: gFFI.chatModel),
  70. ],
  71. child: Consumer<ServerModel>(
  72. builder: (context, serverModel, child) {
  73. final body = Scaffold(
  74. backgroundColor: Theme.of(context).colorScheme.background,
  75. body: ConnectionManager(),
  76. );
  77. return isLinux
  78. ? buildVirtualWindowFrame(context, body)
  79. : workaroundWindowBorder(
  80. context,
  81. Container(
  82. decoration: BoxDecoration(
  83. border:
  84. Border.all(color: MyTheme.color(context).border!)),
  85. child: body,
  86. ));
  87. },
  88. ),
  89. );
  90. }
  91. @override
  92. bool get wantKeepAlive => true;
  93. }
  94. class ConnectionManager extends StatefulWidget {
  95. @override
  96. State<StatefulWidget> createState() => ConnectionManagerState();
  97. }
  98. class ConnectionManagerState extends State<ConnectionManager>
  99. with WidgetsBindingObserver {
  100. final RxBool _controlPageBlock = false.obs;
  101. final RxBool _sidePageBlock = false.obs;
  102. ConnectionManagerState() {
  103. gFFI.serverModel.tabController.onSelected = (client_id_str) {
  104. final client_id = int.tryParse(client_id_str);
  105. if (client_id != null) {
  106. final client =
  107. gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == client_id);
  108. if (client != null) {
  109. gFFI.chatModel.changeCurrentKey(MessageKey(client.peerId, client.id));
  110. if (client.unreadChatMessageCount.value > 0) {
  111. WidgetsBinding.instance.addPostFrameCallback((_) {
  112. client.unreadChatMessageCount.value = 0;
  113. gFFI.chatModel.showChatPage(MessageKey(client.peerId, client.id));
  114. });
  115. }
  116. windowManager.setTitle(getWindowNameWithId(client.peerId));
  117. gFFI.cmFileModel.updateCurrentClientId(client.id);
  118. }
  119. }
  120. };
  121. gFFI.chatModel.isConnManager = true;
  122. }
  123. @override
  124. void didChangeAppLifecycleState(AppLifecycleState state) {
  125. super.didChangeAppLifecycleState(state);
  126. if (state == AppLifecycleState.resumed) {
  127. if (!allowRemoteCMModification()) {
  128. shouldBeBlocked(_controlPageBlock, null);
  129. shouldBeBlocked(_sidePageBlock, null);
  130. }
  131. }
  132. }
  133. @override
  134. void initState() {
  135. gFFI.serverModel.updateClientState();
  136. WidgetsBinding.instance.addObserver(this);
  137. super.initState();
  138. }
  139. @override
  140. void dispose() {
  141. WidgetsBinding.instance.removeObserver(this);
  142. super.dispose();
  143. }
  144. @override
  145. Widget build(BuildContext context) {
  146. final serverModel = Provider.of<ServerModel>(context);
  147. pointerHandler(PointerEvent e) {
  148. if (serverModel.cmHiddenTimer != null) {
  149. serverModel.cmHiddenTimer!.cancel();
  150. serverModel.cmHiddenTimer = null;
  151. debugPrint("CM hidden timer has been canceled");
  152. }
  153. }
  154. return serverModel.clients.isEmpty
  155. ? Column(
  156. children: [
  157. buildTitleBar(),
  158. Expanded(
  159. child: Center(
  160. child: Text(translate("Waiting")),
  161. ),
  162. ),
  163. ],
  164. )
  165. : Listener(
  166. onPointerDown: pointerHandler,
  167. onPointerMove: pointerHandler,
  168. child: DesktopTab(
  169. showTitle: false,
  170. showMaximize: false,
  171. showMinimize: true,
  172. showClose: true,
  173. onWindowCloseButton: handleWindowCloseButton,
  174. controller: serverModel.tabController,
  175. selectedBorderColor: MyTheme.accent,
  176. maxLabelWidth: 100,
  177. tail: null, //buildScrollJumper(),
  178. tabBuilder: (key, icon, label, themeConf) {
  179. final client = serverModel.clients
  180. .firstWhereOrNull((client) => client.id.toString() == key);
  181. return Row(
  182. mainAxisAlignment: MainAxisAlignment.center,
  183. children: [
  184. Tooltip(
  185. message: key,
  186. waitDuration: Duration(seconds: 1),
  187. child: label),
  188. unreadMessageCountBuilder(client?.unreadChatMessageCount)
  189. .marginOnly(left: 4),
  190. ],
  191. );
  192. },
  193. pageViewBuilder: (pageView) => LayoutBuilder(
  194. builder: (context, constrains) {
  195. var borderWidth = 0.0;
  196. if (constrains.maxWidth >
  197. kConnectionManagerWindowSizeClosedChat.width) {
  198. borderWidth = kConnectionManagerWindowSizeOpenChat.width -
  199. constrains.maxWidth;
  200. } else {
  201. borderWidth = kConnectionManagerWindowSizeClosedChat.width -
  202. constrains.maxWidth;
  203. }
  204. if (borderWidth < 0 || borderWidth > 50) {
  205. borderWidth = 0;
  206. }
  207. final realClosedWidth =
  208. kConnectionManagerWindowSizeClosedChat.width -
  209. borderWidth;
  210. final realChatPageWidth =
  211. constrains.maxWidth - realClosedWidth;
  212. final row = Row(children: [
  213. if (constrains.maxWidth >
  214. kConnectionManagerWindowSizeClosedChat.width)
  215. Consumer<ChatModel>(
  216. builder: (_, model, child) => SizedBox(
  217. width: realChatPageWidth,
  218. child: allowRemoteCMModification()
  219. ? buildSidePage()
  220. : buildRemoteBlock(
  221. child: buildSidePage(),
  222. block: _sidePageBlock,
  223. mask: true),
  224. )),
  225. SizedBox(
  226. width: realClosedWidth,
  227. child: SizedBox(
  228. width: realClosedWidth,
  229. child: allowRemoteCMModification()
  230. ? pageView
  231. : buildRemoteBlock(
  232. child: _buildKeyEventBlock(pageView),
  233. block: _controlPageBlock,
  234. mask: false,
  235. ))),
  236. ]);
  237. return Container(
  238. color: Theme.of(context).scaffoldBackgroundColor,
  239. child: row,
  240. );
  241. },
  242. ),
  243. ),
  244. );
  245. }
  246. Widget buildSidePage() {
  247. final selected = gFFI.serverModel.tabController.state.value.selected;
  248. if (selected < 0 || selected >= gFFI.serverModel.clients.length) {
  249. return Offstage();
  250. }
  251. final clientType = gFFI.serverModel.clients[selected].type_();
  252. if (clientType == ClientType.file) {
  253. return _FileTransferLogPage();
  254. } else {
  255. return ChatPage(type: ChatPageType.desktopCM);
  256. }
  257. }
  258. Widget _buildKeyEventBlock(Widget child) {
  259. return ExcludeFocus(child: child, excluding: true);
  260. }
  261. Widget buildTitleBar() {
  262. return SizedBox(
  263. height: kDesktopRemoteTabBarHeight,
  264. child: Row(
  265. crossAxisAlignment: CrossAxisAlignment.center,
  266. children: [
  267. const _AppIcon(),
  268. Expanded(
  269. child: GestureDetector(
  270. onPanStart: (d) {
  271. windowManager.startDragging();
  272. },
  273. child: Container(
  274. color: Theme.of(context).colorScheme.background,
  275. ),
  276. ),
  277. ),
  278. const SizedBox(
  279. width: 4.0,
  280. ),
  281. const _CloseButton()
  282. ],
  283. ),
  284. );
  285. }
  286. Widget buildScrollJumper() {
  287. final offstage = gFFI.serverModel.clients.length < 2;
  288. final sc = gFFI.serverModel.tabController.state.value.scrollController;
  289. return Offstage(
  290. offstage: offstage,
  291. child: Row(
  292. children: [
  293. ActionIcon(
  294. icon: Icons.arrow_left, iconSize: 22, onTap: sc.backward),
  295. ActionIcon(
  296. icon: Icons.arrow_right, iconSize: 22, onTap: sc.forward),
  297. ],
  298. ));
  299. }
  300. Future<bool> handleWindowCloseButton() async {
  301. var tabController = gFFI.serverModel.tabController;
  302. final connLength = tabController.length;
  303. if (connLength <= 1) {
  304. windowManager.close();
  305. return true;
  306. } else {
  307. final bool res;
  308. if (!option2bool(kOptionEnableConfirmClosingTabs,
  309. bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) {
  310. res = true;
  311. } else {
  312. res = await closeConfirmDialog();
  313. }
  314. if (res) {
  315. windowManager.close();
  316. }
  317. return res;
  318. }
  319. }
  320. }
  321. Widget buildConnectionCard(Client client) {
  322. return Consumer<ServerModel>(
  323. builder: (context, value, child) => Column(
  324. mainAxisAlignment: MainAxisAlignment.start,
  325. crossAxisAlignment: CrossAxisAlignment.start,
  326. key: ValueKey(client.id),
  327. children: [
  328. _CmHeader(client: client),
  329. client.type_() == ClientType.file ||
  330. client.type_() == ClientType.portForward ||
  331. client.type_() == ClientType.terminal ||
  332. client.disconnected
  333. ? Offstage()
  334. : _PrivilegeBoard(client: client),
  335. Expanded(
  336. child: Align(
  337. alignment: Alignment.bottomCenter,
  338. child: _CmControlPanel(client: client),
  339. ),
  340. )
  341. ],
  342. ).paddingSymmetric(vertical: 4.0, horizontal: 8.0),
  343. );
  344. }
  345. class _AppIcon extends StatelessWidget {
  346. const _AppIcon({Key? key}) : super(key: key);
  347. @override
  348. Widget build(BuildContext context) {
  349. return Container(
  350. margin: EdgeInsets.symmetric(horizontal: 4.0),
  351. child: loadIcon(30),
  352. );
  353. }
  354. }
  355. class _CloseButton extends StatelessWidget {
  356. const _CloseButton({Key? key}) : super(key: key);
  357. @override
  358. Widget build(BuildContext context) {
  359. return IconButton(
  360. onPressed: () {
  361. windowManager.close();
  362. },
  363. icon: const Icon(
  364. IconFont.close,
  365. size: 18,
  366. ),
  367. splashColor: Colors.transparent,
  368. hoverColor: Colors.transparent,
  369. );
  370. }
  371. }
  372. class _CmHeader extends StatefulWidget {
  373. final Client client;
  374. const _CmHeader({Key? key, required this.client}) : super(key: key);
  375. @override
  376. State<_CmHeader> createState() => _CmHeaderState();
  377. }
  378. class _CmHeaderState extends State<_CmHeader>
  379. with AutomaticKeepAliveClientMixin {
  380. Client get client => widget.client;
  381. final _time = 0.obs;
  382. Timer? _timer;
  383. @override
  384. void initState() {
  385. super.initState();
  386. _timer = Timer.periodic(Duration(seconds: 1), (_) {
  387. if (client.authorized && !client.disconnected) {
  388. _time.value = _time.value + 1;
  389. }
  390. });
  391. // Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
  392. WidgetsBinding.instance.addPostFrameCallback((_) {
  393. gFFI.serverModel.tabController.onSelected?.call(client.id.toString());
  394. });
  395. }
  396. @override
  397. void dispose() {
  398. _timer?.cancel();
  399. super.dispose();
  400. }
  401. @override
  402. Widget build(BuildContext context) {
  403. super.build(context);
  404. return Container(
  405. decoration: BoxDecoration(
  406. borderRadius: BorderRadius.circular(10.0),
  407. gradient: LinearGradient(
  408. begin: Alignment.topRight,
  409. end: Alignment.bottomLeft,
  410. colors: [
  411. Color(0xff00bfe1),
  412. Color(0xff0071ff),
  413. ],
  414. ),
  415. ),
  416. margin: EdgeInsets.symmetric(horizontal: 5.0, vertical: 10.0),
  417. padding: EdgeInsets.only(
  418. top: 10.0,
  419. bottom: 10.0,
  420. left: 10.0,
  421. right: 5.0,
  422. ),
  423. child: Row(
  424. crossAxisAlignment: CrossAxisAlignment.start,
  425. children: [
  426. Container(
  427. width: 70,
  428. height: 70,
  429. alignment: Alignment.center,
  430. decoration: BoxDecoration(
  431. color: str2color(client.name),
  432. borderRadius: BorderRadius.circular(15.0),
  433. ),
  434. child: Text(
  435. client.name[0],
  436. style: TextStyle(
  437. fontWeight: FontWeight.bold,
  438. color: Colors.white,
  439. fontSize: 55,
  440. ),
  441. ),
  442. ).marginOnly(right: 10.0),
  443. Expanded(
  444. child: Column(
  445. mainAxisAlignment: MainAxisAlignment.start,
  446. crossAxisAlignment: CrossAxisAlignment.start,
  447. children: [
  448. FittedBox(
  449. child: Text(
  450. client.name,
  451. style: TextStyle(
  452. color: Colors.white,
  453. fontWeight: FontWeight.bold,
  454. fontSize: 20,
  455. overflow: TextOverflow.ellipsis,
  456. ),
  457. maxLines: 1,
  458. )),
  459. FittedBox(
  460. child: Text(
  461. "(${client.peerId})",
  462. style: TextStyle(color: Colors.white, fontSize: 14),
  463. ),
  464. ),
  465. if (client.type_() == ClientType.terminal)
  466. FittedBox(
  467. child: Text(
  468. translate("Terminal"),
  469. style: TextStyle(color: Colors.white70, fontSize: 12),
  470. ),
  471. ),
  472. if (client.type_() == ClientType.file)
  473. FittedBox(
  474. child: Text(
  475. translate("File Transfer"),
  476. style: TextStyle(color: Colors.white70, fontSize: 12),
  477. ),
  478. ),
  479. if (client.type_() == ClientType.camera)
  480. FittedBox(
  481. child: Text(
  482. translate("View Camera"),
  483. style: TextStyle(color: Colors.white70, fontSize: 12),
  484. ),
  485. ),
  486. if (client.portForward.isNotEmpty)
  487. FittedBox(
  488. child: Text(
  489. "Port Forward: ${client.portForward}",
  490. style: TextStyle(color: Colors.white70, fontSize: 12),
  491. ),
  492. ),
  493. SizedBox(height: 10.0),
  494. FittedBox(
  495. child: Row(
  496. children: [
  497. Text(
  498. client.authorized
  499. ? client.disconnected
  500. ? translate("Disconnected")
  501. : translate("Connected")
  502. : "${translate("Request access to your device")}...",
  503. style: TextStyle(color: Colors.white),
  504. ).marginOnly(right: 8.0),
  505. if (client.authorized)
  506. Obx(
  507. () => Text(
  508. formatDurationToTime(
  509. Duration(seconds: _time.value),
  510. ),
  511. style: TextStyle(color: Colors.white),
  512. ),
  513. )
  514. ],
  515. ))
  516. ],
  517. ),
  518. ),
  519. Offstage(
  520. offstage: !client.authorized ||
  521. (client.type_() != ClientType.remote &&
  522. client.type_() != ClientType.file &&
  523. client.type_() != ClientType.camera),
  524. child: IconButton(
  525. onPressed: () => checkClickTime(client.id, () {
  526. if (client.type_() == ClientType.file) {
  527. gFFI.chatModel.toggleCMFilePage();
  528. } else {
  529. gFFI.chatModel
  530. .toggleCMChatPage(MessageKey(client.peerId, client.id));
  531. }
  532. }),
  533. icon: SvgPicture.asset(client.type_() == ClientType.file
  534. ? 'assets/file_transfer.svg'
  535. : 'assets/chat2.svg'),
  536. splashRadius: kDesktopIconButtonSplashRadius,
  537. ),
  538. )
  539. ],
  540. ),
  541. );
  542. }
  543. @override
  544. bool get wantKeepAlive => true;
  545. }
  546. class _PrivilegeBoard extends StatefulWidget {
  547. final Client client;
  548. const _PrivilegeBoard({Key? key, required this.client}) : super(key: key);
  549. @override
  550. State<StatefulWidget> createState() => _PrivilegeBoardState();
  551. }
  552. class _PrivilegeBoardState extends State<_PrivilegeBoard> {
  553. late final client = widget.client;
  554. Widget buildPermissionIcon(bool enabled, IconData iconData,
  555. Function(bool)? onTap, String tooltipText) {
  556. return Tooltip(
  557. message: "$tooltipText: ${enabled ? "ON" : "OFF"}",
  558. waitDuration: Duration.zero,
  559. child: Container(
  560. decoration: BoxDecoration(
  561. color: enabled ? MyTheme.accent : Colors.grey[700],
  562. borderRadius: BorderRadius.circular(10.0),
  563. ),
  564. padding: EdgeInsets.all(8.0),
  565. child: InkWell(
  566. onTap: () =>
  567. checkClickTime(widget.client.id, () => onTap?.call(!enabled)),
  568. child: Column(
  569. mainAxisAlignment: MainAxisAlignment.spaceAround,
  570. children: [
  571. Expanded(
  572. child: Icon(
  573. iconData,
  574. color: Colors.white,
  575. ),
  576. ),
  577. ],
  578. ),
  579. ),
  580. ),
  581. );
  582. }
  583. @override
  584. Widget build(BuildContext context) {
  585. final crossAxisCount = 4;
  586. final spacing = 10.0;
  587. return Container(
  588. width: double.infinity,
  589. height: 160.0,
  590. margin: EdgeInsets.all(5.0),
  591. padding: EdgeInsets.all(5.0),
  592. decoration: BoxDecoration(
  593. borderRadius: BorderRadius.circular(10.0),
  594. color: Theme.of(context).colorScheme.background,
  595. boxShadow: [
  596. BoxShadow(
  597. color: Colors.black.withOpacity(0.2),
  598. spreadRadius: 1,
  599. blurRadius: 1,
  600. offset: Offset(0, 1.5),
  601. ),
  602. ],
  603. ),
  604. child: Column(
  605. crossAxisAlignment: CrossAxisAlignment.center,
  606. children: [
  607. Text(
  608. translate("Permissions"),
  609. style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
  610. textAlign: TextAlign.center,
  611. ).marginOnly(left: 4.0, bottom: 8.0),
  612. Expanded(
  613. child: GridView.count(
  614. crossAxisCount: crossAxisCount,
  615. padding: EdgeInsets.symmetric(horizontal: spacing),
  616. mainAxisSpacing: spacing,
  617. crossAxisSpacing: spacing,
  618. children: client.type_() == ClientType.camera
  619. ? [
  620. buildPermissionIcon(
  621. client.audio,
  622. Icons.volume_up_rounded,
  623. (enabled) {
  624. bind.cmSwitchPermission(
  625. connId: client.id,
  626. name: "audio",
  627. enabled: enabled);
  628. setState(() {
  629. client.audio = enabled;
  630. });
  631. },
  632. translate('Enable audio'),
  633. ),
  634. buildPermissionIcon(
  635. client.recording,
  636. Icons.videocam_rounded,
  637. (enabled) {
  638. bind.cmSwitchPermission(
  639. connId: client.id,
  640. name: "recording",
  641. enabled: enabled);
  642. setState(() {
  643. client.recording = enabled;
  644. });
  645. },
  646. translate('Enable recording session'),
  647. ),
  648. ]
  649. : [
  650. buildPermissionIcon(
  651. client.keyboard,
  652. Icons.keyboard,
  653. (enabled) {
  654. bind.cmSwitchPermission(
  655. connId: client.id,
  656. name: "keyboard",
  657. enabled: enabled);
  658. setState(() {
  659. client.keyboard = enabled;
  660. });
  661. },
  662. translate('Enable keyboard/mouse'),
  663. ),
  664. buildPermissionIcon(
  665. client.clipboard,
  666. Icons.assignment_rounded,
  667. (enabled) {
  668. bind.cmSwitchPermission(
  669. connId: client.id,
  670. name: "clipboard",
  671. enabled: enabled);
  672. setState(() {
  673. client.clipboard = enabled;
  674. });
  675. },
  676. translate('Enable clipboard'),
  677. ),
  678. buildPermissionIcon(
  679. client.audio,
  680. Icons.volume_up_rounded,
  681. (enabled) {
  682. bind.cmSwitchPermission(
  683. connId: client.id,
  684. name: "audio",
  685. enabled: enabled);
  686. setState(() {
  687. client.audio = enabled;
  688. });
  689. },
  690. translate('Enable audio'),
  691. ),
  692. buildPermissionIcon(
  693. client.file,
  694. Icons.upload_file_rounded,
  695. (enabled) {
  696. bind.cmSwitchPermission(
  697. connId: client.id,
  698. name: "file",
  699. enabled: enabled);
  700. setState(() {
  701. client.file = enabled;
  702. });
  703. },
  704. translate('Enable file copy and paste'),
  705. ),
  706. buildPermissionIcon(
  707. client.restart,
  708. Icons.restart_alt_rounded,
  709. (enabled) {
  710. bind.cmSwitchPermission(
  711. connId: client.id,
  712. name: "restart",
  713. enabled: enabled);
  714. setState(() {
  715. client.restart = enabled;
  716. });
  717. },
  718. translate('Enable remote restart'),
  719. ),
  720. buildPermissionIcon(
  721. client.recording,
  722. Icons.videocam_rounded,
  723. (enabled) {
  724. bind.cmSwitchPermission(
  725. connId: client.id,
  726. name: "recording",
  727. enabled: enabled);
  728. setState(() {
  729. client.recording = enabled;
  730. });
  731. },
  732. translate('Enable recording session'),
  733. ),
  734. // only windows support block input
  735. if (isWindows)
  736. buildPermissionIcon(
  737. client.blockInput,
  738. Icons.block,
  739. (enabled) {
  740. bind.cmSwitchPermission(
  741. connId: client.id,
  742. name: "block_input",
  743. enabled: enabled);
  744. setState(() {
  745. client.blockInput = enabled;
  746. });
  747. },
  748. translate('Enable blocking user input'),
  749. )
  750. ],
  751. ),
  752. ),
  753. ],
  754. ),
  755. );
  756. }
  757. }
  758. const double buttonBottomMargin = 8;
  759. class _CmControlPanel extends StatelessWidget {
  760. final Client client;
  761. const _CmControlPanel({Key? key, required this.client}) : super(key: key);
  762. @override
  763. Widget build(BuildContext context) {
  764. return client.authorized
  765. ? client.disconnected
  766. ? buildDisconnected(context)
  767. : buildAuthorized(context)
  768. : buildUnAuthorized(context);
  769. }
  770. buildAuthorized(BuildContext context) {
  771. final bool canElevate = bind.cmCanElevate();
  772. final model = Provider.of<ServerModel>(context);
  773. final showElevation = canElevate &&
  774. model.showElevation &&
  775. client.type_() == ClientType.remote;
  776. return Column(
  777. mainAxisAlignment: MainAxisAlignment.end,
  778. children: [
  779. Offstage(
  780. offstage: !client.inVoiceCall,
  781. child: Row(
  782. children: [
  783. Expanded(
  784. child: buildButton(context,
  785. color: MyTheme.accent,
  786. onClick: null, onTapDown: (details) async {
  787. final devicesInfo =
  788. await AudioInput.getDevicesInfo(true, true);
  789. List<String> devices = devicesInfo['devices'] as List<String>;
  790. if (devices.isEmpty) {
  791. msgBox(
  792. gFFI.sessionId,
  793. 'custom-nocancel-info',
  794. 'Prompt',
  795. 'no_audio_input_device_tip',
  796. '',
  797. gFFI.dialogManager,
  798. );
  799. return;
  800. }
  801. String currentDevice = devicesInfo['current'] as String;
  802. final x = details.globalPosition.dx;
  803. final y = details.globalPosition.dy;
  804. final position = RelativeRect.fromLTRB(x, y, x, y);
  805. showMenu(
  806. context: context,
  807. position: position,
  808. items: devices
  809. .map((d) => PopupMenuItem<String>(
  810. value: d,
  811. height: 18,
  812. padding: EdgeInsets.zero,
  813. onTap: () => AudioInput.setDevice(d, true, true),
  814. child: IgnorePointer(
  815. child: RadioMenuButton(
  816. value: d,
  817. groupValue: currentDevice,
  818. onChanged: (v) {
  819. if (v != null)
  820. AudioInput.setDevice(v, true, true);
  821. },
  822. child: Container(
  823. child: Text(
  824. d,
  825. overflow: TextOverflow.ellipsis,
  826. maxLines: 1,
  827. ),
  828. constraints: BoxConstraints(
  829. maxWidth:
  830. kConnectionManagerWindowSizeClosedChat
  831. .width -
  832. 80),
  833. ),
  834. )),
  835. ))
  836. .toList(),
  837. );
  838. },
  839. icon: Icon(
  840. Icons.call_rounded,
  841. color: Colors.white,
  842. size: 14,
  843. ),
  844. text: "Audio input",
  845. textColor: Colors.white),
  846. ),
  847. Expanded(
  848. child: buildButton(
  849. context,
  850. color: Colors.red,
  851. onClick: () => closeVoiceCall(),
  852. icon: Icon(
  853. Icons.call_end_rounded,
  854. color: Colors.white,
  855. size: 14,
  856. ),
  857. text: "Stop voice call",
  858. textColor: Colors.white,
  859. ),
  860. )
  861. ],
  862. ),
  863. ),
  864. Offstage(
  865. offstage: !client.incomingVoiceCall,
  866. child: Row(
  867. children: [
  868. Expanded(
  869. child: buildButton(context,
  870. color: MyTheme.accent,
  871. onClick: () => handleVoiceCall(true),
  872. icon: Icon(
  873. Icons.call_rounded,
  874. color: Colors.white,
  875. size: 14,
  876. ),
  877. text: "Accept",
  878. textColor: Colors.white),
  879. ),
  880. Expanded(
  881. child: buildButton(
  882. context,
  883. color: Colors.red,
  884. onClick: () => handleVoiceCall(false),
  885. icon: Icon(
  886. Icons.phone_disabled_rounded,
  887. color: Colors.white,
  888. size: 14,
  889. ),
  890. text: "Dismiss",
  891. textColor: Colors.white,
  892. ),
  893. )
  894. ],
  895. ),
  896. ),
  897. Offstage(
  898. offstage: !client.fromSwitch,
  899. child: buildButton(context,
  900. color: Colors.purple,
  901. onClick: () => handleSwitchBack(context),
  902. icon: Icon(Icons.reply, color: Colors.white),
  903. text: "Switch Sides",
  904. textColor: Colors.white),
  905. ),
  906. Offstage(
  907. offstage: !showElevation,
  908. child: buildButton(
  909. context,
  910. color: MyTheme.accent,
  911. onClick: () {
  912. handleElevate(context);
  913. windowManager.minimize();
  914. },
  915. icon: Icon(
  916. Icons.security_rounded,
  917. color: Colors.white,
  918. size: 14,
  919. ),
  920. text: 'Elevate',
  921. textColor: Colors.white,
  922. ),
  923. ),
  924. Row(
  925. children: [
  926. Expanded(
  927. child: buildButton(context,
  928. color: Colors.redAccent,
  929. onClick: handleDisconnect,
  930. text: 'Disconnect',
  931. icon: Icon(
  932. Icons.link_off_rounded,
  933. color: Colors.white,
  934. size: 14,
  935. ),
  936. textColor: Colors.white),
  937. ),
  938. ],
  939. )
  940. ],
  941. ).marginOnly(bottom: buttonBottomMargin);
  942. }
  943. buildDisconnected(BuildContext context) {
  944. return Row(
  945. mainAxisAlignment: MainAxisAlignment.center,
  946. children: [
  947. Expanded(
  948. child: buildButton(context,
  949. color: MyTheme.accent,
  950. onClick: handleClose,
  951. text: 'Close',
  952. textColor: Colors.white)),
  953. ],
  954. ).marginOnly(bottom: buttonBottomMargin);
  955. }
  956. buildUnAuthorized(BuildContext context) {
  957. final bool canElevate = bind.cmCanElevate();
  958. final model = Provider.of<ServerModel>(context);
  959. final showElevation = canElevate &&
  960. model.showElevation &&
  961. client.type_() == ClientType.remote;
  962. final showAccept = model.approveMode != 'password';
  963. return Column(
  964. mainAxisAlignment: MainAxisAlignment.end,
  965. children: [
  966. Offstage(
  967. offstage: !showElevation || !showAccept,
  968. child: buildButton(context, color: Colors.green[700], onClick: () {
  969. handleAccept(context);
  970. handleElevate(context);
  971. windowManager.minimize();
  972. },
  973. text: 'Accept and Elevate',
  974. icon: Icon(
  975. Icons.security_rounded,
  976. color: Colors.white,
  977. size: 14,
  978. ),
  979. textColor: Colors.white,
  980. tooltip: 'accept_and_elevate_btn_tooltip'),
  981. ),
  982. Row(
  983. mainAxisAlignment: MainAxisAlignment.center,
  984. children: [
  985. if (showAccept)
  986. Expanded(
  987. child: Column(
  988. children: [
  989. buildButton(
  990. context,
  991. color: MyTheme.accent,
  992. onClick: () {
  993. handleAccept(context);
  994. windowManager.minimize();
  995. },
  996. text: 'Accept',
  997. textColor: Colors.white,
  998. ),
  999. ],
  1000. ),
  1001. ),
  1002. Expanded(
  1003. child: buildButton(
  1004. context,
  1005. color: Colors.transparent,
  1006. border: Border.all(color: Colors.grey),
  1007. onClick: handleDisconnect,
  1008. text: 'Cancel',
  1009. textColor: null,
  1010. ),
  1011. ),
  1012. ],
  1013. ),
  1014. ],
  1015. ).marginOnly(bottom: buttonBottomMargin);
  1016. }
  1017. Widget buildButton(BuildContext context,
  1018. {required Color? color,
  1019. GestureTapCallback? onClick,
  1020. Widget? icon,
  1021. BoxBorder? border,
  1022. required String text,
  1023. required Color? textColor,
  1024. String? tooltip,
  1025. GestureTapDownCallback? onTapDown}) {
  1026. assert(!(onClick == null && onTapDown == null));
  1027. Widget textWidget;
  1028. if (icon != null) {
  1029. textWidget = Text(
  1030. translate(text),
  1031. style: TextStyle(color: textColor),
  1032. textAlign: TextAlign.center,
  1033. );
  1034. } else {
  1035. textWidget = Expanded(
  1036. child: Text(
  1037. translate(text),
  1038. style: TextStyle(color: textColor),
  1039. textAlign: TextAlign.center,
  1040. ),
  1041. );
  1042. }
  1043. final borderRadius = BorderRadius.circular(10.0);
  1044. final btn = Container(
  1045. height: 28,
  1046. decoration: BoxDecoration(
  1047. color: color, borderRadius: borderRadius, border: border),
  1048. child: InkWell(
  1049. borderRadius: borderRadius,
  1050. onTap: () {
  1051. if (onClick == null) return;
  1052. checkClickTime(client.id, onClick);
  1053. },
  1054. onTapDown: (details) {
  1055. if (onTapDown == null) return;
  1056. checkClickTime(client.id, () {
  1057. onTapDown.call(details);
  1058. });
  1059. },
  1060. child: Row(
  1061. mainAxisAlignment: MainAxisAlignment.center,
  1062. children: [
  1063. Offstage(offstage: icon == null, child: icon).marginOnly(right: 5),
  1064. textWidget,
  1065. ],
  1066. ),
  1067. ),
  1068. );
  1069. return (tooltip != null
  1070. ? Tooltip(
  1071. message: translate(tooltip),
  1072. child: btn,
  1073. )
  1074. : btn)
  1075. .marginAll(4);
  1076. }
  1077. void handleDisconnect() {
  1078. bind.cmCloseConnection(connId: client.id);
  1079. }
  1080. void handleAccept(BuildContext context) {
  1081. final model = Provider.of<ServerModel>(context, listen: false);
  1082. model.sendLoginResponse(client, true);
  1083. }
  1084. void handleElevate(BuildContext context) {
  1085. final model = Provider.of<ServerModel>(context, listen: false);
  1086. model.setShowElevation(false);
  1087. bind.cmElevatePortable(connId: client.id);
  1088. }
  1089. void handleClose() async {
  1090. await bind.cmRemoveDisconnectedConnection(connId: client.id);
  1091. if (await bind.cmGetClientsLength() == 0) {
  1092. windowManager.close();
  1093. }
  1094. }
  1095. void handleSwitchBack(BuildContext context) {
  1096. bind.cmSwitchBack(connId: client.id);
  1097. }
  1098. void handleVoiceCall(bool accept) {
  1099. bind.cmHandleIncomingVoiceCall(id: client.id, accept: accept);
  1100. }
  1101. void closeVoiceCall() {
  1102. bind.cmCloseVoiceCall(id: client.id);
  1103. }
  1104. }
  1105. void checkClickTime(int id, Function() callback) async {
  1106. if (allowRemoteCMModification()) {
  1107. callback();
  1108. return;
  1109. }
  1110. var clickCallbackTime = DateTime.now().millisecondsSinceEpoch;
  1111. await bind.cmCheckClickTime(connId: id);
  1112. Timer(const Duration(milliseconds: 120), () async {
  1113. var d = clickCallbackTime - await bind.cmGetClickTime();
  1114. if (d > 120) callback();
  1115. });
  1116. }
  1117. bool allowRemoteCMModification() {
  1118. return option2bool(kOptionAllowRemoteCmModification,
  1119. bind.mainGetLocalOption(key: kOptionAllowRemoteCmModification));
  1120. }
  1121. class _FileTransferLogPage extends StatefulWidget {
  1122. _FileTransferLogPage({Key? key}) : super(key: key);
  1123. @override
  1124. State<_FileTransferLogPage> createState() => __FileTransferLogPageState();
  1125. }
  1126. class __FileTransferLogPageState extends State<_FileTransferLogPage> {
  1127. @override
  1128. Widget build(BuildContext context) {
  1129. return statusList();
  1130. }
  1131. Widget generateCard(Widget child) {
  1132. return Container(
  1133. decoration: BoxDecoration(
  1134. color: Theme.of(context).cardColor,
  1135. borderRadius: BorderRadius.all(
  1136. Radius.circular(15.0),
  1137. ),
  1138. ),
  1139. child: child,
  1140. );
  1141. }
  1142. iconLabel(CmFileLog item) {
  1143. switch (item.action) {
  1144. case CmFileAction.none:
  1145. return Container();
  1146. case CmFileAction.localToRemote:
  1147. case CmFileAction.remoteToLocal:
  1148. return Column(
  1149. children: [
  1150. Transform.rotate(
  1151. angle: item.action == CmFileAction.remoteToLocal ? 0 : pi,
  1152. child: SvgPicture.asset(
  1153. "assets/arrow.svg",
  1154. colorFilter: svgColor(Theme.of(context).tabBarTheme.labelColor),
  1155. ),
  1156. ),
  1157. Text(item.action == CmFileAction.remoteToLocal
  1158. ? translate('Send')
  1159. : translate('Receive'))
  1160. ],
  1161. );
  1162. case CmFileAction.remove:
  1163. return Column(
  1164. children: [
  1165. Icon(
  1166. Icons.delete,
  1167. color: Theme.of(context).tabBarTheme.labelColor,
  1168. ),
  1169. Text(translate('Delete'))
  1170. ],
  1171. );
  1172. case CmFileAction.createDir:
  1173. return Column(
  1174. children: [
  1175. Icon(
  1176. Icons.create_new_folder,
  1177. color: Theme.of(context).tabBarTheme.labelColor,
  1178. ),
  1179. Text(translate('Create Folder'))
  1180. ],
  1181. );
  1182. case CmFileAction.rename:
  1183. return Column(
  1184. children: [
  1185. Icon(
  1186. Icons.drive_file_move_outlined,
  1187. color: Theme.of(context).tabBarTheme.labelColor,
  1188. ),
  1189. Text(translate('Rename'))
  1190. ],
  1191. );
  1192. }
  1193. }
  1194. Widget statusList() {
  1195. return PreferredSize(
  1196. preferredSize: const Size(200, double.infinity),
  1197. child: Container(
  1198. padding: const EdgeInsets.all(12.0),
  1199. child: Obx(
  1200. () {
  1201. final jobTable = gFFI.cmFileModel.currentJobTable;
  1202. statusListView(List<CmFileLog> jobs) => ListView.builder(
  1203. controller: ScrollController(),
  1204. itemBuilder: (BuildContext context, int index) {
  1205. final item = jobs[index];
  1206. return Padding(
  1207. padding: const EdgeInsets.only(bottom: 5),
  1208. child: generateCard(
  1209. Column(
  1210. mainAxisSize: MainAxisSize.min,
  1211. children: [
  1212. Row(
  1213. crossAxisAlignment: CrossAxisAlignment.center,
  1214. children: [
  1215. SizedBox(
  1216. width: 50,
  1217. child: iconLabel(item),
  1218. ).paddingOnly(left: 15),
  1219. const SizedBox(
  1220. width: 16.0,
  1221. ),
  1222. Expanded(
  1223. child: Column(
  1224. mainAxisSize: MainAxisSize.min,
  1225. crossAxisAlignment:
  1226. CrossAxisAlignment.start,
  1227. children: [
  1228. Text(
  1229. item.fileName,
  1230. ).paddingSymmetric(vertical: 10),
  1231. if (item.totalSize > 0)
  1232. Text(
  1233. '${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}',
  1234. style: TextStyle(
  1235. fontSize: 12,
  1236. color: MyTheme.darkGray,
  1237. ),
  1238. ),
  1239. if (item.totalSize > 0)
  1240. Offstage(
  1241. offstage: item.state !=
  1242. JobState.inProgress,
  1243. child: Text(
  1244. '${translate("Speed")} ${readableFileSize(item.speed)}/s',
  1245. style: TextStyle(
  1246. fontSize: 12,
  1247. color: MyTheme.darkGray,
  1248. ),
  1249. ),
  1250. ),
  1251. Offstage(
  1252. offstage: !(item.isTransfer() &&
  1253. item.state !=
  1254. JobState.inProgress),
  1255. child: Text(
  1256. translate(
  1257. item.display(),
  1258. ),
  1259. style: TextStyle(
  1260. fontSize: 12,
  1261. color: MyTheme.darkGray,
  1262. ),
  1263. ),
  1264. ),
  1265. if (item.totalSize > 0)
  1266. Offstage(
  1267. offstage: item.state !=
  1268. JobState.inProgress,
  1269. child: LinearPercentIndicator(
  1270. padding:
  1271. EdgeInsets.only(right: 15),
  1272. animateFromLastPercent: true,
  1273. center: Text(
  1274. '${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%',
  1275. ),
  1276. barRadius: Radius.circular(15),
  1277. percent: item.finishedSize /
  1278. item.totalSize,
  1279. progressColor: MyTheme.accent,
  1280. backgroundColor:
  1281. Theme.of(context).hoverColor,
  1282. lineHeight:
  1283. kDesktopFileTransferRowHeight,
  1284. ).paddingSymmetric(vertical: 15),
  1285. ),
  1286. ],
  1287. ),
  1288. ),
  1289. Row(
  1290. mainAxisAlignment: MainAxisAlignment.end,
  1291. children: [],
  1292. ),
  1293. ],
  1294. ),
  1295. ],
  1296. ).paddingSymmetric(vertical: 10),
  1297. ),
  1298. );
  1299. },
  1300. itemCount: jobTable.length,
  1301. );
  1302. return jobTable.isEmpty
  1303. ? generateCard(
  1304. Center(
  1305. child: Column(
  1306. mainAxisAlignment: MainAxisAlignment.center,
  1307. children: [
  1308. SvgPicture.asset(
  1309. "assets/transfer.svg",
  1310. colorFilter: svgColor(
  1311. Theme.of(context).tabBarTheme.labelColor),
  1312. height: 40,
  1313. ).paddingOnly(bottom: 10),
  1314. Text(
  1315. translate("No transfers in progress"),
  1316. textAlign: TextAlign.center,
  1317. textScaler: TextScaler.linear(1.20),
  1318. style: TextStyle(
  1319. color:
  1320. Theme.of(context).tabBarTheme.labelColor),
  1321. ),
  1322. ],
  1323. ),
  1324. ),
  1325. )
  1326. : statusListView(jobTable);
  1327. },
  1328. )),
  1329. );
  1330. }
  1331. }