server_page.dart 44 KB

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