915 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			915 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| // original cm window in Sciter version.
 | |
| 
 | |
| import 'dart:async';
 | |
| import 'dart:io';
 | |
| 
 | |
| import 'package:flutter/material.dart';
 | |
| import 'package:flutter_hbb/consts.dart';
 | |
| import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
 | |
| import 'package:flutter_hbb/models/chat_model.dart';
 | |
| import 'package:flutter_hbb/utils/platform_channel.dart';
 | |
| import 'package:get/get.dart';
 | |
| import 'package:provider/provider.dart';
 | |
| import 'package:window_manager/window_manager.dart';
 | |
| import 'package:flutter_svg/flutter_svg.dart';
 | |
| 
 | |
| import '../../common.dart';
 | |
| import '../../common/widgets/chat_page.dart';
 | |
| import '../../models/platform_model.dart';
 | |
| import '../../models/server_model.dart';
 | |
| 
 | |
| class DesktopServerPage extends StatefulWidget {
 | |
|   const DesktopServerPage({Key? key}) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   State<DesktopServerPage> createState() => _DesktopServerPageState();
 | |
| }
 | |
| 
 | |
| class _DesktopServerPageState extends State<DesktopServerPage>
 | |
|     with WindowListener, AutomaticKeepAliveClientMixin {
 | |
|   final tabController = gFFI.serverModel.tabController;
 | |
|   @override
 | |
|   void initState() {
 | |
|     gFFI.ffiModel.updateEventListener(gFFI.sessionId, "");
 | |
|     windowManager.addListener(this);
 | |
|     tabController.onRemoved = (_, id) {
 | |
|       onRemoveId(id);
 | |
|     };
 | |
|     super.initState();
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void dispose() {
 | |
|     windowManager.removeListener(this);
 | |
|     super.dispose();
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void onWindowClose() {
 | |
|     Future.wait([gFFI.serverModel.closeAll(), gFFI.close()]).then((_) {
 | |
|       if (Platform.isMacOS) {
 | |
|         RdPlatformChannel.instance.terminate();
 | |
|       } else {
 | |
|         windowManager.setPreventClose(false);
 | |
|         windowManager.close();
 | |
|       }
 | |
|     });
 | |
|     super.onWindowClose();
 | |
|   }
 | |
| 
 | |
|   void onRemoveId(String id) {
 | |
|     if (tabController.state.value.tabs.isEmpty) {
 | |
|       windowManager.close();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     super.build(context);
 | |
|     return MultiProvider(
 | |
|       providers: [
 | |
|         ChangeNotifierProvider.value(value: gFFI.serverModel),
 | |
|         ChangeNotifierProvider.value(value: gFFI.chatModel),
 | |
|       ],
 | |
|       child: Consumer<ServerModel>(
 | |
|         builder: (context, serverModel, child) => Container(
 | |
|           decoration: BoxDecoration(
 | |
|               border: Border.all(color: MyTheme.color(context).border!)),
 | |
|           child: Scaffold(
 | |
|             backgroundColor: Theme.of(context).scaffoldBackgroundColor,
 | |
|             body: ConnectionManager(),
 | |
|           ),
 | |
|         ),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   bool get wantKeepAlive => true;
 | |
| }
 | |
| 
 | |
| class ConnectionManager extends StatefulWidget {
 | |
|   @override
 | |
|   State<StatefulWidget> createState() => ConnectionManagerState();
 | |
| }
 | |
| 
 | |
| class ConnectionManagerState extends State<ConnectionManager> {
 | |
|   @override
 | |
|   void initState() {
 | |
|     gFFI.serverModel.updateClientState();
 | |
|     gFFI.serverModel.tabController.onSelected = (client_id_str) {
 | |
|       final client_id = int.tryParse(client_id_str);
 | |
|       if (client_id != null) {
 | |
|         final client =
 | |
|             gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == client_id);
 | |
|         if (client != null) {
 | |
|           gFFI.chatModel.changeCurrentKey(MessageKey(client.peerId, client.id));
 | |
|           if (client.unreadChatMessageCount.value > 0) {
 | |
|             Future.delayed(Duration.zero, () {
 | |
|               client.unreadChatMessageCount.value = 0;
 | |
|               gFFI.chatModel.showChatPage(MessageKey(client.peerId, client.id));
 | |
|             });
 | |
|           }
 | |
|           windowManager.setTitle(getWindowNameWithId(client.peerId));
 | |
|         }
 | |
|       }
 | |
|     };
 | |
|     gFFI.chatModel.isConnManager = true;
 | |
|     super.initState();
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     final serverModel = Provider.of<ServerModel>(context);
 | |
|     pointerHandler(PointerEvent e) {
 | |
|       if (serverModel.cmHiddenTimer != null) {
 | |
|         serverModel.cmHiddenTimer!.cancel();
 | |
|         serverModel.cmHiddenTimer = null;
 | |
|         debugPrint("CM hidden timer has been canceled");
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return serverModel.clients.isEmpty
 | |
|         ? Column(
 | |
|             children: [
 | |
|               buildTitleBar(),
 | |
|               Expanded(
 | |
|                 child: Center(
 | |
|                   child: Text(translate("Waiting")),
 | |
|                 ),
 | |
|               ),
 | |
|             ],
 | |
|           )
 | |
|         : Listener(
 | |
|             onPointerDown: pointerHandler,
 | |
|             onPointerMove: pointerHandler,
 | |
|             child: DesktopTab(
 | |
|               showTitle: false,
 | |
|               showMaximize: false,
 | |
|               showMinimize: true,
 | |
|               showClose: true,
 | |
|               onWindowCloseButton: handleWindowCloseButton,
 | |
|               controller: serverModel.tabController,
 | |
|               selectedBorderColor: MyTheme.accent,
 | |
|               maxLabelWidth: 100,
 | |
|               tail: buildScrollJumper(),
 | |
|               selectedTabBackgroundColor:
 | |
|                   Theme.of(context).hintColor.withOpacity(0),
 | |
|               tabBuilder: (key, icon, label, themeConf) {
 | |
|                 final client = serverModel.clients
 | |
|                     .firstWhereOrNull((client) => client.id.toString() == key);
 | |
|                 return Row(
 | |
|                   mainAxisAlignment: MainAxisAlignment.center,
 | |
|                   children: [
 | |
|                     Tooltip(
 | |
|                         message: key,
 | |
|                         waitDuration: Duration(seconds: 1),
 | |
|                         child: label),
 | |
|                     unreadMessageCountBuilder(client?.unreadChatMessageCount)
 | |
|                         .marginOnly(left: 4),
 | |
|                   ],
 | |
|                 );
 | |
|               },
 | |
|               pageViewBuilder: (pageView) => Row(
 | |
|                 children: [
 | |
|                   Consumer<ChatModel>(
 | |
|                     builder: (_, model, child) => model.isShowCMChatPage
 | |
|                         ? Expanded(
 | |
|                             child: buildRemoteBlock(
 | |
|                               child: Container(
 | |
|                                   decoration: BoxDecoration(
 | |
|                                       border: Border(
 | |
|                                           right: BorderSide(
 | |
|                                               color: Theme.of(context)
 | |
|                                                   .dividerColor))),
 | |
|                                   child:
 | |
|                                       ChatPage(type: ChatPageType.desktopCM)),
 | |
|                             ),
 | |
|                             flex: (kConnectionManagerWindowSizeOpenChat.width -
 | |
|                                     kConnectionManagerWindowSizeClosedChat
 | |
|                                         .width)
 | |
|                                 .toInt(),
 | |
|                           )
 | |
|                         : Offstage(),
 | |
|                   ),
 | |
|                   Expanded(
 | |
|                       child: pageView,
 | |
|                       flex: kConnectionManagerWindowSizeClosedChat.width
 | |
|                               .toInt() -
 | |
|                           4 // prevent stretch of the page view when chat is open,
 | |
|                       ),
 | |
|                 ],
 | |
|               ),
 | |
|             ),
 | |
|           );
 | |
|   }
 | |
| 
 | |
|   Widget buildTitleBar() {
 | |
|     return SizedBox(
 | |
|       height: kDesktopRemoteTabBarHeight,
 | |
|       child: Row(
 | |
|         crossAxisAlignment: CrossAxisAlignment.center,
 | |
|         children: [
 | |
|           const _AppIcon(),
 | |
|           Expanded(
 | |
|             child: GestureDetector(
 | |
|               onPanStart: (d) {
 | |
|                 windowManager.startDragging();
 | |
|               },
 | |
|               child: Container(
 | |
|                 color: Theme.of(context).colorScheme.background,
 | |
|               ),
 | |
|             ),
 | |
|           ),
 | |
|           const SizedBox(
 | |
|             width: 4.0,
 | |
|           ),
 | |
|           const _CloseButton()
 | |
|         ],
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   Widget buildScrollJumper() {
 | |
|     final offstage = gFFI.serverModel.clients.length < 2;
 | |
|     final sc = gFFI.serverModel.tabController.state.value.scrollController;
 | |
|     return Offstage(
 | |
|         offstage: offstage,
 | |
|         child: Row(
 | |
|           children: [
 | |
|             ActionIcon(
 | |
|                 icon: Icons.arrow_left, iconSize: 22, onTap: sc.backward),
 | |
|             ActionIcon(
 | |
|                 icon: Icons.arrow_right, iconSize: 22, onTap: sc.forward),
 | |
|           ],
 | |
|         ));
 | |
|   }
 | |
| 
 | |
|   Future<bool> handleWindowCloseButton() async {
 | |
|     var tabController = gFFI.serverModel.tabController;
 | |
|     final connLength = tabController.length;
 | |
|     if (connLength <= 1) {
 | |
|       windowManager.close();
 | |
|       return true;
 | |
|     } else {
 | |
|       final opt = "enable-confirm-closing-tabs";
 | |
|       final bool res;
 | |
|       if (!option2bool(opt, bind.mainGetLocalOption(key: opt))) {
 | |
|         res = true;
 | |
|       } else {
 | |
|         res = await closeConfirmDialog();
 | |
|       }
 | |
|       if (res) {
 | |
|         windowManager.close();
 | |
|       }
 | |
|       return res;
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| Widget buildConnectionCard(Client client) {
 | |
|   return Consumer<ServerModel>(
 | |
|     builder: (context, value, child) => Column(
 | |
|       mainAxisAlignment: MainAxisAlignment.start,
 | |
|       crossAxisAlignment: CrossAxisAlignment.start,
 | |
|       key: ValueKey(client.id),
 | |
|       children: [
 | |
|         _CmHeader(client: client),
 | |
|         client.type_() != ClientType.remote || client.disconnected
 | |
|             ? Offstage()
 | |
|             : _PrivilegeBoard(client: client),
 | |
|         Expanded(
 | |
|           child: Align(
 | |
|             alignment: Alignment.bottomCenter,
 | |
|             child: _CmControlPanel(client: client),
 | |
|           ),
 | |
|         )
 | |
|       ],
 | |
|     ).paddingSymmetric(vertical: 4.0, horizontal: 8.0),
 | |
|   );
 | |
| }
 | |
| 
 | |
| class _AppIcon extends StatelessWidget {
 | |
|   const _AppIcon({Key? key}) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return Container(
 | |
|       margin: EdgeInsets.symmetric(horizontal: 4.0),
 | |
|       child: SvgPicture.asset(
 | |
|         'assets/logo.svg',
 | |
|         width: 30,
 | |
|         height: 30,
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _CloseButton extends StatelessWidget {
 | |
|   const _CloseButton({Key? key}) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return IconButton(
 | |
|       onPressed: () {
 | |
|         windowManager.close();
 | |
|       },
 | |
|       icon: const Icon(
 | |
|         IconFont.close,
 | |
|         size: 18,
 | |
|       ),
 | |
|       splashColor: Colors.transparent,
 | |
|       hoverColor: Colors.transparent,
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _CmHeader extends StatefulWidget {
 | |
|   final Client client;
 | |
| 
 | |
|   const _CmHeader({Key? key, required this.client}) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   State<_CmHeader> createState() => _CmHeaderState();
 | |
| }
 | |
| 
 | |
| class _CmHeaderState extends State<_CmHeader>
 | |
|     with AutomaticKeepAliveClientMixin {
 | |
|   Client get client => widget.client;
 | |
| 
 | |
|   final _time = 0.obs;
 | |
|   Timer? _timer;
 | |
| 
 | |
|   @override
 | |
|   void initState() {
 | |
|     super.initState();
 | |
|     _timer = Timer.periodic(Duration(seconds: 1), (_) {
 | |
|       if (client.authorized && !client.disconnected) {
 | |
|         _time.value = _time.value + 1;
 | |
|       }
 | |
|     });
 | |
|     gFFI.serverModel.tabController.onSelected?.call(client.id.toString());
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void dispose() {
 | |
|     _timer?.cancel();
 | |
|     super.dispose();
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     super.build(context);
 | |
|     return Container(
 | |
|       decoration: BoxDecoration(
 | |
|         borderRadius: BorderRadius.circular(10.0),
 | |
|         gradient: LinearGradient(
 | |
|           begin: Alignment.topRight,
 | |
|           end: Alignment.bottomLeft,
 | |
|           colors: [
 | |
|             Color(0xff00bfe1),
 | |
|             Color(0xff0071ff),
 | |
|           ],
 | |
|         ),
 | |
|       ),
 | |
|       margin: EdgeInsets.symmetric(horizontal: 5.0, vertical: 10.0),
 | |
|       padding: EdgeInsets.only(
 | |
|         top: 10.0,
 | |
|         bottom: 10.0,
 | |
|         left: 10.0,
 | |
|         right: 5.0,
 | |
|       ),
 | |
|       child: Row(
 | |
|         crossAxisAlignment: CrossAxisAlignment.start,
 | |
|         children: [
 | |
|           Container(
 | |
|             width: 70,
 | |
|             height: 70,
 | |
|             alignment: Alignment.center,
 | |
|             decoration: BoxDecoration(
 | |
|               color: str2color(client.name),
 | |
|               borderRadius: BorderRadius.circular(15.0),
 | |
|             ),
 | |
|             child: Text(
 | |
|               client.name[0],
 | |
|               style: TextStyle(
 | |
|                 fontWeight: FontWeight.bold,
 | |
|                 color: Colors.white,
 | |
|                 fontSize: 55,
 | |
|               ),
 | |
|             ),
 | |
|           ).marginOnly(right: 10.0),
 | |
|           Expanded(
 | |
|             child: Column(
 | |
|               mainAxisAlignment: MainAxisAlignment.start,
 | |
|               crossAxisAlignment: CrossAxisAlignment.start,
 | |
|               children: [
 | |
|                 FittedBox(
 | |
|                     child: Text(
 | |
|                   client.name,
 | |
|                   style: TextStyle(
 | |
|                     color: Colors.white,
 | |
|                     fontWeight: FontWeight.bold,
 | |
|                     fontSize: 20,
 | |
|                     overflow: TextOverflow.ellipsis,
 | |
|                   ),
 | |
|                   maxLines: 1,
 | |
|                 )),
 | |
|                 FittedBox(
 | |
|                   child: Text(
 | |
|                     "(${client.peerId})",
 | |
|                     style: TextStyle(color: Colors.white, fontSize: 14),
 | |
|                   ),
 | |
|                 ).marginOnly(bottom: 10.0),
 | |
|                 FittedBox(
 | |
|                     child: Row(
 | |
|                   children: [
 | |
|                     Text(
 | |
|                       client.authorized
 | |
|                           ? client.disconnected
 | |
|                               ? translate("Disconnected")
 | |
|                               : translate("Connected")
 | |
|                           : "${translate("Request access to your device")}...",
 | |
|                       style: TextStyle(color: Colors.white),
 | |
|                     ).marginOnly(right: 8.0),
 | |
|                     if (client.authorized)
 | |
|                       Obx(
 | |
|                         () => Text(
 | |
|                           formatDurationToTime(
 | |
|                             Duration(seconds: _time.value),
 | |
|                           ),
 | |
|                           style: TextStyle(color: Colors.white),
 | |
|                         ),
 | |
|                       )
 | |
|                   ],
 | |
|                 ))
 | |
|               ],
 | |
|             ),
 | |
|           ),
 | |
|           Offstage(
 | |
|             offstage: !client.authorized || client.type_() != ClientType.remote,
 | |
|             child: IconButton(
 | |
|               onPressed: () => checkClickTime(
 | |
|                 client.id,
 | |
|                 () => gFFI.chatModel
 | |
|                     .toggleCMChatPage(MessageKey(client.peerId, client.id)),
 | |
|               ),
 | |
|               icon: SvgPicture.asset('assets/chat2.svg'),
 | |
|               splashRadius: kDesktopIconButtonSplashRadius,
 | |
|             ),
 | |
|           )
 | |
|         ],
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   bool get wantKeepAlive => true;
 | |
| }
 | |
| 
 | |
| class _PrivilegeBoard extends StatefulWidget {
 | |
|   final Client client;
 | |
| 
 | |
|   const _PrivilegeBoard({Key? key, required this.client}) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   State<StatefulWidget> createState() => _PrivilegeBoardState();
 | |
| }
 | |
| 
 | |
| class _PrivilegeBoardState extends State<_PrivilegeBoard> {
 | |
|   late final client = widget.client;
 | |
|   Widget buildPermissionIcon(bool enabled, IconData iconData,
 | |
|       Function(bool)? onTap, String tooltipText) {
 | |
|     return Tooltip(
 | |
|       message: "$tooltipText: ${enabled ? "ON" : "OFF"}",
 | |
|       child: Container(
 | |
|         decoration: BoxDecoration(
 | |
|           color: enabled ? MyTheme.accent : Colors.grey[700],
 | |
|           borderRadius: BorderRadius.circular(10.0),
 | |
|         ),
 | |
|         padding: EdgeInsets.all(8.0),
 | |
|         child: InkWell(
 | |
|           onTap: () =>
 | |
|               checkClickTime(widget.client.id, () => onTap?.call(!enabled)),
 | |
|           child: Column(
 | |
|             mainAxisAlignment: MainAxisAlignment.spaceAround,
 | |
|             children: [
 | |
|               Expanded(
 | |
|                 child: Icon(
 | |
|                   iconData,
 | |
|                   color: Colors.white,
 | |
|                   size: 32,
 | |
|                 ),
 | |
|               ),
 | |
|             ],
 | |
|           ),
 | |
|         ),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return Container(
 | |
|       width: double.infinity,
 | |
|       height: 200.0,
 | |
|       margin: EdgeInsets.all(5.0),
 | |
|       padding: EdgeInsets.all(5.0),
 | |
|       decoration: BoxDecoration(
 | |
|         borderRadius: BorderRadius.circular(10.0),
 | |
|         color: Theme.of(context).colorScheme.background,
 | |
|         boxShadow: [
 | |
|           BoxShadow(
 | |
|             color: Colors.black.withOpacity(0.2),
 | |
|             spreadRadius: 1,
 | |
|             blurRadius: 1,
 | |
|             offset: Offset(0, 1.5),
 | |
|           ),
 | |
|         ],
 | |
|       ),
 | |
|       child: Column(
 | |
|         crossAxisAlignment: CrossAxisAlignment.center,
 | |
|         children: [
 | |
|           Text(
 | |
|             translate("Permissions"),
 | |
|             style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
 | |
|             textAlign: TextAlign.center,
 | |
|           ).marginOnly(left: 4.0, bottom: 8.0),
 | |
|           Expanded(
 | |
|             child: GridView.count(
 | |
|               crossAxisCount: 3,
 | |
|               padding: EdgeInsets.symmetric(horizontal: 20.0),
 | |
|               mainAxisSpacing: 20.0,
 | |
|               crossAxisSpacing: 20.0,
 | |
|               children: [
 | |
|                 buildPermissionIcon(
 | |
|                   client.keyboard,
 | |
|                   Icons.keyboard,
 | |
|                   (enabled) {
 | |
|                     bind.cmSwitchPermission(
 | |
|                         connId: client.id, name: "keyboard", enabled: enabled);
 | |
|                     setState(() {
 | |
|                       client.keyboard = enabled;
 | |
|                     });
 | |
|                   },
 | |
|                   translate('Allow using keyboard and mouse'),
 | |
|                 ),
 | |
|                 buildPermissionIcon(
 | |
|                   client.clipboard,
 | |
|                   Icons.assignment_rounded,
 | |
|                   (enabled) {
 | |
|                     bind.cmSwitchPermission(
 | |
|                         connId: client.id, name: "clipboard", enabled: enabled);
 | |
|                     setState(() {
 | |
|                       client.clipboard = enabled;
 | |
|                     });
 | |
|                   },
 | |
|                   translate('Allow using clipboard'),
 | |
|                 ),
 | |
|                 buildPermissionIcon(
 | |
|                   client.audio,
 | |
|                   Icons.volume_up_rounded,
 | |
|                   (enabled) {
 | |
|                     bind.cmSwitchPermission(
 | |
|                         connId: client.id, name: "audio", enabled: enabled);
 | |
|                     setState(() {
 | |
|                       client.audio = enabled;
 | |
|                     });
 | |
|                   },
 | |
|                   translate('Allow hearing sound'),
 | |
|                 ),
 | |
|                 buildPermissionIcon(
 | |
|                   client.file,
 | |
|                   Icons.upload_file_rounded,
 | |
|                   (enabled) {
 | |
|                     bind.cmSwitchPermission(
 | |
|                         connId: client.id, name: "file", enabled: enabled);
 | |
|                     setState(() {
 | |
|                       client.file = enabled;
 | |
|                     });
 | |
|                   },
 | |
|                   translate('Allow file copy and paste'),
 | |
|                 ),
 | |
|                 buildPermissionIcon(
 | |
|                   client.restart,
 | |
|                   Icons.restart_alt_rounded,
 | |
|                   (enabled) {
 | |
|                     bind.cmSwitchPermission(
 | |
|                         connId: client.id, name: "restart", enabled: enabled);
 | |
|                     setState(() {
 | |
|                       client.restart = enabled;
 | |
|                     });
 | |
|                   },
 | |
|                   translate('Allow remote restart'),
 | |
|                 ),
 | |
|                 buildPermissionIcon(
 | |
|                   client.recording,
 | |
|                   Icons.videocam_rounded,
 | |
|                   (enabled) {
 | |
|                     bind.cmSwitchPermission(
 | |
|                         connId: client.id, name: "recording", enabled: enabled);
 | |
|                     setState(() {
 | |
|                       client.recording = enabled;
 | |
|                     });
 | |
|                   },
 | |
|                   translate('Allow recording session'),
 | |
|                 )
 | |
|               ],
 | |
|             ),
 | |
|           ),
 | |
|         ],
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| const double buttonBottomMargin = 8;
 | |
| 
 | |
| class _CmControlPanel extends StatelessWidget {
 | |
|   final Client client;
 | |
| 
 | |
|   const _CmControlPanel({Key? key, required this.client}) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return client.authorized
 | |
|         ? client.disconnected
 | |
|             ? buildDisconnected(context)
 | |
|             : buildAuthorized(context)
 | |
|         : buildUnAuthorized(context);
 | |
|   }
 | |
| 
 | |
|   buildAuthorized(BuildContext context) {
 | |
|     final bool canElevate = bind.cmCanElevate();
 | |
|     final model = Provider.of<ServerModel>(context);
 | |
|     final showElevation = canElevate &&
 | |
|         model.showElevation &&
 | |
|         client.type_() == ClientType.remote;
 | |
|     return Column(
 | |
|       mainAxisAlignment: MainAxisAlignment.end,
 | |
|       children: [
 | |
|         Offstage(
 | |
|           offstage: !client.inVoiceCall,
 | |
|           child: buildButton(
 | |
|             context,
 | |
|             color: Colors.red,
 | |
|             onClick: () => closeVoiceCall(),
 | |
|             icon: Icon(
 | |
|               Icons.call_end_rounded,
 | |
|               color: Colors.white,
 | |
|               size: 14,
 | |
|             ),
 | |
|             text: "Stop voice call",
 | |
|             textColor: Colors.white,
 | |
|           ),
 | |
|         ),
 | |
|         Offstage(
 | |
|           offstage: !client.incomingVoiceCall,
 | |
|           child: Row(
 | |
|             children: [
 | |
|               Expanded(
 | |
|                 child: buildButton(context,
 | |
|                     color: MyTheme.accent,
 | |
|                     onClick: () => handleVoiceCall(true),
 | |
|                     icon: Icon(
 | |
|                       Icons.call_rounded,
 | |
|                       color: Colors.white,
 | |
|                       size: 14,
 | |
|                     ),
 | |
|                     text: "Accept",
 | |
|                     textColor: Colors.white),
 | |
|               ),
 | |
|               Expanded(
 | |
|                 child: buildButton(
 | |
|                   context,
 | |
|                   color: Colors.red,
 | |
|                   onClick: () => handleVoiceCall(false),
 | |
|                   icon: Icon(
 | |
|                     Icons.phone_disabled_rounded,
 | |
|                     color: Colors.white,
 | |
|                     size: 14,
 | |
|                   ),
 | |
|                   text: "Dismiss",
 | |
|                   textColor: Colors.white,
 | |
|                 ),
 | |
|               )
 | |
|             ],
 | |
|           ),
 | |
|         ),
 | |
|         Offstage(
 | |
|           offstage: !client.fromSwitch,
 | |
|           child: buildButton(context,
 | |
|               color: Colors.purple,
 | |
|               onClick: () => handleSwitchBack(context),
 | |
|               icon: Icon(Icons.reply, color: Colors.white),
 | |
|               text: "Switch Sides",
 | |
|               textColor: Colors.white),
 | |
|         ),
 | |
|         Offstage(
 | |
|           offstage: !showElevation,
 | |
|           child: buildButton(
 | |
|             context,
 | |
|             color: MyTheme.accent,
 | |
|             onClick: () {
 | |
|               handleElevate(context);
 | |
|               windowManager.minimize();
 | |
|             },
 | |
|             icon: Icon(
 | |
|               Icons.security_rounded,
 | |
|               color: Colors.white,
 | |
|               size: 14,
 | |
|             ),
 | |
|             text: 'Elevate',
 | |
|             textColor: Colors.white,
 | |
|           ),
 | |
|         ),
 | |
|         Row(
 | |
|           children: [
 | |
|             Expanded(
 | |
|               child: buildButton(context,
 | |
|                   color: Colors.redAccent,
 | |
|                   onClick: handleDisconnect,
 | |
|                   text: 'Disconnect',
 | |
|                   icon: Icon(
 | |
|                     Icons.link_off_rounded,
 | |
|                     color: Colors.white,
 | |
|                     size: 14,
 | |
|                   ),
 | |
|                   textColor: Colors.white),
 | |
|             ),
 | |
|           ],
 | |
|         )
 | |
|       ],
 | |
|     ).marginOnly(bottom: buttonBottomMargin);
 | |
|   }
 | |
| 
 | |
|   buildDisconnected(BuildContext context) {
 | |
|     return Row(
 | |
|       mainAxisAlignment: MainAxisAlignment.center,
 | |
|       children: [
 | |
|         Expanded(
 | |
|             child: buildButton(context,
 | |
|                 color: MyTheme.accent,
 | |
|                 onClick: handleClose,
 | |
|                 text: 'Close',
 | |
|                 textColor: Colors.white)),
 | |
|       ],
 | |
|     ).marginOnly(bottom: buttonBottomMargin);
 | |
|   }
 | |
| 
 | |
|   buildUnAuthorized(BuildContext context) {
 | |
|     final bool canElevate = bind.cmCanElevate();
 | |
|     final model = Provider.of<ServerModel>(context);
 | |
|     final showElevation = canElevate &&
 | |
|         model.showElevation &&
 | |
|         client.type_() == ClientType.remote;
 | |
|     final showAccept = model.approveMode != 'password';
 | |
|     return Column(
 | |
|       mainAxisAlignment: MainAxisAlignment.end,
 | |
|       children: [
 | |
|         Offstage(
 | |
|           offstage: !showElevation || !showAccept,
 | |
|           child: buildButton(context, color: Colors.green[700], onClick: () {
 | |
|             handleAccept(context);
 | |
|             handleElevate(context);
 | |
|             windowManager.minimize();
 | |
|           },
 | |
|               text: 'Accept and Elevate',
 | |
|               icon: Icon(
 | |
|                 Icons.security_rounded,
 | |
|                 color: Colors.white,
 | |
|                 size: 14,
 | |
|               ),
 | |
|               textColor: Colors.white,
 | |
|               tooltip: 'accept_and_elevate_btn_tooltip'),
 | |
|         ),
 | |
|         Row(
 | |
|           mainAxisAlignment: MainAxisAlignment.center,
 | |
|           children: [
 | |
|             if (showAccept)
 | |
|               Expanded(
 | |
|                 child: Column(
 | |
|                   children: [
 | |
|                     buildButton(
 | |
|                       context,
 | |
|                       color: MyTheme.accent,
 | |
|                       onClick: () {
 | |
|                         handleAccept(context);
 | |
|                         windowManager.minimize();
 | |
|                       },
 | |
|                       text: 'Accept',
 | |
|                       textColor: Colors.white,
 | |
|                     ),
 | |
|                   ],
 | |
|                 ),
 | |
|               ),
 | |
|             Expanded(
 | |
|               child: buildButton(
 | |
|                 context,
 | |
|                 color: Colors.transparent,
 | |
|                 border: Border.all(color: Colors.grey),
 | |
|                 onClick: handleDisconnect,
 | |
|                 text: 'Cancel',
 | |
|                 textColor: null,
 | |
|               ),
 | |
|             ),
 | |
|           ],
 | |
|         ),
 | |
|       ],
 | |
|     ).marginOnly(bottom: buttonBottomMargin);
 | |
|   }
 | |
| 
 | |
|   Widget buildButton(BuildContext context,
 | |
|       {required Color? color,
 | |
|       required Function() onClick,
 | |
|       Icon? icon,
 | |
|       BoxBorder? border,
 | |
|       required String text,
 | |
|       required Color? textColor,
 | |
|       String? tooltip}) {
 | |
|     Widget textWidget;
 | |
|     if (icon != null) {
 | |
|       textWidget = Text(
 | |
|         translate(text),
 | |
|         style: TextStyle(color: textColor),
 | |
|         textAlign: TextAlign.center,
 | |
|       );
 | |
|     } else {
 | |
|       textWidget = Expanded(
 | |
|         child: Text(
 | |
|           translate(text),
 | |
|           style: TextStyle(color: textColor),
 | |
|           textAlign: TextAlign.center,
 | |
|         ),
 | |
|       );
 | |
|     }
 | |
|     final borderRadius = BorderRadius.circular(10.0);
 | |
|     final btn = Container(
 | |
|       height: 28,
 | |
|       decoration: BoxDecoration(
 | |
|           color: color, borderRadius: borderRadius, border: border),
 | |
|       child: InkWell(
 | |
|         borderRadius: borderRadius,
 | |
|         onTap: () => checkClickTime(client.id, onClick),
 | |
|         child: Row(
 | |
|           mainAxisAlignment: MainAxisAlignment.center,
 | |
|           children: [
 | |
|             Offstage(offstage: icon == null, child: icon).marginOnly(right: 5),
 | |
|             textWidget,
 | |
|           ],
 | |
|         ),
 | |
|       ),
 | |
|     );
 | |
|     return (tooltip != null
 | |
|             ? Tooltip(
 | |
|                 message: translate(tooltip),
 | |
|                 child: btn,
 | |
|               )
 | |
|             : btn)
 | |
|         .marginAll(4);
 | |
|   }
 | |
| 
 | |
|   void handleDisconnect() {
 | |
|     bind.cmCloseConnection(connId: client.id);
 | |
|   }
 | |
| 
 | |
|   void handleAccept(BuildContext context) {
 | |
|     final model = Provider.of<ServerModel>(context, listen: false);
 | |
|     model.sendLoginResponse(client, true);
 | |
|   }
 | |
| 
 | |
|   void handleElevate(BuildContext context) {
 | |
|     final model = Provider.of<ServerModel>(context, listen: false);
 | |
|     model.setShowElevation(false);
 | |
|     bind.cmElevatePortable(connId: client.id);
 | |
|   }
 | |
| 
 | |
|   void handleClose() async {
 | |
|     await bind.cmRemoveDisconnectedConnection(connId: client.id);
 | |
|     if (await bind.cmGetClientsLength() == 0) {
 | |
|       windowManager.close();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   void handleSwitchBack(BuildContext context) {
 | |
|     bind.cmSwitchBack(connId: client.id);
 | |
|   }
 | |
| 
 | |
|   void handleVoiceCall(bool accept) {
 | |
|     bind.cmHandleIncomingVoiceCall(id: client.id, accept: accept);
 | |
|   }
 | |
| 
 | |
|   void closeVoiceCall() {
 | |
|     bind.cmCloseVoiceCall(id: client.id);
 | |
|   }
 | |
| }
 | |
| 
 | |
| void checkClickTime(int id, Function() callback) async {
 | |
|   var clickCallbackTime = DateTime.now().millisecondsSinceEpoch;
 | |
|   await bind.cmCheckClickTime(connId: id);
 | |
|   Timer(const Duration(milliseconds: 120), () async {
 | |
|     var d = clickCallbackTime - await bind.cmGetClickTime();
 | |
|     if (d > 120) callback();
 | |
|   });
 | |
| }
 |