1174 lines
		
	
	
		
			38 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			1174 lines
		
	
	
		
			38 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| // original cm window in Sciter version.
 | |
| 
 | |
| import 'dart:async';
 | |
| import 'dart:math';
 | |
| 
 | |
| 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/models/cm_file_model.dart';
 | |
| import 'package:flutter_hbb/utils/platform_channel.dart';
 | |
| import 'package:get/get.dart';
 | |
| import 'package:percent_indicator/linear_percent_indicator.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/file_model.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);
 | |
|     Get.put(tabController);
 | |
|     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 (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.cmFileModel.updateCurrentClientId(client.id);
 | |
|         }
 | |
|       }
 | |
|     };
 | |
|     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) => LayoutBuilder(
 | |
|                 builder: (context, constrains) {
 | |
|                   var borderWidth = 0.0;
 | |
|                   if (constrains.maxWidth >
 | |
|                       kConnectionManagerWindowSizeClosedChat.width) {
 | |
|                     borderWidth = kConnectionManagerWindowSizeOpenChat.width -
 | |
|                         constrains.maxWidth;
 | |
|                   } else {
 | |
|                     borderWidth = kConnectionManagerWindowSizeClosedChat.width -
 | |
|                         constrains.maxWidth;
 | |
|                   }
 | |
|                   if (borderWidth < 0 || borderWidth > 50) {
 | |
|                     borderWidth = 0;
 | |
|                   }
 | |
|                   final realClosedWidth =
 | |
|                       kConnectionManagerWindowSizeClosedChat.width -
 | |
|                           borderWidth;
 | |
|                   final realChatPageWidth =
 | |
|                       constrains.maxWidth - realClosedWidth;
 | |
|                   return Row(children: [
 | |
|                     if (constrains.maxWidth >
 | |
|                         kConnectionManagerWindowSizeClosedChat.width)
 | |
|                       Consumer<ChatModel>(
 | |
|                           builder: (_, model, child) => SizedBox(
 | |
|                                 width: realChatPageWidth,
 | |
|                                 child: buildRemoteBlock(
 | |
|                                   child: Container(
 | |
|                                       decoration: BoxDecoration(
 | |
|                                           border: Border(
 | |
|                                               right: BorderSide(
 | |
|                                                   color: Theme.of(context)
 | |
|                                                       .dividerColor))),
 | |
|                                       child: buildSidePage()),
 | |
|                                 ),
 | |
|                               )),
 | |
|                     SizedBox(
 | |
|                         width: realClosedWidth,
 | |
|                         child:
 | |
|                             SizedBox(width: realClosedWidth, child: pageView)),
 | |
|                   ]);
 | |
|                 },
 | |
|               ),
 | |
|             ),
 | |
|           );
 | |
|   }
 | |
| 
 | |
|   Widget buildSidePage() {
 | |
|     final selected = gFFI.serverModel.tabController.state.value.selected;
 | |
|     if (selected < 0 || selected >= gFFI.serverModel.clients.length) {
 | |
|       return Offstage();
 | |
|     }
 | |
|     final clientType = gFFI.serverModel.clients[selected].type_();
 | |
|     if (clientType == ClientType.file) {
 | |
|       return _FileTransferLogPage();
 | |
|     } else {
 | |
|       return ChatPage(type: ChatPageType.desktopCM);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   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: loadIcon(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 &&
 | |
|                     client.type_() != ClientType.file),
 | |
|             child: IconButton(
 | |
|               onPressed: () => checkClickTime(client.id, () {
 | |
|                 if (client.type_() == ClientType.file) {
 | |
|                   gFFI.chatModel.toggleCMFilePage();
 | |
|                 } else {
 | |
|                   gFFI.chatModel
 | |
|                       .toggleCMChatPage(MessageKey(client.peerId, client.id));
 | |
|                 }
 | |
|               }),
 | |
|               icon: SvgPicture.asset(client.type_() == ClientType.file
 | |
|                   ? 'assets/file_transfer.svg'
 | |
|                   : '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"}",
 | |
|       waitDuration: Duration.zero,
 | |
|       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,
 | |
|                 ),
 | |
|               ),
 | |
|             ],
 | |
|           ),
 | |
|         ),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     final crossAxisCount = 4;
 | |
|     final spacing = 10.0;
 | |
|     return Container(
 | |
|       width: double.infinity,
 | |
|       height: 160.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: crossAxisCount,
 | |
|               padding: EdgeInsets.symmetric(horizontal: spacing),
 | |
|               mainAxisSpacing: spacing,
 | |
|               crossAxisSpacing: spacing,
 | |
|               children: [
 | |
|                 buildPermissionIcon(
 | |
|                   client.keyboard,
 | |
|                   Icons.keyboard,
 | |
|                   (enabled) {
 | |
|                     bind.cmSwitchPermission(
 | |
|                         connId: client.id, name: "keyboard", enabled: enabled);
 | |
|                     setState(() {
 | |
|                       client.keyboard = enabled;
 | |
|                     });
 | |
|                   },
 | |
|                   translate('Enable keyboard/mouse'),
 | |
|                 ),
 | |
|                 buildPermissionIcon(
 | |
|                   client.clipboard,
 | |
|                   Icons.assignment_rounded,
 | |
|                   (enabled) {
 | |
|                     bind.cmSwitchPermission(
 | |
|                         connId: client.id, name: "clipboard", enabled: enabled);
 | |
|                     setState(() {
 | |
|                       client.clipboard = enabled;
 | |
|                     });
 | |
|                   },
 | |
|                   translate('Enable clipboard'),
 | |
|                 ),
 | |
|                 buildPermissionIcon(
 | |
|                   client.audio,
 | |
|                   Icons.volume_up_rounded,
 | |
|                   (enabled) {
 | |
|                     bind.cmSwitchPermission(
 | |
|                         connId: client.id, name: "audio", enabled: enabled);
 | |
|                     setState(() {
 | |
|                       client.audio = enabled;
 | |
|                     });
 | |
|                   },
 | |
|                   translate('Enable audio'),
 | |
|                 ),
 | |
|                 buildPermissionIcon(
 | |
|                   client.file,
 | |
|                   Icons.upload_file_rounded,
 | |
|                   (enabled) {
 | |
|                     bind.cmSwitchPermission(
 | |
|                         connId: client.id, name: "file", enabled: enabled);
 | |
|                     setState(() {
 | |
|                       client.file = enabled;
 | |
|                     });
 | |
|                   },
 | |
|                   translate('Enable 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('Enable remote restart'),
 | |
|                 ),
 | |
|                 buildPermissionIcon(
 | |
|                   client.recording,
 | |
|                   Icons.videocam_rounded,
 | |
|                   (enabled) {
 | |
|                     bind.cmSwitchPermission(
 | |
|                         connId: client.id, name: "recording", enabled: enabled);
 | |
|                     setState(() {
 | |
|                       client.recording = enabled;
 | |
|                     });
 | |
|                   },
 | |
|                   translate('Enable recording session'),
 | |
|                 ),
 | |
|                 // only windows support block input
 | |
|                 if (isWindows)
 | |
|                   buildPermissionIcon(
 | |
|                     client.blockInput,
 | |
|                     Icons.block,
 | |
|                     (enabled) {
 | |
|                       bind.cmSwitchPermission(
 | |
|                           connId: client.id,
 | |
|                           name: "block_input",
 | |
|                           enabled: enabled);
 | |
|                       setState(() {
 | |
|                         client.blockInput = enabled;
 | |
|                       });
 | |
|                     },
 | |
|                     translate('Enable blocking user input'),
 | |
|                   )
 | |
|               ],
 | |
|             ),
 | |
|           ),
 | |
|         ],
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| 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();
 | |
|   });
 | |
| }
 | |
| 
 | |
| class _FileTransferLogPage extends StatefulWidget {
 | |
|   _FileTransferLogPage({Key? key}) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   State<_FileTransferLogPage> createState() => __FileTransferLogPageState();
 | |
| }
 | |
| 
 | |
| class __FileTransferLogPageState extends State<_FileTransferLogPage> {
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return statusList();
 | |
|   }
 | |
| 
 | |
|   Widget generateCard(Widget child) {
 | |
|     return Container(
 | |
|       decoration: BoxDecoration(
 | |
|         color: Theme.of(context).cardColor,
 | |
|         borderRadius: BorderRadius.all(
 | |
|           Radius.circular(15.0),
 | |
|         ),
 | |
|       ),
 | |
|       child: child,
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   iconLabel(CmFileLog item) {
 | |
|     switch (item.action) {
 | |
|       case CmFileAction.none:
 | |
|         return Container();
 | |
|       case CmFileAction.localToRemote:
 | |
|       case CmFileAction.remoteToLocal:
 | |
|         return Column(
 | |
|           children: [
 | |
|             Transform.rotate(
 | |
|               angle: item.action == CmFileAction.remoteToLocal ? 0 : pi,
 | |
|               child: SvgPicture.asset(
 | |
|                 "assets/arrow.svg",
 | |
|                 colorFilter: svgColor(Theme.of(context).tabBarTheme.labelColor),
 | |
|               ),
 | |
|             ),
 | |
|             Text(item.action == CmFileAction.remoteToLocal
 | |
|                 ? translate('Send')
 | |
|                 : translate('Receive'))
 | |
|           ],
 | |
|         );
 | |
|       case CmFileAction.remove:
 | |
|         return Column(
 | |
|           children: [
 | |
|             Icon(
 | |
|               Icons.delete,
 | |
|               color: Theme.of(context).tabBarTheme.labelColor,
 | |
|             ),
 | |
|             Text(translate('Delete'))
 | |
|           ],
 | |
|         );
 | |
|       case CmFileAction.createDir:
 | |
|         return Column(
 | |
|           children: [
 | |
|             Icon(
 | |
|               Icons.create_new_folder,
 | |
|               color: Theme.of(context).tabBarTheme.labelColor,
 | |
|             ),
 | |
|             Text(translate('Create Folder'))
 | |
|           ],
 | |
|         );
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   Widget statusList() {
 | |
|     return PreferredSize(
 | |
|       preferredSize: const Size(200, double.infinity),
 | |
|       child: Container(
 | |
|           padding: const EdgeInsets.all(12.0),
 | |
|           child: Obx(
 | |
|             () {
 | |
|               final jobTable = gFFI.cmFileModel.currentJobTable;
 | |
|               statusListView(List<CmFileLog> jobs) => ListView.builder(
 | |
|                     controller: ScrollController(),
 | |
|                     itemBuilder: (BuildContext context, int index) {
 | |
|                       final item = jobs[index];
 | |
|                       return Padding(
 | |
|                         padding: const EdgeInsets.only(bottom: 5),
 | |
|                         child: generateCard(
 | |
|                           Column(
 | |
|                             mainAxisSize: MainAxisSize.min,
 | |
|                             children: [
 | |
|                               Row(
 | |
|                                 crossAxisAlignment: CrossAxisAlignment.center,
 | |
|                                 children: [
 | |
|                                   SizedBox(
 | |
|                                     width: 50,
 | |
|                                     child: iconLabel(item),
 | |
|                                   ).paddingOnly(left: 15),
 | |
|                                   const SizedBox(
 | |
|                                     width: 16.0,
 | |
|                                   ),
 | |
|                                   Expanded(
 | |
|                                     child: Column(
 | |
|                                       mainAxisSize: MainAxisSize.min,
 | |
|                                       crossAxisAlignment:
 | |
|                                           CrossAxisAlignment.start,
 | |
|                                       children: [
 | |
|                                         Text(
 | |
|                                           item.fileName,
 | |
|                                         ).paddingSymmetric(vertical: 10),
 | |
|                                         if (item.totalSize > 0)
 | |
|                                           Text(
 | |
|                                             '${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}',
 | |
|                                             style: TextStyle(
 | |
|                                               fontSize: 12,
 | |
|                                               color: MyTheme.darkGray,
 | |
|                                             ),
 | |
|                                           ),
 | |
|                                         if (item.totalSize > 0)
 | |
|                                           Offstage(
 | |
|                                             offstage: item.state !=
 | |
|                                                 JobState.inProgress,
 | |
|                                             child: Text(
 | |
|                                               '${translate("Speed")} ${readableFileSize(item.speed)}/s',
 | |
|                                               style: TextStyle(
 | |
|                                                 fontSize: 12,
 | |
|                                                 color: MyTheme.darkGray,
 | |
|                                               ),
 | |
|                                             ),
 | |
|                                           ),
 | |
|                                         Offstage(
 | |
|                                           offstage: !(item.isTransfer() &&
 | |
|                                               item.state !=
 | |
|                                                   JobState.inProgress),
 | |
|                                           child: Text(
 | |
|                                             translate(
 | |
|                                               item.display(),
 | |
|                                             ),
 | |
|                                             style: TextStyle(
 | |
|                                               fontSize: 12,
 | |
|                                               color: MyTheme.darkGray,
 | |
|                                             ),
 | |
|                                           ),
 | |
|                                         ),
 | |
|                                         if (item.totalSize > 0)
 | |
|                                           Offstage(
 | |
|                                             offstage: item.state !=
 | |
|                                                 JobState.inProgress,
 | |
|                                             child: LinearPercentIndicator(
 | |
|                                               padding:
 | |
|                                                   EdgeInsets.only(right: 15),
 | |
|                                               animateFromLastPercent: true,
 | |
|                                               center: Text(
 | |
|                                                 '${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%',
 | |
|                                               ),
 | |
|                                               barRadius: Radius.circular(15),
 | |
|                                               percent: item.finishedSize /
 | |
|                                                   item.totalSize,
 | |
|                                               progressColor: MyTheme.accent,
 | |
|                                               backgroundColor:
 | |
|                                                   Theme.of(context).hoverColor,
 | |
|                                               lineHeight:
 | |
|                                                   kDesktopFileTransferRowHeight,
 | |
|                                             ).paddingSymmetric(vertical: 15),
 | |
|                                           ),
 | |
|                                       ],
 | |
|                                     ),
 | |
|                                   ),
 | |
|                                   Row(
 | |
|                                     mainAxisAlignment: MainAxisAlignment.end,
 | |
|                                     children: [],
 | |
|                                   ),
 | |
|                                 ],
 | |
|                               ),
 | |
|                             ],
 | |
|                           ).paddingSymmetric(vertical: 10),
 | |
|                         ),
 | |
|                       );
 | |
|                     },
 | |
|                     itemCount: jobTable.length,
 | |
|                   );
 | |
| 
 | |
|               return jobTable.isEmpty
 | |
|                   ? generateCard(
 | |
|                       Center(
 | |
|                         child: Column(
 | |
|                           mainAxisAlignment: MainAxisAlignment.center,
 | |
|                           children: [
 | |
|                             SvgPicture.asset(
 | |
|                               "assets/transfer.svg",
 | |
|                               colorFilter: svgColor(
 | |
|                                   Theme.of(context).tabBarTheme.labelColor),
 | |
|                               height: 40,
 | |
|                             ).paddingOnly(bottom: 10),
 | |
|                             Text(
 | |
|                               translate("No transfers in progress"),
 | |
|                               textAlign: TextAlign.center,
 | |
|                               textScaler: TextScaler.linear(1.20),
 | |
|                               style: TextStyle(
 | |
|                                   color:
 | |
|                                       Theme.of(context).tabBarTheme.labelColor),
 | |
|                             ),
 | |
|                           ],
 | |
|                         ),
 | |
|                       ),
 | |
|                     )
 | |
|                   : statusListView(jobTable);
 | |
|             },
 | |
|           )),
 | |
|     );
 | |
|   }
 | |
| }
 |