From 39a1545e94af91c39915e73cd33fc2d85af8122b Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 1 Sep 2022 12:07:05 +0800 Subject: [PATCH 01/19] add close confirmation dialog Signed-off-by: 21pages --- flutter/lib/desktop/pages/server_page.dart | 38 +++++++------ .../lib/desktop/widgets/tabbar_widget.dart | 55 ++++++++++++++++--- 2 files changed, 67 insertions(+), 26 deletions(-) diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index e7922403b..b4573297a 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -48,22 +48,25 @@ class _DesktopServerPageState extends State ], child: Consumer( builder: (context, serverModel, child) => Container( - decoration: BoxDecoration( - border: - Border.all(color: MyTheme.color(context).border!)), - child: Scaffold( - backgroundColor: MyTheme.color(context).bg, - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Expanded(child: ConnectionManager()), - SizedBox.fromSize(size: Size(0, 15.0)), - ], - ), - ), - ), - ))); + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: Scaffold( + backgroundColor: MyTheme.color(context).bg, + body: Overlay(initialEntries: [ + OverlayEntry(builder: (context) { + gFFI.dialogManager.setOverlayState(Overlay.of(context)); + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded(child: ConnectionManager()), + SizedBox.fromSize(size: Size(0, 15.0)), + ], + ), + ); + }) + ]), + )))); } @override @@ -109,7 +112,8 @@ class ConnectionManagerState extends State { theme: isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light(), showTitle: false, showMaximize: false, - showMinimize: false, + showMinimize: true, + showClose: true, controller: serverModel.tabController, tabType: DesktopTabType.cm, pageViewBuilder: (pageView) => Row(children: [ diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 38e724bad..755d6946c 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -314,6 +314,8 @@ class DesktopTab extends StatelessWidget { Offstage(offstage: tail == null, child: tail), WindowActionPanel( mainTab: isMainWindow, + tabType: tabType, + state: state, theme: theme, showMinimize: showMinimize, showMaximize: showMaximize, @@ -327,6 +329,8 @@ class DesktopTab extends StatelessWidget { class WindowActionPanel extends StatelessWidget { final bool mainTab; + final DesktopTabType tabType; + final Rx state; final TarBarTheme theme; final bool showMinimize; @@ -337,6 +341,8 @@ class WindowActionPanel extends StatelessWidget { const WindowActionPanel( {Key? key, required this.mainTab, + required this.tabType, + required this.state, required this.theme, this.showMinimize = true, this.showMaximize = true, @@ -411,22 +417,53 @@ class WindowActionPanel extends StatelessWidget { message: 'Close', icon: IconFont.close, theme: theme, - onTap: () { - if (mainTab) { - windowManager.close(); - } else { - // only hide for multi window, not close - Future.delayed(Duration.zero, () { - WindowController.fromWindowId(windowId!).hide(); - }); + onTap: () async { + action() { + if (mainTab) { + windowManager.close(); + } else { + // only hide for multi window, not close + Future.delayed(Duration.zero, () { + WindowController.fromWindowId(windowId!).hide(); + }); + } + onClose?.call(); + } + + if (tabType != DesktopTabType.main && + state.value.tabs.length > 1) { + closeConfirmDialog(action); + } else { + action(); } - onClose?.call(); }, is_close: true, )), ], ); } + + closeConfirmDialog(Function() callback) async { + final res = await gFFI.dialogManager + .show((setState, close) => CustomAlertDialog( + title: Row(children: [ + Icon(Icons.warning_amber_sharp, + color: Colors.redAccent, size: 28), + SizedBox(width: 10), + Text(translate("Warning")), + ]), + content: Text(translate("Disconnect all devices?")), + actions: [ + TextButton( + onPressed: () => close(), child: Text(translate("Cancel"))), + ElevatedButton( + onPressed: () => close(true), child: Text(translate("OK"))), + ], + )); + if (res == true) { + callback(); + } + } } // ignore: must_be_immutable From f6bc448cec584021a686cf16afca97a2db29df00 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 1 Sep 2022 21:18:53 +0800 Subject: [PATCH 02/19] adjust cm display behavior Signed-off-by: 21pages --- .../desktop/pages/connection_tab_page.dart | 5 ++- .../lib/desktop/pages/desktop_tab_page.dart | 3 +- .../desktop/pages/file_manager_tab_page.dart | 3 +- .../desktop/pages/port_forward_tab_page.dart | 5 ++- flutter/lib/desktop/pages/server_page.dart | 16 ++++++-- .../lib/desktop/widgets/tabbar_widget.dart | 38 +++++++++++++----- flutter/lib/main.dart | 1 + flutter/lib/mobile/pages/chat_page.dart | 1 + flutter/lib/models/chat_model.dart | 11 ++++- flutter/lib/models/server_model.dart | 40 ++++++++++--------- 10 files changed, 83 insertions(+), 40 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index 5687c5c7e..d9bc86fe2 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -20,7 +20,8 @@ class ConnectionTabPage extends StatefulWidget { } class _ConnectionTabPageState extends State { - final tabController = Get.put(DesktopTabController()); + final tabController = + Get.put(DesktopTabController(tabType: DesktopTabType.remoteScreen)); static const IconData selectedIcon = Icons.desktop_windows_sharp; static const IconData unselectedIcon = Icons.desktop_windows_outlined; @@ -60,6 +61,7 @@ class _ConnectionTabPageState extends State { if (call.method == "new_remote_desktop") { final args = jsonDecode(call.arguments); final id = args['id']; + ConnectionTypeState.init(id); window_on_top(windowId()); ConnectionTypeState.init(id); tabController.add(TabInfo( @@ -94,7 +96,6 @@ class _ConnectionTabPageState extends State { body: Obx(() => DesktopTab( controller: tabController, theme: theme, - tabType: DesktopTabType.remoteScreen, showTabBar: fullscreen.isFalse, onClose: () { tabController.clear(); diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 57ee43e14..874a71dcf 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -15,7 +15,7 @@ class DesktopTabPage extends StatefulWidget { } class _DesktopTabPageState extends State { - final tabController = DesktopTabController(); + final tabController = DesktopTabController(tabType: DesktopTabType.main); @override void initState() { @@ -46,7 +46,6 @@ class _DesktopTabPageState extends State { body: DesktopTab( controller: tabController, theme: dark ? TarBarTheme.dark() : TarBarTheme.light(), - tabType: DesktopTabType.main, tail: ActionIcon( message: 'Settings', icon: IconFont.menu, diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 6c8b58a30..add5eed9f 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -25,7 +25,7 @@ class _FileManagerTabPageState extends State { static final IconData unselectedIcon = Icons.file_copy_outlined; _FileManagerTabPageState(Map params) { - Get.put(DesktopTabController()); + Get.put(DesktopTabController(tabType: DesktopTabType.fileTransfer)); tabController.add(TabInfo( key: params['id'], label: params['id'], @@ -74,7 +74,6 @@ class _FileManagerTabPageState extends State { body: DesktopTab( controller: tabController, theme: theme, - tabType: DesktopTabType.fileTransfer, onClose: () { tabController.clear(); }, diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index 1e2c8e2bc..2340a4ca1 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -18,7 +18,7 @@ class PortForwardTabPage extends StatefulWidget { } class _PortForwardTabPageState extends State { - final tabController = Get.put(DesktopTabController()); + late final DesktopTabController tabController; late final bool isRDP; static const IconData selectedIcon = Icons.forward_sharp; @@ -26,6 +26,8 @@ class _PortForwardTabPageState extends State { _PortForwardTabPageState(Map params) { isRDP = params['isRDP']; + tabController = Get.put(DesktopTabController( + tabType: isRDP ? DesktopTabType.rdp : DesktopTabType.portForward)); tabController.add(TabInfo( key: params['id'], label: params['id'], @@ -78,7 +80,6 @@ class _PortForwardTabPageState extends State { body: DesktopTab( controller: tabController, theme: theme, - tabType: isRDP ? DesktopTabType.rdp : DesktopTabType.portForward, onClose: () { tabController.clear(); }, diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index b4573297a..f64adfca2 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -19,10 +19,12 @@ class DesktopServerPage extends StatefulWidget { class _DesktopServerPageState extends State with WindowListener, AutomaticKeepAliveClientMixin { + final tabController = gFFI.serverModel.tabController; @override void initState() { gFFI.ffiModel.updateEventListener(""); windowManager.addListener(this); + tabController.onRemove = (_, id) => onRemoveId(id); super.initState(); } @@ -39,6 +41,13 @@ class _DesktopServerPageState extends State super.onWindowClose(); } + void onRemoveId(String id) { + if (tabController.state.value.tabs.isEmpty) { + windowManager.close(); + } + } + + @override Widget build(BuildContext context) { super.build(context); return MultiProvider( @@ -115,7 +124,6 @@ class ConnectionManagerState extends State { showMinimize: true, showClose: true, controller: serverModel.tabController, - tabType: DesktopTabType.cm, pageViewBuilder: (pageView) => Row(children: [ Expanded(child: pageView), Consumer( @@ -454,8 +462,10 @@ class _CmControlPanel extends StatelessWidget { decoration: BoxDecoration( color: MyTheme.accent, borderRadius: BorderRadius.circular(10)), child: InkWell( - onTap: () => - checkClickTime(client.id, () => handleAccept(context)), + onTap: () => checkClickTime(client.id, () { + handleAccept(context); + windowManager.minimize(); + }), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 755d6946c..1a19dd833 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -59,13 +59,15 @@ class DesktopTabState { class DesktopTabController { final state = DesktopTabState().obs; + final DesktopTabType tabType; /// index, key Function(int, String)? onRemove; - Function(int)? onSelected; - void add(TabInfo tab) { + DesktopTabController({required this.tabType}); + + void add(TabInfo tab, {bool authorized = false}) { if (!isDesktop) return; final index = state.value.tabs.indexWhere((e) => e.key == tab.key); int toIndex; @@ -79,6 +81,16 @@ class DesktopTabController { toIndex = state.value.tabs.length - 1; assert(toIndex >= 0); } + if (tabType == DesktopTabType.cm) { + Future.delayed(Duration.zero, () async { + window_on_top(null); + }); + if (authorized) { + Future.delayed(const Duration(seconds: 3), () { + windowManager.minimize(); + }); + } + } try { jumpTo(toIndex); } catch (e) { @@ -106,6 +118,7 @@ class DesktopTabController { } void jumpTo(int index) { + if (!isDesktop || index < 0) return; state.update((val) { val!.selected = index; Future.delayed(Duration.zero, (() { @@ -114,12 +127,14 @@ class DesktopTabController { } if (val.scrollController.hasClients && val.scrollController.canScroll && - val.scrollController.itemCount >= index) { + val.scrollController.itemCount > index) { val.scrollController.scrollToItem(index, center: true, animate: true); } })); }); - onSelected?.call(index); + if (state.value.tabs.length > index) { + onSelected?.call(index); + } } void closeBy(String? key) { @@ -154,8 +169,6 @@ typedef LabelGetter = Rx Function(String key); class DesktopTab extends StatelessWidget { final Function(String)? onTabClose; final TarBarTheme theme; - final DesktopTabType tabType; - final bool isMainWindow; final bool showTabBar; final bool showLogo; final bool showTitle; @@ -170,10 +183,12 @@ class DesktopTab extends StatelessWidget { final DesktopTabController controller; Rx get state => controller.state; + late final DesktopTabType tabType; + late final bool isMainWindow; - const DesktopTab({ + DesktopTab({ + Key? key, required this.controller, - required this.tabType, this.theme = const TarBarTheme.light(), this.onTabClose, this.showTabBar = true, @@ -187,8 +202,11 @@ class DesktopTab extends StatelessWidget { this.onClose, this.tabBuilder, this.labelGetter, - }) : isMainWindow = - tabType == DesktopTabType.main || tabType == DesktopTabType.cm; + }) : super(key: key) { + tabType = controller.tabType; + isMainWindow = + tabType == DesktopTabType.main || tabType == DesktopTabType.cm; + } @override Widget build(BuildContext context) { diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index e1c254942..2f1d0680f 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -165,6 +165,7 @@ void runConnectionManagerScreen() async { await windowManager.setAlignment(Alignment.topRight); await windowManager.show(); await windowManager.focus(); + await windowManager.setAlignment(Alignment.topRight); // ensure }) ]); runApp(GetMaterialApp( diff --git a/flutter/lib/mobile/pages/chat_page.dart b/flutter/lib/mobile/pages/chat_page.dart index 738f34e89..b265f6995 100644 --- a/flutter/lib/mobile/pages/chat_page.dart +++ b/flutter/lib/mobile/pages/chat_page.dart @@ -59,6 +59,7 @@ class ChatPage extends StatelessWidget implements PageShape { messages: chatModel .messages[chatModel.currentID]?.chatMessages ?? [], + inputOptions: const InputOptions(sendOnEnter: true), messageOptions: MessageOptions( showOtherUsersAvatar: false, showTime: true, diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index de949c782..a9c791ef7 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -209,10 +209,19 @@ class ChatModel with ChangeNotifier { id: await bind.mainGetLastRemoteId(), ); } else { - final client = _ffi.target?.serverModel.clients[id]; + final client = _ffi.target?.serverModel.clients + .firstWhere((client) => client.id == id); if (client == null) { return debugPrint("Failed to receive msg,user doesn't exist"); } + if (isDesktop) { + window_on_top(null); + var index = _ffi.target?.serverModel.clients + .indexWhere((client) => client.id == id); + if (index != null && index >= 0) { + gFFI.serverModel.tabController.jumpTo(index); + } + } chatUser = ChatUser(id: client.peerId, firstName: client.name); } diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index fa7f15e54..31c579f83 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -32,7 +32,7 @@ class ServerModel with ChangeNotifier { late final TextEditingController _serverId; final _serverPasswd = TextEditingController(text: ""); - final tabController = DesktopTabController(); + final tabController = DesktopTabController(tabType: DesktopTabType.cm); List _clients = []; @@ -347,20 +347,18 @@ class ServerModel with ChangeNotifier { var res = await bind.mainGetClientsState(); try { final List clientsJson = jsonDecode(res); - if (isDesktop && clientsJson.isEmpty && _clients.isNotEmpty) { - // exit cm when >1 peers to no peers - exit(0); - } _clients.clear(); tabController.state.value.tabs.clear(); for (var clientJson in clientsJson) { final client = Client.fromJson(clientJson); _clients.add(client); - tabController.add(TabInfo( - key: client.id.toString(), - label: client.name, - closable: false, - page: Desktop.buildConnectionCard(client))); + tabController.add( + TabInfo( + key: client.id.toString(), + label: client.name, + closable: false, + page: Desktop.buildConnectionCard(client)), + authorized: client.authorized); } notifyListeners(); } catch (e) { @@ -471,14 +469,18 @@ class ServerModel with ChangeNotifier { } else { _clients[index].authorized = true; } - tabController.add(TabInfo( - key: client.id.toString(), - label: client.name, - closable: false, - page: Desktop.buildConnectionCard(client))); + tabController.add( + TabInfo( + key: client.id.toString(), + label: client.name, + closable: false, + page: Desktop.buildConnectionCard(client)), + authorized: true); scrollToBottom(); notifyListeners(); - } catch (e) {} + } catch (e) { + debugPrint("onClientAuthorized:$e"); + } } void onClientRemove(Map evt) { @@ -486,8 +488,10 @@ class ServerModel with ChangeNotifier { final id = int.parse(evt['id'] as String); if (_clients.any((c) => c.id == id)) { final index = _clients.indexWhere((client) => client.id == id); - _clients.removeAt(index); - tabController.remove(index); + if (index >= 0) { + _clients.removeAt(index); + tabController.remove(index); + } parent.target?.dialogManager.dismissByTag(getLoginDialogTag(id)); parent.target?.invokeMethod("cancel_notification", id); } From 2dc8c02d15daf2d2b53ba07b56afac4804e21180 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 1 Sep 2022 19:39:51 -0700 Subject: [PATCH 03/19] flutter_desktop: custom image quality ui Signed-off-by: fufesou --- flutter/lib/desktop/widgets/peercard_widget.dart | 2 +- flutter/lib/desktop/widgets/remote_menubar.dart | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index 114f4146e..a91f300fd 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -276,7 +276,7 @@ class _PeerCardState extends State<_PeerCard> color: _iconMoreHover.value ? MyTheme.color(context).text : MyTheme.color(context).lightText), - position: mod_menu.PopupMenuPosition.under, + position: mod_menu.PopupMenuPosition.over, itemBuilder: (BuildContext context) => snapshot.data!, )))); } else { diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 47536011d..a6399b77b 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -489,17 +489,24 @@ class _RemoteMenubarState extends State { final slider = Obx(() { return Slider( value: sliderValue.value, - max: 100, - divisions: 100, - label: sliderValue.value.round().toString(), + min: 10.0, + max: 100.0, + divisions: 90, + // label: sliderValue.value.round().toString(), onChanged: (double value) { sliderValue.value = value; rxReplay.add(value); }, ); }); + final content = Row( + children: [ + slider, + Obx(() => Text('${sliderValue.value.round()}% Bitrate')) + ], + ); msgBoxCommon(widget.ffi.dialogManager, 'Custom Image Quality', - slider, [btnCancel]); + content, [btnCancel]); } }), MenuEntryDivider(), From 722a4d3de7683de72e854a6d2bbc60269c494ab1 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 1 Sep 2022 22:36:40 -0700 Subject: [PATCH 04/19] flutter desktop: ui changes Signed-off-by: fufesou --- .../lib/desktop/pages/desktop_home_page.dart | 3 + flutter/lib/desktop/pages/remote_page.dart | 407 +----------------- .../lib/desktop/widgets/peercard_widget.dart | 97 +++-- .../lib/desktop/widgets/remote_menubar.dart | 22 +- 4 files changed, 88 insertions(+), 441 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 632177e29..5a082a8fd 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -6,6 +6,9 @@ import 'package:flutter/services.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/connection_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart'; +import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; +import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart' + as mod_menu; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 16c04f572..a245f1f12 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -17,7 +17,6 @@ import '../../mobile/widgets/dialog.dart'; import '../../mobile/widgets/overlay.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; -import '../../models/chat_model.dart'; import '../../common/shared_state.dart'; final initText = '\1' * 1024; @@ -39,7 +38,6 @@ class RemotePage extends StatefulWidget { class _RemotePageState extends State with AutomaticKeepAliveClientMixin { Timer? _timer; - bool _showBar = !isWebDesktop; String _value = ''; final _cursorOverImage = false.obs; @@ -131,7 +129,7 @@ class _RemotePageState extends State common < oldValue.length && common < newValue.length && newValue[common] == oldValue[common]; - ++common); + ++common) {} for (i = 0; i < oldValue.length - common; ++i) { _ffi.inputKey('VK_BACK'); } @@ -145,8 +143,8 @@ class _RemotePageState extends State } return; } - if (oldValue.length > 0 && - newValue.length > 0 && + if (oldValue.isNotEmpty && + newValue.isNotEmpty && oldValue[0] == '\1' && newValue[0] != '\1') { // clipboard @@ -155,7 +153,7 @@ class _RemotePageState extends State if (newValue.length == oldValue.length) { // ? } else if (newValue.length < oldValue.length) { - final char = 'VK_BACK'; + const char = 'VK_BACK'; _ffi.inputKey(char); } else { final content = newValue.substring(oldValue.length); @@ -200,24 +198,9 @@ class _RemotePageState extends State } Widget buildBody(BuildContext context, FfiModel ffiModel) { - final hasDisplays = ffiModel.pi.displays.length > 0; final keyboard = ffiModel.permissions['keyboard'] != false; return Scaffold( backgroundColor: MyTheme.color(context).bg, - // resizeToAvoidBottomInset: true, - // floatingActionButton: _showBar - // ? null - // : FloatingActionButton( - // mini: true, - // child: Icon(Icons.expand_less), - // backgroundColor: MyTheme.accent, - // onPressed: () { - // setState(() { - // _showBar = !_showBar; - // }); - // }), - // bottomNavigationBar: - // _showBar && hasDisplays ? getBottomAppBar(ffiModel) : null, body: Overlay( initialEntries: [ OverlayEntry(builder: (context) { @@ -249,7 +232,7 @@ class _RemotePageState extends State ChangeNotifierProvider.value(value: _ffi.canvasModel), ], child: Consumer( - builder: (context, ffiModel, _child) => + builder: (context, ffiModel, child) => buildBody(context, ffiModel)))); } @@ -307,100 +290,6 @@ class _RemotePageState extends State child: child)))); } - Widget? getBottomAppBar(FfiModel ffiModel) { - final RxBool fullscreen = Get.find(tag: 'fullscreen'); - return MouseRegion( - cursor: SystemMouseCursors.basic, - child: BottomAppBar( - elevation: 10, - color: MyTheme.accent, - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - IconButton( - color: Colors.white, - icon: Icon(Icons.clear), - onPressed: () { - clientClose(_ffi.dialogManager); - }, - ) - ] + - [ - IconButton( - color: Colors.white, - icon: Icon(Icons.tv), - onPressed: () { - _ffi.dialogManager.dismissAll(); - showOptions(widget.id); - }, - ) - ] + - (isWebDesktop - ? [] - : [ - IconButton( - color: Colors.white, - icon: Icon(fullscreen.isTrue - ? Icons.fullscreen - : Icons.close_fullscreen), - onPressed: () { - fullscreen.value = !fullscreen.value; - }, - ) - ]) + - (isWebDesktop - ? [] - : _ffi.ffiModel.isPeerAndroid - ? [ - IconButton( - color: Colors.white, - icon: Icon(Icons.build), - onPressed: () { - if (mobileActionsOverlayEntry == null) { - showMobileActionsOverlay(); - } else { - hideMobileActionsOverlay(); - } - }, - ) - ] - : []) + - (isWeb - ? [] - : [ - IconButton( - color: Colors.white, - icon: Icon(Icons.message), - onPressed: () { - _ffi.chatModel - .changeCurrentID(ChatModel.clientModeID); - _ffi.chatModel.toggleChatOverlay(); - }, - ) - ]) + - [ - IconButton( - color: Colors.white, - icon: Icon(Icons.more_vert), - onPressed: () { - showActions(widget.id, ffiModel); - }, - ), - ]), - IconButton( - color: Colors.white, - icon: Icon(Icons.expand_more), - onPressed: () { - setState(() => _showBar = !_showBar); - }), - ], - ), - )); - } - /// touchMode only: /// LongPress -> right click /// OneFingerPan -> start/end -> left down start/end @@ -458,12 +347,16 @@ class _RemotePageState extends State if (e is PointerScrollEvent) { var dx = e.scrollDelta.dx.toInt(); var dy = e.scrollDelta.dy.toInt(); - if (dx > 0) + if (dx > 0) { dx = -1; - else if (dx < 0) dx = 1; - if (dy > 0) + } else if (dx < 0) { + dx = 1; + } + if (dy > 0) { dy = -1; - else if (dy < 0) dy = 1; + } else if (dy < 0) { + dy = 1; + } bind.sessionSendMouse( id: widget.id, msg: '{"type": "wheel", "x": "$dx", "y": "$dy"}'); } @@ -546,106 +439,6 @@ class _RemotePageState extends State return out; } - void showActions(String id, FfiModel ffiModel) async { - final size = MediaQuery.of(context).size; - final x = 120.0; - final y = size.height - super.widget.tabBarHeight; - final more = >[]; - final pi = _ffi.ffiModel.pi; - final perms = _ffi.ffiModel.permissions; - if (pi.version.isNotEmpty) { - more.add(PopupMenuItem( - child: Text(translate('Refresh')), value: 'refresh')); - } - more.add(PopupMenuItem( - child: Row( - children: ([ - Text(translate('OS Password')), - TextButton( - style: flatButtonStyle, - onPressed: () { - showSetOSPassword(widget.id, false, _ffi.dialogManager); - }, - child: Icon(Icons.edit, color: MyTheme.accent), - ) - ])), - value: 'enter_os_password')); - if (!isWebDesktop) { - if (perms['keyboard'] != false && perms['clipboard'] != false) { - more.add(PopupMenuItem( - child: Text(translate('Paste')), value: 'paste')); - } - more.add(PopupMenuItem( - child: Text(translate('Reset canvas')), value: 'reset_canvas')); - } - if (perms['keyboard'] != false) { - if (pi.platform == 'Linux' || pi.sasEnabled) { - more.add(PopupMenuItem( - child: Text(translate('Insert') + ' Ctrl + Alt + Del'), - value: 'cad')); - } - more.add(PopupMenuItem( - child: Text(translate('Insert Lock')), value: 'lock')); - if (pi.platform == 'Windows' && - await bind.sessionGetToggleOption(id: id, arg: 'privacy-mode') != - true) { - more.add(PopupMenuItem( - child: Text(translate( - (ffiModel.inputBlocked ? 'Unb' : 'B') + 'lock user input')), - value: 'block-input')); - } - } - if (gFFI.ffiModel.permissions["restart"] != false && - (pi.platform == "Linux" || - pi.platform == "Windows" || - pi.platform == "Mac OS")) { - more.add(PopupMenuItem( - child: Text(translate('Restart Remote Device')), value: 'restart')); - } - () async { - var value = await showMenu( - context: context, - position: RelativeRect.fromLTRB(x, y, x, y), - items: more, - elevation: 8, - ); - if (value == 'cad') { - bind.sessionCtrlAltDel(id: widget.id); - } else if (value == 'lock') { - bind.sessionLockScreen(id: widget.id); - } else if (value == 'block-input') { - bind.sessionToggleOption( - id: widget.id, - value: (_ffi.ffiModel.inputBlocked ? 'un' : '') + 'block-input'); - _ffi.ffiModel.inputBlocked = !_ffi.ffiModel.inputBlocked; - } else if (value == 'refresh') { - bind.sessionRefresh(id: widget.id); - } else if (value == 'paste') { - () async { - ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); - if (data != null && data.text != null) { - bind.sessionInputString(id: widget.id, value: data.text ?? ""); - } - }(); - } else if (value == 'enter_os_password') { - // FIXME: - // TODO icon diff - // null means no session of id - // empty string means no password - var password = await bind.sessionGetOption(id: id, arg: "os-password"); - if (password != null) { - bind.sessionInputOsPassword(id: widget.id, value: password); - } else { - showSetOSPassword(widget.id, true, _ffi.dialogManager); - } - } else if (value == 'reset_canvas') { - _ffi.cursorModel.reset(); - } else if (value == 'restart') { - showRestartRemoteDevice(pi, widget.id, gFFI.dialogManager); - } - }(); - } - @override void onWindowEvent(String eventName) { print("window event: $eventName"); @@ -676,7 +469,7 @@ class ImagePaint extends StatelessWidget { {Key? key, required this.id, required this.cursorOverImage, - this.listenerBuilder = null}) + this.listenerBuilder}) : super(key: key); @override @@ -855,177 +648,7 @@ class QualityMonitor extends StatelessWidget { ], ), ) - : SizedBox.shrink()))); -} - -void showOptions(String id) async { - final _ffi = ffi(id); - String quality = await bind.sessionGetImageQuality(id: id) ?? 'balanced'; - if (quality == '') quality = 'balanced'; - String viewStyle = - await bind.sessionGetOption(id: id, arg: 'view-style') ?? ''; - String scrollStyle = - await bind.sessionGetOption(id: id, arg: 'scroll-style') ?? ''; - var displays = []; - final pi = _ffi.ffiModel.pi; - final image = _ffi.ffiModel.getConnectionImage(); - if (image != null) - displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image)); - if (pi.displays.length > 1) { - final cur = pi.currentDisplay; - final children = []; - for (var i = 0; i < pi.displays.length; ++i) - children.add(InkWell( - onTap: () { - if (i == cur) return; - bind.sessionSwitchDisplay(id: id, value: i); - _ffi.dialogManager.dismissAll(); - }, - child: Ink( - width: 40, - height: 40, - decoration: BoxDecoration( - border: Border.all(color: Colors.black87), - color: i == cur ? Colors.black87 : Colors.white), - child: Center( - child: Text((i + 1).toString(), - style: TextStyle( - color: i == cur ? Colors.white : Colors.black87)))))); - displays.add(Padding( - padding: const EdgeInsets.only(top: 8), - child: Wrap( - alignment: WrapAlignment.center, - spacing: 8, - children: children, - ))); - } - if (displays.isNotEmpty) { - displays.add(Divider(color: MyTheme.border)); - } - final perms = _ffi.ffiModel.permissions; - - _ffi.dialogManager.show((setState, close) { - final more = []; - if (perms['audio'] != false) { - more.add(getToggle(id, setState, 'disable-audio', 'Mute')); - } - if (perms['keyboard'] != false) { - if (perms['clipboard'] != false) - more.add( - getToggle(id, setState, 'disable-clipboard', 'Disable clipboard')); - more.add(getToggle( - id, setState, 'lock-after-session-end', 'Lock after session end')); - if (pi.platform == 'Windows') { - more.add(Consumer( - builder: (_context, _ffiModel, _child) => () { - return getToggle( - id, setState, 'privacy-mode', 'Privacy mode'); - }())); - } - } - var setQuality = (String? value) { - if (value == null) return; - setState(() { - quality = value; - bind.sessionSetImageQuality(id: id, value: value); - }); - }; - var setViewStyle = (String? value) { - if (value == null) return; - setState(() { - viewStyle = value; - bind.sessionPeerOption(id: id, name: "view-style", value: value); - _ffi.canvasModel.updateViewStyle(); - }); - }; - var setScrollStyle = (String? value) { - if (value == null) return; - setState(() { - scrollStyle = value; - bind.sessionPeerOption(id: id, name: "scroll-style", value: value); - _ffi.canvasModel.updateScrollStyle(); - }); - }; - return CustomAlertDialog( - title: SizedBox.shrink(), - content: Column( - mainAxisSize: MainAxisSize.min, - children: displays + - [ - getRadio('Original', 'original', viewStyle, setViewStyle), - getRadio('Shrink', 'shrink', viewStyle, setViewStyle), - getRadio('Stretch', 'stretch', viewStyle, setViewStyle), - Divider(color: MyTheme.border), - getRadio( - 'ScrollAuto', 'scrollauto', scrollStyle, setScrollStyle), - getRadio('Scrollbar', 'scrollbar', scrollStyle, setScrollStyle), - Divider(color: MyTheme.border), - getRadio('Good image quality', 'best', quality, setQuality), - getRadio('Balanced', 'balanced', quality, setQuality), - getRadio('Optimize reaction time', 'low', quality, setQuality), - Divider(color: MyTheme.border), - getToggle( - id, setState, 'show-remote-cursor', 'Show remote cursor'), - getToggle(id, setState, 'show-quality-monitor', - 'Show quality monitor', - ffi: _ffi), - ] + - more), - actions: [], - contentPadding: 0, - ); - }, clickMaskDismiss: true, backDismiss: true); -} - -void showSetOSPassword( - String id, bool login, OverlayDialogManager dialogManager) async { - final controller = TextEditingController(); - var password = await bind.sessionGetOption(id: id, arg: "os-password") ?? ""; - var autoLogin = await bind.sessionGetOption(id: id, arg: "auto-login") != ""; - controller.text = password; - dialogManager.show((setState, close) { - return CustomAlertDialog( - title: Text(translate('OS Password')), - content: Column(mainAxisSize: MainAxisSize.min, children: [ - PasswordWidget(controller: controller), - CheckboxListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - controlAffinity: ListTileControlAffinity.leading, - title: Text( - translate('Auto Login'), - ), - value: autoLogin, - onChanged: (v) { - if (v == null) return; - setState(() => autoLogin = v); - }, - ), - ]), - actions: [ - TextButton( - style: flatButtonStyle, - onPressed: () { - close(); - }, - child: Text(translate('Cancel')), - ), - TextButton( - style: flatButtonStyle, - onPressed: () { - var text = controller.text.trim(); - bind.sessionPeerOption(id: id, name: "os-password", value: text); - bind.sessionPeerOption( - id: id, name: "auto-login", value: autoLogin ? 'Y' : ''); - if (text != "" && login) { - bind.sessionInputOsPassword(id: id, value: text); - } - close(); - }, - child: Text(translate('OK')), - ), - ]); - }); + : const SizedBox.shrink()))); } void sendPrompt(String id, bool isMac, String key) { diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index a91f300fd..d9c0015f8 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -15,7 +15,7 @@ class _PopupMenuTheme { static const Color commonColor = MyTheme.accent; // kMinInteractiveDimension static const double height = 25.0; - static const double dividerHeight = 12.0; + static const double dividerHeight = 3.0; } typedef PopupMenuEntryBuilder = Future>> @@ -46,6 +46,7 @@ class _PeerCard extends StatefulWidget { /// State for the connection page. class _PeerCardState extends State<_PeerCard> with AutomaticKeepAliveClientMixin { + var _menuPos = RelativeRect.fill; final double _cardRadis = 20; final double _borderWidth = 2; final RxBool _iconMoreHover = false.obs; @@ -253,36 +254,36 @@ class _PeerCardState extends State<_PeerCard> ); } - Widget _actionMore(Peer peer) { - return FutureBuilder( - future: widget.popupMenuEntryBuilder(context), - initialData: const >[], - builder: (BuildContext context, - AsyncSnapshot>> snapshot) { - if (snapshot.hasData) { - return Listener( - child: MouseRegion( - onEnter: (_) => _iconMoreHover.value = true, - onExit: (_) => _iconMoreHover.value = false, - child: CircleAvatar( - radius: 14, - backgroundColor: _iconMoreHover.value - ? MyTheme.color(context).grayBg! - : MyTheme.color(context).bg!, - child: mod_menu.PopupMenuButton( - padding: EdgeInsets.zero, - icon: Icon(Icons.more_vert, - size: 18, - color: _iconMoreHover.value - ? MyTheme.color(context).text - : MyTheme.color(context).lightText), - position: mod_menu.PopupMenuPosition.over, - itemBuilder: (BuildContext context) => snapshot.data!, - )))); - } else { - return Container(); - } - }); + Widget _actionMore(Peer peer) => Listener( + onPointerDown: (e) { + final x = e.position.dx; + final y = e.position.dy; + _menuPos = RelativeRect.fromLTRB(x, y, x, y); + }, + onPointerUp: (_) => _showPeerMenu(context, peer.id), + child: MouseRegion( + onEnter: (_) => _iconMoreHover.value = true, + onExit: (_) => _iconMoreHover.value = false, + child: CircleAvatar( + radius: 14, + backgroundColor: _iconMoreHover.value + ? MyTheme.color(context).grayBg! + : MyTheme.color(context).bg!, + child: Icon(Icons.more_vert, + size: 18, + color: _iconMoreHover.value + ? MyTheme.color(context).text + : MyTheme.color(context).lightText)))); + + /// Show the peer menu and handle user's choice. + /// User might remove the peer or send a file to the peer. + void _showPeerMenu(BuildContext context, String id) async { + await mod_menu.showMenu( + context: context, + position: _menuPos, + items: await super.widget.popupMenuEntryBuilder(context), + elevation: 8, + ); } /// Get the image for the current [platform]. @@ -411,19 +412,26 @@ abstract class BasePeerCard extends StatelessWidget { @protected MenuEntryBase _rdpAction(BuildContext context, String id) { return MenuEntryButton( - childBuilder: (TextStyle? style) => Row( - children: [ - Text( - translate('RDP'), - style: style, - ), - SizedBox(width: 20), - IconButton( - icon: Icon(Icons.edit), - onPressed: () => _rdpDialog(id), - ) - ], - ), + childBuilder: (TextStyle? style) => Container( + alignment: AlignmentDirectional.center, + height: _PopupMenuTheme.height, + child: Row( + children: [ + Text( + translate('RDP'), + style: style, + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: IconButton( + padding: EdgeInsets.zero, + icon: Icon(Icons.edit), + onPressed: () => _rdpDialog(id), + ), + )) + ], + )), proc: () { _connect(context, id, isRDP: true); }, @@ -614,6 +622,7 @@ class RecentPeerCard extends BasePeerCard { if (peer.platform == 'Windows') { menuItems.add(_rdpAction(context, peer.id)); } + menuItems.add(MenuEntryDivider()); menuItems.add(await _forceAlwaysRelayAction(peer.id)); menuItems.add(_renameAction(peer.id, false)); menuItems.add(_removeAction(peer.id, () async { diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index a6399b77b..dbe7592e6 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -472,9 +472,17 @@ class _RemoteMenubarState extends State { }); final quality = await bind.sessionGetCustomImageQuality(id: widget.id); - final double initValue = quality != null && quality.isNotEmpty + double initValue = quality != null && quality.isNotEmpty ? quality[0].toDouble() : 50.0; + const minValue = 10.0; + const maxValue = 100.0; + if (initValue < minValue) { + initValue = minValue; + } + if (initValue > maxValue) { + initValue = maxValue; + } final RxDouble sliderValue = RxDouble(initValue); final rxReplay = rxdart.ReplaySubject(); rxReplay @@ -489,10 +497,9 @@ class _RemoteMenubarState extends State { final slider = Obx(() { return Slider( value: sliderValue.value, - min: 10.0, - max: 100.0, + min: minValue, + max: maxValue, divisions: 90, - // label: sliderValue.value.round().toString(), onChanged: (double value) { sliderValue.value = value; rxReplay.add(value); @@ -502,7 +509,12 @@ class _RemoteMenubarState extends State { final content = Row( children: [ slider, - Obx(() => Text('${sliderValue.value.round()}% Bitrate')) + SizedBox( + width: 90, + child: Obx(() => Text( + '${sliderValue.value.round()}% Bitrate', + style: const TextStyle(fontSize: 15), + ))) ], ); msgBoxCommon(widget.ffi.dialogManager, 'Custom Image Quality', From f69bedeac5052be74687d123c1dde3c6e45dfba5 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 2 Sep 2022 18:56:03 +0800 Subject: [PATCH 05/19] sciter_desktop: fix cursor size(resize window) and id(after connection) Signed-off-by: fufesou --- src/ui/remote.tis | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/ui/remote.tis b/src/ui/remote.tis index 65e7e5030..835136442 100644 --- a/src/ui/remote.tis +++ b/src/ui/remote.tis @@ -67,6 +67,7 @@ function adaptDisplay() { } } } + refreshCursor(); handler.style.set { width: w / scaleFactor + "px", height: h / scaleFactor + "px", @@ -98,6 +99,7 @@ var acc_wheel_delta_y0 = 0; var total_wheel_time = 0; var wheeling = false; var dragging = false; +var is_mouse_event_triggered = false; // https://stackoverflow.com/questions/5833399/calculating-scroll-inertia-momentum function resetWheel() { @@ -139,6 +141,7 @@ function accWheel(v, is_x) { function handler.onMouse(evt) { + is_mouse_event_triggered = true; if (is_file_transfer || is_port_forward) return false; if (view.windowState == View.WINDOW_FULL_SCREEN && !dragging) { var dy = evt.y - scroll_body.scroll(#top); @@ -317,6 +320,7 @@ function handler.onMouse(evt) return true; }; +var cur_id = -1; var cur_hotx = 0; var cur_hoty = 0; var cur_img = null; @@ -345,7 +349,7 @@ function scaleCursorImage(img) { var useSystemCursor = true; function updateCursor(system=false) { stdout.println("Update cursor, system: " + system); - useSystemCursor= system; + useSystemCursor = system; if (system) { handler.style#cursor = undefined; } else if (cur_img) { @@ -353,6 +357,12 @@ function updateCursor(system=false) { } } +function refreshCursor() { + if (cur_id != -1) { + handler.setCursorId(cur_id); + } +} + handler.setCursorData = function(id, hotx, hoty, width, height, colors) { cur_hotx = hotx; cur_hoty = hoty; @@ -360,8 +370,9 @@ handler.setCursorData = function(id, hotx, hoty, width, height, colors) { if (img) { image_binded = true; cursors[id] = [img, hotx, hoty, width, height]; + cur_id = id; img = scaleCursorImage(img); - if (cursor_img.style#display == 'none') { + if (!first_mouse_event_triggered || cursor_img.style#display == 'none') { self.timer(1ms, updateCursor); } cur_img = img; @@ -371,11 +382,12 @@ handler.setCursorData = function(id, hotx, hoty, width, height, colors) { handler.setCursorId = function(id) { var img = cursors[id]; if (img) { + cur_id = id; image_binded = true; cur_hotx = img[1]; cur_hoty = img[2]; img = scaleCursorImage(img[0]); - if (cursor_img.style#display == 'none') { + if (!first_mouse_event_triggered || cursor_img.style#display == 'none') { self.timer(1ms, updateCursor); } cur_img = img; From 11c5364e71179c59e8aefaa53a26fac0f5c6a364 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 3 Sep 2022 10:39:33 +0800 Subject: [PATCH 06/19] flutter_desktop: fix cursor, mid commit Signed-off-by: fufesou --- flutter/lib/common/shared_state.dart | 89 ++++++- flutter/lib/desktop/pages/remote_page.dart | 231 +++++++++--------- .../lib/desktop/widgets/remote_menubar.dart | 25 +- flutter/lib/models/model.dart | 61 ++--- 4 files changed, 242 insertions(+), 164 deletions(-) diff --git a/flutter/lib/common/shared_state.dart b/flutter/lib/common/shared_state.dart index 7232cb6ad..67752d888 100644 --- a/flutter/lib/common/shared_state.dart +++ b/flutter/lib/common/shared_state.dart @@ -1,16 +1,26 @@ import 'package:get/get.dart'; import '../consts.dart'; +import '../models/platform_model.dart'; class PrivacyModeState { static String tag(String id) => 'privacy_mode_$id'; static void init(String id) { - final RxBool state = false.obs; - Get.put(state, tag: tag(id)); + final key = tag(id); + if (!Get.isRegistered(tag: key)) { + final RxBool state = false.obs; + Get.put(state, tag: key); + } + } + + static void delete(String id) { + final key = tag(id); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } } - static void delete(String id) => Get.delete(tag: tag(id)); static RxBool find(String id) => Get.find(tag: tag(id)); } @@ -18,11 +28,20 @@ class BlockInputState { static String tag(String id) => 'block_input_$id'; static void init(String id) { - final RxBool state = false.obs; - Get.put(state, tag: tag(id)); + final key = tag(id); + if (!Get.isRegistered(tag: key)) { + final RxBool state = false.obs; + Get.put(state, tag: key); + } + } + + static void delete(String id) { + final key = tag(id); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } } - static void delete(String id) => Get.delete(tag: tag(id)); static RxBool find(String id) => Get.find(tag: tag(id)); } @@ -30,11 +49,20 @@ class CurrentDisplayState { static String tag(String id) => 'current_display_$id'; static void init(String id) { - final RxInt state = RxInt(0); - Get.put(state, tag: tag(id)); + final key = tag(id); + if (!Get.isRegistered(tag: key)) { + final RxInt state = RxInt(0); + Get.put(state, tag: key); + } + } + + static void delete(String id) { + final key = tag(id); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } } - static void delete(String id) => Get.delete(tag: tag(id)); static RxInt find(String id) => Get.find(tag: tag(id)); } @@ -85,3 +113,46 @@ class ConnectionTypeState { static ConnectionType find(String id) => Get.find(tag: tag(id)); } + +class ShowRemoteCursorState { + static String tag(String id) => 'show_remote_cursor_$id'; + + static void init(String id) { + final key = tag(id); + if (!Get.isRegistered(tag: key)) { + final RxBool state = false.obs; + Get.put(state, tag: key); + } + } + + static void delete(String id) { + final key = tag(id); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } + } + + static RxBool find(String id) => Get.find(tag: tag(id)); +} + +class KeyboardEnabledState { + static String tag(String id) => 'keyboard_enabled_$id'; + + static void init(String id) { + final key = tag(id); + if (!Get.isRegistered(tag: key)) { + // Server side, default true + final RxBool state = true.obs; + Get.put(state, tag: key); + } + } + + static void delete(String id) { + final key = tag(id); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } + } + + static RxBool find(String id) => Get.find(tag: tag(id)); +} diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index a245f1f12..23e4e4900 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -40,6 +40,8 @@ class _RemotePageState extends State Timer? _timer; String _value = ''; final _cursorOverImage = false.obs; + late RxBool _showRemoteCursor; + late RxBool _keyboardEnabled; final FocusNode _mobileFocusNode = FocusNode(); final FocusNode _physicalFocusNode = FocusNode(); @@ -56,17 +58,24 @@ class _RemotePageState extends State PrivacyModeState.init(id); BlockInputState.init(id); CurrentDisplayState.init(id); + KeyboardEnabledState.init(id); + ShowRemoteCursorState.init(id); + _showRemoteCursor = ShowRemoteCursorState.find(id); + _keyboardEnabled = KeyboardEnabledState.find(id); } void _removeStates(String id) { PrivacyModeState.delete(id); BlockInputState.delete(id); CurrentDisplayState.delete(id); + ShowRemoteCursorState.delete(id); + KeyboardEnabledState.delete(id); } @override void initState() { super.initState(); + _initStates(widget.id); _ffi = FFI(); _updateTabBarHeight(); Get.put(_ffi, tag: widget.id); @@ -84,7 +93,8 @@ class _RemotePageState extends State _ffi.listenToMouse(true); _ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); // WindowManager.instance.addListener(this); - _initStates(widget.id); + _showRemoteCursor.value = bind.sessionGetToggleOptionSync( + id: widget.id, arg: 'show-remote-cursor'); } @override @@ -197,8 +207,7 @@ class _RemotePageState extends State _ffi.inputKey(label, down: down, press: press ?? false); } - Widget buildBody(BuildContext context, FfiModel ffiModel) { - final keyboard = ffiModel.permissions['keyboard'] != false; + Widget buildBody(BuildContext context) { return Scaffold( backgroundColor: MyTheme.color(context).bg, body: Overlay( @@ -208,8 +217,7 @@ class _RemotePageState extends State _ffi.dialogManager.setOverlayState(Overlay.of(context)); return Container( color: Colors.black, - child: getRawPointerAndKeyBody( - getBodyForDesktop(context, keyboard))); + child: getRawPointerAndKeyBody(getBodyForDesktop(context))); }) ], )); @@ -224,70 +232,61 @@ class _RemotePageState extends State clientClose(_ffi.dialogManager); return false; }, - child: MultiProvider( - providers: [ - ChangeNotifierProvider.value(value: _ffi.ffiModel), - ChangeNotifierProvider.value(value: _ffi.imageModel), - ChangeNotifierProvider.value(value: _ffi.cursorModel), - ChangeNotifierProvider.value(value: _ffi.canvasModel), - ], - child: Consumer( - builder: (context, ffiModel, child) => - buildBody(context, ffiModel)))); + child: MultiProvider(providers: [ + ChangeNotifierProvider.value(value: _ffi.ffiModel), + ChangeNotifierProvider.value(value: _ffi.imageModel), + ChangeNotifierProvider.value(value: _ffi.cursorModel), + ChangeNotifierProvider.value(value: _ffi.canvasModel), + ], child: buildBody(context))); } Widget getRawPointerAndKeyBody(Widget child) { - return Consumer( - builder: (context, FfiModel, _child) => MouseRegion( - cursor: FfiModel.permissions['keyboard'] != false - ? SystemMouseCursors.none - : MouseCursor.defer, - child: FocusScope( - autofocus: true, - child: Focus( - autofocus: true, - canRequestFocus: true, - focusNode: _physicalFocusNode, - onFocusChange: (bool v) { - _imageFocused = v; - }, - onKey: (data, e) { - final key = e.logicalKey; - if (e is RawKeyDownEvent) { - if (e.repeat) { - sendRawKey(e, press: true); - } else { - if (e.isAltPressed && !_ffi.alt) { - _ffi.alt = true; - } else if (e.isControlPressed && !_ffi.ctrl) { - _ffi.ctrl = true; - } else if (e.isShiftPressed && !_ffi.shift) { - _ffi.shift = true; - } else if (e.isMetaPressed && !_ffi.command) { - _ffi.command = true; - } - sendRawKey(e, down: true); - } - } - if (e is RawKeyUpEvent) { - if (key == LogicalKeyboardKey.altLeft || - key == LogicalKeyboardKey.altRight) { - _ffi.alt = false; - } else if (key == LogicalKeyboardKey.controlLeft || - key == LogicalKeyboardKey.controlRight) { - _ffi.ctrl = false; - } else if (key == LogicalKeyboardKey.shiftRight || - key == LogicalKeyboardKey.shiftLeft) { - _ffi.shift = false; - } else if (key == LogicalKeyboardKey.metaLeft || - key == LogicalKeyboardKey.metaRight) { - _ffi.command = false; - } - sendRawKey(e); - } - return KeyEventResult.handled; - }, - child: child)))); + return FocusScope( + autofocus: true, + child: Focus( + autofocus: true, + canRequestFocus: true, + focusNode: _physicalFocusNode, + onFocusChange: (bool v) { + _imageFocused = v; + }, + onKey: (data, e) { + final key = e.logicalKey; + if (e is RawKeyDownEvent) { + if (e.repeat) { + sendRawKey(e, press: true); + } else { + if (e.isAltPressed && !_ffi.alt) { + _ffi.alt = true; + } else if (e.isControlPressed && !_ffi.ctrl) { + _ffi.ctrl = true; + } else if (e.isShiftPressed && !_ffi.shift) { + _ffi.shift = true; + } else if (e.isMetaPressed && !_ffi.command) { + _ffi.command = true; + } + sendRawKey(e, down: true); + } + } + if (e is RawKeyUpEvent) { + if (key == LogicalKeyboardKey.altLeft || + key == LogicalKeyboardKey.altRight) { + _ffi.alt = false; + } else if (key == LogicalKeyboardKey.controlLeft || + key == LogicalKeyboardKey.controlRight) { + _ffi.ctrl = false; + } else if (key == LogicalKeyboardKey.shiftRight || + key == LogicalKeyboardKey.shiftLeft) { + _ffi.shift = false; + } else if (key == LogicalKeyboardKey.metaLeft || + key == LogicalKeyboardKey.metaRight) { + _ffi.command = false; + } + sendRawKey(e); + } + return KeyEventResult.handled; + }, + child: child)); } /// touchMode only: @@ -382,32 +381,30 @@ class _RemotePageState extends State child: child)); } - Widget getBodyForDesktop(BuildContext context, bool keyboard) { + Widget getBodyForDesktop(BuildContext context) { var paints = [ MouseRegion(onEnter: (evt) { bind.hostStopSystemKeyPropagate(stopped: false); }, onExit: (evt) { bind.hostStopSystemKeyPropagate(stopped: true); - }, child: Container( - child: LayoutBuilder(builder: (context, constraints) { - Future.delayed(Duration.zero, () { - Provider.of(context, listen: false).updateViewStyle(); - }); - return ImagePaint( - id: widget.id, - cursorOverImage: _cursorOverImage, - listenerBuilder: _buildImageListener, - ); - }), - )) + }, child: LayoutBuilder(builder: (context, constraints) { + Future.delayed(Duration.zero, () { + Provider.of(context, listen: false).updateViewStyle(); + }); + return ImagePaint( + id: widget.id, + cursorOverImage: _cursorOverImage, + keyboardEnabled: _keyboardEnabled, + listenerBuilder: _buildImageListener, + ); + })) ]; - final cursor = bind.sessionGetToggleOptionSync( - id: widget.id, arg: 'show-remote-cursor'); - if (keyboard || cursor) { - paints.add(CursorPaint( - id: widget.id, - )); - } + + paints.add(Obx(() => Visibility( + visible: _keyboardEnabled.isTrue || _showRemoteCursor.isTrue, + child: CursorPaint( + id: widget.id, + )))); paints.add(QualityMonitor(_ffi.qualityMonitorModel)); paints.add(RemoteMenubar( id: widget.id, @@ -447,7 +444,7 @@ class _RemotePageState extends State _ffi.canvasModel.updateViewStyle(); break; case 'maximize': - Future.delayed(Duration(milliseconds: 100), () { + Future.delayed(const Duration(milliseconds: 100), () { _ffi.canvasModel.updateViewStyle(); }); break; @@ -461,6 +458,7 @@ class _RemotePageState extends State class ImagePaint extends StatelessWidget { final String id; final Rx cursorOverImage; + final Rx keyboardEnabled; final Widget Function(Widget)? listenerBuilder; final ScrollController _horizontal = ScrollController(); final ScrollController _vertical = ScrollController(); @@ -469,6 +467,7 @@ class ImagePaint extends StatelessWidget { {Key? key, required this.id, required this.cursorOverImage, + required this.keyboardEnabled, this.listenerBuilder}) : super(key: key); @@ -485,25 +484,26 @@ class ImagePaint extends StatelessWidget { painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s), )); return Center( - child: NotificationListener( - onNotification: (notification) { - final percentX = _horizontal.position.extentBefore / - (_horizontal.position.extentBefore + - _horizontal.position.extentInside + - _horizontal.position.extentAfter); - final percentY = _vertical.position.extentBefore / - (_vertical.position.extentBefore + - _vertical.position.extentInside + - _vertical.position.extentAfter); - c.setScrollPercent(percentX, percentY); - return false; - }, - child: Obx(() => MouseRegion( - cursor: cursorOverImage.value - ? SystemMouseCursors.none - : SystemMouseCursors.basic, - child: _buildCrossScrollbar(_buildListener(imageWidget)))), - )); + child: NotificationListener( + onNotification: (notification) { + final percentX = _horizontal.position.extentBefore / + (_horizontal.position.extentBefore + + _horizontal.position.extentInside + + _horizontal.position.extentAfter); + final percentY = _vertical.position.extentBefore / + (_vertical.position.extentBefore + + _vertical.position.extentInside + + _vertical.position.extentAfter); + c.setScrollPercent(percentX, percentY); + return false; + }, + child: Obx(() => MouseRegion( + cursor: (keyboardEnabled.isTrue && cursorOverImage.isTrue) + ? SystemMouseCursors.none + : MouseCursor.defer, + child: _buildCrossScrollbar(_buildListener(imageWidget)))), + ), + ); } else { final imageWidget = SizedBox( width: c.size.width, @@ -562,13 +562,12 @@ class CursorPaint extends StatelessWidget { final m = Provider.of(context); final c = Provider.of(context); // final adjust = m.adjustForKeyboard(); - var s = c.scale; return CustomPaint( painter: ImagePainter( image: m.image, - x: m.x * s - m.hotx + c.x, - y: m.y * s - m.hoty + c.y, - scale: 1), + x: m.x - m.hotx + c.x / c.scale, + y: m.y - m.hoty + c.y / c.scale, + scale: c.scale), ); } } @@ -620,30 +619,30 @@ class QualityMonitor extends StatelessWidget { right: 10, child: qualityMonitorModel.show ? Container( - padding: EdgeInsets.all(8), + padding: const EdgeInsets.all(8), color: MyTheme.canvasColor.withAlpha(120), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Speed: ${qualityMonitorModel.data.speed ?? ''}", - style: TextStyle(color: MyTheme.grayBg), + style: const TextStyle(color: MyTheme.grayBg), ), Text( "FPS: ${qualityMonitorModel.data.fps ?? ''}", - style: TextStyle(color: MyTheme.grayBg), + style: const TextStyle(color: MyTheme.grayBg), ), Text( "Delay: ${qualityMonitorModel.data.delay ?? ''} ms", - style: TextStyle(color: MyTheme.grayBg), + style: const TextStyle(color: MyTheme.grayBg), ), Text( "Target Bitrate: ${qualityMonitorModel.data.targetBitrate ?? ''}kb", - style: TextStyle(color: MyTheme.grayBg), + style: const TextStyle(color: MyTheme.grayBg), ), Text( "Codec: ${qualityMonitorModel.data.codecFormat ?? ''}", - style: TextStyle(color: MyTheme.grayBg), + style: const TextStyle(color: MyTheme.grayBg), ), ], ), diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index dbe7592e6..dc3c249f0 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -522,16 +522,19 @@ class _RemoteMenubarState extends State { } }), MenuEntryDivider(), - MenuEntrySwitch( - text: translate('Show remote cursor'), - getter: () async { - return bind.sessionGetToggleOptionSync( - id: widget.id, arg: 'show-remote-cursor'); - }, - setter: (bool v) async { - await bind.sessionToggleOption( - id: widget.id, value: 'show-remote-cursor'); - }), + () { + final state = ShowRemoteCursorState.find(widget.id); + return MenuEntrySwitch2( + text: translate('Show remote cursor'), + getter: () { + return state; + }, + setter: (bool v) async { + state.value = v; + await bind.sessionToggleOption( + id: widget.id, value: 'show-remote-cursor'); + }); + }(), MenuEntrySwitch( text: translate('Show quality monitor'), getter: () async { @@ -560,12 +563,12 @@ class _RemoteMenubarState extends State { 'Lock after session end', 'lock-after-session-end')); if (pi.platform == 'Windows') { displayMenu.add(MenuEntrySwitch2( + dismissOnClicked: true, text: translate('Privacy mode'), getter: () { return PrivacyModeState.find(widget.id); }, setter: (bool v) async { - Navigator.pop(context); await bind.sessionToggleOption( id: widget.id, value: 'privacy-mode'); })); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 887bf7d35..384d7692a 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -32,7 +32,7 @@ class FfiModel with ChangeNotifier { Display _display = Display(); var _inputBlocked = false; - final _permissions = Map(); + final _permissions = {}; bool? _secure; bool? _direct; bool _touchMode = false; @@ -71,12 +71,13 @@ class FfiModel with ChangeNotifier { } } - void updatePermission(Map evt) { + void updatePermission(Map evt, String id) { evt.forEach((k, v) { if (k == 'name' || k.isEmpty) return; _permissions[k] = v == 'true'; }); - print('$_permissions'); + KeyboardEnabledState.find(id).value = _permissions['keyboard'] != false; + debugPrint('$_permissions'); notifyListeners(); } @@ -146,7 +147,7 @@ class FfiModel with ChangeNotifier { } else if (name == 'clipboard') { Clipboard.setData(ClipboardData(text: evt['content'])); } else if (name == 'permission') { - parent.target?.ffiModel.updatePermission(evt); + parent.target?.ffiModel.updatePermission(evt, peerId); } else if (name == 'chat_client_mode') { parent.target?.chatModel .receive(ChatModel.clientModeID, evt['text'] ?? ""); @@ -185,7 +186,7 @@ class FfiModel with ChangeNotifier { /// Bind the event listener to receive events from the Rust core. void updateEventListener(String peerId) { - final void Function(Map) cb = (evt) { + cb(evt) { var name = evt['name']; if (name == 'msgbox') { handleMsgBox(evt, peerId); @@ -205,7 +206,7 @@ class FfiModel with ChangeNotifier { } else if (name == 'clipboard') { Clipboard.setData(ClipboardData(text: evt['content'])); } else if (name == 'permission') { - parent.target?.ffiModel.updatePermission(evt); + parent.target?.ffiModel.updatePermission(evt, peerId); } else if (name == 'chat_client_mode') { parent.target?.chatModel .receive(ChatModel.clientModeID, evt['text'] ?? ""); @@ -239,7 +240,8 @@ class FfiModel with ChangeNotifier { } else if (name == 'update_privacy_mode') { updatePrivacyMode(evt, peerId); } - }; + } + platformFFI.setEventCallback(cb); } @@ -321,7 +323,7 @@ class FfiModel with ChangeNotifier { if (isPeerAndroid) { _touchMode = true; if (parent.target?.ffiModel.permissions['keyboard'] != false) { - Timer(Duration(milliseconds: 100), showMobileActionsOverlay); + Timer(const Duration(milliseconds: 100), showMobileActionsOverlay); } } else { _touchMode = @@ -464,15 +466,20 @@ enum ScrollStyle { } class CanvasModel with ChangeNotifier { + // image offset of canvas + double _x = 0; + // image offset of canvas + double _y = 0; + // image scale + double _scale = 1.0; + // the tabbar over the image + double tabBarHeight = 0.0; + // TODO multi canvas model + String id = ""; // scroll offset x percent double _scrollX = 0.0; // scroll offset y percent double _scrollY = 0.0; - double _x = 0; - double _y = 0; - double _scale = 1.0; - double _tabBarHeight = 0.0; - String id = ""; // TODO multi canvas model ScrollStyle _scrollStyle = ScrollStyle.scrollauto; WeakReference parent; @@ -492,9 +499,6 @@ class CanvasModel with ChangeNotifier { double get scrollX => _scrollX; double get scrollY => _scrollY; - set tabBarHeight(double h) => _tabBarHeight = h; - double get tabBarHeight => _tabBarHeight; - void updateViewStyle() async { final style = await bind.sessionGetOption(id: id, arg: 'view-style'); if (style == null) { @@ -548,12 +552,11 @@ class CanvasModel with ChangeNotifier { Size get size { final size = MediaQueryData.fromWindow(ui.window).size; - return Size(size.width, size.height - _tabBarHeight); + return Size(size.width, size.height - tabBarHeight); } void moveDesktopMouse(double x, double y) { // On mobile platforms, move the canvas with the cursor. - //if (!isDesktop) { final dw = getDisplayWidth() * _scale; final dh = getDisplayHeight() * _scale; var dxOffset = 0; @@ -579,8 +582,13 @@ class CanvasModel with ChangeNotifier { if (dxOffset != 0 || dyOffset != 0) { notifyListeners(); } - //} - parent.target?.cursorModel.moveLocal(x, y); + + // If keyboard is not permitted, do not move cursor when mouse is moving. + if (parent.target != null) { + if (parent.target!.ffiModel.keyboard()) { + parent.target!.cursorModel.moveLocal(x, y); + } + } } set scale(v) { @@ -597,11 +605,8 @@ class CanvasModel with ChangeNotifier { if (isWebDesktop) { updateViewStyle(); } else { - final size = MediaQueryData.fromWindow(ui.window).size; - final canvasWidth = size.width; - final canvasHeight = size.height - _tabBarHeight; - _x = (canvasWidth - getDisplayWidth() * _scale) / 2; - _y = (canvasHeight - getDisplayHeight() * _scale) / 2; + _x = (size.width - getDisplayWidth() * _scale) / 2; + _y = (size.height - getDisplayHeight() * _scale) / 2; } notifyListeners(); } @@ -613,7 +618,7 @@ class CanvasModel with ChangeNotifier { void updateScale(double v) { if (parent.target?.imageModel.image == null) return; - final offset = parent.target?.cursorModel.offset ?? Offset(0, 0); + final offset = parent.target?.cursorModel.offset ?? const Offset(0, 0); var r = parent.target?.cursorModel.getVisibleRect() ?? Rect.zero; final px0 = (offset.dx - r.left) * _scale; final py0 = (offset.dy - r.top) * _scale; @@ -640,7 +645,7 @@ class CanvasModel with ChangeNotifier { class CursorModel with ChangeNotifier { ui.Image? _image; - final _images = Map>(); + final _images = >{}; double _x = -10000; double _y = -10000; double _hotx = 0; @@ -807,7 +812,7 @@ class CursorModel with ChangeNotifier { // my throw exception, because the listener maybe already dispose notifyListeners(); } catch (e) { - print('notify cursor: $e'); + debugPrint('notify cursor: $e'); } }); } From 1b56304d9ad461c18b4ee4f9a08faf0036c4e011 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 2 Sep 2022 17:19:44 +0800 Subject: [PATCH 07/19] format id Signed-off-by: 21pages --- .../lib/common/formatter/id_formatter.dart | 52 +++++++++++++- .../lib/desktop/pages/connection_page.dart | 71 ++++++++++--------- .../lib/desktop/widgets/peercard_widget.dart | 5 +- flutter/lib/models/server_model.dart | 13 ++-- 4 files changed, 99 insertions(+), 42 deletions(-) diff --git a/flutter/lib/common/formatter/id_formatter.dart b/flutter/lib/common/formatter/id_formatter.dart index 29aea84ff..c7ce14da4 100644 --- a/flutter/lib/common/formatter/id_formatter.dart +++ b/flutter/lib/common/formatter/id_formatter.dart @@ -1,4 +1,52 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; -/// TODO: Divide every 3 number to display ID -class IdFormController extends TextEditingController {} +class IDTextEditingController extends TextEditingController { + IDTextEditingController({String? text}) : super(text: text); + + String get id => trimID(value.text); + + set id(String newID) => text = formatID(newID); +} + +class IDTextInputFormatter extends TextInputFormatter { + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, TextEditingValue newValue) { + if (newValue.text.isEmpty) { + return newValue.copyWith(text: ''); + } else if (newValue.text.compareTo(oldValue.text) == 0) { + return newValue; + } else { + int selectionIndexFromTheRight = + newValue.text.length - newValue.selection.extentOffset; + String newID = formatID(newValue.text); + return TextEditingValue( + text: newID, + selection: TextSelection.collapsed( + offset: newID.length - selectionIndexFromTheRight, + ), + ); + } + } +} + +String formatID(String id) { + String id2 = id.replaceAll(' ', ''); + String newID = ''; + if (id2.length <= 3) { + newID = id2; + } else { + var n = id2.length; + var a = n % 3 != 0 ? n % 3 : 3; + newID = id2.substring(0, a); + for (var i = a; i < n; i += 3) { + newID += " ${id2.substring(i, i + 3)}"; + } + } + return newID; +} + +String trimID(String id) { + return id.replaceAll(' ', ''); +} diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 5fd6b4a28..fe363c4c9 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -12,6 +12,7 @@ import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher_string.dart'; import '../../common.dart'; +import '../../common/formatter/id_formatter.dart'; import '../../mobile/pages/scan_page.dart'; import '../../mobile/pages/settings_page.dart'; import '../../models/model.dart'; @@ -30,7 +31,7 @@ class ConnectionPage extends StatefulWidget { /// State for the connection page. class _ConnectionPageState extends State { /// Controller for the id input bar. - final _idController = TextEditingController(); + final _idController = IDTextEditingController(); /// Update url. If it's not null, means an update is available. final _updateUrl = ''; @@ -43,9 +44,9 @@ class _ConnectionPageState extends State { if (_idController.text.isEmpty) { () async { final lastRemoteId = await bind.mainGetLastRemoteId(); - if (lastRemoteId != _idController.text) { + if (lastRemoteId != _idController.id) { setState(() { - _idController.text = lastRemoteId; + _idController.id = lastRemoteId; }); } }(); @@ -110,7 +111,7 @@ class _ConnectionPageState extends State { /// Callback for the connect button. /// Connects to the selected peer. void onConnect({bool isFileTransfer = false}) { - final id = _idController.text.trim(); + final id = _idController.id; connect(id, isFileTransfer: isFileTransfer); } @@ -185,35 +186,41 @@ class _ConnectionPageState extends State { Row( children: [ Expanded( - child: TextField( - autocorrect: false, - enableSuggestions: false, - keyboardType: TextInputType.visiblePassword, - style: TextStyle( - fontFamily: 'WorkSans', - fontSize: 22, - height: 1, - ), - decoration: InputDecoration( - hintText: translate('Enter Remote ID'), - hintStyle: TextStyle( - color: MyTheme.color(context).placeholder), - border: OutlineInputBorder( + child: Obx( + () => TextField( + autocorrect: false, + enableSuggestions: false, + keyboardType: TextInputType.visiblePassword, + focusNode: focusNode, + style: TextStyle( + fontFamily: 'WorkSans', + fontSize: 22, + height: 1, + ), + decoration: InputDecoration( + hintText: inputFocused.value + ? null + : translate('Enter Remote ID'), + hintStyle: TextStyle( + color: MyTheme.color(context).placeholder), + border: OutlineInputBorder( + borderRadius: BorderRadius.zero, + borderSide: BorderSide( + color: MyTheme.color(context).placeholder!)), + focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.zero, - borderSide: BorderSide( - color: MyTheme.color(context).placeholder!)), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.zero, - borderSide: - BorderSide(color: MyTheme.button, width: 3), - ), - isDense: true, - contentPadding: - EdgeInsets.symmetric(horizontal: 10, vertical: 12)), - controller: _idController, - onSubmitted: (s) { - onConnect(); - }, + borderSide: + BorderSide(color: MyTheme.button, width: 3), + ), + isDense: true, + contentPadding: EdgeInsets.symmetric( + horizontal: 10, vertical: 12)), + controller: _idController, + inputFormatters: [IDTextInputFormatter()], + onSubmitted: (s) { + onConnect(); + }, + ), ), ), ], diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index d9c0015f8..13cf02699 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -5,6 +5,7 @@ import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; import '../../common.dart'; +import '../../common/formatter/id_formatter.dart'; import '../../models/model.dart'; import '../../models/peer_model.dart'; import '../../models/platform_model.dart'; @@ -119,7 +120,7 @@ class _PeerCardState extends State<_PeerCard> ? Colors.green : Colors.yellow)), Text( - '${peer.id}', + formatID('${peer.id}'), style: TextStyle(fontWeight: FontWeight.w400), ), ]), @@ -240,7 +241,7 @@ class _PeerCardState extends State<_PeerCard> backgroundColor: peer.online ? Colors.green : Colors.yellow)), - Text(peer.id) + Text(formatID(peer.id)) ]).paddingSymmetric(vertical: 8), _actionMore(peer), ], diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 31c579f83..5d23dc949 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -7,6 +7,7 @@ import 'package:flutter_hbb/models/platform_model.dart'; import 'package:wakelock/wakelock.dart'; import '../common.dart'; +import '../common/formatter/id_formatter.dart'; import '../desktop/pages/server_page.dart' as Desktop; import '../desktop/widgets/tabbar_widget.dart'; import '../mobile/pages/server_page.dart'; @@ -29,7 +30,7 @@ class ServerModel with ChangeNotifier { String _temporaryPasswordLength = ""; late String _emptyIdShow; - late final TextEditingController _serverId; + late final IDTextEditingController _serverId; final _serverPasswd = TextEditingController(text: ""); final tabController = DesktopTabController(tabType: DesktopTabType.cm); @@ -88,7 +89,7 @@ class ServerModel with ChangeNotifier { ServerModel(this.parent) { _emptyIdShow = translate("Generating ..."); - _serverId = TextEditingController(text: this._emptyIdShow); + _serverId = IDTextEditingController(text: _emptyIdShow); Timer.periodic(Duration(seconds: 1), (timer) async { var status = await bind.mainGetOnlineStatue(); @@ -300,7 +301,7 @@ class ServerModel with ChangeNotifier { } _fetchID() async { - final old = _serverId.text; + final old = _serverId.id; var count = 0; const maxCount = 10; while (count < maxCount) { @@ -309,12 +310,12 @@ class ServerModel with ChangeNotifier { if (id.isEmpty) { continue; } else { - _serverId.text = id; + _serverId.id = id; } - debugPrint("fetch id again at $count:id:${_serverId.text}"); + debugPrint("fetch id again at $count:id:${_serverId.id}"); count++; - if (_serverId.text != old) { + if (_serverId.id != old) { break; } } From a553334157c6af108f1fd672aa2d102aa84bf87c Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 3 Sep 2022 18:19:50 +0800 Subject: [PATCH 08/19] dialog focus && deal with Enter/Esc key Signed-off-by: 21pages --- flutter/lib/common.dart | 148 ++++++--- .../lib/desktop/pages/connection_page.dart | 207 ++++++------ .../lib/desktop/pages/desktop_home_page.dart | 314 +++++++++--------- .../desktop/pages/desktop_setting_page.dart | 300 ++++++++--------- .../lib/desktop/pages/desktop_tab_page.dart | 46 +-- .../lib/desktop/pages/file_manager_page.dart | 84 ++--- .../lib/desktop/pages/port_forward_page.dart | 69 ++-- .../lib/desktop/widgets/peercard_widget.dart | 140 ++++---- .../lib/desktop/widgets/remote_menubar.dart | 81 ++--- .../lib/desktop/widgets/tabbar_widget.dart | 34 +- flutter/lib/models/file_model.dart | 194 ++++++----- flutter/lib/models/server_model.dart | 201 +++++------ 12 files changed, 959 insertions(+), 859 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 75328c840..4a0a0dc82 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -7,6 +7,7 @@ import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/models/peer_model.dart'; import 'package:get/get.dart'; @@ -340,34 +341,41 @@ class OverlayDialogManager { {bool clickMaskDismiss = false, bool showCancel = true, VoidCallback? onCancel}) { - show((setState, close) => CustomAlertDialog( + show((setState, close) { + cancel() { + dismissAll(); + if (onCancel != null) { + onCancel(); + } + } + + return CustomAlertDialog( content: Container( - constraints: BoxConstraints(maxWidth: 240), + constraints: const BoxConstraints(maxWidth: 240), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox(height: 30), - Center(child: CircularProgressIndicator()), - SizedBox(height: 20), + const SizedBox(height: 30), + const Center(child: CircularProgressIndicator()), + const SizedBox(height: 20), Center( child: Text(translate(text), - style: TextStyle(fontSize: 15))), - SizedBox(height: 20), + style: const TextStyle(fontSize: 15))), + const SizedBox(height: 20), Offstage( offstage: !showCancel, child: Center( child: TextButton( style: flatButtonStyle, - onPressed: () { - dismissAll(); - if (onCancel != null) { - onCancel(); - } - }, + onPressed: cancel, child: Text(translate('Cancel'), - style: TextStyle(color: MyTheme.accent))))) - ])))); + style: + const TextStyle(color: MyTheme.accent))))) + ])), + onCancel: showCancel ? cancel : null, + ); + }); } } @@ -377,18 +385,18 @@ void showToast(String text, {Duration timeout = const Duration(seconds: 2)}) { final entry = OverlayEntry(builder: (_) { return IgnorePointer( child: Align( - alignment: Alignment(0.0, 0.8), + alignment: const Alignment(0.0, 0.8), child: Container( decoration: BoxDecoration( color: Colors.black.withOpacity(0.6), - borderRadius: BorderRadius.all( + borderRadius: const BorderRadius.all( Radius.circular(20), ), ), - padding: EdgeInsets.symmetric(horizontal: 20, vertical: 5), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5), child: Text( text, - style: TextStyle( + style: const TextStyle( decoration: TextDecoration.none, fontWeight: FontWeight.w300, fontSize: 18, @@ -403,23 +411,54 @@ void showToast(String text, {Duration timeout = const Duration(seconds: 2)}) { } class CustomAlertDialog extends StatelessWidget { - CustomAlertDialog( - {this.title, required this.content, this.actions, this.contentPadding}); + const CustomAlertDialog( + {Key? key, + this.title, + required this.content, + this.actions, + this.contentPadding, + this.onSubmit, + this.onCancel}) + : super(key: key); final Widget? title; final Widget content; final List? actions; final double? contentPadding; + final Function()? onSubmit; + final Function()? onCancel; @override Widget build(BuildContext context) { - return AlertDialog( - scrollable: true, - title: title, - contentPadding: - EdgeInsets.symmetric(horizontal: contentPadding ?? 25, vertical: 10), - content: content, - actions: actions, + FocusNode focusNode = FocusNode(); + // request focus if there is no focused FocusNode in the dialog + Future.delayed(Duration.zero, () { + if (!focusNode.hasFocus) focusNode.requestFocus(); + }); + return Focus( + focusNode: focusNode, + autofocus: true, + onKey: (node, key) { + if (key.logicalKey == LogicalKeyboardKey.escape) { + if (key is RawKeyDownEvent) { + onCancel?.call(); + } + return KeyEventResult.handled; // avoid TextField exception on escape + } else if (onSubmit != null && + key.logicalKey == LogicalKeyboardKey.enter) { + if (key is RawKeyDownEvent) onSubmit?.call(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }, + child: AlertDialog( + scrollable: true, + title: title, + contentPadding: EdgeInsets.symmetric( + horizontal: contentPadding ?? 25, vertical: 10), + content: content, + actions: actions, + ), ); } } @@ -429,26 +468,28 @@ void msgBox( {bool? hasCancel}) { dialogManager.dismissAll(); List buttons = []; + bool hasOk = false; + submit() { + dialogManager.dismissAll(); + // https://github.com/fufesou/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263 + if (!type.contains("custom")) { + closeConnection(); + } + } + + cancel() { + dialogManager.dismissAll(); + } + if (type != "connecting" && type != "success" && !type.contains("nook")) { - buttons.insert( - 0, - msgBoxButton(translate('OK'), () { - dialogManager.dismissAll(); - // https://github.com/fufesou/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263 - if (!type.contains("custom")) { - closeConnection(); - } - })); + hasOk = true; + buttons.insert(0, msgBoxButton(translate('OK'), submit)); } hasCancel ??= !type.contains("error") && !type.contains("nocancel") && type != "restarting"; if (hasCancel) { - buttons.insert( - 0, - msgBoxButton(translate('Cancel'), () { - dialogManager.dismissAll(); - })); + buttons.insert(0, msgBoxButton(translate('Cancel'), cancel)); } // TODO: test this button if (type.contains("hasclose")) { @@ -459,9 +500,12 @@ void msgBox( })); } dialogManager.show((setState, close) => CustomAlertDialog( - title: _msgBoxTitle(title), - content: Text(translate(text), style: TextStyle(fontSize: 15)), - actions: buttons)); + title: _msgBoxTitle(title), + content: Text(translate(text), style: const TextStyle(fontSize: 15)), + actions: buttons, + onSubmit: hasOk ? submit : null, + onCancel: hasCancel == true ? cancel : null, + )); } Widget msgBoxButton(String text, void Function() onPressed) { @@ -479,15 +523,19 @@ Widget msgBoxButton(String text, void Function() onPressed) { Text(translate(text), style: TextStyle(color: MyTheme.accent)))); } -Widget _msgBoxTitle(String title) => Text(translate(title), style: TextStyle(fontSize: 21)); +Widget _msgBoxTitle(String title) => + Text(translate(title), style: TextStyle(fontSize: 21)); void msgBoxCommon(OverlayDialogManager dialogManager, String title, - Widget content, List buttons) { + Widget content, List buttons, + {bool hasCancel = true}) { dialogManager.dismissAll(); dialogManager.show((setState, close) => CustomAlertDialog( - title: _msgBoxTitle(title), - content: content, - actions: buttons)); + title: _msgBoxTitle(title), + content: content, + actions: buttons, + onCancel: hasCancel ? close : null, + )); } Color str2color(String str, [alpha = 0xFF]) { diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index fe363c4c9..e4f6527ca 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -167,7 +167,7 @@ class _ConnectionPageState extends State { }); var w = Container( width: 320 + 20 * 2, - padding: EdgeInsets.fromLTRB(20, 24, 20, 22), + padding: const EdgeInsets.fromLTRB(20, 24, 20, 22), decoration: BoxDecoration( color: MyTheme.color(context).bg, borderRadius: const BorderRadius.all(Radius.circular(13)), @@ -179,7 +179,7 @@ class _ConnectionPageState extends State { children: [ Text( translate('Control Remote Desktop'), - style: TextStyle(fontSize: 19, height: 1), + style: const TextStyle(fontSize: 19, height: 1), ), ], ).marginOnly(bottom: 15), @@ -192,11 +192,13 @@ class _ConnectionPageState extends State { enableSuggestions: false, keyboardType: TextInputType.visiblePassword, focusNode: focusNode, - style: TextStyle( + style: const TextStyle( fontFamily: 'WorkSans', fontSize: 22, height: 1, ), + maxLines: 1, + cursorColor: MyTheme.color(context).text!, decoration: InputDecoration( hintText: inputFocused.value ? null @@ -206,14 +208,18 @@ class _ConnectionPageState extends State { border: OutlineInputBorder( borderRadius: BorderRadius.zero, borderSide: BorderSide( - color: MyTheme.color(context).placeholder!)), - focusedBorder: OutlineInputBorder( + color: MyTheme.color(context).border!)), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.zero, + borderSide: BorderSide( + color: MyTheme.color(context).border!)), + focusedBorder: const OutlineInputBorder( borderRadius: BorderRadius.zero, borderSide: BorderSide(color: MyTheme.button, width: 3), ), isDense: true, - contentPadding: EdgeInsets.symmetric( + contentPadding: const EdgeInsets.symmetric( horizontal: 10, vertical: 12)), controller: _idController, inputFormatters: [IDTextInputFormatter()], @@ -266,7 +272,7 @@ class _ConnectionPageState extends State { ).marginSymmetric(horizontal: 12), ), )), - SizedBox( + const SizedBox( width: 17, ), Obx( @@ -311,7 +317,8 @@ class _ConnectionPageState extends State { ), ); return Center( - child: Container(constraints: BoxConstraints(maxWidth: 600), child: w)); + child: Container( + constraints: const BoxConstraints(maxWidth: 600), child: w)); } @override @@ -661,71 +668,69 @@ class _ConnectionPageState extends State { var field = ""; var msg = ""; var isInProgress = false; + TextEditingController controller = TextEditingController(text: field); + gFFI.dialogManager.show((setState, close) { + submit() async { + setState(() { + msg = ""; + isInProgress = true; + }); + field = controller.text.trim(); + if (field.isEmpty) { + // pass + } else { + final ids = field.trim().split(RegExp(r"[\s,;\n]+")); + field = ids.join(','); + for (final newId in ids) { + if (gFFI.abModel.idContainBy(newId)) { + continue; + } + gFFI.abModel.addId(newId); + } + await gFFI.abModel.updateAb(); + this.setState(() {}); + // final currentPeers + } + close(); + } + return CustomAlertDialog( title: Text(translate("Add ID")), content: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(translate("whitelist_sep")), - SizedBox( + const SizedBox( height: 8.0, ), Row( children: [ Expanded( child: TextField( - onChanged: (s) { - field = s; - }, - maxLines: null, - decoration: InputDecoration( - border: OutlineInputBorder(), - errorText: msg.isEmpty ? null : translate(msg), - ), - controller: TextEditingController(text: field), - ), + maxLines: null, + decoration: InputDecoration( + border: const OutlineInputBorder(), + errorText: msg.isEmpty ? null : translate(msg), + ), + controller: controller, + focusNode: FocusNode()..requestFocus()), ), ], ), - SizedBox( + const SizedBox( height: 4.0, ), - Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + Offstage( + offstage: !isInProgress, child: const LinearProgressIndicator()) ], ), actions: [ - TextButton( - onPressed: () { - close(); - }, - child: Text(translate("Cancel"))), - TextButton( - onPressed: () async { - setState(() { - msg = ""; - isInProgress = true; - }); - field = field.trim(); - if (field.isEmpty) { - // pass - } else { - final ids = field.trim().split(RegExp(r"[\s,;\n]+")); - field = ids.join(','); - for (final newId in ids) { - if (gFFI.abModel.idContainBy(newId)) { - continue; - } - gFFI.abModel.addId(newId); - } - await gFFI.abModel.updateAb(); - this.setState(() {}); - // final currentPeers - } - close(); - }, - child: Text(translate("OK"))), + TextButton(onPressed: close, child: Text(translate("Cancel"))), + TextButton(onPressed: submit, child: Text(translate("OK"))), ], + onSubmit: submit, + onCancel: close, ); }); } @@ -734,67 +739,65 @@ class _ConnectionPageState extends State { var field = ""; var msg = ""; var isInProgress = false; + TextEditingController controller = TextEditingController(text: field); gFFI.dialogManager.show((setState, close) { + submit() async { + setState(() { + msg = ""; + isInProgress = true; + }); + field = controller.text.trim(); + if (field.isEmpty) { + // pass + } else { + final tags = field.trim().split(RegExp(r"[\s,;\n]+")); + field = tags.join(','); + for (final tag in tags) { + gFFI.abModel.addTag(tag); + } + await gFFI.abModel.updateAb(); + // final currentPeers + } + close(); + } + return CustomAlertDialog( title: Text(translate("Add Tag")), content: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(translate("whitelist_sep")), - SizedBox( + const SizedBox( height: 8.0, ), Row( children: [ Expanded( child: TextField( - onChanged: (s) { - field = s; - }, maxLines: null, decoration: InputDecoration( - border: OutlineInputBorder(), + border: const OutlineInputBorder(), errorText: msg.isEmpty ? null : translate(msg), ), - controller: TextEditingController(text: field), + controller: controller, + focusNode: FocusNode()..requestFocus(), ), ), ], ), - SizedBox( + const SizedBox( height: 4.0, ), - Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + Offstage( + offstage: !isInProgress, child: const LinearProgressIndicator()) ], ), actions: [ - TextButton( - onPressed: () { - close(); - }, - child: Text(translate("Cancel"))), - TextButton( - onPressed: () async { - setState(() { - msg = ""; - isInProgress = true; - }); - field = field.trim(); - if (field.isEmpty) { - // pass - } else { - final tags = field.trim().split(RegExp(r"[\s,;\n]+")); - field = tags.join(','); - for (final tag in tags) { - gFFI.abModel.addTag(tag); - } - await gFFI.abModel.updateAb(); - // final currentPeers - } - close(); - }, - child: Text(translate("OK"))), + TextButton(onPressed: close, child: Text(translate("Cancel"))), + TextButton(onPressed: submit, child: Text(translate("OK"))), ], + onSubmit: submit, + onCancel: close, ); }); } @@ -806,13 +809,23 @@ class _ConnectionPageState extends State { var selectedTag = gFFI.abModel.getPeerTags(id).obs; gFFI.dialogManager.show((setState, close) { + submit() async { + setState(() { + isInProgress = true; + }); + gFFI.abModel.changeTagForPeer(id, selectedTag); + await gFFI.abModel.updateAb(); + close(); + } + return CustomAlertDialog( title: Text(translate("Edit Tag")), content: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + padding: + const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Wrap( children: tags .map((e) => buildTag(e, selectedTag, onTap: () { @@ -825,26 +838,16 @@ class _ConnectionPageState extends State { .toList(growable: false), ), ), - Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + Offstage( + offstage: !isInProgress, child: const LinearProgressIndicator()) ], ), actions: [ - TextButton( - onPressed: () { - close(); - }, - child: Text(translate("Cancel"))), - TextButton( - onPressed: () async { - setState(() { - isInProgress = true; - }); - gFFI.abModel.changeTagForPeer(id, selectedTag); - await gFFI.abModel.updateAb(); - close(); - }, - child: Text(translate("OK"))), + TextButton(onPressed: close, child: Text(translate("Cancel"))), + TextButton(onPressed: submit, child: Text(translate("OK"))), ], + onSubmit: submit, + onCancel: close, ); }); } diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 5a082a8fd..0ccb86d1f 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -55,7 +55,7 @@ class _DesktopHomePageState extends State return Row( children: [ buildServerInfo(context), - VerticalDivider( + const VerticalDivider( width: 1, thickness: 1, ), @@ -93,7 +93,7 @@ class _DesktopHomePageState extends State buildIDBoard(BuildContext context) { final model = gFFI.serverModel; return Container( - margin: EdgeInsets.only(left: 20, right: 16), + margin: const EdgeInsets.only(left: 20, right: 16), height: 52, child: Row( crossAxisAlignment: CrossAxisAlignment.baseline, @@ -101,7 +101,7 @@ class _DesktopHomePageState extends State children: [ Container( width: 2, - decoration: BoxDecoration(color: MyTheme.accent), + decoration: const BoxDecoration(color: MyTheme.accent), ), Expanded( child: Padding( @@ -109,7 +109,7 @@ class _DesktopHomePageState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( + SizedBox( height: 25, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -135,11 +135,11 @@ class _DesktopHomePageState extends State child: TextFormField( controller: model.serverId, readOnly: true, - decoration: InputDecoration( + decoration: const InputDecoration( border: InputBorder.none, contentPadding: EdgeInsets.only(bottom: 18), ), - style: TextStyle( + style: const TextStyle( fontSize: 22, ), ), @@ -642,76 +642,76 @@ class _DesktopHomePageState extends State var newId = ""; var msg = ""; var isInProgress = false; + TextEditingController controller = TextEditingController(); gFFI.dialogManager.show((setState, close) { + submit() async { + newId = controller.text.trim(); + setState(() { + msg = ""; + isInProgress = true; + bind.mainChangeId(newId: newId); + }); + + var status = await bind.mainGetAsyncStatus(); + while (status == " ") { + await Future.delayed(const Duration(milliseconds: 100)); + status = await bind.mainGetAsyncStatus(); + } + if (status.isEmpty) { + // ok + close(); + return; + } + setState(() { + isInProgress = false; + msg = translate(status); + }); + } + return CustomAlertDialog( title: Text(translate("Change ID")), content: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(translate("id_change_tip")), - SizedBox( + const SizedBox( height: 8.0, ), Row( children: [ - Text("ID:").marginOnly(bottom: 16.0), - SizedBox( + const Text("ID:").marginOnly(bottom: 16.0), + const SizedBox( width: 24.0, ), Expanded( child: TextField( - onChanged: (s) { - newId = s; - }, decoration: InputDecoration( - border: OutlineInputBorder(), + border: const OutlineInputBorder(), errorText: msg.isEmpty ? null : translate(msg)), inputFormatters: [ LengthLimitingTextInputFormatter(16), // FilteringTextInputFormatter(RegExp(r"[a-zA-z][a-zA-z0-9\_]*"), allow: true) ], maxLength: 16, + controller: controller, + focusNode: FocusNode()..requestFocus(), ), ), ], ), - SizedBox( + const SizedBox( height: 4.0, ), - Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + Offstage( + offstage: !isInProgress, child: const LinearProgressIndicator()) ], ), actions: [ - TextButton( - onPressed: () { - close(); - }, - child: Text(translate("Cancel"))), - TextButton( - onPressed: () async { - setState(() { - msg = ""; - isInProgress = true; - bind.mainChangeId(newId: newId); - }); - - var status = await bind.mainGetAsyncStatus(); - while (status == " ") { - await Future.delayed(Duration(milliseconds: 100)); - status = await bind.mainGetAsyncStatus(); - } - if (status.isEmpty) { - // ok - close(); - return; - } - setState(() { - isInProgress = false; - msg = translate(status); - }); - }, - child: Text(translate("OK"))), + TextButton(onPressed: close, child: Text(translate("Cancel"))), + TextButton(onPressed: submit, child: Text(translate("OK"))), ], + onSubmit: submit, + onCancel: close, ); }); } @@ -720,16 +720,16 @@ class _DesktopHomePageState extends State final appName = await bind.mainGetAppName(); final license = await bind.mainGetLicense(); final version = await bind.mainGetVersion(); - final linkStyle = TextStyle(decoration: TextDecoration.underline); + const linkStyle = TextStyle(decoration: TextDecoration.underline); gFFI.dialogManager.show((setState, close) { return CustomAlertDialog( title: Text("About $appName"), content: ConstrainedBox( - constraints: BoxConstraints(minWidth: 500), + constraints: const BoxConstraints(minWidth: 500), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( + const SizedBox( height: 8.0, ), Text("Version: $version").marginSymmetric(vertical: 4.0), @@ -737,7 +737,7 @@ class _DesktopHomePageState extends State onTap: () { launchUrlString("https://rustdesk.com/privacy"); }, - child: Text( + child: const Text( "Privacy Statement", style: linkStyle, ).marginSymmetric(vertical: 4.0)), @@ -745,13 +745,14 @@ class _DesktopHomePageState extends State onTap: () { launchUrlString("https://rustdesk.com"); }, - child: Text( + child: const Text( "Website", style: linkStyle, ).marginSymmetric(vertical: 4.0)), Container( - decoration: BoxDecoration(color: Color(0xFF2c8cff)), - padding: EdgeInsets.symmetric(vertical: 24, horizontal: 8), + decoration: const BoxDecoration(color: Color(0xFF2c8cff)), + padding: + const EdgeInsets.symmetric(vertical: 24, horizontal: 8), child: Row( children: [ Expanded( @@ -760,9 +761,9 @@ class _DesktopHomePageState extends State children: [ Text( "Copyright © 2022 Purslane Ltd.\n$license", - style: TextStyle(color: Colors.white), + style: const TextStyle(color: Colors.white), ), - Text( + const Text( "Made with heart in this chaotic world!", style: TextStyle( fontWeight: FontWeight.w800, @@ -778,12 +779,10 @@ class _DesktopHomePageState extends State ), ), actions: [ - TextButton( - onPressed: () async { - close(); - }, - child: Text(translate("OK"))), + TextButton(onPressed: close, child: Text(translate("OK"))), ], + onSubmit: close, + onCancel: close, ); }); } @@ -815,118 +814,124 @@ Future loginDialog() async { var isInProgress = false; var completer = Completer(); gFFI.dialogManager.show((setState, close) { + submit() async { + setState(() { + userNameMsg = ""; + passMsg = ""; + isInProgress = true; + }); + cancel() { + setState(() { + isInProgress = false; + }); + } + + userName = userContontroller.text; + pass = pwdController.text; + if (userName.isEmpty) { + userNameMsg = translate("Username missed"); + cancel(); + return; + } + if (pass.isEmpty) { + passMsg = translate("Password missed"); + cancel(); + return; + } + try { + final resp = await gFFI.userModel.login(userName, pass); + if (resp.containsKey('error')) { + passMsg = resp['error']; + cancel(); + return; + } + // {access_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJndWlkIjoiMDFkZjQ2ZjgtZjg3OS00MDE0LTk5Y2QtMGMwYzM2MmViZGJlIiwiZXhwIjoxNjYxNDg2NzYwfQ.GZpe1oI8TfM5yTYNrpcwbI599P4Z_-b2GmnwNl2Lr-w, + // token_type: Bearer, user: {id: , name: admin, email: null, note: null, status: null, grp: null, is_admin: true}} + debugPrint("$resp"); + completer.complete(true); + } catch (err) { + // ignore: avoid_print + print(err.toString()); + cancel(); + return; + } + close(); + } + + cancel() { + completer.complete(false); + close(); + } + return CustomAlertDialog( title: Text(translate("Login")), content: ConstrainedBox( - constraints: BoxConstraints(minWidth: 500), + constraints: const BoxConstraints(minWidth: 500), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( + const SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( - constraints: BoxConstraints(minWidth: 100), + constraints: const BoxConstraints(minWidth: 100), child: Text( "${translate('Username')}:", textAlign: TextAlign.start, ).marginOnly(bottom: 16.0)), - SizedBox( + const SizedBox( width: 24.0, ), Expanded( child: TextField( decoration: InputDecoration( - border: OutlineInputBorder(), + border: const OutlineInputBorder(), errorText: userNameMsg.isNotEmpty ? userNameMsg : null), controller: userContontroller, + focusNode: FocusNode()..requestFocus(), ), ), ], ), - SizedBox( + const SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( - constraints: BoxConstraints(minWidth: 100), + constraints: const BoxConstraints(minWidth: 100), child: Text("${translate('Password')}:") .marginOnly(bottom: 16.0)), - SizedBox( + const SizedBox( width: 24.0, ), Expanded( child: TextField( obscureText: true, decoration: InputDecoration( - border: OutlineInputBorder(), + border: const OutlineInputBorder(), errorText: passMsg.isNotEmpty ? passMsg : null), controller: pwdController, ), ), ], ), - SizedBox( + const SizedBox( height: 4.0, ), - Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + Offstage( + offstage: !isInProgress, child: const LinearProgressIndicator()) ], ), ), actions: [ - TextButton( - onPressed: () { - completer.complete(false); - close(); - }, - child: Text(translate("Cancel"))), - TextButton( - onPressed: () async { - setState(() { - userNameMsg = ""; - passMsg = ""; - isInProgress = true; - }); - final cancel = () { - setState(() { - isInProgress = false; - }); - }; - userName = userContontroller.text; - pass = pwdController.text; - if (userName.isEmpty) { - userNameMsg = translate("Username missed"); - cancel(); - return; - } - if (pass.isEmpty) { - passMsg = translate("Password missed"); - cancel(); - return; - } - try { - final resp = await gFFI.userModel.login(userName, pass); - if (resp.containsKey('error')) { - passMsg = resp['error']; - cancel(); - return; - } - // {access_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJndWlkIjoiMDFkZjQ2ZjgtZjg3OS00MDE0LTk5Y2QtMGMwYzM2MmViZGJlIiwiZXhwIjoxNjYxNDg2NzYwfQ.GZpe1oI8TfM5yTYNrpcwbI599P4Z_-b2GmnwNl2Lr-w, - // token_type: Bearer, user: {id: , name: admin, email: null, note: null, status: null, grp: null, is_admin: true}} - debugPrint("$resp"); - completer.complete(true); - } catch (err) { - print(err.toString()); - cancel(); - return; - } - close(); - }, - child: Text(translate("OK"))), + TextButton(onPressed: cancel, child: Text(translate("Cancel"))), + TextButton(onPressed: submit, child: Text(translate("OK"))), ], + onSubmit: submit, + onCancel: cancel, ); }); return completer.future; @@ -940,55 +945,78 @@ void setPasswordDialog() async { var errMsg1 = ""; gFFI.dialogManager.show((setState, close) { + submit() { + setState(() { + errMsg0 = ""; + errMsg1 = ""; + }); + final pass = p0.text.trim(); + if (pass.length < 6) { + setState(() { + errMsg0 = translate("Too short, at least 6 characters."); + }); + return; + } + if (p1.text.trim() != pass) { + setState(() { + errMsg1 = translate("The confirmation is not identical."); + }); + return; + } + bind.mainSetPermanentPassword(password: pass); + close(); + } + return CustomAlertDialog( title: Text(translate("Set Password")), content: ConstrainedBox( - constraints: BoxConstraints(minWidth: 500), + constraints: const BoxConstraints(minWidth: 500), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( + const SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( - constraints: BoxConstraints(minWidth: 100), + constraints: const BoxConstraints(minWidth: 100), child: Text( "${translate('Password')}:", textAlign: TextAlign.start, ).marginOnly(bottom: 16.0)), - SizedBox( + const SizedBox( width: 24.0, ), Expanded( child: TextField( obscureText: true, decoration: InputDecoration( - border: OutlineInputBorder(), + border: const OutlineInputBorder(), errorText: errMsg0.isNotEmpty ? errMsg0 : null), controller: p0, + focusNode: FocusNode()..requestFocus(), ), ), ], ), - SizedBox( + const SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( - constraints: BoxConstraints(minWidth: 100), + constraints: const BoxConstraints(minWidth: 100), child: Text("${translate('Confirmation')}:") .marginOnly(bottom: 16.0)), - SizedBox( + const SizedBox( width: 24.0, ), Expanded( child: TextField( obscureText: true, decoration: InputDecoration( - border: OutlineInputBorder(), + border: const OutlineInputBorder(), errorText: errMsg1.isNotEmpty ? errMsg1 : null), controller: p1, ), @@ -999,35 +1027,11 @@ void setPasswordDialog() async { ), ), actions: [ - TextButton( - onPressed: () { - close(); - }, - child: Text(translate("Cancel"))), - TextButton( - onPressed: () { - setState(() { - errMsg0 = ""; - errMsg1 = ""; - }); - final pass = p0.text.trim(); - if (pass.length < 6) { - setState(() { - errMsg0 = translate("Too short, at least 6 characters."); - }); - return; - } - if (p1.text.trim() != pass) { - setState(() { - errMsg1 = translate("The confirmation is not identical."); - }); - return; - } - bind.mainSetPermanentPassword(password: pass); - close(); - }, - child: Text(translate("OK"))), + TextButton(onPressed: close, child: Text(translate("Cancel"))), + TextButton(onPressed: submit, child: Text(translate("OK"))), ], + onSubmit: submit, + onCancel: close, ); }); } diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 120f8bc7a..867c8a54c 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1038,52 +1038,117 @@ void changeServer() async { var keyController = TextEditingController(text: key); var isInProgress = false; + gFFI.dialogManager.show((setState, close) { + submit() async { + setState(() { + [idServerMsg, relayServerMsg, apiServerMsg].forEach((element) { + element = ""; + }); + isInProgress = true; + }); + cancel() { + setState(() { + isInProgress = false; + }); + } + + idServer = idController.text.trim(); + relayServer = relayController.text.trim(); + apiServer = apiController.text.trim().toLowerCase(); + key = keyController.text.trim(); + + if (idServer.isNotEmpty) { + idServerMsg = + translate(await bind.mainTestIfValidServer(server: idServer)); + if (idServerMsg.isEmpty) { + oldOptions['custom-rendezvous-server'] = idServer; + } else { + cancel(); + return; + } + } else { + oldOptions['custom-rendezvous-server'] = ""; + } + + if (relayServer.isNotEmpty) { + relayServerMsg = + translate(await bind.mainTestIfValidServer(server: relayServer)); + if (relayServerMsg.isEmpty) { + oldOptions['relay-server'] = relayServer; + } else { + cancel(); + return; + } + } else { + oldOptions['relay-server'] = ""; + } + + if (apiServer.isNotEmpty) { + if (apiServer.startsWith('http://') || + apiServer.startsWith("https://")) { + oldOptions['api-server'] = apiServer; + return; + } else { + apiServerMsg = translate("invalid_http"); + cancel(); + return; + } + } else { + oldOptions['api-server'] = ""; + } + // ok + oldOptions['key'] = key; + await bind.mainSetOptions(json: jsonEncode(oldOptions)); + close(); + } + return CustomAlertDialog( title: Text(translate("ID/Relay Server")), content: ConstrainedBox( - constraints: BoxConstraints(minWidth: 500), + constraints: const BoxConstraints(minWidth: 500), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( + const SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( - constraints: BoxConstraints(minWidth: 100), + constraints: const BoxConstraints(minWidth: 100), child: Text("${translate('ID Server')}:") .marginOnly(bottom: 16.0)), - SizedBox( + const SizedBox( width: 24.0, ), Expanded( child: TextField( decoration: InputDecoration( - border: OutlineInputBorder(), + border: const OutlineInputBorder(), errorText: idServerMsg.isNotEmpty ? idServerMsg : null), controller: idController, + focusNode: FocusNode()..requestFocus(), ), ), ], ), - SizedBox( + const SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( - constraints: BoxConstraints(minWidth: 100), + constraints: const BoxConstraints(minWidth: 100), child: Text("${translate('Relay Server')}:") .marginOnly(bottom: 16.0)), - SizedBox( + const SizedBox( width: 24.0, ), Expanded( child: TextField( decoration: InputDecoration( - border: OutlineInputBorder(), + border: const OutlineInputBorder(), errorText: relayServerMsg.isNotEmpty ? relayServerMsg : null), controller: relayController, @@ -1091,22 +1156,22 @@ void changeServer() async { ), ], ), - SizedBox( + const SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( - constraints: BoxConstraints(minWidth: 100), + constraints: const BoxConstraints(minWidth: 100), child: Text("${translate('API Server')}:") .marginOnly(bottom: 16.0)), - SizedBox( + const SizedBox( width: 24.0, ), Expanded( child: TextField( decoration: InputDecoration( - border: OutlineInputBorder(), + border: const OutlineInputBorder(), errorText: apiServerMsg.isNotEmpty ? apiServerMsg : null), controller: apiController, @@ -1114,21 +1179,21 @@ void changeServer() async { ), ], ), - SizedBox( + const SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( - constraints: BoxConstraints(minWidth: 100), + constraints: const BoxConstraints(minWidth: 100), child: Text("${translate('Key')}:").marginOnly(bottom: 16.0)), - SizedBox( + const SizedBox( width: 24.0, ), Expanded( child: TextField( - decoration: InputDecoration( + decoration: const InputDecoration( border: OutlineInputBorder(), ), controller: keyController, @@ -1136,83 +1201,20 @@ void changeServer() async { ), ], ), - SizedBox( + const SizedBox( height: 4.0, ), - Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + Offstage( + offstage: !isInProgress, child: const LinearProgressIndicator()) ], ), ), actions: [ - TextButton( - onPressed: () { - close(); - }, - child: Text(translate("Cancel"))), - TextButton( - onPressed: () async { - setState(() { - [idServerMsg, relayServerMsg, apiServerMsg].forEach((element) { - element = ""; - }); - isInProgress = true; - }); - final cancel = () { - setState(() { - isInProgress = false; - }); - }; - idServer = idController.text.trim(); - relayServer = relayController.text.trim(); - apiServer = apiController.text.trim().toLowerCase(); - key = keyController.text.trim(); - - if (idServer.isNotEmpty) { - idServerMsg = translate( - await bind.mainTestIfValidServer(server: idServer)); - if (idServerMsg.isEmpty) { - oldOptions['custom-rendezvous-server'] = idServer; - } else { - cancel(); - return; - } - } else { - oldOptions['custom-rendezvous-server'] = ""; - } - - if (relayServer.isNotEmpty) { - relayServerMsg = translate( - await bind.mainTestIfValidServer(server: relayServer)); - if (relayServerMsg.isEmpty) { - oldOptions['relay-server'] = relayServer; - } else { - cancel(); - return; - } - } else { - oldOptions['relay-server'] = ""; - } - - if (apiServer.isNotEmpty) { - if (apiServer.startsWith('http://') || - apiServer.startsWith("https://")) { - oldOptions['api-server'] = apiServer; - return; - } else { - apiServerMsg = translate("invalid_http"); - cancel(); - return; - } - } else { - oldOptions['api-server'] = ""; - } - // ok - oldOptions['key'] = key; - await bind.mainSetOptions(json: jsonEncode(oldOptions)); - close(); - }, - child: Text(translate("OK"))), + TextButton(onPressed: close, child: Text(translate("Cancel"))), + TextButton(onPressed: submit, child: Text(translate("OK"))), ], + onSubmit: submit, + onCancel: close, ); }); } @@ -1231,27 +1233,28 @@ void changeWhiteList() async { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(translate("whitelist_sep")), - SizedBox( + const SizedBox( height: 8.0, ), Row( children: [ Expanded( child: TextField( - maxLines: null, - decoration: InputDecoration( - border: OutlineInputBorder(), - errorText: msg.isEmpty ? null : translate(msg), - ), - controller: controller, - ), + maxLines: null, + decoration: InputDecoration( + border: const OutlineInputBorder(), + errorText: msg.isEmpty ? null : translate(msg), + ), + controller: controller, + focusNode: FocusNode()..requestFocus()), ), ], ), - SizedBox( + const SizedBox( height: 4.0, ), - Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + Offstage( + offstage: !isInProgress, child: const LinearProgressIndicator()) ], ), actions: [ @@ -1277,7 +1280,7 @@ void changeWhiteList() async { final ipMatch = RegExp(r"^\d+\.\d+\.\d+\.\d+$"); for (final ip in ips) { if (!ipMatch.hasMatch(ip)) { - msg = translate("Invalid IP") + " $ip"; + msg = "${translate("Invalid IP")} $ip"; setState(() { isInProgress = false; }); @@ -1292,6 +1295,7 @@ void changeWhiteList() async { }, child: Text(translate("OK"))), ], + onCancel: close, ); }); } @@ -1314,50 +1318,80 @@ void changeSocks5Proxy() async { var isInProgress = false; gFFI.dialogManager.show((setState, close) { + submit() async { + setState(() { + proxyMsg = ""; + isInProgress = true; + }); + cancel() { + setState(() { + isInProgress = false; + }); + } + + proxy = proxyController.text.trim(); + username = userController.text.trim(); + password = pwdController.text.trim(); + + if (proxy.isNotEmpty) { + proxyMsg = translate(await bind.mainTestIfValidServer(server: proxy)); + if (proxyMsg.isEmpty) { + // ignore + } else { + cancel(); + return; + } + } + await bind.mainSetSocks( + proxy: proxy, username: username, password: password); + close(); + } + return CustomAlertDialog( title: Text(translate("Socks5 Proxy")), content: ConstrainedBox( - constraints: BoxConstraints(minWidth: 500), + constraints: const BoxConstraints(minWidth: 500), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( + const SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( - constraints: BoxConstraints(minWidth: 100), + constraints: const BoxConstraints(minWidth: 100), child: Text("${translate('Hostname')}:") .marginOnly(bottom: 16.0)), - SizedBox( + const SizedBox( width: 24.0, ), Expanded( child: TextField( decoration: InputDecoration( - border: OutlineInputBorder(), + border: const OutlineInputBorder(), errorText: proxyMsg.isNotEmpty ? proxyMsg : null), controller: proxyController, + focusNode: FocusNode()..requestFocus(), ), ), ], ), - SizedBox( + const SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( - constraints: BoxConstraints(minWidth: 100), + constraints: const BoxConstraints(minWidth: 100), child: Text("${translate('Username')}:") .marginOnly(bottom: 16.0)), - SizedBox( + const SizedBox( width: 24.0, ), Expanded( child: TextField( - decoration: InputDecoration( + decoration: const InputDecoration( border: OutlineInputBorder(), ), controller: userController, @@ -1365,21 +1399,21 @@ void changeSocks5Proxy() async { ), ], ), - SizedBox( + const SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( - constraints: BoxConstraints(minWidth: 100), + constraints: const BoxConstraints(minWidth: 100), child: Text("${translate('Password')}:") .marginOnly(bottom: 16.0)), - SizedBox( + const SizedBox( width: 24.0, ), Expanded( child: TextField( - decoration: InputDecoration( + decoration: const InputDecoration( border: OutlineInputBorder(), ), controller: pwdController, @@ -1387,50 +1421,20 @@ void changeSocks5Proxy() async { ), ], ), - SizedBox( + const SizedBox( height: 8.0, ), - Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + Offstage( + offstage: !isInProgress, child: const LinearProgressIndicator()) ], ), ), actions: [ - TextButton( - onPressed: () { - close(); - }, - child: Text(translate("Cancel"))), - TextButton( - onPressed: () async { - setState(() { - proxyMsg = ""; - isInProgress = true; - }); - final cancel = () { - setState(() { - isInProgress = false; - }); - }; - proxy = proxyController.text.trim(); - username = userController.text.trim(); - password = pwdController.text.trim(); - - if (proxy.isNotEmpty) { - proxyMsg = - translate(await bind.mainTestIfValidServer(server: proxy)); - if (proxyMsg.isEmpty) { - // ignore - } else { - cancel(); - return; - } - } - await bind.mainSetSocks( - proxy: proxy, username: username, password: password); - close(); - }, - child: Text(translate("OK"))), + TextButton(onPressed: close, child: Text(translate("Cancel"))), + TextButton(onPressed: submit, child: Text(translate("OK"))), ], + onSubmit: submit, + onCancel: close, ); }); } diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 874a71dcf..0546f0503 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -37,25 +37,33 @@ class _DesktopTabPageState extends State { RxBool fullscreen = false.obs; Get.put(fullscreen, tag: 'fullscreen'); return Obx(() => DragToResizeArea( - resizeEdgeSize: fullscreen.value ? 1.0 : 8.0, - child: Container( - decoration: BoxDecoration( - border: Border.all(color: MyTheme.color(context).border!)), - child: Scaffold( - backgroundColor: MyTheme.color(context).bg, - body: DesktopTab( - controller: tabController, - theme: dark ? TarBarTheme.dark() : TarBarTheme.light(), - tail: ActionIcon( - message: 'Settings', - icon: IconFont.menu, - theme: dark ? TarBarTheme.dark() : TarBarTheme.light(), - onTap: onAddSetting, - is_close: false, - ), - )), - ), - )); + resizeEdgeSize: fullscreen.value ? 1.0 : 8.0, + child: Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: Overlay(initialEntries: [ + OverlayEntry(builder: (context) { + gFFI.dialogManager.setOverlayState(Overlay.of(context)); + return Scaffold( + backgroundColor: MyTheme.color(context).bg, + body: DesktopTab( + controller: tabController, + theme: dark + ? const TarBarTheme.dark() + : const TarBarTheme.light(), + tail: ActionIcon( + message: 'Settings', + icon: IconFont.menu, + theme: dark + ? const TarBarTheme.dark() + : const TarBarTheme.light(), + onTap: onAddSetting, + is_close: false, + ), + )); + }) + ]), + ))); } void onAddSetting() { diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 4a2f11553..b13f40a5f 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -642,47 +642,51 @@ class _FileManagerPageState extends State IconButton( onPressed: () { final name = TextEditingController(); - _ffi.dialogManager - .show((setState, close) => CustomAlertDialog( - title: Text(translate("Create Folder")), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextFormField( - decoration: InputDecoration( - labelText: translate( - "Please enter the folder name"), - ), - controller: name, - ), - ], - ), - actions: [ - TextButton( - style: flatButtonStyle, - onPressed: () => close(false), - child: Text(translate("Cancel"))), - ElevatedButton( - style: flatButtonStyle, - onPressed: () { - if (name.value.text.isNotEmpty) { - model.createDir( - PathUtil.join( - model - .getCurrentDir( - isLocal) - .path, - name.value.text, - model.getCurrentIsWindows( - isLocal)), - isLocal: isLocal); - close(); - } - }, - child: Text(translate("OK"))) - ])); + _ffi.dialogManager.show((setState, close) { + submit() { + if (name.value.text.isNotEmpty) { + model.createDir( + PathUtil.join( + model.getCurrentDir(isLocal).path, + name.value.text, + model.getCurrentIsWindows(isLocal)), + isLocal: isLocal); + close(); + } + } + + cancel() => close(false); + return CustomAlertDialog( + title: Text(translate("Create Folder")), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + decoration: InputDecoration( + labelText: translate( + "Please enter the folder name"), + ), + controller: name, + focusNode: FocusNode()..requestFocus(), + ), + ], + ), + actions: [ + TextButton( + style: flatButtonStyle, + onPressed: cancel, + child: Text(translate("Cancel"))), + ElevatedButton( + style: flatButtonStyle, + onPressed: submit, + child: Text(translate("OK"))) + ], + onSubmit: submit, + onCancel: cancel, + ); + }); }, - icon: Icon(Icons.create_new_folder_outlined)), + icon: const Icon(Icons.create_new_folder_outlined)), IconButton( onPressed: () async { final items = isLocal diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart index 6cfd0cdb2..28ee0d70e 100644 --- a/flutter/lib/desktop/pages/port_forward_page.dart +++ b/flutter/lib/desktop/pages/port_forward_page.dart @@ -70,38 +70,45 @@ class _PortForwardPageState extends State @override Widget build(BuildContext context) { super.build(context); - return Scaffold( - backgroundColor: MyTheme.color(context).grayBg, - body: FutureBuilder(future: () async { - if (!isRdp) { - refreshTunnelConfig(); - } - }(), builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - return Container( - decoration: BoxDecoration( - border: Border.all( - width: 20, color: MyTheme.color(context).grayBg!)), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - buildPrompt(context), - Flexible( - child: Container( - decoration: BoxDecoration( - color: MyTheme.color(context).bg, - border: Border.all(width: 1, color: MyTheme.border)), - child: - widget.isRDP ? buildRdp(context) : buildTunnel(context), - ), + return Overlay(initialEntries: [ + OverlayEntry(builder: (context) { + _ffi.dialogManager.setOverlayState(Overlay.of(context)); + return Scaffold( + backgroundColor: MyTheme.color(context).grayBg, + body: FutureBuilder(future: () async { + if (!isRdp) { + refreshTunnelConfig(); + } + }(), builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return Container( + decoration: BoxDecoration( + border: Border.all( + width: 20, color: MyTheme.color(context).grayBg!)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + buildPrompt(context), + Flexible( + child: Container( + decoration: BoxDecoration( + color: MyTheme.color(context).bg, + border: + Border.all(width: 1, color: MyTheme.border)), + child: widget.isRDP + ? buildRdp(context) + : buildTunnel(context), + ), + ), + ], ), - ], - ), - ); - } - return const Offstage(); - }), - ); + ); + } + return const Offstage(); + }), + ); + }) + ]); } buildPrompt(BuildContext context) { diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index 13cf02699..1bff02508 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -563,47 +563,47 @@ abstract class BasePeerCard extends StatelessWidget { } } gFFI.dialogManager.show((setState, close) { + submit() async { + isInProgress.value = true; + name = controller.text; + await bind.mainSetPeerOption(id: id, key: 'alias', value: name); + if (isAddressBook) { + gFFI.abModel.setPeerOption(id, 'alias', name); + await gFFI.abModel.updateAb(); + } + alias.value = await bind.mainGetPeerOption(id: peer.id, key: 'alias'); + close(); + isInProgress.value = false; + } + return CustomAlertDialog( title: Text(translate('Rename')), content: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + padding: + const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Form( child: TextFormField( controller: controller, - decoration: InputDecoration(border: OutlineInputBorder()), + focusNode: FocusNode()..requestFocus(), + decoration: + const InputDecoration(border: OutlineInputBorder()), ), ), ), Obx(() => Offstage( offstage: isInProgress.isFalse, - child: LinearProgressIndicator())), + child: const LinearProgressIndicator())), ], ), actions: [ - TextButton( - onPressed: () { - close(); - }, - child: Text(translate("Cancel"))), - TextButton( - onPressed: () async { - isInProgress.value = true; - name = controller.text; - await bind.mainSetPeerOption(id: id, key: 'alias', value: name); - if (isAddressBook) { - gFFI.abModel.setPeerOption(id, 'alias', name); - await gFFI.abModel.updateAb(); - } - alias.value = - await bind.mainGetPeerOption(id: peer.id, key: 'alias'); - close(); - isInProgress.value = false; - }, - child: Text(translate("OK"))), + TextButton(onPressed: close, child: Text(translate("Cancel"))), + TextButton(onPressed: submit, child: Text(translate("OK"))), ], + onSubmit: submit, + onCancel: close, ); }); } @@ -750,13 +750,23 @@ class AddressBookPeerCard extends BasePeerCard { var selectedTag = gFFI.abModel.getPeerTags(id).obs; gFFI.dialogManager.show((setState, close) { + submit() async { + setState(() { + isInProgress = true; + }); + gFFI.abModel.changeTagForPeer(id, selectedTag); + await gFFI.abModel.updateAb(); + close(); + } + return CustomAlertDialog( title: Text(translate("Edit Tag")), content: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + padding: + const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Wrap( children: tags .map((e) => _buildTag(e, selectedTag, onTap: () { @@ -769,26 +779,16 @@ class AddressBookPeerCard extends BasePeerCard { .toList(growable: false), ), ), - Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + Offstage( + offstage: !isInProgress, child: const LinearProgressIndicator()) ], ), actions: [ - TextButton( - onPressed: () { - close(); - }, - child: Text(translate("Cancel"))), - TextButton( - onPressed: () async { - setState(() { - isInProgress = true; - }); - gFFI.abModel.changeTagForPeer(id, selectedTag); - await gFFI.abModel.updateAb(); - close(); - }, - child: Text(translate("OK"))), + TextButton(onPressed: close, child: Text(translate("Cancel"))), + TextButton(onPressed: submit, child: Text(translate("OK"))), ], + onSubmit: submit, + onCancel: close, ); }); } @@ -871,25 +871,35 @@ void _rdpDialog(String id) async { RxBool secure = true.obs; gFFI.dialogManager.show((setState, close) { + submit() async { + await bind.mainSetPeerOption( + id: id, key: 'rdp_port', value: portController.text.trim()); + await bind.mainSetPeerOption( + id: id, key: 'rdp_username', value: userController.text); + await bind.mainSetPeerOption( + id: id, key: 'rdp_password', value: passwordContorller.text); + close(); + } + return CustomAlertDialog( - title: Text('RDP ' + translate('Settings')), + title: Text('RDP ${translate('Settings')}'), content: ConstrainedBox( - constraints: BoxConstraints(minWidth: 500), + constraints: const BoxConstraints(minWidth: 500), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( + const SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( - constraints: BoxConstraints(minWidth: 100), + constraints: const BoxConstraints(minWidth: 100), child: Text( "${translate('Port')}:", textAlign: TextAlign.start, ).marginOnly(bottom: 16.0)), - SizedBox( + const SizedBox( width: 24.0, ), Expanded( @@ -898,52 +908,54 @@ void _rdpDialog(String id) async { FilteringTextInputFormatter.allow(RegExp( r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')) ], - decoration: InputDecoration( + decoration: const InputDecoration( border: OutlineInputBorder(), hintText: '3389'), controller: portController, + focusNode: FocusNode()..requestFocus(), ), ), ], ), - SizedBox( + const SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( - constraints: BoxConstraints(minWidth: 100), + constraints: const BoxConstraints(minWidth: 100), child: Text( "${translate('Username')}:", textAlign: TextAlign.start, ).marginOnly(bottom: 16.0)), - SizedBox( + const SizedBox( width: 24.0, ), Expanded( child: TextField( - decoration: InputDecoration(border: OutlineInputBorder()), + decoration: + const InputDecoration(border: OutlineInputBorder()), controller: userController, ), ), ], ), - SizedBox( + const SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( - constraints: BoxConstraints(minWidth: 100), + constraints: const BoxConstraints(minWidth: 100), child: Text("${translate('Password')}:") .marginOnly(bottom: 16.0)), - SizedBox( + const SizedBox( width: 24.0, ), Expanded( child: Obx(() => TextField( obscureText: secure.value, decoration: InputDecoration( - border: OutlineInputBorder(), + border: const OutlineInputBorder(), suffixIcon: IconButton( onPressed: () => secure.value = !secure.value, icon: Icon(secure.value @@ -958,23 +970,11 @@ void _rdpDialog(String id) async { ), ), actions: [ - TextButton( - onPressed: () { - close(); - }, - child: Text(translate("Cancel"))), - TextButton( - onPressed: () async { - await bind.mainSetPeerOption( - id: id, key: 'rdp_port', value: portController.text.trim()); - await bind.mainSetPeerOption( - id: id, key: 'rdp_username', value: userController.text); - await bind.mainSetPeerOption( - id: id, key: 'rdp_password', value: passwordContorller.text); - close(); - }, - child: Text(translate("OK"))), + TextButton(onPressed: close, child: Text(translate("Cancel"))), + TextButton(onPressed: submit, child: Text(translate("OK"))), ], + onSubmit: submit, + onCancel: close, ); }); } diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index dc3c249f0..c83f61a17 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -596,46 +596,49 @@ void showSetOSPassword( var autoLogin = await bind.sessionGetOption(id: id, arg: "auto-login") != ""; controller.text = password; dialogManager.show((setState, close) { + submit() { + var text = controller.text.trim(); + bind.sessionPeerOption(id: id, name: "os-password", value: text); + bind.sessionPeerOption( + id: id, name: "auto-login", value: autoLogin ? 'Y' : ''); + if (text != "" && login) { + bind.sessionInputOsPassword(id: id, value: text); + } + close(); + } + return CustomAlertDialog( - title: Text(translate('OS Password')), - content: Column(mainAxisSize: MainAxisSize.min, children: [ - PasswordWidget(controller: controller), - CheckboxListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - controlAffinity: ListTileControlAffinity.leading, - title: Text( - translate('Auto Login'), - ), - value: autoLogin, - onChanged: (v) { - if (v == null) return; - setState(() => autoLogin = v); - }, + title: Text(translate('OS Password')), + content: Column(mainAxisSize: MainAxisSize.min, children: [ + PasswordWidget(controller: controller), + CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Text( + translate('Auto Login'), ), - ]), - actions: [ - TextButton( - style: flatButtonStyle, - onPressed: () { - close(); - }, - child: Text(translate('Cancel')), - ), - TextButton( - style: flatButtonStyle, - onPressed: () { - var text = controller.text.trim(); - bind.sessionPeerOption(id: id, name: "os-password", value: text); - bind.sessionPeerOption( - id: id, name: "auto-login", value: autoLogin ? 'Y' : ''); - if (text != "" && login) { - bind.sessionInputOsPassword(id: id, value: text); - } - close(); - }, - child: Text(translate('OK')), - ), - ]); + value: autoLogin, + onChanged: (v) { + if (v == null) return; + setState(() => autoLogin = v); + }, + ), + ]), + actions: [ + TextButton( + style: flatButtonStyle, + onPressed: close, + child: Text(translate('Cancel')), + ), + TextButton( + style: flatButtonStyle, + onPressed: submit, + child: Text(translate('OK')), + ), + ], + onSubmit: submit, + onCancel: close, + ); }); } diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 1a19dd833..3c2b28ab0 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -462,22 +462,24 @@ class WindowActionPanel extends StatelessWidget { } closeConfirmDialog(Function() callback) async { - final res = await gFFI.dialogManager - .show((setState, close) => CustomAlertDialog( - title: Row(children: [ - Icon(Icons.warning_amber_sharp, - color: Colors.redAccent, size: 28), - SizedBox(width: 10), - Text(translate("Warning")), - ]), - content: Text(translate("Disconnect all devices?")), - actions: [ - TextButton( - onPressed: () => close(), child: Text(translate("Cancel"))), - ElevatedButton( - onPressed: () => close(true), child: Text(translate("OK"))), - ], - )); + final res = await gFFI.dialogManager.show((setState, close) { + submit() => close(true); + return CustomAlertDialog( + title: Row(children: [ + const Icon(Icons.warning_amber_sharp, + color: Colors.redAccent, size: 28), + const SizedBox(width: 10), + Text(translate("Warning")), + ]), + content: Text(translate("Disconnect all devices?")), + actions: [ + TextButton(onPressed: close, child: Text(translate("Cancel"))), + ElevatedButton(onPressed: submit, child: Text(translate("OK"))), + ], + onSubmit: submit, + onCancel: close, + ); + }); if (res == true) { callback(); } diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 74c2cd515..dedca5efa 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -559,49 +559,55 @@ class FileModel extends ChangeNotifier { Future showRemoveDialog( String title, String content, bool showCheckbox) async { return await parent.target?.dialogManager.show( - (setState, Function(bool v) close) => CustomAlertDialog( - title: Row( - children: [ - Icon(Icons.warning, color: Colors.red), - SizedBox(width: 20), - Text(title) - ], - ), - content: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text(content), - SizedBox(height: 5), - Text(translate("This is irreversible!"), - style: TextStyle(fontWeight: FontWeight.bold)), - showCheckbox - ? CheckboxListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - controlAffinity: ListTileControlAffinity.leading, - title: Text( - translate("Do this for all conflicts"), - ), - value: removeCheckboxRemember, - onChanged: (v) { - if (v == null) return; - setState(() => removeCheckboxRemember = v); - }, - ) - : SizedBox.shrink() - ]), - actions: [ - TextButton( - style: flatButtonStyle, - onPressed: () => close(false), - child: Text(translate("Cancel"))), - TextButton( - style: flatButtonStyle, - onPressed: () => close(true), - child: Text(translate("OK"))), - ]), - useAnimation: false); + (setState, Function(bool v) close) { + cancel() => close(false); + submit() => close(true); + return CustomAlertDialog( + title: Row( + children: [ + const Icon(Icons.warning, color: Colors.red), + const SizedBox(width: 20), + Text(title) + ], + ), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text(content), + const SizedBox(height: 5), + Text(translate("This is irreversible!"), + style: const TextStyle(fontWeight: FontWeight.bold)), + showCheckbox + ? CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Text( + translate("Do this for all conflicts"), + ), + value: removeCheckboxRemember, + onChanged: (v) { + if (v == null) return; + setState(() => removeCheckboxRemember = v); + }, + ) + : const SizedBox.shrink() + ]), + actions: [ + TextButton( + style: flatButtonStyle, + onPressed: cancel, + child: Text(translate("Cancel"))), + TextButton( + style: flatButtonStyle, + onPressed: submit, + child: Text(translate("OK"))), + ], + onSubmit: submit, + onCancel: cancel, + ); + }, useAnimation: false); } bool fileConfirmCheckboxRemember = false; @@ -610,55 +616,59 @@ class FileModel extends ChangeNotifier { String title, String content, bool showCheckbox) async { fileConfirmCheckboxRemember = false; return await parent.target?.dialogManager.show( - (setState, Function(bool? v) close) => CustomAlertDialog( - title: Row( - children: [ - Icon(Icons.warning, color: Colors.red), - SizedBox(width: 20), - Text(title) - ], - ), - content: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - translate( - "This file exists, skip or overwrite this file?"), - style: TextStyle(fontWeight: FontWeight.bold)), - SizedBox(height: 5), - Text(content), - showCheckbox - ? CheckboxListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - controlAffinity: ListTileControlAffinity.leading, - title: Text( - translate("Do this for all conflicts"), - ), - value: fileConfirmCheckboxRemember, - onChanged: (v) { - if (v == null) return; - setState(() => fileConfirmCheckboxRemember = v); - }, - ) - : SizedBox.shrink() - ]), - actions: [ - TextButton( - style: flatButtonStyle, - onPressed: () => close(false), - child: Text(translate("Cancel"))), - TextButton( - style: flatButtonStyle, - onPressed: () => close(null), - child: Text(translate("Skip"))), - TextButton( - style: flatButtonStyle, - onPressed: () => close(true), - child: Text(translate("OK"))), - ]), - useAnimation: false); + (setState, Function(bool? v) close) { + cancel() => close(false); + submit() => close(true); + return CustomAlertDialog( + title: Row( + children: [ + const Icon(Icons.warning, color: Colors.red), + const SizedBox(width: 20), + Text(title) + ], + ), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text(translate("This file exists, skip or overwrite this file?"), + style: const TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 5), + Text(content), + showCheckbox + ? CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Text( + translate("Do this for all conflicts"), + ), + value: fileConfirmCheckboxRemember, + onChanged: (v) { + if (v == null) return; + setState(() => fileConfirmCheckboxRemember = v); + }, + ) + : const SizedBox.shrink() + ]), + actions: [ + TextButton( + style: flatButtonStyle, + onPressed: cancel, + child: Text(translate("Cancel"))), + TextButton( + style: flatButtonStyle, + onPressed: () => close(null), + child: Text(translate("Skip"))), + TextButton( + style: flatButtonStyle, + onPressed: submit, + child: Text(translate("OK"))), + ], + onSubmit: submit, + onCancel: cancel, + ); + }, useAnimation: false); } sendRemoveFile(String path, int fileNum, bool isLocal) { diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 5d23dc949..f78f8cf70 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -209,46 +209,48 @@ class ServerModel with ChangeNotifier { /// Toggle the screen sharing service. toggleService() async { if (_isStart) { - final res = await parent.target?.dialogManager - .show((setState, close) => CustomAlertDialog( - title: Row(children: [ - Icon(Icons.warning_amber_sharp, - color: Colors.redAccent, size: 28), - SizedBox(width: 10), - Text(translate("Warning")), - ]), - content: Text(translate("android_stop_service_tip")), - actions: [ - TextButton( - onPressed: () => close(), - child: Text(translate("Cancel"))), - ElevatedButton( - onPressed: () => close(true), - child: Text(translate("OK"))), - ], - )); + final res = + await parent.target?.dialogManager.show((setState, close) { + submit() => close(true); + return CustomAlertDialog( + title: Row(children: [ + const Icon(Icons.warning_amber_sharp, + color: Colors.redAccent, size: 28), + const SizedBox(width: 10), + Text(translate("Warning")), + ]), + content: Text(translate("android_stop_service_tip")), + actions: [ + TextButton(onPressed: close, child: Text(translate("Cancel"))), + ElevatedButton(onPressed: submit, child: Text(translate("OK"))), + ], + onSubmit: submit, + onCancel: close, + ); + }); if (res == true) { stopService(); } } else { - final res = await parent.target?.dialogManager - .show((setState, close) => CustomAlertDialog( - title: Row(children: [ - Icon(Icons.warning_amber_sharp, - color: Colors.redAccent, size: 28), - SizedBox(width: 10), - Text(translate("Warning")), - ]), - content: Text(translate("android_service_will_start_tip")), - actions: [ - TextButton( - onPressed: () => close(), - child: Text(translate("Cancel"))), - ElevatedButton( - onPressed: () => close(true), - child: Text(translate("OK"))), - ], - )); + final res = + await parent.target?.dialogManager.show((setState, close) { + submit() => close(true); + return CustomAlertDialog( + title: Row(children: [ + const Icon(Icons.warning_amber_sharp, + color: Colors.redAccent, size: 28), + const SizedBox(width: 10), + Text(translate("Warning")), + ]), + content: Text(translate("android_service_will_start_tip")), + actions: [ + TextButton(onPressed: close, child: Text(translate("Cancel"))), + ElevatedButton(onPressed: submit, child: Text(translate("OK"))), + ], + onSubmit: submit, + onCancel: close, + ); + }); if (res == true) { startService(); } @@ -388,49 +390,49 @@ class ServerModel with ChangeNotifier { } void showLoginDialog(Client client) { - parent.target?.dialogManager.show( - (setState, close) => CustomAlertDialog( - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(translate(client.isFileTransfer - ? "File Connection" - : "Screen Connection")), - IconButton( - onPressed: () { - close(); - }, - icon: Icon(Icons.close)) - ]), - content: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(translate("Do you accept?")), - clientInfo(client), - Text( - translate("android_new_connection_tip"), - style: TextStyle(color: Colors.black54), - ), - ], - ), - actions: [ - TextButton( - child: Text(translate("Dismiss")), - onPressed: () { - sendLoginResponse(client, false); - close(); - }), - ElevatedButton( - child: Text(translate("Accept")), - onPressed: () { - sendLoginResponse(client, true); - close(); - }), - ], + parent.target?.dialogManager.show((setState, close) { + cancel() { + sendLoginResponse(client, false); + close(); + } + + submit() { + sendLoginResponse(client, true); + close(); + } + + return CustomAlertDialog( + title: + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Text(translate( + client.isFileTransfer ? "File Connection" : "Screen Connection")), + IconButton( + onPressed: () { + close(); + }, + icon: const Icon(Icons.close)) + ]), + content: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(translate("Do you accept?")), + clientInfo(client), + Text( + translate("android_new_connection_tip"), + style: const TextStyle(color: Colors.black54), ), - tag: getLoginDialogTag(client.id)); + ], + ), + actions: [ + TextButton(onPressed: cancel, child: Text(translate("Dismiss"))), + ElevatedButton(onPressed: submit, child: Text(translate("Accept"))), + ], + onSubmit: submit, + onCancel: cancel, + ); + }, tag: getLoginDialogTag(client.id)); } scrollToBottom() { @@ -563,24 +565,29 @@ String getLoginDialogTag(int id) { } showInputWarnAlert(FFI ffi) { - ffi.dialogManager.show((setState, close) => CustomAlertDialog( - title: Text(translate("How to get Android input permission?")), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(translate("android_input_permission_tip1")), - SizedBox(height: 10), - Text(translate("android_input_permission_tip2")), - ], - ), - actions: [ - TextButton(child: Text(translate("Cancel")), onPressed: close), - ElevatedButton( - child: Text(translate("Open System Setting")), - onPressed: () { - ffi.serverModel.initInput(); - close(); - }), + ffi.dialogManager.show((setState, close) { + submit() { + ffi.serverModel.initInput(); + close(); + } + + return CustomAlertDialog( + title: Text(translate("How to get Android input permission?")), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(translate("android_input_permission_tip1")), + const SizedBox(height: 10), + Text(translate("android_input_permission_tip2")), ], - )); + ), + actions: [ + TextButton(onPressed: close, child: Text(translate("Cancel"))), + ElevatedButton( + onPressed: submit, child: Text(translate("Open System Setting"))), + ], + onSubmit: submit, + onCancel: close, + ); + }); } From 62870e453ca0b876ca481dc67566d0d787d04e29 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 4 Sep 2022 11:03:16 +0800 Subject: [PATCH 09/19] add tabbar theme extension to fix theme update failure after overlay added Signed-off-by: 21pages --- flutter/lib/common.dart | 10 +- .../desktop/pages/connection_tab_page.dart | 6 +- .../lib/desktop/pages/desktop_tab_page.dart | 8 +- .../desktop/pages/file_manager_tab_page.dart | 7 +- .../desktop/pages/port_forward_tab_page.dart | 6 +- flutter/lib/desktop/pages/server_page.dart | 1 - .../lib/desktop/widgets/tabbar_widget.dart | 262 ++++++++++-------- 7 files changed, 162 insertions(+), 138 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 4a0a0dc82..309ae9892 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -155,7 +155,7 @@ class MyTheme { brightness: Brightness.light, primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, - tabBarTheme: TabBarTheme( + tabBarTheme: const TabBarTheme( labelColor: Colors.black87, ), splashColor: Colors.transparent, @@ -163,13 +163,14 @@ class MyTheme { ).copyWith( extensions: >[ ColorThemeExtension.light, + TabbarTheme.light, ], ); static ThemeData darkTheme = ThemeData( brightness: Brightness.dark, primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, - tabBarTheme: TabBarTheme( + tabBarTheme: const TabBarTheme( labelColor: Colors.white70, ), splashColor: Colors.transparent, @@ -177,12 +178,17 @@ class MyTheme { ).copyWith( extensions: >[ ColorThemeExtension.dark, + TabbarTheme.dark, ], ); static ColorThemeExtension color(BuildContext context) { return Theme.of(context).extension()!; } + + static TabbarTheme tabbar(BuildContext context) { + return Theme.of(context).extension()!; + } } bool isDarkTheme() { diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index d9bc86fe2..8f5350792 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -83,7 +83,6 @@ class _ConnectionTabPageState extends State { @override Widget build(BuildContext context) { - final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light(); final RxBool fullscreen = Get.find(tag: 'fullscreen'); return Obx(() => SubWindowDragToResizeArea( resizeEdgeSize: fullscreen.value ? 1.0 : 8.0, @@ -95,14 +94,11 @@ class _ConnectionTabPageState extends State { backgroundColor: MyTheme.color(context).bg, body: Obx(() => DesktopTab( controller: tabController, - theme: theme, showTabBar: fullscreen.isFalse, onClose: () { tabController.clear(); }, - tail: AddButton( - theme: theme, - ).paddingOnly(left: 10), + tail: AddButton().paddingOnly(left: 10), pageViewBuilder: (pageView) { WindowController.fromWindowId(windowId()) .setFullscreen(fullscreen.isTrue); diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 0546f0503..87082284b 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -48,17 +48,11 @@ class _DesktopTabPageState extends State { backgroundColor: MyTheme.color(context).bg, body: DesktopTab( controller: tabController, - theme: dark - ? const TarBarTheme.dark() - : const TarBarTheme.light(), tail: ActionIcon( message: 'Settings', icon: IconFont.menu, - theme: dark - ? const TarBarTheme.dark() - : const TarBarTheme.light(), onTap: onAddSetting, - is_close: false, + isClose: false, ), )); }) diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index add5eed9f..18ea039a7 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -62,8 +62,6 @@ class _FileManagerTabPageState extends State { @override Widget build(BuildContext context) { - final theme = - isDarkTheme() ? const TarBarTheme.dark() : const TarBarTheme.light(); return SubWindowDragToResizeArea( windowId: windowId(), child: Container( @@ -73,13 +71,10 @@ class _FileManagerTabPageState extends State { backgroundColor: MyTheme.color(context).bg, body: DesktopTab( controller: tabController, - theme: theme, onClose: () { tabController.clear(); }, - tail: AddButton( - theme: theme, - ).paddingOnly(left: 10), + tail: AddButton().paddingOnly(left: 10), )), ), ); diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index 2340a4ca1..e0384b614 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -69,7 +69,6 @@ class _PortForwardTabPageState extends State { @override Widget build(BuildContext context) { - final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light(); return SubWindowDragToResizeArea( windowId: windowId(), child: Container( @@ -79,13 +78,10 @@ class _PortForwardTabPageState extends State { backgroundColor: MyTheme.color(context).bg, body: DesktopTab( controller: tabController, - theme: theme, onClose: () { tabController.clear(); }, - tail: AddButton( - theme: theme, - ).paddingOnly(left: 10), + tail: AddButton().paddingOnly(left: 10), )), ), ); diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index f64adfca2..ac2fb7caa 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -118,7 +118,6 @@ class ConnectionManagerState extends State { ], ) : DesktopTab( - theme: isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light(), showTitle: false, showMaximize: false, showMinimize: true, diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 3c2b28ab0..5daa1aeb6 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'dart:math'; import 'package:desktop_multi_window/desktop_multi_window.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide TabBarTheme; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/main.dart'; @@ -158,8 +158,7 @@ class DesktopTabController { class TabThemeConf { double iconSize; - TarBarTheme theme; - TabThemeConf({required this.iconSize, required this.theme}); + TabThemeConf({required this.iconSize}); } typedef TabBuilder = Widget Function( @@ -168,7 +167,6 @@ typedef LabelGetter = Rx Function(String key); class DesktopTab extends StatelessWidget { final Function(String)? onTabClose; - final TarBarTheme theme; final bool showTabBar; final bool showLogo; final bool showTitle; @@ -189,7 +187,6 @@ class DesktopTab extends StatelessWidget { DesktopTab({ Key? key, required this.controller, - this.theme = const TarBarTheme.light(), this.onTabClose, this.showTabBar = true, this.showLogo = true, @@ -213,15 +210,15 @@ class DesktopTab extends StatelessWidget { return Column(children: [ Offstage( offstage: !showTabBar, - child: Container( + child: SizedBox( height: _kTabBarHeight, child: Column( children: [ - Container( + SizedBox( height: _kTabBarHeight - 1, child: _buildBar(), ), - Divider( + const Divider( height: 1, thickness: 1, ), @@ -300,7 +297,7 @@ class DesktopTab extends StatelessWidget { )), Offstage( offstage: !showTitle, - child: Text( + child: const Text( "RustDesk", style: TextStyle(fontSize: 13), ).marginOnly(left: 2)) @@ -321,7 +318,6 @@ class DesktopTab extends StatelessWidget { child: _ListView( controller: controller, onTabClose: onTabClose, - theme: theme, tabBuilder: tabBuilder, labelGetter: labelGetter, )), @@ -334,7 +330,6 @@ class DesktopTab extends StatelessWidget { mainTab: isMainWindow, tabType: tabType, state: state, - theme: theme, showMinimize: showMinimize, showMaximize: showMaximize, showClose: showClose, @@ -349,7 +344,6 @@ class WindowActionPanel extends StatelessWidget { final bool mainTab; final DesktopTabType tabType; final Rx state; - final TarBarTheme theme; final bool showMinimize; final bool showMaximize; @@ -361,7 +355,6 @@ class WindowActionPanel extends StatelessWidget { required this.mainTab, required this.tabType, required this.state, - required this.theme, this.showMinimize = true, this.showMaximize = true, this.showClose = true, @@ -377,7 +370,6 @@ class WindowActionPanel extends StatelessWidget { child: ActionIcon( message: 'Minimize', icon: IconFont.min, - theme: theme, onTap: () { if (mainTab) { windowManager.minimize(); @@ -385,31 +377,30 @@ class WindowActionPanel extends StatelessWidget { WindowController.fromWindowId(windowId!).minimize(); } }, - is_close: false, + isClose: false, )), // TODO: drag makes window restore Offstage( offstage: !showMaximize, child: FutureBuilder(builder: (context, snapshot) { - RxBool is_maximized = false.obs; + RxBool isMaximized = false.obs; if (mainTab) { windowManager.isMaximized().then((maximized) { - is_maximized.value = maximized; + isMaximized.value = maximized; }); } else { final wc = WindowController.fromWindowId(windowId!); wc.isMaximized().then((maximized) { - is_maximized.value = maximized; + isMaximized.value = maximized; }); } return Obx( () => ActionIcon( - message: is_maximized.value ? "Restore" : "Maximize", - icon: is_maximized.value ? IconFont.restore : IconFont.max, - theme: theme, + message: isMaximized.value ? "Restore" : "Maximize", + icon: isMaximized.value ? IconFont.restore : IconFont.max, onTap: () { if (mainTab) { - if (is_maximized.value) { + if (isMaximized.value) { windowManager.unmaximize(); } else { windowManager.maximize(); @@ -417,15 +408,15 @@ class WindowActionPanel extends StatelessWidget { } else { // TODO: subwindow is maximized but first query result is not maximized. final wc = WindowController.fromWindowId(windowId!); - if (is_maximized.value) { + if (isMaximized.value) { wc.unmaximize(); } else { wc.maximize(); } } - is_maximized.value = !is_maximized.value; + isMaximized.value = !isMaximized.value; }, - is_close: false, + isClose: false, ), ); })), @@ -434,7 +425,6 @@ class WindowActionPanel extends StatelessWidget { child: ActionIcon( message: 'Close', icon: IconFont.close, - theme: theme, onTap: () async { action() { if (mainTab) { @@ -455,7 +445,7 @@ class WindowActionPanel extends StatelessWidget { action(); } }, - is_close: true, + isClose: true, )), ], ); @@ -490,17 +480,15 @@ class WindowActionPanel extends StatelessWidget { class _ListView extends StatelessWidget { final DesktopTabController controller; final Function(String key)? onTabClose; - final TarBarTheme theme; final TabBuilder? tabBuilder; final LabelGetter? labelGetter; Rx get state => controller.state; - _ListView( + const _ListView( {required this.controller, required this.onTabClose, - required this.theme, this.tabBuilder, this.labelGetter}); @@ -510,7 +498,7 @@ class _ListView extends StatelessWidget { controller: state.value.scrollController, scrollDirection: Axis.horizontal, shrinkWrap: true, - physics: BouncingScrollPhysics(), + physics: const BouncingScrollPhysics(), children: state.value.tabs.asMap().entries.map((e) { final index = e.key; final tab = e.value; @@ -525,7 +513,6 @@ class _ListView extends StatelessWidget { selected: state.value.selected, onClose: () => controller.remove(index), onSelected: () => controller.jumpTo(index), - theme: theme, tabBuilder: tabBuilder == null ? null : (Widget icon, Widget labelWidget, TabThemeConf themeConf) { @@ -542,31 +529,29 @@ class _ListView extends StatelessWidget { } class _Tab extends StatefulWidget { - late final int index; - late final Rx label; - late final IconData? selectedIcon; - late final IconData? unselectedIcon; - late final bool closable; - late final int selected; - late final Function() onClose; - late final Function() onSelected; - late final TarBarTheme theme; + final int index; + final Rx label; + final IconData? selectedIcon; + final IconData? unselectedIcon; + final bool closable; + final int selected; + final Function() onClose; + final Function() onSelected; final Widget Function(Widget icon, Widget label, TabThemeConf themeConf)? tabBuilder; - _Tab( - {Key? key, - required this.index, - required this.label, - this.selectedIcon, - this.unselectedIcon, - this.tabBuilder, - required this.closable, - required this.selected, - required this.onClose, - required this.onSelected, - required this.theme}) - : super(key: key); + const _Tab({ + Key? key, + required this.index, + required this.label, + this.selectedIcon, + this.unselectedIcon, + this.tabBuilder, + required this.closable, + required this.selected, + required this.onClose, + required this.onSelected, + }) : super(key: key); @override State<_Tab> createState() => _TabState(); @@ -586,8 +571,8 @@ class _TabState extends State<_Tab> with RestorationMixin { isSelected ? widget.selectedIcon : widget.unselectedIcon, size: _kIconSize, color: isSelected - ? widget.theme.selectedtabIconColor - : widget.theme.unSelectedtabIconColor, + ? MyTheme.tabbar(context).selectedTabIconColor + : MyTheme.tabbar(context).unSelectedTabIconColor, ).paddingOnly(right: 5)); final labelWidget = Obx(() { return Text( @@ -595,8 +580,8 @@ class _TabState extends State<_Tab> with RestorationMixin { textAlign: TextAlign.center, style: TextStyle( color: isSelected - ? widget.theme.selectedTextColor - : widget.theme.unSelectedTextColor), + ? MyTheme.tabbar(context).selectedTextColor + : MyTheme.tabbar(context).unSelectedTextColor), ); }); @@ -609,8 +594,8 @@ class _TabState extends State<_Tab> with RestorationMixin { ], ); } else { - return widget.tabBuilder!(icon, labelWidget, - TabThemeConf(iconSize: _kIconSize, theme: widget.theme)); + return widget.tabBuilder!( + icon, labelWidget, TabThemeConf(iconSize: _kIconSize)); } } @@ -639,7 +624,6 @@ class _TabState extends State<_Tab> with RestorationMixin { visiable: hover.value && widget.closable, tabSelected: isSelected, onClose: () => widget.onClose(), - theme: widget.theme, ))) ])).paddingSymmetric(horizontal: 10), Offstage( @@ -648,7 +632,7 @@ class _TabState extends State<_Tab> with RestorationMixin { width: 1, indent: _kDividerIndent, endIndent: _kDividerIndent, - color: widget.theme.dividerColor, + color: MyTheme.tabbar(context).dividerColor, thickness: 1, ), ) @@ -671,14 +655,12 @@ class _CloseButton extends StatelessWidget { final bool visiable; final bool tabSelected; final Function onClose; - late final TarBarTheme theme; - _CloseButton({ + const _CloseButton({ Key? key, required this.visiable, required this.tabSelected, required this.onClose, - required this.theme, }) : super(key: key); @override @@ -694,8 +676,8 @@ class _CloseButton extends StatelessWidget { Icons.close, size: _kIconSize, color: tabSelected - ? theme.selectedIconColor - : theme.unSelectedIconColor, + ? MyTheme.tabbar(context).selectedIconColor + : MyTheme.tabbar(context).unSelectedIconColor, ), ), )).paddingOnly(left: 5); @@ -705,16 +687,14 @@ class _CloseButton extends StatelessWidget { class ActionIcon extends StatelessWidget { final String message; final IconData icon; - final TarBarTheme theme; final Function() onTap; - final bool is_close; + final bool isClose; const ActionIcon({ Key? key, required this.message, required this.icon, - required this.theme, required this.onTap, - required this.is_close, + required this.isClose, }) : super(key: key); @override @@ -722,34 +702,32 @@ class ActionIcon extends StatelessWidget { RxBool hover = false.obs; return Obx(() => Tooltip( message: translate(message), - waitDuration: Duration(seconds: 1), + waitDuration: const Duration(seconds: 1), child: InkWell( - hoverColor: - is_close ? Color.fromARGB(255, 196, 43, 28) : theme.hoverColor, + hoverColor: isClose + ? const Color.fromARGB(255, 196, 43, 28) + : MyTheme.tabbar(context).hoverColor, onHover: (value) => hover.value = value, - child: Container( + onTap: onTap, + child: SizedBox( height: _kTabBarHeight - 1, width: _kTabBarHeight - 1, child: Icon( icon, - color: hover.value && is_close + color: hover.value && isClose ? Colors.white - : theme.unSelectedIconColor, + : MyTheme.tabbar(context).unSelectedIconColor, size: _kActionIconSize, ), ), - onTap: onTap, ), )); } } class AddButton extends StatelessWidget { - late final TarBarTheme theme; - - AddButton({ + const AddButton({ Key? key, - required this.theme, }) : super(key: key); @override @@ -757,41 +735,101 @@ class AddButton extends StatelessWidget { return ActionIcon( message: 'New Connection', icon: IconFont.add, - theme: theme, onTap: () => rustDeskWinManager.call(WindowType.Main, "main_window_on_top", ""), - is_close: false); + isClose: false); } } -class TarBarTheme { - final Color unSelectedtabIconColor; - final Color selectedtabIconColor; - final Color selectedTextColor; - final Color unSelectedTextColor; - final Color selectedIconColor; - final Color unSelectedIconColor; - final Color dividerColor; - final Color hoverColor; +class TabbarTheme extends ThemeExtension { + final Color? selectedTabIconColor; + final Color? unSelectedTabIconColor; + final Color? selectedTextColor; + final Color? unSelectedTextColor; + final Color? selectedIconColor; + final Color? unSelectedIconColor; + final Color? dividerColor; + final Color? hoverColor; - const TarBarTheme.light() - : unSelectedtabIconColor = const Color.fromARGB(255, 162, 203, 241), - selectedtabIconColor = MyTheme.accent, - selectedTextColor = const Color.fromARGB(255, 26, 26, 26), - unSelectedTextColor = const Color.fromARGB(255, 96, 96, 96), - selectedIconColor = const Color.fromARGB(255, 26, 26, 26), - unSelectedIconColor = const Color.fromARGB(255, 96, 96, 96), - dividerColor = const Color.fromARGB(255, 238, 238, 238), - hoverColor = const Color.fromARGB( - 51, 158, 158, 158); // Colors.grey; //0xFF9E9E9E + const TabbarTheme( + {required this.selectedTabIconColor, + required this.unSelectedTabIconColor, + required this.selectedTextColor, + required this.unSelectedTextColor, + required this.selectedIconColor, + required this.unSelectedIconColor, + required this.dividerColor, + required this.hoverColor}); - const TarBarTheme.dark() - : unSelectedtabIconColor = const Color.fromARGB(255, 30, 65, 98), - selectedtabIconColor = MyTheme.accent, - selectedTextColor = const Color.fromARGB(255, 255, 255, 255), - unSelectedTextColor = const Color.fromARGB(255, 207, 207, 207), - selectedIconColor = const Color.fromARGB(255, 215, 215, 215), - unSelectedIconColor = const Color.fromARGB(255, 255, 255, 255), - dividerColor = const Color.fromARGB(255, 64, 64, 64), - hoverColor = Colors.black26; + static const light = TabbarTheme( + selectedTabIconColor: MyTheme.accent, + unSelectedTabIconColor: Color.fromARGB(255, 162, 203, 241), + selectedTextColor: Color.fromARGB(255, 26, 26, 26), + unSelectedTextColor: Color.fromARGB(255, 96, 96, 96), + selectedIconColor: Color.fromARGB(255, 26, 26, 26), + unSelectedIconColor: Color.fromARGB(255, 96, 96, 96), + dividerColor: Color.fromARGB(255, 238, 238, 238), + hoverColor: Color.fromARGB(51, 158, 158, 158)); + + static const dark = TabbarTheme( + selectedTabIconColor: MyTheme.accent, + unSelectedTabIconColor: Color.fromARGB(255, 30, 65, 98), + selectedTextColor: Color.fromARGB(255, 255, 255, 255), + unSelectedTextColor: Color.fromARGB(255, 207, 207, 207), + selectedIconColor: Color.fromARGB(255, 215, 215, 215), + unSelectedIconColor: Color.fromARGB(255, 255, 255, 255), + dividerColor: Color.fromARGB(255, 64, 64, 64), + hoverColor: Colors.black26); + + @override + ThemeExtension copyWith({ + Color? selectedTabIconColor, + Color? unSelectedTabIconColor, + Color? selectedTextColor, + Color? unSelectedTextColor, + Color? selectedIconColor, + Color? unSelectedIconColor, + Color? dividerColor, + Color? hoverColor, + }) { + return TabbarTheme( + selectedTabIconColor: selectedTabIconColor ?? this.selectedTabIconColor, + unSelectedTabIconColor: + unSelectedTabIconColor ?? this.unSelectedTabIconColor, + selectedTextColor: selectedTextColor ?? this.selectedTextColor, + unSelectedTextColor: unSelectedTextColor ?? this.unSelectedTextColor, + selectedIconColor: selectedIconColor ?? this.selectedIconColor, + unSelectedIconColor: unSelectedIconColor ?? this.unSelectedIconColor, + dividerColor: dividerColor ?? this.dividerColor, + hoverColor: hoverColor ?? this.hoverColor, + ); + } + + @override + ThemeExtension lerp( + ThemeExtension? other, double t) { + if (other is! TabbarTheme) { + return this; + } + return TabbarTheme( + selectedTabIconColor: + Color.lerp(selectedTabIconColor, other.selectedTabIconColor, t), + unSelectedTabIconColor: + Color.lerp(unSelectedTabIconColor, other.unSelectedTabIconColor, t), + selectedTextColor: + Color.lerp(selectedTextColor, other.selectedTextColor, t), + unSelectedTextColor: + Color.lerp(unSelectedTextColor, other.unSelectedTextColor, t), + selectedIconColor: + Color.lerp(selectedIconColor, other.selectedIconColor, t), + unSelectedIconColor: + Color.lerp(unSelectedIconColor, other.unSelectedIconColor, t), + dividerColor: Color.lerp(dividerColor, other.dividerColor, t), + hoverColor: Color.lerp(hoverColor, other.hoverColor, t), + ); + } + + static color(BuildContext context) { + return Theme.of(context).extension()!; + } } From 760ab519198e28cb8e50eef301f092f151bf998e Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 4 Sep 2022 16:26:08 +0800 Subject: [PATCH 10/19] dark theme adjustment Signed-off-by: 21pages --- flutter/lib/desktop/widgets/popup_menu.dart | 25 +++++++++++---------- flutter/lib/mobile/pages/chat_page.dart | 11 +++++++-- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/flutter/lib/desktop/widgets/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart index 45e52cf81..ea678673a 100644 --- a/flutter/lib/desktop/widgets/popup_menu.dart +++ b/flutter/lib/desktop/widgets/popup_menu.dart @@ -1,6 +1,7 @@ import 'dart:core'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; import 'package:get/get.dart'; import './material_mod_popup_menu.dart' as mod_menu; @@ -174,8 +175,8 @@ class MenuEntryRadios extends MenuEntryBase { children: [ Text( opt.text, - style: const TextStyle( - color: Colors.black, + style: TextStyle( + color: MyTheme.color(context).text, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal), ), @@ -256,8 +257,8 @@ class MenuEntrySubRadios extends MenuEntryBase { children: [ Text( opt.text, - style: const TextStyle( - color: Colors.black, + style: TextStyle( + color: MyTheme.color(context).text, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal), ), @@ -300,8 +301,8 @@ class MenuEntrySubRadios extends MenuEntryBase { const SizedBox(width: MenuConfig.midPadding), Text( text, - style: const TextStyle( - color: Colors.black, + style: TextStyle( + color: MyTheme.color(context).text, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal), ), @@ -346,8 +347,8 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { // const SizedBox(width: MenuConfig.midPadding), Text( text, - style: const TextStyle( - color: Colors.black, + style: TextStyle( + color: MyTheme.color(context).text, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal), ), @@ -450,8 +451,8 @@ class MenuEntrySubMenu extends MenuEntryBase { const SizedBox(width: MenuConfig.midPadding), Text( text, - style: const TextStyle( - color: Colors.black, + style: TextStyle( + color: MyTheme.color(context).text, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal), ), @@ -491,8 +492,8 @@ class MenuEntryButton extends MenuEntryBase { alignment: AlignmentDirectional.centerStart, constraints: BoxConstraints(minHeight: conf.height), child: childBuilder( - const TextStyle( - color: Colors.black, + TextStyle( + color: MyTheme.color(context).text, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal), )), diff --git a/flutter/lib/mobile/pages/chat_page.dart b/flutter/lib/mobile/pages/chat_page.dart index b265f6995..2151f17be 100644 --- a/flutter/lib/mobile/pages/chat_page.dart +++ b/flutter/lib/mobile/pages/chat_page.dart @@ -45,7 +45,7 @@ class ChatPage extends StatelessWidget implements PageShape { return ChangeNotifierProvider.value( value: chatModel, child: Container( - color: MyTheme.grayBg, + color: MyTheme.color(context).grayBg, child: Consumer(builder: (context, chatModel, child) { final currentUser = chatModel.currentUser; return Stack( @@ -59,7 +59,14 @@ class ChatPage extends StatelessWidget implements PageShape { messages: chatModel .messages[chatModel.currentID]?.chatMessages ?? [], - inputOptions: const InputOptions(sendOnEnter: true), + inputOptions: InputOptions( + sendOnEnter: true, + inputDecoration: defaultInputDecoration( + fillColor: MyTheme.color(context).bg), + sendButtonBuilder: defaultSendButton( + color: MyTheme.color(context).text!), + inputTextStyle: + TextStyle(color: MyTheme.color(context).text)), messageOptions: MessageOptions( showOtherUsersAvatar: false, showTime: true, From f47254c5e2fb4c2244f9329355dafca61469f54a Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 4 Sep 2022 20:57:57 +0800 Subject: [PATCH 11/19] adjust geometry Signed-off-by: 21pages --- .../lib/desktop/pages/desktop_home_page.dart | 24 +++++++++---------- .../desktop/pages/desktop_setting_page.dart | 4 ++-- .../lib/desktop/widgets/peercard_widget.dart | 2 +- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 0ccb86d1f..3ce956c23 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -93,8 +93,8 @@ class _DesktopHomePageState extends State buildIDBoard(BuildContext context) { final model = gFFI.serverModel; return Container( - margin: const EdgeInsets.only(left: 20, right: 16), - height: 52, + margin: const EdgeInsets.only(left: 20, right: 11), + height: 57, child: Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, @@ -102,10 +102,10 @@ class _DesktopHomePageState extends State Container( width: 2, decoration: const BoxDecoration(color: MyTheme.accent), - ), + ).marginOnly(top: 5), Expanded( child: Padding( - padding: const EdgeInsets.only(left: 8.0), + padding: const EdgeInsets.only(left: 7), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -120,7 +120,7 @@ class _DesktopHomePageState extends State style: TextStyle( fontSize: 14, color: MyTheme.color(context).lightText), - ), + ).marginOnly(top: 5), buildPopupMenu(context) ], ), @@ -137,7 +137,7 @@ class _DesktopHomePageState extends State readOnly: true, decoration: const InputDecoration( border: InputBorder.none, - contentPadding: EdgeInsets.only(bottom: 18), + contentPadding: EdgeInsets.only(bottom: 20), ), style: const TextStyle( fontSize: 22, @@ -244,7 +244,7 @@ class _DesktopHomePageState extends State }, child: Obx( () => CircleAvatar( - radius: 12, + radius: 15, backgroundColor: hover.value ? MyTheme.color(context).grayBg! : MyTheme.color(context).bg!, @@ -277,7 +277,7 @@ class _DesktopHomePageState extends State ), Expanded( child: Padding( - padding: const EdgeInsets.only(left: 8.0), + padding: const EdgeInsets.only(left: 7), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -303,7 +303,7 @@ class _DesktopHomePageState extends State readOnly: true, decoration: InputDecoration( border: InputBorder.none, - contentPadding: EdgeInsets.only(bottom: 8), + contentPadding: EdgeInsets.only(bottom: 2), ), style: TextStyle(fontSize: 15), ), @@ -317,7 +317,7 @@ class _DesktopHomePageState extends State ? MyTheme.color(context).text : Color(0xFFDDDDDD), size: 22, - ).marginOnly(right: 10, bottom: 8), + ).marginOnly(right: 8, bottom: 2), ), onTap: () => bind.mainUpdateTemporaryPassword(), onHover: (value) => refreshHover.value = value, @@ -425,13 +425,13 @@ class _DesktopHomePageState extends State color: editHover.value ? MyTheme.color(context).text : Color(0xFFDDDDDD)) - .marginOnly(bottom: 8))); + .marginOnly(bottom: 2))); } buildTip(BuildContext context) { return Padding( padding: - const EdgeInsets.only(left: 20.0, right: 16, top: 16.0, bottom: 14), + const EdgeInsets.only(left: 20.0, right: 16, top: 16.0, bottom: 5), child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 867c8a54c..b4bb00ab8 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -49,7 +49,7 @@ class _DesktopSettingPageState extends State 'Display', Icons.desktop_windows_outlined, Icons.desktop_windows_sharp), _TabInfo('Audio', Icons.volume_up_outlined, Icons.volume_up_sharp), _TabInfo('Connection', Icons.link_outlined, Icons.link_sharp), - _TabInfo('About RustDesk', Icons.info_outline, Icons.info_sharp) + _TabInfo('About', Icons.info_outline, Icons.info_sharp) ]; late PageController controller; @@ -714,7 +714,7 @@ class _AboutState extends State<_About> { ], ).marginOnly(left: _kContentHMargin) ]), - ]).marginOnly(left: _kCardLeftMargin); + ]); }); } } diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index 1bff02508..13ab92ffe 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -48,7 +48,7 @@ class _PeerCard extends StatefulWidget { class _PeerCardState extends State<_PeerCard> with AutomaticKeepAliveClientMixin { var _menuPos = RelativeRect.fill; - final double _cardRadis = 20; + final double _cardRadis = 16; final double _borderWidth = 2; final RxBool _iconMoreHover = false.obs; From 09b769d92f927679b119d59b3aea2d0636a1f53b Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 5 Sep 2022 10:27:33 +0800 Subject: [PATCH 12/19] WIP file transfer --- src/client/io_loop.rs | 89 +++++++++--------------- src/common.rs | 46 ------------ src/flutter.rs | 135 +++++++++++++++++++++++------------- src/flutter_ffi.rs | 24 ++++--- src/ui/file_transfer.tis | 2 +- src/ui/remote.rs | 46 +++++++++--- src/ui_session_interface.rs | 45 +++++++----- 7 files changed, 196 insertions(+), 191 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index e61690c32..0913251dd 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -2,6 +2,7 @@ use crate::client::{ Client, CodecFormat, FileManager, MediaData, MediaSender, QualityStatus, MILLI1, SEC30, SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED, }; +use crate::common; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::common::{check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}; @@ -21,7 +22,7 @@ use hbb_common::tokio::{ sync::mpsc, time::{self, Duration, Instant, Interval}, }; -use hbb_common::{allow_err, message_proto::*}; +use hbb_common::{allow_err, message_proto::*, sleep}; use hbb_common::{fs, log, Stream}; use std::collections::HashMap; @@ -270,7 +271,6 @@ impl Remote { // TODO fn load_last_jobs(&mut self) { - log::info!("start load last jobs"); self.handler.clear_all_jobs(); let pc = self.handler.load_config(); if pc.transfer.write_jobs.is_empty() && pc.transfer.read_jobs.is_empty() { @@ -280,33 +280,17 @@ impl Remote { // TODO: can add a confirm dialog let mut cnt = 1; for job_str in pc.transfer.read_jobs.iter() { - let job: Result = serde_json::from_str(&job_str); - if let Ok(job) = job { - self.handler.add_job( - cnt, - job.to.clone(), - job.remote.clone(), - job.file_num, - job.show_hidden, - false, - ); + if !job_str.is_empty() { + self.handler.load_last_job(cnt, job_str); cnt += 1; - println!("restore read_job: {:?}", job); + log::info!("restore read_job: {:?}", job_str); } } for job_str in pc.transfer.write_jobs.iter() { - let job: Result = serde_json::from_str(&job_str); - if let Ok(job) = job { - self.handler.add_job( - cnt, - job.remote.clone(), - job.to.clone(), - job.file_num, - job.show_hidden, - true, - ); + if !job_str.is_empty() { + self.handler.load_last_job(cnt, job_str); cnt += 1; - println!("restore write_job: {:?}", job); + log::info!("restore write_job: {:?}", job_str); } } self.handler.update_transfer_list(); @@ -373,8 +357,7 @@ impl Remote { to, job.files().len() ); - // let m = make_fd(job.id(), job.files(), true); - // self.handler.call("updateFolderFiles", &make_args!(m)); // TODO + self.handler.update_folder_files(job.id(), job.files(), path,!is_remote, true); #[cfg(not(windows))] let files = job.files().clone(); #[cfg(windows)] @@ -433,8 +416,7 @@ impl Remote { to, job.files().len() ); - // let m = make_fd(job.id(), job.files(), true); - // self.handler.call("updateFolderFiles", &make_args!(m)); + self.handler.update_folder_files(job.id(), job.files(), path,!is_remote, true); job.is_last_job = true; self.read_jobs.push(job); self.timer = time::interval(MILLI1); @@ -546,8 +528,7 @@ impl Remote { } else { match fs::get_recursive_files(&path, include_hidden) { Ok(entries) => { - // let m = make_fd(id, &entries, true); - // self.handler.call("updateFolderFiles", &make_args!(m)); + self.handler.update_folder_files(id, &entries, path.clone(),!is_remote, false); self.remove_jobs .insert(id, RemoveJob::new(entries, path, sep, is_remote)); } @@ -749,28 +730,28 @@ impl Remote { } Some(login_response::Union::PeerInfo(pi)) => { self.handler.handle_peer_info(pi); - // self.check_clipboard_file_context(); - // if !(self.handler.is_file_transfer() - // || self.handler.is_port_forward() - // || !SERVER_CLIPBOARD_ENABLED.load(Ordering::SeqCst) - // || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) - // || self.handler.lc.read().unwrap().disable_clipboard) - // { - // let txt = self.old_clipboard.lock().unwrap().clone(); - // if !txt.is_empty() { - // let msg_out = crate::create_clipboard_msg(txt); - // let sender = self.sender.clone(); - // tokio::spawn(async move { - // // due to clipboard service interval time - // sleep(common::CLIPBOARD_INTERVAL as f32 / 1_000.).await; - // sender.send(Data::Message(msg_out)).ok(); - // }); - // } - // } + self.check_clipboard_file_context(); + if !(self.handler.is_file_transfer() + || self.handler.is_port_forward() + || !SERVER_CLIPBOARD_ENABLED.load(Ordering::SeqCst) + || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) + || self.handler.lc.read().unwrap().disable_clipboard) + { + let txt = self.old_clipboard.lock().unwrap().clone(); + if !txt.is_empty() { + let msg_out = crate::create_clipboard_msg(txt); + let sender = self.sender.clone(); + tokio::spawn(async move { + // due to clipboard service interval time + sleep(common::CLIPBOARD_INTERVAL as f32 / 1_000.).await; + sender.send(Data::Message(msg_out)).ok(); + }); + } + } - // if self.handler.is_file_transfer() { - // self.load_last_jobs().await; - // } + if self.handler.is_file_transfer() { + self.load_last_jobs(); + } } _ => {} }, @@ -823,11 +804,7 @@ impl Remote { fs::transform_windows_path(&mut entries); } } - // let mut m = make_fd(fd.id, &entries, fd.id > 0); - // if fd.id <= 0 { - // m.set_item("path", fd.path); - // } - // self.handler.call("updateFolderFiles", &make_args!(m)); + self.handler.update_folder_files(fd.id, &entries, fd.path, false, fd.id > 0); if let Some(job) = fs::get_job(fd.id, &mut self.write_jobs) { log::info!("job set_files: {:?}", entries); job.set_files(entries); diff --git a/src/common.rs b/src/common.rs index 5c387c07e..471d6d4e2 100644 --- a/src/common.rs +++ b/src/common.rs @@ -666,49 +666,3 @@ pub fn make_privacy_mode_msg(state: back_notification::PrivacyModeState) -> Mess msg_out.set_misc(misc); msg_out } - -pub fn make_fd_to_json(fd: FileDirectory) -> String { - use serde_json::json; - let mut fd_json = serde_json::Map::new(); - fd_json.insert("id".into(), json!(fd.id)); - fd_json.insert("path".into(), json!(fd.path)); - - let mut entries = vec![]; - for entry in fd.entries { - let mut entry_map = serde_json::Map::new(); - entry_map.insert("entry_type".into(), json!(entry.entry_type.value())); - entry_map.insert("name".into(), json!(entry.name)); - entry_map.insert("size".into(), json!(entry.size)); - entry_map.insert("modified_time".into(), json!(entry.modified_time)); - entries.push(entry_map); - } - fd_json.insert("entries".into(), json!(entries)); - serde_json::to_string(&fd_json).unwrap_or("".into()) -} - -pub fn make_fd_flutter(id: i32, entries: &Vec, only_count: bool) -> String { - let mut m = serde_json::Map::new(); - m.insert("id".into(), json!(id)); - let mut a = vec![]; - let mut n: u64 = 0; - for entry in entries { - n += entry.size; - if only_count { - continue; - } - let mut e = serde_json::Map::new(); - e.insert("name".into(), json!(entry.name.to_owned())); - let tmp = entry.entry_type.value(); - e.insert("type".into(), json!(if tmp == 0 { 1 } else { tmp })); - e.insert("time".into(), json!(entry.modified_time as f64)); - e.insert("size".into(), json!(entry.size as f64)); - a.push(e); - } - if only_count { - m.insert("num_entries".into(), json!(entries.len() as i32)); - } else { - m.insert("entries".into(), json!(a)); - } - m.insert("total_size".into(), json!(n as f64)); - serde_json::to_string(&m).unwrap_or("".into()) -} diff --git a/src/flutter.rs b/src/flutter.rs index b84e91ce8..a2f03d2ff 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -5,7 +5,10 @@ use std::{ use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; -use hbb_common::{bail, config::LocalConfig, message_proto::*, ResultType, rendezvous_proto::ConnType}; +use hbb_common::{ + bail, config::LocalConfig, message_proto::*, rendezvous_proto::ConnType, ResultType, +}; +use serde_json::json; use crate::ui_session_interface::{io_loop, InvokeUi, Session}; @@ -85,6 +88,7 @@ impl InvokeUi for FlutterHandler { self.push_event("permission", vec![(name, &value.to_string())]); } + // unused in flutter fn close_success(&self) {} fn update_quality_status(&self, status: QualityStatus) { @@ -118,7 +122,14 @@ impl InvokeUi for FlutterHandler { } fn job_error(&self, id: i32, err: String, file_num: i32) { - self.push_event("job_error", vec![("id", &id.to_string()), ("err", &err)]); + self.push_event( + "job_error", + vec![ + ("id", &id.to_string()), + ("err", &err), + ("file_num", &file_num.to_string()), + ], + ); } fn job_done(&self, id: i32, file_num: i32) { @@ -128,29 +139,43 @@ impl InvokeUi for FlutterHandler { ); } - fn clear_all_jobs(&self) { - // todo!() + // unused in flutter + fn clear_all_jobs(&self) {} + + fn load_last_job(&self, _cnt: i32, job_json: &str) { + self.push_event("load_last_job", vec![("value", job_json)]); } - fn add_job( + fn update_folder_files( &self, id: i32, + entries: &Vec, path: String, - to: String, - file_num: i32, - show_hidden: bool, - is_remote: bool, + is_local: bool, + only_count: bool, ) { - // todo!() + // TODO opt + if only_count { + self.push_event( + "update_folder_files", + vec![("info", &make_fd_flutter(id, entries, only_count))], + ); + } else { + self.push_event( + "file_dir", + vec![ + ("value", &make_fd_to_json(id, path, entries)), + ("is_local", "false"), + ], + ); + } } - fn update_transfer_list(&self) { - // todo!() - } + // unused in flutter + fn update_transfer_list(&self) {} - fn confirm_delete_files(&self, id: i32, i: i32, name: String) { - // todo!() - } + // unused in flutter // TEST flutter + fn confirm_delete_files(&self, _id: i32, _i: i32, _name: String) {} fn override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: bool) { self.push_event( @@ -176,6 +201,7 @@ impl InvokeUi for FlutterHandler { ); } + // unused in flutter fn adapt_size(&self) {} fn on_rgba(&self, data: &[u8]) { @@ -283,11 +309,7 @@ pub fn session_add(id: &str, is_file_transfer: bool, is_port_forward: bool) -> R .unwrap() .initialize(session_id, conn_type); - if let Some(same_id_session) = SESSIONS - .write() - .unwrap() - .insert(id.to_owned(), session) - { + if let Some(same_id_session) = SESSIONS.write().unwrap().insert(id.to_owned(), session) { same_id_session.close(); } @@ -946,30 +968,47 @@ pub fn get_session_id(id: String) -> String { }; } -// async fn start_one_port_forward( -// handler: Session, -// port: i32, -// remote_host: String, -// remote_port: i32, -// receiver: mpsc::UnboundedReceiver, -// key: &str, -// token: &str, -// ) { -// if let Err(err) = crate::port_forward::listen( -// handler.id.clone(), -// String::new(), // TODO -// port, -// handler.clone(), -// receiver, -// key, -// token, -// handler.lc.clone(), -// remote_host, -// remote_port, -// ) -// .await -// { -// handler.on_error(&format!("Failed to listen on {}: {}", port, err)); -// } -// log::info!("port forward (:{}) exit", port); -// } +pub fn make_fd_to_json(id: i32, path: String, entries: &Vec) -> String { + let mut fd_json = serde_json::Map::new(); + fd_json.insert("id".into(), json!(id)); + fd_json.insert("path".into(), json!(path)); + + let mut entries_out = vec![]; + for entry in entries { + let mut entry_map = serde_json::Map::new(); + entry_map.insert("entry_type".into(), json!(entry.entry_type.value())); + entry_map.insert("name".into(), json!(entry.name)); + entry_map.insert("size".into(), json!(entry.size)); + entry_map.insert("modified_time".into(), json!(entry.modified_time)); + entries_out.push(entry_map); + } + fd_json.insert("entries".into(), json!(entries_out)); + serde_json::to_string(&fd_json).unwrap_or("".into()) +} + +pub fn make_fd_flutter(id: i32, entries: &Vec, only_count: bool) -> String { + let mut m = serde_json::Map::new(); + m.insert("id".into(), json!(id)); + let mut a = vec![]; + let mut n: u64 = 0; + for entry in entries { + n += entry.size; + if only_count { + continue; + } + let mut e = serde_json::Map::new(); + e.insert("name".into(), json!(entry.name.to_owned())); + let tmp = entry.entry_type.value(); + e.insert("type".into(), json!(if tmp == 0 { 1 } else { tmp })); + e.insert("time".into(), json!(entry.modified_time as f64)); + e.insert("size".into(), json!(entry.size as f64)); + a.push(e); + } + if only_count { + m.insert("num_entries".into(), json!(entries.len() as i32)); + } else { + m.insert("entries".into(), json!(a)); + } + m.insert("total_size".into(), json!(n as f64)); + serde_json::to_string(&m).unwrap_or("".into()) +} diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 6a3d19880..032be5b1f 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -5,16 +5,14 @@ use std::{ }; use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; -use serde_json::{json, Number, Value}; +use serde_json::json; +use hbb_common::ResultType; use hbb_common::{ - config::{self, Config, LocalConfig, PeerConfig, ONLINE}, + config::{self, LocalConfig, PeerConfig, ONLINE}, fs, log, }; -use hbb_common::{password_security, ResultType}; -use crate::{client::file_trait::FileManager, flutter::{session_add, session_start_}}; -use crate::common::make_fd_to_json; use crate::flutter::connection_manager::{self, get_clients_length, get_clients_state}; use crate::flutter::{self, SESSIONS}; use crate::start_server; @@ -30,6 +28,10 @@ use crate::ui_interface::{ set_peer_option, set_permanent_password, set_socks, store_fav, test_if_valid_server, update_temporary_password, using_public_server, }; +use crate::{ + client::file_trait::FileManager, + flutter::{make_fd_to_json, session_add, session_start_}, +}; fn initialize(app_dir: &str) { *config::APP_DIR.write().unwrap() = app_dir.to_owned(); @@ -110,7 +112,11 @@ pub fn host_stop_system_key_propagate(stopped: bool) { // FIXME: -> ResultType<()> cannot be parsed by frb_codegen // thread 'main' panicked at 'Failed to parse function output type `ResultType<()>`', $HOME\.cargo\git\checkouts\flutter_rust_bridge-ddba876d3ebb2a1e\e5adce5\frb_codegen\src\parser\mod.rs:151:25 -pub fn session_add_sync(id: String, is_file_transfer: bool, is_port_forward: bool) -> SyncReturn { +pub fn session_add_sync( + id: String, + is_file_transfer: bool, + is_port_forward: bool, +) -> SyncReturn { if let Err(e) = session_add(&id, is_file_transfer, is_port_forward) { SyncReturn(format!("Failed to add session with id {}, {}", &id, e)) } else { @@ -346,10 +352,8 @@ pub fn session_create_dir(id: String, act_id: i32, path: String, is_remote: bool } pub fn session_read_local_dir_sync(id: String, path: String, show_hidden: bool) -> String { - if let Some(session) = SESSIONS.read().unwrap().get(&id) { - if let Ok(fd) = fs::read_dir(&fs::get_path(&path), show_hidden) { - return make_fd_to_json(fd); - } + if let Ok(fd) = fs::read_dir(&fs::get_path(&path), show_hidden) { + return make_fd_to_json(fd.id, path, &fd.entries); } "".to_string() } diff --git a/src/ui/file_transfer.tis b/src/ui/file_transfer.tis index f32540b33..38c6321dc 100644 --- a/src/ui/file_transfer.tis +++ b/src/ui/file_transfer.tis @@ -695,7 +695,7 @@ handler.clearAllJobs = function() { file_transfer.job_table.clearAllJobs(); } -handler.addJob = function (id, path, to, file_num, show_hidden, is_remote) { +handler.addJob = function (id, path, to, file_num, show_hidden, is_remote) { // load last job // stdout.println("restore job: " + is_remote); file_transfer.job_table.addJob(id,path,to,file_num,show_hidden,is_remote); } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 7e2c5cd9c..b377b8583 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -23,7 +23,9 @@ use clipboard::{ get_rx_clip_client, server_clip_file, }; -use hbb_common::{allow_err, log, message_proto::*, rendezvous_proto::ConnType}; +use hbb_common::{ + allow_err, fs::TransferJobMeta, log, message_proto::*, rendezvous_proto::ConnType, +}; #[cfg(windows)] use crate::clipboard_file::*; @@ -43,7 +45,6 @@ static mut IS_ALT_GR: bool = false; /// SciterHandler /// * element -/// * thread TODO check if flutter need /// * close_state for file path when close #[derive(Clone, Default)] pub struct SciterHandler { @@ -155,16 +156,36 @@ impl InvokeUi for SciterHandler { self.call("clearAllJobs", &make_args!()); } - fn add_job( + fn load_last_job(&self, cnt: i32, job_json: &str) { + let job: Result = serde_json::from_str(job_json); + if let Ok(job) = job { + let path; + let to; + if job.is_remote { + path = job.remote.clone(); + to = job.to.clone(); + } else { + path = job.to.clone(); + to = job.remote.clone(); + } + self.call( + "addJob", + &make_args!(cnt, path, to, job.file_num, job.show_hidden, job.is_remote), + ); + } + } + + fn update_folder_files( &self, id: i32, + entries: &Vec, path: String, - to: String, - file_num: i32, - show_hidden: bool, - is_remote: bool, + _is_local: bool, + only_count: bool, ) { - todo!() + let mut m = make_fd(id, entries, only_count); + m.set_item("path", path); + self.call("updateFolderFiles", &make_args!(m)); } fn update_transfer_list(&self) { @@ -686,15 +707,18 @@ impl SciterSession { } pub fn make_fd(id: i32, entries: &Vec, only_count: bool) -> Value { + log::debug!("make_fd"); let mut m = Value::map(); m.set_item("id", id); let mut a = Value::array(0); let mut n: u64 = 0; for entry in entries { + log::debug!("for"); n += entry.size; if only_count { continue; } + log::debug!("for1"); let mut e = Value::map(); e.set_item("name", entry.name.to_owned()); let tmp = entry.entry_type.value(); @@ -703,11 +727,11 @@ pub fn make_fd(id: i32, entries: &Vec, only_count: bool) -> Value { e.set_item("size", entry.size as f64); a.push(e); } - if only_count { - m.set_item("num_entries", entries.len() as i32); - } else { + if !only_count { m.set_item("entries", a); } + m.set_item("num_entries", entries.len() as i32); m.set_item("total_size", n as f64); + log::debug!("make_fd end"); m } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 5ab6089a0..f117aae6d 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1,11 +1,11 @@ -use crate::client::io_loop::Remote; -use crate::client::{ - check_if_retry, handle_hash, handle_login_from_ui, handle_test_delay, - input_os_password, load_config, send_mouse, start_video_audio_threads, FileManager, Key, - LoginConfigHandler, QualityStatus, KEY_MAP, SERVER_KEYBOARD_ENABLED, -}; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::client::get_key_state; +use crate::client::io_loop::Remote; +use crate::client::{ + check_if_retry, handle_hash, handle_login_from_ui, handle_test_delay, input_os_password, + load_config, send_mouse, start_video_audio_threads, FileManager, Key, LoginConfigHandler, + QualityStatus, KEY_MAP, SERVER_KEYBOARD_ENABLED, +}; use crate::common; use crate::{client::Data, client::Interface}; use async_trait::async_trait; @@ -19,7 +19,7 @@ use hbb_common::{allow_err, message_proto::*}; use hbb_common::{fs, get_version_number, log, Stream}; use std::collections::HashMap; use std::ops::{Deref, DerefMut}; -use std::sync::atomic::{AtomicUsize, Ordering, AtomicBool}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex, RwLock}; /// IS_IN KEYBOARD_HOOKED sciter only @@ -556,18 +556,17 @@ pub trait InvokeUi: Send + Sync + Clone + 'static + Sized + Default { fn job_error(&self, id: i32, err: String, file_num: i32); fn job_done(&self, id: i32, file_num: i32); fn clear_all_jobs(&self); - fn add_job( - &self, - id: i32, - path: String, - to: String, - file_num: i32, - show_hidden: bool, - is_remote: bool, - ); fn new_message(&self, msg: String); fn update_transfer_list(&self); - // fn update_folder_files(&self); // TODO flutter with file_dir and update_folder_files + fn load_last_job(&self, cnt: i32, job_json: &str); + fn update_folder_files( + &self, + id: i32, + entries: &Vec, + path: String, + is_local: bool, + only_count: bool, + ); fn confirm_delete_files(&self, id: i32, i: i32, name: String); fn override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: bool); fn update_block_input_state(&self, on: bool); @@ -604,11 +603,19 @@ impl Interface for Session { } fn is_file_transfer(&self) -> bool { - self.lc.read().unwrap().conn_type.eq(&ConnType::FILE_TRANSFER) + self.lc + .read() + .unwrap() + .conn_type + .eq(&ConnType::FILE_TRANSFER) } fn is_port_forward(&self) -> bool { - self.lc.read().unwrap().conn_type.eq(&ConnType::PORT_FORWARD) + self.lc + .read() + .unwrap() + .conn_type + .eq(&ConnType::PORT_FORWARD) } fn is_rdp(&self) -> bool { From 4c499ecf2e0b47ef634e71e0457983bc419b860a Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 5 Sep 2022 14:00:40 +0800 Subject: [PATCH 13/19] fix: linux wayland frame of subwindow exists Signed-off-by: Kingtous --- flutter/linux/my_application.cc | 2 ++ flutter/pubspec.lock | 4 ++-- flutter/pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/flutter/linux/my_application.cc b/flutter/linux/my_application.cc index 20513032d..deea3f549 100644 --- a/flutter/linux/my_application.cc +++ b/flutter/linux/my_application.cc @@ -19,6 +19,8 @@ static void my_application_activate(GApplication* application) { MyApplication* self = MY_APPLICATION(application); GtkWindow* window = GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + // we have custom window frame + gtk_window_set_decorated(window, FALSE); // Use a header bar when running in GNOME as this is the common style used // by applications and is the setup most users will be using (e.g. Ubuntu diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 61fbfc293..b1c603ff0 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -252,8 +252,8 @@ packages: dependency: "direct main" description: path: "." - ref: e0368a023ba195462acc00d33ab361b499f0e413 - resolved-ref: e0368a023ba195462acc00d33ab361b499f0e413 + ref: fee851fa43116e0b91c39acd0ec37063dc6015f8 + resolved-ref: fee851fa43116e0b91c39acd0ec37063dc6015f8 url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index f2d038af3..f86c59755 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -62,7 +62,7 @@ dependencies: desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: e0368a023ba195462acc00d33ab361b499f0e413 + ref: fee851fa43116e0b91c39acd0ec37063dc6015f8 freezed_annotation: ^2.0.3 tray_manager: git: From 7eeb0f733586d59dd4d45397b8b5e994b8680136 Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 5 Sep 2022 19:41:09 +0800 Subject: [PATCH 14/19] refactor cm -> ui_cm_interface for sciter and flutter --- .../com/carriez/flutter_hbb/MainService.kt | 31 +- flutter/lib/models/model.dart | 12 +- flutter/lib/models/server_model.dart | 47 +- src/client/io_loop.rs | 6 +- src/flutter.rs | 657 ++--------------- src/flutter_ffi.rs | 27 +- src/lib.rs | 1 + src/ui.rs | 2 +- src/ui/cm.rs | 623 ++-------------- src/ui/remote.rs | 14 +- src/ui_cm_interface.rs | 665 ++++++++++++++++++ src/ui_session_interface.rs | 30 +- 12 files changed, 878 insertions(+), 1237 deletions(-) create mode 100644 src/ui_cm_interface.rs diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt index b9f9ff872..ac736ffdc 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt @@ -105,43 +105,30 @@ class MainService : Service() { @Keep fun rustSetByName(name: String, arg1: String, arg2: String) { when (name) { - "try_start_without_auth" -> { - try { - val jsonObject = JSONObject(arg1) - val id = jsonObject["id"] as Int - val username = jsonObject["name"] as String - val peerId = jsonObject["peer_id"] as String - val type = if (jsonObject["is_file_transfer"] as Boolean) { - translate("File Connection") - } else { - translate("Screen Connection") - } - loginRequestNotification(id, type, username, peerId) - } catch (e: JSONException) { - e.printStackTrace() - } - } - "on_client_authorized" -> { - Log.d(logTag, "from rust:on_client_authorized") + "add_connection" -> { try { val jsonObject = JSONObject(arg1) val id = jsonObject["id"] as Int val username = jsonObject["name"] as String val peerId = jsonObject["peer_id"] as String + val authorized = jsonObject["authorized"] as Boolean val isFileTransfer = jsonObject["is_file_transfer"] as Boolean val type = if (isFileTransfer) { translate("File Connection") } else { translate("Screen Connection") } - if (!isFileTransfer && !isStart) { - startCapture() + if (authorized) { + if (!isFileTransfer && !isStart) { + startCapture() + } + onClientAuthorizedNotification(id, type, username, peerId) + } else { + loginRequestNotification(id, type, username, peerId) } - onClientAuthorizedNotification(id, type, username, peerId) } catch (e: JSONException) { e.printStackTrace() } - } "stop_capture" -> { Log.d(logTag, "from rust:stop_capture") diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 384d7692a..1993145e7 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -168,10 +168,8 @@ class FfiModel with ChangeNotifier { parent.target?.fileModel.loadLastJob(evt); } else if (name == 'update_folder_files') { parent.target?.fileModel.updateFolderFiles(evt); - } else if (name == 'try_start_without_auth') { - parent.target?.serverModel.loginRequest(evt); - } else if (name == 'on_client_authorized') { - parent.target?.serverModel.onClientAuthorized(evt); + } else if (name == 'add_connection') { + parent.target?.serverModel.addConnection(evt); } else if (name == 'on_client_remove') { parent.target?.serverModel.onClientRemove(evt); } else if (name == 'update_quality_status') { @@ -227,10 +225,8 @@ class FfiModel with ChangeNotifier { parent.target?.fileModel.loadLastJob(evt); } else if (name == 'update_folder_files') { parent.target?.fileModel.updateFolderFiles(evt); - } else if (name == 'try_start_without_auth') { - parent.target?.serverModel.loginRequest(evt); - } else if (name == 'on_client_authorized') { - parent.target?.serverModel.onClientAuthorized(evt); + } else if (name == 'add_connection') { + parent.target?.serverModel.addConnection(evt); } else if (name == 'on_client_remove') { parent.target?.serverModel.onClientRemove(evt); } else if (name == 'update_quality_status') { diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index f78f8cf70..9d921ef48 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -100,7 +100,7 @@ class ServerModel with ChangeNotifier { _connectStatus = status; notifyListeners(); } - final res = await bind.mainCheckClientsLength(length: _clients.length); + final res = await bind.cmCheckClientsLength(length: _clients.length); if (res != null) { debugPrint("clients not match!"); updateClientState(res); @@ -347,7 +347,7 @@ class ServerModel with ChangeNotifier { // force updateClientState([String? json]) async { - var res = await bind.mainGetClientsState(); + var res = await bind.cmGetClientsState(); try { final List clientsJson = jsonDecode(res); _clients.clear(); @@ -369,21 +369,40 @@ class ServerModel with ChangeNotifier { } } - void loginRequest(Map evt) { + void addConnection(Map evt) { try { final client = Client.fromJson(jsonDecode(evt["client"])); - if (_clients.any((c) => c.id == client.id)) { - return; + if (client.authorized) { + parent.target?.dialogManager.dismissByTag(getLoginDialogTag(client.id)); + final index = _clients.indexWhere((c) => c.id == client.id); + if (index < 0) { + _clients.add(client); + } else { + _clients[index].authorized = true; + } + tabController.add( + TabInfo( + key: client.id.toString(), + label: client.name, + closable: false, + page: Desktop.buildConnectionCard(client)), + authorized: true); + scrollToBottom(); + notifyListeners(); + } else { + if (_clients.any((c) => c.id == client.id)) { + return; + } + _clients.add(client); + tabController.add(TabInfo( + key: client.id.toString(), + label: client.name, + closable: false, + page: Desktop.buildConnectionCard(client))); + scrollToBottom(); + notifyListeners(); + if (isAndroid) showLoginDialog(client); } - _clients.add(client); - tabController.add(TabInfo( - key: client.id.toString(), - label: client.name, - closable: false, - page: Desktop.buildConnectionCard(client))); - scrollToBottom(); - notifyListeners(); - if (isAndroid) showLoginDialog(client); } catch (e) { debugPrint("Failed to call loginRequest,error:$e"); } diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 0913251dd..54c3be26e 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -6,7 +6,7 @@ use crate::common; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::common::{check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}; -use crate::ui_session_interface::{InvokeUi, Session}; +use crate::ui_session_interface::{InvokeUiSession, Session}; use crate::{client::Data, client::Interface}; use hbb_common::config::{PeerConfig, TransferSerde}; @@ -29,7 +29,7 @@ use std::collections::HashMap; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; -pub struct Remote { +pub struct Remote { handler: Session, video_sender: MediaSender, audio_sender: MediaSender, @@ -49,7 +49,7 @@ pub struct Remote { video_format: CodecFormat, } -impl Remote { +impl Remote { pub fn new( handler: Session, video_sender: MediaSender, diff --git a/src/flutter.rs b/src/flutter.rs index a2f03d2ff..53b79949a 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -10,7 +10,7 @@ use hbb_common::{ }; use serde_json::json; -use crate::ui_session_interface::{io_loop, InvokeUi, Session}; +use crate::ui_session_interface::{io_loop, InvokeUiSession, Session}; use crate::{client::*, flutter_ffi::EventToUI}; @@ -47,7 +47,7 @@ impl FlutterHandler { } } -impl InvokeUi for FlutterHandler { +impl InvokeUiSession for FlutterHandler { fn set_cursor_data(&self, cd: CursorData) { let colors = hbb_common::compress::decompress(&cd.colors); self.push_event( @@ -338,625 +338,82 @@ pub fn session_start_(id: &str, event_stream: StreamSink) -> ResultTy // Server Side #[cfg(not(any(target_os = "ios")))] pub mod connection_manager { - use std::{ - collections::HashMap, - iter::FromIterator, - sync::{ - atomic::{AtomicI64, Ordering}, - RwLock, - }, - }; + use std::collections::HashMap; - use serde_derive::Serialize; - - use hbb_common::{ - allow_err, - config::Config, - fs::is_write_need_confirmation, - fs::{self, get_string, new_send_confirm, DigestCheckResult}, - log, - message_proto::*, - protobuf::Message as _, - tokio::{ - self, - sync::mpsc::{self, UnboundedReceiver, UnboundedSender}, - task::spawn_blocking, - }, - }; #[cfg(any(target_os = "android"))] use scrap::android::call_main_service_set_by_name; - use crate::ipc::Data; - use crate::ipc::{self, new_listener, Connection}; + use crate::ui_cm_interface::InvokeUiCM; use super::GLOBAL_EVENT_STREAM; - #[derive(Debug, Serialize, Clone)] - struct Client { - id: i32, - pub authorized: bool, - is_file_transfer: bool, - name: String, - peer_id: String, - keyboard: bool, - clipboard: bool, - audio: bool, - file: bool, - restart: bool, - #[serde(skip)] - tx: UnboundedSender, + #[derive(Clone)] + struct FlutterHandler {} + + impl InvokeUiCM for FlutterHandler { + //TODO port_forward + fn add_connection(&self, client: &crate::ui_cm_interface::Client) { + let client_json = serde_json::to_string(&client).unwrap_or("".into()); + // send to Android service, active notification no matter UI is shown or not. + #[cfg(any(target_os = "android"))] + if let Err(e) = + call_main_service_set_by_name("add_connection", Some(&client_json), None) + { + log::debug!("call_service_set_by_name fail,{}", e); + } + // send to UI, refresh widget + self.push_event("add_connection", vec![("client", &client_json)]); // TODO use add_connection + } + + fn remove_connection(&self, id: i32) { + self.push_event("on_client_remove", vec![("id", &id.to_string())]); + } + + fn new_message(&self, id: i32, text: String) { + self.push_event( + "chat_server_mode", + vec![("id", &id.to_string()), ("text", &text)], + ); + } } - lazy_static::lazy_static! { - static ref CLIENTS: RwLock> = Default::default(); + impl FlutterHandler { + fn push_event(&self, name: &str, event: Vec<(&str, &str)>) { + let mut h: HashMap<&str, &str> = event.iter().cloned().collect(); + assert!(h.get("name").is_none()); + h.insert("name", name); + + if let Some(s) = GLOBAL_EVENT_STREAM + .read() + .unwrap() + .get(super::APP_TYPE_MAIN) + { + s.add(serde_json::ser::to_string(&h).unwrap_or("".to_owned())); + }; + } } - static CLICK_TIME: AtomicI64 = AtomicI64::new(0); - - // // TODO clipboard_file - // enum ClipboardFileData { - // #[cfg(windows)] - // Clip((i32, ipc::ClipbaordFile)), - // Enable((i32, bool)), - // } - #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn start_listen_ipc_thread() { - std::thread::spawn(move || start_ipc()); - } + use crate::{ + ipc::start_pa, + ui_cm_interface::{start_ipc, ConnectionManager}, + }; - #[cfg(not(any(target_os = "android", target_os = "ios")))] - #[tokio::main(flavor = "current_thread")] - async fn start_ipc() { - // TODO clipboard_file - // let (tx_file, _rx_file) = mpsc::unbounded_channel::(); - // #[cfg(windows)] - // let cm_clip = cm.clone(); - // #[cfg(windows)] - // std::thread::spawn(move || start_clipboard_file(cm_clip, _rx_file)); + #[cfg(target_os = "linux")] + std::thread::spawn(start_pa); - #[cfg(windows)] - std::thread::spawn(move || { - log::info!("try create privacy mode window"); - #[cfg(windows)] - { - if let Err(e) = crate::platform::windows::check_update_broker_process() { - log::warn!( - "Failed to check update broker process. Privacy mode may not work properly. {}", - e - ); - } - } - allow_err!(crate::ui::win_privacy::start()); - }); - - match new_listener("_cm").await { - Ok(mut incoming) => { - while let Some(result) = incoming.next().await { - match result { - Ok(stream) => { - log::debug!("Got new connection"); - let mut stream = Connection::new(stream); - // let tx_file = tx_file.clone(); - tokio::spawn(async move { - // for tmp use, without real conn id - let conn_id_tmp = -1; - let mut conn_id: i32 = 0; - let (tx, mut rx) = mpsc::unbounded_channel::(); - let mut write_jobs: Vec = Vec::new(); - loop { - tokio::select! { - res = stream.next() => { - match res { - Err(err) => { - log::info!("cm ipc connection closed: {}", err); - break; - } - Ok(Some(data)) => { - match data { - Data::Login{id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled, restart} => { - log::debug!("conn_id: {}", id); - conn_id = id; - // tx_file.send(ClipboardFileData::Enable((id, file_transfer_enabled))).ok(); - on_login(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, tx.clone()); - } - Data::Close => { - // tx_file.send(ClipboardFileData::Enable((conn_id, false))).ok(); - log::info!("cm ipc connection closed from connection request"); - break; - } - Data::PrivacyModeState((_, _)) => { - conn_id = conn_id_tmp; - allow_err!(tx.send(data)); - } - Data::ClickTime(ms) => { - CLICK_TIME.store(ms, Ordering::SeqCst); - } - Data::ChatMessage { text } => { - handle_chat(conn_id, text); - } - Data::FS(fs) => { - handle_fs(fs, &mut write_jobs, &tx).await; - } - // TODO ClipbaordFile - // #[cfg(windows)] - // Data::ClipbaordFile(_clip) => { - // tx_file - // .send(ClipboardFileData::Clip((id, _clip))) - // .ok(); - // } - // #[cfg(windows)] - // Data::ClipboardFileEnabled(enabled) => { - // tx_file - // .send(ClipboardFileData::Enable((id, enabled))) - // .ok(); - // } - _ => {} - } - } - _ => {} - } - } - Some(data) = rx.recv() => { - if stream.send(&data).await.is_err() { - break; - } - } - } - } - if conn_id != conn_id_tmp { - remove_connection(conn_id); - } - }); - } - Err(err) => { - log::error!("Couldn't get cm client: {:?}", err); - } - } - } - } - Err(err) => { - log::error!("Failed to start cm ipc server: {}", err); - } - } - // crate::platform::quit_gui(); - // TODO flutter quit_gui + let cm = ConnectionManager { + ui_handler: FlutterHandler {}, + }; + std::thread::spawn(move || start_ipc(cm)); } #[cfg(target_os = "android")] pub fn start_channel(rx: UnboundedReceiver, tx: UnboundedSender) { + use crate::ui_cm_interface::start_listen; std::thread::spawn(move || start_listen(rx, tx)); } - - #[cfg(target_os = "android")] - #[tokio::main(flavor = "current_thread")] - async fn start_listen(mut rx: UnboundedReceiver, tx: UnboundedSender) { - let mut current_id = 0; - let mut write_jobs: Vec = Vec::new(); - loop { - match rx.recv().await { - Some(Data::Login { - id, - is_file_transfer, - port_forward, - peer_id, - name, - authorized, - keyboard, - clipboard, - audio, - file, - restart, - .. - }) => { - current_id = id; - on_login( - id, - is_file_transfer, - port_forward, - peer_id, - name, - authorized, - keyboard, - clipboard, - audio, - file, - restart, - tx.clone(), - ); - } - Some(Data::ChatMessage { text }) => { - handle_chat(current_id, text); - } - Some(Data::FS(fs)) => { - handle_fs(fs, &mut write_jobs, &tx).await; - } - Some(Data::Close) => { - break; - } - None => { - break; - } - _ => {} - } - } - remove_connection(current_id); - } - - fn on_login( - id: i32, - is_file_transfer: bool, - _port_forward: String, - peer_id: String, - name: String, - authorized: bool, - keyboard: bool, - clipboard: bool, - audio: bool, - file: bool, - restart: bool, - tx: mpsc::UnboundedSender, - ) { - let mut client = Client { - id, - authorized, - is_file_transfer, - name: name.clone(), - peer_id: peer_id.clone(), - keyboard, - clipboard, - audio, - file, - restart, - tx, - }; - if authorized { - client.authorized = true; - let client_json = serde_json::to_string(&client).unwrap_or("".into()); - // send to Android service, active notification no matter UI is shown or not. - #[cfg(any(target_os = "android"))] - if let Err(e) = - call_main_service_set_by_name("on_client_authorized", Some(&client_json), None) - { - log::debug!("call_service_set_by_name fail,{}", e); - } - // send to UI, refresh widget - push_event("on_client_authorized", vec![("client", &client_json)]); - } else { - let client_json = serde_json::to_string(&client).unwrap_or("".into()); - // send to Android service, active notification no matter UI is shown or not. - #[cfg(any(target_os = "android"))] - if let Err(e) = - call_main_service_set_by_name("try_start_without_auth", Some(&client_json), None) - { - log::debug!("call_service_set_by_name fail,{}", e); - } - // send to UI, refresh widget - push_event("try_start_without_auth", vec![("client", &client_json)]); - } - CLIENTS.write().unwrap().insert(id, client); - } - - fn push_event(name: &str, event: Vec<(&str, &str)>) { - let mut h: HashMap<&str, &str> = event.iter().cloned().collect(); - assert!(h.get("name").is_none()); - h.insert("name", name); - - if let Some(s) = GLOBAL_EVENT_STREAM - .read() - .unwrap() - .get(super::APP_TYPE_MAIN) - { - s.add(serde_json::ser::to_string(&h).unwrap_or("".to_owned())); - }; - } - - pub fn get_click_time() -> i64 { - CLICK_TIME.load(Ordering::SeqCst) - } - - pub fn check_click_time(id: i32) { - if let Some(client) = CLIENTS.read().unwrap().get(&id) { - allow_err!(client.tx.send(Data::ClickTime(0))); - }; - } - - pub fn switch_permission(id: i32, name: String, enabled: bool) { - if let Some(client) = CLIENTS.read().unwrap().get(&id) { - allow_err!(client.tx.send(Data::SwitchPermission { name, enabled })); - }; - } - - pub fn get_clients_state() -> String { - let clients = CLIENTS.read().unwrap(); - let res = Vec::from_iter(clients.values().cloned()); - serde_json::to_string(&res).unwrap_or("".into()) - } - - pub fn get_clients_length() -> usize { - let clients = CLIENTS.read().unwrap(); - clients.len() - } - - pub fn close_conn(id: i32) { - if let Some(client) = CLIENTS.read().unwrap().get(&id) { - allow_err!(client.tx.send(Data::Close)); - }; - } - - pub fn on_login_res(id: i32, res: bool) { - if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { - if res { - allow_err!(client.tx.send(Data::Authorize)); - client.authorized = true; - } else { - allow_err!(client.tx.send(Data::Close)); - } - }; - } - - fn remove_connection(id: i32) { - let mut clients = CLIENTS.write().unwrap(); - clients.remove(&id); - - if clients - .iter() - .filter(|(_k, v)| !v.is_file_transfer) - .next() - .is_none() - { - #[cfg(any(target_os = "android"))] - if let Err(e) = call_main_service_set_by_name("stop_capture", None, None) { - log::debug!("stop_capture err:{}", e); - } - } - - push_event("on_client_remove", vec![("id", &id.to_string())]); - } - - // server mode handle chat from other peers - fn handle_chat(id: i32, text: String) { - push_event( - "chat_server_mode", - vec![("id", &id.to_string()), ("text", &text)], - ); - } - - // server mode send chat to peer - pub fn send_chat(id: i32, text: String) { - let clients = CLIENTS.read().unwrap(); - if let Some(client) = clients.get(&id) { - allow_err!(client.tx.send(Data::ChatMessage { text })); - } - } - - // handle FS server - async fn handle_fs( - fs: ipc::FS, - write_jobs: &mut Vec, - tx: &UnboundedSender, - ) { - match fs { - ipc::FS::ReadDir { - dir, - include_hidden, - } => { - read_dir(&dir, include_hidden, tx).await; - } - ipc::FS::RemoveDir { - path, - id, - recursive, - } => { - remove_dir(path, id, recursive, tx).await; - } - ipc::FS::RemoveFile { path, id, file_num } => { - remove_file(path, id, file_num, tx).await; - } - ipc::FS::CreateDir { path, id } => { - create_dir(path, id, tx).await; - } - ipc::FS::NewWrite { - path, - id, - file_num, - mut files, - overwrite_detection, - } => { - write_jobs.push(fs::TransferJob::new_write( - id, - "".to_string(), - path, - file_num, - false, - false, - files - .drain(..) - .map(|f| FileEntry { - name: f.0, - modified_time: f.1, - ..Default::default() - }) - .collect(), - overwrite_detection, - )); - } - ipc::FS::CancelWrite { id } => { - if let Some(job) = fs::get_job(id, write_jobs) { - job.remove_download_file(); - fs::remove_job(id, write_jobs); - } - } - ipc::FS::WriteDone { id, file_num } => { - if let Some(job) = fs::get_job(id, write_jobs) { - job.modify_time(); - send_raw(fs::new_done(id, file_num), tx); - fs::remove_job(id, write_jobs); - } - } - ipc::FS::WriteBlock { - id, - file_num, - data, - compressed, - } => { - if let Some(job) = fs::get_job(id, write_jobs) { - if let Err(err) = job - .write( - FileTransferBlock { - id, - file_num, - data, - compressed, - ..Default::default() - }, - None, - ) - .await - { - send_raw(fs::new_error(id, err, file_num), &tx); - } - } - } - ipc::FS::CheckDigest { - id, - file_num, - file_size, - last_modified, - is_upload, - } => { - if let Some(job) = fs::get_job(id, write_jobs) { - let mut req = FileTransferSendConfirmRequest { - id, - file_num, - union: Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)), - ..Default::default() - }; - let digest = FileTransferDigest { - id, - file_num, - last_modified, - file_size, - ..Default::default() - }; - if let Some(file) = job.files().get(file_num as usize) { - let path = get_string(&job.join(&file.name)); - match is_write_need_confirmation(&path, &digest) { - Ok(digest_result) => { - match digest_result { - DigestCheckResult::IsSame => { - req.set_skip(true); - let msg_out = new_send_confirm(req); - send_raw(msg_out, &tx); - } - DigestCheckResult::NeedConfirm(mut digest) => { - // upload to server, but server has the same file, request - digest.is_upload = is_upload; - let mut msg_out = Message::new(); - let mut fr = FileResponse::new(); - fr.set_digest(digest); - msg_out.set_file_response(fr); - send_raw(msg_out, &tx); - } - DigestCheckResult::NoSuchFile => { - let msg_out = new_send_confirm(req); - send_raw(msg_out, &tx); - } - } - } - Err(err) => { - send_raw(fs::new_error(id, err, file_num), &tx); - } - } - } - } - } - _ => {} - } - } - - async fn read_dir(dir: &str, include_hidden: bool, tx: &UnboundedSender) { - let path = { - if dir.is_empty() { - Config::get_home() - } else { - fs::get_path(dir) - } - }; - if let Ok(Ok(fd)) = spawn_blocking(move || fs::read_dir(&path, include_hidden)).await { - let mut msg_out = Message::new(); - let mut file_response = FileResponse::new(); - file_response.set_dir(fd); - msg_out.set_file_response(file_response); - send_raw(msg_out, tx); - } - } - - async fn handle_result( - res: std::result::Result, S>, - id: i32, - file_num: i32, - tx: &UnboundedSender, - ) { - match res { - Err(err) => { - send_raw(fs::new_error(id, err, file_num), tx); - } - Ok(Err(err)) => { - send_raw(fs::new_error(id, err, file_num), tx); - } - Ok(Ok(())) => { - send_raw(fs::new_done(id, file_num), tx); - } - } - } - - async fn remove_file(path: String, id: i32, file_num: i32, tx: &UnboundedSender) { - handle_result( - spawn_blocking(move || fs::remove_file(&path)).await, - id, - file_num, - tx, - ) - .await; - } - - async fn create_dir(path: String, id: i32, tx: &UnboundedSender) { - handle_result( - spawn_blocking(move || fs::create_dir(&path)).await, - id, - 0, - tx, - ) - .await; - } - - async fn remove_dir(path: String, id: i32, recursive: bool, tx: &UnboundedSender) { - let path = fs::get_path(&path); - handle_result( - spawn_blocking(move || { - if recursive { - fs::remove_all_empty_dir(&path) - } else { - std::fs::remove_dir(&path).map_err(|err| err.into()) - } - }) - .await, - id, - 0, - tx, - ) - .await; - } - - fn send_raw(msg: Message, tx: &UnboundedSender) { - match msg.write_to_bytes() { - Ok(bytes) => { - allow_err!(tx.send(Data::RawMessage(bytes))); - } - err => allow_err!(err), - } - } } #[inline] diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 032be5b1f..2a41264f0 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -13,7 +13,6 @@ use hbb_common::{ fs, log, }; -use crate::flutter::connection_manager::{self, get_clients_length, get_clients_state}; use crate::flutter::{self, SESSIONS}; use crate::start_server; use crate::ui_interface; @@ -673,13 +672,13 @@ pub fn main_get_online_statue() -> i64 { ONLINE.lock().unwrap().values().max().unwrap_or(&0).clone() } -pub fn main_get_clients_state() -> String { - get_clients_state() +pub fn cm_get_clients_state() -> String { + crate::ui_cm_interface::get_clients_state() } -pub fn main_check_clients_length(length: usize) -> Option { - if length != get_clients_length() { - Some(get_clients_state()) +pub fn cm_check_clients_length(length: usize) -> Option { + if length != crate::ui_cm_interface::get_clients_length() { + Some(crate::ui_cm_interface::get_clients_state()) } else { None } @@ -791,27 +790,31 @@ pub fn main_get_mouse_time() -> f64 { } pub fn cm_send_chat(conn_id: i32, msg: String) { - connection_manager::send_chat(conn_id, msg); + crate::ui_cm_interface::send_chat(conn_id, msg); } pub fn cm_login_res(conn_id: i32, res: bool) { - connection_manager::on_login_res(conn_id, res); + if res { + crate::ui_cm_interface::authorize(conn_id); + } else { + crate::ui_cm_interface::close(conn_id); + } } pub fn cm_close_connection(conn_id: i32) { - connection_manager::close_conn(conn_id); + crate::ui_cm_interface::close(conn_id); } pub fn cm_check_click_time(conn_id: i32) { - connection_manager::check_click_time(conn_id) + crate::ui_cm_interface::check_click_time(conn_id) } pub fn cm_get_click_time() -> f64 { - connection_manager::get_click_time() as _ + crate::ui_cm_interface::get_click_time() as _ } pub fn cm_switch_permission(conn_id: i32, name: String, enabled: bool) { - connection_manager::switch_permission(conn_id, name, enabled) + crate::ui_cm_interface::switch_permission(conn_id, name, enabled) } pub fn main_get_icon() -> String { diff --git a/src/lib.rs b/src/lib.rs index f554d447e..b427c3301 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,6 +49,7 @@ mod tray; mod ui_interface; mod ui_session_interface; +mod ui_cm_interface; #[cfg(windows)] pub mod clipboard_file; diff --git a/src/ui.rs b/src/ui.rs index b66d1453b..b8b136c45 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -125,7 +125,7 @@ pub fn start(args: &mut [String]) { page = "install.html"; } else if args[0] == "--cm" { frame.register_behavior("connection-manager", move || { - Box::new(cm::ConnectionManager::new()) + Box::new(cm::SciterConnectionManager::new()) }); page = "cm.html"; } else if (args[0] == "--connect" diff --git a/src/ui/cm.rs b/src/ui/cm.rs index 222b9b5c9..2b1e3e791 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -1,60 +1,83 @@ #[cfg(target_os = "linux")] use crate::ipc::start_pa; -use crate::ipc::{self, new_listener, Connection, Data}; -use crate::VERSION; +use crate::ui_cm_interface::{start_ipc, ConnectionManager, InvokeUiCM}; + #[cfg(windows)] use clipboard::{ create_cliprdr_context, empty_clipboard, get_rx_clip_client, server_clip_file, set_conn_enabled, }; -use hbb_common::fs::{ - can_enable_overwrite_detection, get_string, is_write_need_confirmation, new_send_confirm, - DigestCheckResult, -}; -use hbb_common::{ - allow_err, - config::Config, - fs, get_version_number, log, - message_proto::*, - protobuf::Message as _, - tokio::{self, sync::mpsc, task::spawn_blocking}, -}; -use sciter::{make_args, Element, Value, HELEMENT}; -use std::{ - collections::HashMap, - ops::Deref, - sync::{Arc, RwLock}, -}; -pub struct ConnectionManagerInner { - root: Option, - senders: HashMap>, - click_time: i64, +use hbb_common::{allow_err, log}; +use sciter::{make_args, Element, Value, HELEMENT}; +use std::sync::Mutex; +use std::{ops::Deref, sync::Arc}; + +#[derive(Clone, Default)] +pub struct SciterHandler { + pub element: Arc>>, } -#[derive(Clone)] -pub struct ConnectionManager(Arc>); +impl InvokeUiCM for SciterHandler { + fn add_connection(&self, client: &crate::ui_cm_interface::Client) { + self.call( + "addConnection", + &make_args!( + client.id, + client.is_file_transfer, + client.port_forward.clone(), + client.peer_id.clone(), + client.name.clone(), + client.authorized, + client.keyboard, + client.clipboard, + client.audio, + client.file, + client.restart + ), + ); + } -impl Deref for ConnectionManager { - type Target = Arc>; + fn remove_connection(&self, id: i32) { + self.call("removeConnection", &make_args!(id)); + if crate::ui_cm_interface::get_clients_length().eq(&0) { + crate::platform::quit_gui(); + } + } + + fn new_message(&self, id: i32, text: String) { + self.call("newMessage", &make_args!(id, text)); + } +} + +impl SciterHandler { + #[inline] + fn call(&self, func: &str, args: &[Value]) { + if let Some(e) = self.element.lock().unwrap().as_ref() { + allow_err!(e.call_method(func, &super::value_crash_workaround(args)[..])); + } + } +} + +pub struct SciterConnectionManager(ConnectionManager); + +impl Deref for SciterConnectionManager { + type Target = ConnectionManager; fn deref(&self) -> &Self::Target { &self.0 } } -impl ConnectionManager { +impl SciterConnectionManager { pub fn new() -> Self { #[cfg(target_os = "linux")] std::thread::spawn(start_pa); - let inner = ConnectionManagerInner { - root: None, - senders: HashMap::new(), - click_time: Default::default(), + let cm = ConnectionManager { + ui_handler: SciterHandler::default(), }; - let cm = Self(Arc::new(RwLock::new(inner))); let cloned = cm.clone(); std::thread::spawn(move || start_ipc(cloned)); - cm + SciterConnectionManager(cm) } fn get_icon(&mut self) -> String { @@ -62,359 +85,27 @@ impl ConnectionManager { } fn check_click_time(&mut self, id: i32) { - let lock = self.read().unwrap(); - if let Some(s) = lock.senders.get(&id) { - allow_err!(s.send(Data::ClickTime(0))); - } + crate::ui_cm_interface::check_click_time(id); } fn get_click_time(&self) -> f64 { - self.read().unwrap().click_time as _ - } - - #[inline] - fn call(&self, func: &str, args: &[Value]) { - let r = self.read().unwrap(); - if let Some(ref e) = r.root { - allow_err!(e.call_method(func, &super::value_crash_workaround(args)[..])); - } - } - - fn add_connection( - &self, - id: i32, - is_file_transfer: bool, - port_forward: String, - peer_id: String, - name: String, - authorized: bool, - keyboard: bool, - clipboard: bool, - audio: bool, - file: bool, - restart: bool, - tx: mpsc::UnboundedSender, - ) { - self.call( - "addConnection", - &make_args!( - id, - is_file_transfer, - port_forward, - peer_id, - name, - authorized, - keyboard, - clipboard, - audio, - file, - restart - ), - ); - self.write().unwrap().senders.insert(id, tx); - } - - fn remove_connection(&self, id: i32) { - self.write().unwrap().senders.remove(&id); - if self.read().unwrap().senders.len() == 0 { - crate::platform::quit_gui(); - } - self.call("removeConnection", &make_args!(id)); - } - - async fn handle_data( - &self, - id: i32, - data: Data, - _tx_clip_file: &mpsc::UnboundedSender, - write_jobs: &mut Vec, - conn: &mut Connection, - ) { - match data { - Data::ChatMessage { text } => { - self.call("newMessage", &make_args!(id, text)); - } - Data::ClickTime(ms) => { - self.write().unwrap().click_time = ms; - } - Data::FS(v) => match v { - ipc::FS::ReadDir { - dir, - include_hidden, - } => { - Self::read_dir(&dir, include_hidden, conn).await; - } - ipc::FS::RemoveDir { - path, - id, - recursive, - } => { - Self::remove_dir(path, id, recursive, conn).await; - } - ipc::FS::RemoveFile { path, id, file_num } => { - Self::remove_file(path, id, file_num, conn).await; - } - ipc::FS::CreateDir { path, id } => { - Self::create_dir(path, id, conn).await; - } - ipc::FS::NewWrite { - path, - id, - file_num, - mut files, - overwrite_detection, - } => { - // cm has no show_hidden context - // dummy remote, show_hidden, is_remote - write_jobs.push(fs::TransferJob::new_write( - id, - "".to_string(), - path, - file_num, - false, - false, - files - .drain(..) - .map(|f| FileEntry { - name: f.0, - modified_time: f.1, - ..Default::default() - }) - .collect(), - overwrite_detection, - )); - } - ipc::FS::CancelWrite { id } => { - if let Some(job) = fs::get_job(id, write_jobs) { - job.remove_download_file(); - fs::remove_job(id, write_jobs); - } - } - ipc::FS::WriteDone { id, file_num } => { - if let Some(job) = fs::get_job(id, write_jobs) { - job.modify_time(); - Self::send(fs::new_done(id, file_num), conn).await; - fs::remove_job(id, write_jobs); - } - } - ipc::FS::CheckDigest { - id, - file_num, - file_size, - last_modified, - is_upload, - } => { - if let Some(job) = fs::get_job(id, write_jobs) { - let mut req = FileTransferSendConfirmRequest { - id, - file_num, - union: Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)), - ..Default::default() - }; - let digest = FileTransferDigest { - id, - file_num, - last_modified, - file_size, - ..Default::default() - }; - if let Some(file) = job.files().get(file_num as usize) { - let path = get_string(&job.join(&file.name)); - match is_write_need_confirmation(&path, &digest) { - Ok(digest_result) => { - match digest_result { - DigestCheckResult::IsSame => { - req.set_skip(true); - let msg_out = new_send_confirm(req); - Self::send(msg_out, conn).await; - } - DigestCheckResult::NeedConfirm(mut digest) => { - // upload to server, but server has the same file, request - digest.is_upload = is_upload; - let mut msg_out = Message::new(); - let mut fr = FileResponse::new(); - fr.set_digest(digest); - msg_out.set_file_response(fr); - Self::send(msg_out, conn).await; - } - DigestCheckResult::NoSuchFile => { - let msg_out = new_send_confirm(req); - Self::send(msg_out, conn).await; - } - } - } - Err(err) => { - Self::send(fs::new_error(id, err, file_num), conn).await; - } - } - } - } - } - ipc::FS::WriteBlock { - id, - file_num, - data, - compressed, - } => { - let raw = if let Ok(bytes) = conn.next_raw().await { - Some(bytes) - } else { - None - }; - if let Some(job) = fs::get_job(id, write_jobs) { - if let Err(err) = job - .write( - FileTransferBlock { - id, - file_num, - data, - compressed, - ..Default::default() - }, - raw.as_ref().map(|x| &x[..]), - ) - .await - { - Self::send(fs::new_error(id, err, file_num), conn).await; - } - } - } - ipc::FS::WriteOffset { - id: _, - file_num: _, - offset_blk: _, - } => {} - }, - #[cfg(windows)] - Data::ClipbaordFile(_clip) => { - _tx_clip_file - .send(ClipboardFileData::Clip((id, _clip))) - .ok(); - } - #[cfg(windows)] - Data::ClipboardFileEnabled(enabled) => { - _tx_clip_file - .send(ClipboardFileData::Enable((id, enabled))) - .ok(); - } - _ => {} - } - } - - async fn read_dir(dir: &str, include_hidden: bool, conn: &mut Connection) { - let path = { - if dir.is_empty() { - Config::get_home() - } else { - fs::get_path(dir) - } - }; - if let Ok(Ok(fd)) = spawn_blocking(move || fs::read_dir(&path, include_hidden)).await { - let mut msg_out = Message::new(); - let mut file_response = FileResponse::new(); - file_response.set_dir(fd); - msg_out.set_file_response(file_response); - Self::send(msg_out, conn).await; - } - } - - async fn handle_result( - res: std::result::Result, S>, - id: i32, - file_num: i32, - conn: &mut Connection, - ) { - match res { - Err(err) => { - Self::send(fs::new_error(id, err, file_num), conn).await; - } - Ok(Err(err)) => { - Self::send(fs::new_error(id, err, file_num), conn).await; - } - Ok(Ok(())) => { - Self::send(fs::new_done(id, file_num), conn).await; - } - } - } - - async fn remove_file(path: String, id: i32, file_num: i32, conn: &mut Connection) { - Self::handle_result( - spawn_blocking(move || fs::remove_file(&path)).await, - id, - file_num, - conn, - ) - .await; - } - - async fn create_dir(path: String, id: i32, conn: &mut Connection) { - Self::handle_result( - spawn_blocking(move || fs::create_dir(&path)).await, - id, - 0, - conn, - ) - .await; - } - - async fn remove_dir(path: String, id: i32, recursive: bool, conn: &mut Connection) { - let path = fs::get_path(&path); - Self::handle_result( - spawn_blocking(move || { - if recursive { - fs::remove_all_empty_dir(&path) - } else { - std::fs::remove_dir(&path).map_err(|err| err.into()) - } - }) - .await, - id, - 0, - conn, - ) - .await; - } - - async fn send(msg: Message, conn: &mut Connection) { - match msg.write_to_bytes() { - Ok(bytes) => allow_err!(conn.send(&Data::RawMessage(bytes)).await), - err => allow_err!(err), - } + crate::ui_cm_interface::get_click_time() as _ } fn switch_permission(&self, id: i32, name: String, enabled: bool) { - let lock = self.read().unwrap(); - if let Some(s) = lock.senders.get(&id) { - allow_err!(s.send(Data::SwitchPermission { name, enabled })); - } + crate::ui_cm_interface::switch_permission(id, name, enabled); } fn close(&self, id: i32) { - let lock = self.read().unwrap(); - if let Some(s) = lock.senders.get(&id) { - allow_err!(s.send(Data::Close)); - } - } - - fn send_msg(&self, id: i32, text: String) { - let lock = self.read().unwrap(); - if let Some(s) = lock.senders.get(&id) { - allow_err!(s.send(Data::ChatMessage { text })); - } - } - - fn send_data(&self, id: i32, data: Data) { - let lock = self.read().unwrap(); - if let Some(s) = lock.senders.get(&id) { - allow_err!(s.send(data)); - } + crate::ui_cm_interface::close(id); } fn authorize(&self, id: i32) { - let lock = self.read().unwrap(); - if let Some(s) = lock.senders.get(&id) { - allow_err!(s.send(Data::Authorize)); - } + crate::ui_cm_interface::authorize(id); + } + + fn send_msg(&self, id: i32, text: String) { + crate::ui_cm_interface::send_chat(id, text); } fn t(&self, name: String) -> String { @@ -422,9 +113,9 @@ impl ConnectionManager { } } -impl sciter::EventHandler for ConnectionManager { +impl sciter::EventHandler for SciterConnectionManager { fn attached(&mut self, root: HELEMENT) { - self.write().unwrap().root = Some(Element::from(root)); + *self.ui_handler.element.lock().unwrap() = Some(Element::from(root)); } sciter::dispatch_script_call! { @@ -438,179 +129,3 @@ impl sciter::EventHandler for ConnectionManager { fn send_msg(i32, String); } } - -pub enum ClipboardFileData { - #[cfg(windows)] - Clip((i32, ipc::ClipbaordFile)), - Enable((i32, bool)), -} - -#[tokio::main(flavor = "current_thread")] -async fn start_ipc(cm: ConnectionManager) { - let (tx_file, _rx_file) = mpsc::unbounded_channel::(); - #[cfg(windows)] - let cm_clip = cm.clone(); - #[cfg(windows)] - std::thread::spawn(move || start_clipboard_file(cm_clip, _rx_file)); - - #[cfg(windows)] - std::thread::spawn(move || { - log::info!("try create privacy mode window"); - #[cfg(windows)] - { - if let Err(e) = crate::platform::windows::check_update_broker_process() { - log::warn!( - "Failed to check update broker process. Privacy mode may not work properly. {}", - e - ); - } - } - allow_err!(crate::ui::win_privacy::start()); - }); - - match new_listener("_cm").await { - Ok(mut incoming) => { - while let Some(result) = incoming.next().await { - match result { - Ok(stream) => { - log::debug!("Got new connection"); - let mut stream = Connection::new(stream); - let cm = cm.clone(); - let tx_file = tx_file.clone(); - tokio::spawn(async move { - // for tmp use, without real conn id - let conn_id_tmp = -1; - let mut conn_id: i32 = 0; - let (tx, mut rx) = mpsc::unbounded_channel::(); - let mut write_jobs: Vec = Vec::new(); - loop { - tokio::select! { - res = stream.next() => { - match res { - Err(err) => { - log::info!("cm ipc connection closed: {}", err); - break; - } - Ok(Some(data)) => { - match data { - Data::Login{id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled, restart} => { - log::debug!("conn_id: {}", id); - conn_id = id; - tx_file.send(ClipboardFileData::Enable((id, file_transfer_enabled))).ok(); - cm.add_connection(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, tx.clone()); - } - Data::Close => { - tx_file.send(ClipboardFileData::Enable((conn_id, false))).ok(); - log::info!("cm ipc connection closed from connection request"); - break; - } - Data::PrivacyModeState((id, _)) => { - conn_id = conn_id_tmp; - cm.send_data(id, data) - } - _ => { - cm.handle_data(conn_id, data, &tx_file, &mut write_jobs, &mut stream).await; - } - } - } - _ => {} - } - } - Some(data) = rx.recv() => { - if stream.send(&data).await.is_err() { - break; - } - } - } - } - if conn_id != conn_id_tmp { - cm.remove_connection(conn_id); - } - }); - } - Err(err) => { - log::error!("Couldn't get cm client: {:?}", err); - } - } - } - } - Err(err) => { - log::error!("Failed to start cm ipc server: {}", err); - } - } - crate::platform::quit_gui(); -} - -#[cfg(windows)] -#[tokio::main(flavor = "current_thread")] -pub async fn start_clipboard_file( - cm: ConnectionManager, - mut rx: mpsc::UnboundedReceiver, -) { - let mut cliprdr_context = None; - let mut rx_clip_client = get_rx_clip_client().lock().await; - - loop { - tokio::select! { - clip_file = rx_clip_client.recv() => match clip_file { - Some((conn_id, clip)) => { - cmd_inner_send( - &cm, - conn_id, - Data::ClipbaordFile(clip) - ); - } - None => { - // - } - }, - server_msg = rx.recv() => match server_msg { - Some(ClipboardFileData::Clip((conn_id, clip))) => { - if let Some(ctx) = cliprdr_context.as_mut() { - server_clip_file(ctx, conn_id, clip); - } - } - Some(ClipboardFileData::Enable((id, enabled))) => { - if enabled && cliprdr_context.is_none() { - cliprdr_context = Some(match create_cliprdr_context(true, false) { - Ok(context) => { - log::info!("clipboard context for file transfer created."); - context - } - Err(err) => { - log::error!( - "Create clipboard context for file transfer: {}", - err.to_string() - ); - return; - } - }); - } - set_conn_enabled(id, enabled); - if !enabled { - if let Some(ctx) = cliprdr_context.as_mut() { - empty_clipboard(ctx, id); - } - } - } - None => { - break - } - } - } - } -} - -#[cfg(windows)] -fn cmd_inner_send(cm: &ConnectionManager, id: i32, data: Data) { - let lock = cm.read().unwrap(); - if id != 0 { - if let Some(s) = lock.senders.get(&id) { - allow_err!(s.send(data)); - } - } else { - for s in lock.senders.values() { - allow_err!(s.send(data.clone())); - } - } -} diff --git a/src/ui/remote.rs b/src/ui/remote.rs index b377b8583..f6b3acec6 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -2,7 +2,7 @@ use std::{ collections::HashMap, ops::{Deref, DerefMut}, sync::{ - atomic::{AtomicBool, Ordering}, + atomic::Ordering, Arc, Mutex, }, }; @@ -31,7 +31,7 @@ use hbb_common::{ use crate::clipboard_file::*; use crate::{ client::*, - ui_session_interface::{InvokeUi, Session, IS_IN}, + ui_session_interface::{InvokeUiSession, Session, IS_IN}, }; type Video = AssetPtr; @@ -68,7 +68,7 @@ impl SciterHandler { } } -impl InvokeUi for SciterHandler { +impl InvokeUiSession for SciterHandler { fn set_cursor_data(&self, cd: CursorData) { let mut colors = hbb_common::compress::decompress(&cd.colors); if colors.iter().filter(|x| **x != 0).next().is_none() { @@ -445,6 +445,14 @@ impl SciterSession { v } + pub fn t(&self, name: String) -> String { + crate::client::translate(name) + } + + pub fn get_icon(&self) -> String { + crate::get_icon() + } + fn supported_hwcodec(&self) -> Value { #[cfg(feature = "hwcodec")] { diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs new file mode 100644 index 000000000..8a26a9558 --- /dev/null +++ b/src/ui_cm_interface.rs @@ -0,0 +1,665 @@ +use std::ops::{Deref, DerefMut}; +use std::{ + collections::HashMap, + iter::FromIterator, + sync::{ + atomic::{AtomicI64, Ordering}, + RwLock, + }, +}; + +use serde_derive::Serialize; + +use crate::ipc::Data; +use crate::ipc::{self, new_listener, Connection}; +use hbb_common::{ + allow_err, + config::Config, + fs::is_write_need_confirmation, + fs::{self, get_string, new_send_confirm, DigestCheckResult}, + log, + message_proto::*, + protobuf::Message as _, + tokio::{ + self, + sync::mpsc::{self, UnboundedSender}, + task::spawn_blocking, + }, +}; + +#[derive(Serialize, Clone)] +pub struct Client { + pub id: i32, + pub authorized: bool, + pub is_file_transfer: bool, + pub port_forward: String, + pub name: String, + pub peer_id: String, + pub keyboard: bool, + pub clipboard: bool, + pub audio: bool, + pub file: bool, + pub restart: bool, + #[serde(skip)] + tx: UnboundedSender, +} + +lazy_static::lazy_static! { + static ref CLIENTS: RwLock> = Default::default(); + static ref CLICK_TIME: AtomicI64 = AtomicI64::new(0); +} + +#[derive(Clone)] +pub struct ConnectionManager { + pub ui_handler: T, +} + +pub trait InvokeUiCM: Send + Clone + 'static + Sized { + fn add_connection(&self, client: &Client); + + fn remove_connection(&self, id: i32); + + fn new_message(&self, id: i32, text: String); +} + +impl Deref for ConnectionManager { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.ui_handler + } +} + +impl DerefMut for ConnectionManager { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.ui_handler + } +} + +impl ConnectionManager { + fn add_connection( + &self, + id: i32, + is_file_transfer: bool, + port_forward: String, + peer_id: String, + name: String, + authorized: bool, + keyboard: bool, + clipboard: bool, + audio: bool, + file: bool, + restart: bool, + tx: mpsc::UnboundedSender, + ) { + let client = Client { + id, + authorized, + is_file_transfer, + port_forward, + name: name.clone(), + peer_id: peer_id.clone(), + keyboard, + clipboard, + audio, + file, + restart, + tx, + }; + self.ui_handler.add_connection(&client); + CLIENTS.write().unwrap().insert(id, client); + } + + fn remove_connection(&self, id: i32) { + CLIENTS.write().unwrap().remove(&id); + + #[cfg(any(target_os = "android"))] + if clients + .iter() + .filter(|(_k, v)| !v.is_file_transfer) + .next() + .is_none() + { + if let Err(e) = call_main_service_set_by_name("stop_capture", None, None) { + log::debug!("stop_capture err:{}", e); + } + } + + self.ui_handler.remove_connection(id); + } +} + +#[inline] +pub fn check_click_time(id: i32) { + if let Some(client) = CLIENTS.read().unwrap().get(&id) { + allow_err!(client.tx.send(Data::ClickTime(0))); + }; +} + +#[inline] +pub fn get_click_time() -> i64 { + CLICK_TIME.load(Ordering::SeqCst) +} + +#[inline] +pub fn authorize(id: i32) { + if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { + client.authorized = true; + allow_err!(client.tx.send(Data::Authorize)); + }; +} + +#[inline] +pub fn close(id: i32) { + if let Some(client) = CLIENTS.read().unwrap().get(&id) { + allow_err!(client.tx.send(Data::Close)); + }; +} + +// server mode send chat to peer +#[inline] +pub fn send_chat(id: i32, text: String) { + let clients = CLIENTS.read().unwrap(); + if let Some(client) = clients.get(&id) { + allow_err!(client.tx.send(Data::ChatMessage { text })); + } +} + +#[inline] +pub fn switch_permission(id: i32, name: String, enabled: bool) { + if let Some(client) = CLIENTS.read().unwrap().get(&id) { + allow_err!(client.tx.send(Data::SwitchPermission { name, enabled })); + }; +} + +#[inline] +pub fn get_clients_state() -> String { + let clients = CLIENTS.read().unwrap(); + let res = Vec::from_iter(clients.values().cloned()); + serde_json::to_string(&res).unwrap_or("".into()) +} + +#[inline] +pub fn get_clients_length() -> usize { + let clients = CLIENTS.read().unwrap(); + clients.len() +} + +pub enum ClipboardFileData { + #[cfg(windows)] + Clip((i32, ipc::ClipbaordFile)), + Enable((i32, bool)), +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[tokio::main(flavor = "current_thread")] +pub async fn start_ipc(cm: ConnectionManager) { + let (tx_file, _rx_file) = mpsc::unbounded_channel::(); + #[cfg(windows)] + let cm_clip = cm.clone(); + #[cfg(windows)] + std::thread::spawn(move || start_clipboard_file(cm_clip, _rx_file)); + + #[cfg(windows)] + std::thread::spawn(move || { + log::info!("try create privacy mode window"); + #[cfg(windows)] + { + if let Err(e) = crate::platform::windows::check_update_broker_process() { + log::warn!( + "Failed to check update broker process. Privacy mode may not work properly. {}", + e + ); + } + } + allow_err!(crate::ui::win_privacy::start()); + }); + + match new_listener("_cm").await { + Ok(mut incoming) => { + while let Some(result) = incoming.next().await { + match result { + Ok(stream) => { + log::debug!("Got new connection"); + let mut stream = Connection::new(stream); + let cm = cm.clone(); + let tx_file = tx_file.clone(); + tokio::spawn(async move { + // for tmp use, without real conn id + let conn_id_tmp = -1; + let mut conn_id: i32 = 0; + let (tx, mut rx) = mpsc::unbounded_channel::(); + let mut write_jobs: Vec = Vec::new(); + loop { + tokio::select! { + res = stream.next() => { + match res { + Err(err) => { + log::info!("cm ipc connection closed: {}", err); + break; + } + Ok(Some(data)) => { + match data { + Data::Login{id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled, restart} => { + log::debug!("conn_id: {}", id); + conn_id = id; + tx_file.send(ClipboardFileData::Enable((id, file_transfer_enabled))).ok(); + cm.add_connection(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, tx.clone()); + } + Data::Close => { + tx_file.send(ClipboardFileData::Enable((conn_id, false))).ok(); + log::info!("cm ipc connection closed from connection request"); + break; + } + Data::PrivacyModeState((id, _)) => { + conn_id = conn_id_tmp; + allow_err!(tx.send(data)); + } + Data::ClickTime(ms) => { + CLICK_TIME.store(ms, Ordering::SeqCst); + } + Data::ChatMessage { text } => { + cm.new_message(conn_id, text); + } + Data::FS(fs) => { + handle_fs(fs, &mut write_jobs, &tx).await; + } + // TODO ClipbaordFile + // #[cfg(windows)] + // Data::ClipbaordFile(_clip) => { + // tx_file + // .send(ClipboardFileData::Clip((id, _clip))) + // .ok(); + // } + // #[cfg(windows)] + // Data::ClipboardFileEnabled(enabled) => { + // tx_file + // .send(ClipboardFileData::Enable((id, enabled))) + // .ok(); + // } + _ => { + + } + } + } + _ => {} + } + } + Some(data) = rx.recv() => { + if stream.send(&data).await.is_err() { + break; + } + } + } + } + if conn_id != conn_id_tmp { + cm.remove_connection(conn_id); + } + }); + } + Err(err) => { + log::error!("Couldn't get cm client: {:?}", err); + } + } + } + } + Err(err) => { + log::error!("Failed to start cm ipc server: {}", err); + } + } + crate::platform::quit_gui(); +} + +#[cfg(target_os = "android")] +#[tokio::main(flavor = "current_thread")] +pub async fn start_listen(mut rx: UnboundedReceiver, tx: UnboundedSender) { + let mut current_id = 0; + let mut write_jobs: Vec = Vec::new(); + loop { + match rx.recv().await { + Some(Data::Login { + id, + is_file_transfer, + port_forward, + peer_id, + name, + authorized, + keyboard, + clipboard, + audio, + file, + restart, + .. + }) => { + current_id = id; + on_login( + id, + is_file_transfer, + port_forward, + peer_id, + name, + authorized, + keyboard, + clipboard, + audio, + file, + restart, + tx.clone(), + ); + } + Some(Data::ChatMessage { text }) => { + cm.new_message(conn_id, text); + } + Some(Data::FS(fs)) => { + handle_fs(fs, &mut write_jobs, &tx).await; + } + Some(Data::Close) => { + break; + } + None => { + break; + } + _ => {} + } + } + remove_connection(current_id); +} + +async fn handle_fs(fs: ipc::FS, write_jobs: &mut Vec, tx: &UnboundedSender) { + match fs { + ipc::FS::ReadDir { + dir, + include_hidden, + } => { + read_dir(&dir, include_hidden, tx).await; + } + ipc::FS::RemoveDir { + path, + id, + recursive, + } => { + remove_dir(path, id, recursive, tx).await; + } + ipc::FS::RemoveFile { path, id, file_num } => { + remove_file(path, id, file_num, tx).await; + } + ipc::FS::CreateDir { path, id } => { + create_dir(path, id, tx).await; + } + ipc::FS::NewWrite { + path, + id, + file_num, + mut files, + overwrite_detection, + } => { + // cm has no show_hidden context + // dummy remote, show_hidden, is_remote + write_jobs.push(fs::TransferJob::new_write( + id, + "".to_string(), + path, + file_num, + false, + false, + files + .drain(..) + .map(|f| FileEntry { + name: f.0, + modified_time: f.1, + ..Default::default() + }) + .collect(), + overwrite_detection, + )); + } + ipc::FS::CancelWrite { id } => { + if let Some(job) = fs::get_job(id, write_jobs) { + job.remove_download_file(); + fs::remove_job(id, write_jobs); + } + } + ipc::FS::WriteDone { id, file_num } => { + if let Some(job) = fs::get_job(id, write_jobs) { + job.modify_time(); + send_raw(fs::new_done(id, file_num), tx); + fs::remove_job(id, write_jobs); + } + } + ipc::FS::WriteBlock { + id, + file_num, + data, + compressed, + } => { + if let Some(job) = fs::get_job(id, write_jobs) { + if let Err(err) = job + .write( + FileTransferBlock { + id, + file_num, + data, + compressed, + ..Default::default() + }, + None, + ) + .await + { + send_raw(fs::new_error(id, err, file_num), &tx); + } + } + } + ipc::FS::CheckDigest { + id, + file_num, + file_size, + last_modified, + is_upload, + } => { + if let Some(job) = fs::get_job(id, write_jobs) { + let mut req = FileTransferSendConfirmRequest { + id, + file_num, + union: Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)), + ..Default::default() + }; + let digest = FileTransferDigest { + id, + file_num, + last_modified, + file_size, + ..Default::default() + }; + if let Some(file) = job.files().get(file_num as usize) { + let path = get_string(&job.join(&file.name)); + match is_write_need_confirmation(&path, &digest) { + Ok(digest_result) => { + match digest_result { + DigestCheckResult::IsSame => { + req.set_skip(true); + let msg_out = new_send_confirm(req); + send_raw(msg_out, &tx); + } + DigestCheckResult::NeedConfirm(mut digest) => { + // upload to server, but server has the same file, request + digest.is_upload = is_upload; + let mut msg_out = Message::new(); + let mut fr = FileResponse::new(); + fr.set_digest(digest); + msg_out.set_file_response(fr); + send_raw(msg_out, &tx); + } + DigestCheckResult::NoSuchFile => { + let msg_out = new_send_confirm(req); + send_raw(msg_out, &tx); + } + } + } + Err(err) => { + send_raw(fs::new_error(id, err, file_num), &tx); + } + } + } + } + } + _ => {} + } +} + +async fn read_dir(dir: &str, include_hidden: bool, tx: &UnboundedSender) { + let path = { + if dir.is_empty() { + Config::get_home() + } else { + fs::get_path(dir) + } + }; + if let Ok(Ok(fd)) = spawn_blocking(move || fs::read_dir(&path, include_hidden)).await { + let mut msg_out = Message::new(); + let mut file_response = FileResponse::new(); + file_response.set_dir(fd); + msg_out.set_file_response(file_response); + send_raw(msg_out, tx); + } +} + +async fn handle_result( + res: std::result::Result, S>, + id: i32, + file_num: i32, + tx: &UnboundedSender, +) { + match res { + Err(err) => { + send_raw(fs::new_error(id, err, file_num), tx); + } + Ok(Err(err)) => { + send_raw(fs::new_error(id, err, file_num), tx); + } + Ok(Ok(())) => { + send_raw(fs::new_done(id, file_num), tx); + } + } +} + +async fn remove_file(path: String, id: i32, file_num: i32, tx: &UnboundedSender) { + handle_result( + spawn_blocking(move || fs::remove_file(&path)).await, + id, + file_num, + tx, + ) + .await; +} + +async fn create_dir(path: String, id: i32, tx: &UnboundedSender) { + handle_result( + spawn_blocking(move || fs::create_dir(&path)).await, + id, + 0, + tx, + ) + .await; +} + +async fn remove_dir(path: String, id: i32, recursive: bool, tx: &UnboundedSender) { + let path = fs::get_path(&path); + handle_result( + spawn_blocking(move || { + if recursive { + fs::remove_all_empty_dir(&path) + } else { + std::fs::remove_dir(&path).map_err(|err| err.into()) + } + }) + .await, + id, + 0, + tx, + ) + .await; +} + +fn send_raw(msg: Message, tx: &UnboundedSender) { + match msg.write_to_bytes() { + Ok(bytes) => { + allow_err!(tx.send(Data::RawMessage(bytes))); + } + err => allow_err!(err), + } +} + +#[cfg(windows)] +#[tokio::main(flavor = "current_thread")] +pub async fn start_clipboard_file( + cm: ConnectionManager, + mut rx: mpsc::UnboundedReceiver, +) { + let mut cliprdr_context = None; + let mut rx_clip_client = get_rx_clip_client().lock().await; + + loop { + tokio::select! { + clip_file = rx_clip_client.recv() => match clip_file { + Some((conn_id, clip)) => { + cmd_inner_send( + &cm, + conn_id, + Data::ClipbaordFile(clip) + ); + } + None => { + // + } + }, + server_msg = rx.recv() => match server_msg { + Some(ClipboardFileData::Clip((conn_id, clip))) => { + if let Some(ctx) = cliprdr_context.as_mut() { + server_clip_file(ctx, conn_id, clip); + } + } + Some(ClipboardFileData::Enable((id, enabled))) => { + if enabled && cliprdr_context.is_none() { + cliprdr_context = Some(match create_cliprdr_context(true, false) { + Ok(context) => { + log::info!("clipboard context for file transfer created."); + context + } + Err(err) => { + log::error!( + "Create clipboard context for file transfer: {}", + err.to_string() + ); + return; + } + }); + } + set_conn_enabled(id, enabled); + if !enabled { + if let Some(ctx) = cliprdr_context.as_mut() { + empty_clipboard(ctx, id); + } + } + } + None => { + break + } + } + } + } +} + +#[cfg(windows)] +fn cmd_inner_send(cm: &ConnectionManager, id: i32, data: Data) { + let lock = cm.read().unwrap(); + if id != 0 { + if let Some(s) = lock.senders.get(&id) { + allow_err!(s.send(data)); + } + } else { + for s in lock.senders.values() { + allow_err!(s.send(data.clone())); + } + } +} diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index f117aae6d..9fca2dfbb 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -27,7 +27,7 @@ pub static IS_IN: AtomicBool = AtomicBool::new(false); static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); #[derive(Clone, Default)] -pub struct Session { +pub struct Session { pub cmd: String, pub id: String, pub password: String, @@ -38,7 +38,7 @@ pub struct Session { pub ui_handler: T, } -impl Session { +impl Session { pub fn get_view_style(&self) -> String { self.lc.read().unwrap().view_style.clone() } @@ -135,11 +135,6 @@ impl Session { self.send(Data::Message(msg)); } - #[cfg(not(any(target_os = "android", target_os = "ios")))] - pub fn t(&self, name: String) -> String { - crate::client::translate(name) - } - pub fn get_audit_server(&self) -> String { if self.lc.read().unwrap().conn_id <= 0 || LocalConfig::get_option("access_token").is_empty() @@ -327,11 +322,6 @@ impl Session { return "".to_owned(); } - #[cfg(not(any(target_os = "android", target_os = "ios")))] - pub fn get_icon(&self) -> String { - crate::get_icon() - } - pub fn send_chat(&self, text: String) { let mut misc = Misc::new(); misc.set_chat_message(ChatMessage { @@ -541,7 +531,7 @@ impl Session { } } -pub trait InvokeUi: Send + Sync + Clone + 'static + Sized + Default { +pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn set_cursor_data(&self, cd: CursorData); fn set_cursor_id(&self, id: String); fn set_cursor_position(&self, cp: CursorPosition); @@ -578,7 +568,7 @@ pub trait InvokeUi: Send + Sync + Clone + 'static + Sized + Default { fn clipboard(&self, content: String); } -impl Deref for Session { +impl Deref for Session { type Target = T; fn deref(&self) -> &Self::Target { @@ -586,16 +576,16 @@ impl Deref for Session { } } -impl DerefMut for Session { +impl DerefMut for Session { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.ui_handler } } -impl FileManager for Session {} +impl FileManager for Session {} #[async_trait] -impl Interface for Session { +impl Interface for Session { fn send(&self, data: Data) { if let Some(sender) = self.sender.read().unwrap().as_ref() { sender.send(data).ok(); @@ -723,7 +713,7 @@ impl Interface for Session { // TODO use event callbcak // sciter only #[cfg(not(any(target_os = "android", target_os = "ios")))] -impl Session { +impl Session { fn start_keyboard_hook(&self) { if self.is_port_forward() || self.is_file_transfer() { return; @@ -958,7 +948,7 @@ impl Session { } #[tokio::main(flavor = "current_thread")] -pub async fn io_loop(handler: Session) { +pub async fn io_loop(handler: Session) { let (sender, mut receiver) = mpsc::unbounded_channel::(); *handler.sender.write().unwrap() = Some(sender.clone()); let mut options = crate::ipc::get_options_async().await; @@ -1074,7 +1064,7 @@ pub async fn io_loop(handler: Session) { } #[cfg(not(any(target_os = "android", target_os = "ios")))] -async fn start_one_port_forward( +async fn start_one_port_forward( handler: Session, port: i32, remote_host: String, From bd733bc108110bf4d0db829591e515d76a04b67b Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 5 Sep 2022 20:05:23 +0800 Subject: [PATCH 15/19] mobile build --- src/client/file_trait.rs | 4 ++-- src/flutter.rs | 14 ++++++++++++-- src/flutter_ffi.rs | 4 ++-- src/ui_cm_interface.rs | 16 ++++++++++------ src/ui_interface.rs | 7 ++++--- 5 files changed, 30 insertions(+), 15 deletions(-) diff --git a/src/client/file_trait.rs b/src/client/file_trait.rs index d2f7b1648..b94177c51 100644 --- a/src/client/file_trait.rs +++ b/src/client/file_trait.rs @@ -22,9 +22,9 @@ pub trait FileManager: Interface { #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] fn read_dir(&self, path: &str, include_hidden: bool) -> String { - use crate::common::make_fd_to_json; + use crate::flutter::make_fd_to_json; match fs::read_dir(&fs::get_path(path), include_hidden) { - Ok(fd) => make_fd_to_json(fd), + Ok(fd) => make_fd_to_json(fd.id, fd.path, &fd.entries), Err(_) => "".into(), } } diff --git a/src/flutter.rs b/src/flutter.rs index 53b79949a..a3c7ea70f 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -340,6 +340,7 @@ pub fn session_start_(id: &str, event_stream: StreamSink) -> ResultTy pub mod connection_manager { use std::collections::HashMap; + use hbb_common::log; #[cfg(any(target_os = "android"))] use scrap::android::call_main_service_set_by_name; @@ -410,9 +411,18 @@ pub mod connection_manager { } #[cfg(target_os = "android")] - pub fn start_channel(rx: UnboundedReceiver, tx: UnboundedSender) { + use hbb_common::tokio::sync::mpsc::{UnboundedReceiver,UnboundedSender}; + + #[cfg(target_os = "android")] + pub fn start_channel( + rx: UnboundedReceiver, + tx: UnboundedSender, + ) { use crate::ui_cm_interface::start_listen; - std::thread::spawn(move || start_listen(rx, tx)); + let cm = crate::ui_cm_interface::ConnectionManager { + ui_handler: FlutterHandler {}, + }; + std::thread::spawn(move || start_listen(cm, rx, tx)); } } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 2a41264f0..5da94c3c1 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -754,7 +754,7 @@ pub fn main_set_home_dir(home: String) { pub fn main_stop_service() { #[cfg(target_os = "android")] { - Config::set_option("stop-service".into(), "Y".into()); + config::Config::set_option("stop-service".into(), "Y".into()); crate::rendezvous_mediator::RendezvousMediator::restart(); } } @@ -762,7 +762,7 @@ pub fn main_stop_service() { pub fn main_start_service() { #[cfg(target_os = "android")] { - Config::set_option("stop-service".into(), "".into()); + config::Config::set_option("stop-service".into(), "".into()); crate::rendezvous_mediator::RendezvousMediator::restart(); } #[cfg(not(target_os = "android"))] diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 8a26a9558..b9045532b 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -114,13 +114,17 @@ impl ConnectionManager { CLIENTS.write().unwrap().remove(&id); #[cfg(any(target_os = "android"))] - if clients + if CLIENTS + .read() + .unwrap() .iter() .filter(|(_k, v)| !v.is_file_transfer) .next() .is_none() { - if let Err(e) = call_main_service_set_by_name("stop_capture", None, None) { + if let Err(e) = + scrap::android::call_main_service_set_by_name("stop_capture", None, None) + { log::debug!("stop_capture err:{}", e); } } @@ -312,7 +316,7 @@ pub async fn start_ipc(cm: ConnectionManager) { #[cfg(target_os = "android")] #[tokio::main(flavor = "current_thread")] -pub async fn start_listen(mut rx: UnboundedReceiver, tx: UnboundedSender) { +pub async fn start_listen(cm: ConnectionManager, mut rx: mpsc::UnboundedReceiver, tx: mpsc::UnboundedSender) { let mut current_id = 0; let mut write_jobs: Vec = Vec::new(); loop { @@ -332,7 +336,7 @@ pub async fn start_listen(mut rx: UnboundedReceiver, tx: UnboundedSender { current_id = id; - on_login( + cm.add_connection( id, is_file_transfer, port_forward, @@ -348,7 +352,7 @@ pub async fn start_listen(mut rx: UnboundedReceiver, tx: UnboundedSender { - cm.new_message(conn_id, text); + cm.new_message(current_id, text); } Some(Data::FS(fs)) => { handle_fs(fs, &mut write_jobs, &tx).await; @@ -362,7 +366,7 @@ pub async fn start_listen(mut rx: UnboundedReceiver, tx: UnboundedSender {} } } - remove_connection(current_id); + cm.remove_connection(current_id); } async fn handle_fs(fs: ipc::FS, write_jobs: &mut Vec, tx: &UnboundedSender) { diff --git a/src/ui_interface.rs b/src/ui_interface.rs index a8e3be980..b8d59ac8f 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -372,10 +372,11 @@ pub fn get_mouse_time() -> f64 { return res; } -#[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn check_mouse_time() { - let sender = SENDER.lock().unwrap(); - allow_err!(sender.send(ipc::Data::MouseMoveTime(0))); + #[cfg(not(any(target_os = "android", target_os = "ios")))]{ + let sender = SENDER.lock().unwrap(); + allow_err!(sender.send(ipc::Data::MouseMoveTime(0))); + } } pub fn get_connect_status() -> Status { From 948580b2882b43e937ab32b3746b0f8c46c16d34 Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 5 Sep 2022 05:32:21 -0700 Subject: [PATCH 16/19] Windows build --- src/client/io_loop.rs | 44 +++++++++++++++++++++------- src/flutter.rs | 9 ++---- src/ui/remote.rs | 8 +---- src/ui_cm_interface.rs | 58 +++++++++++++++++++------------------ src/ui_session_interface.rs | 3 ++ 5 files changed, 71 insertions(+), 51 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 54c3be26e..552fea7a8 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -43,7 +43,7 @@ pub struct Remote { last_update_jobs_status: (Instant, HashMap), first_frame: bool, #[cfg(windows)] - clipboard_file_context: Option>, + clipboard_file_context: Option>, data_count: Arc, frame_count: Arc, video_format: CodecFormat, @@ -107,7 +107,7 @@ impl Remote { #[cfg(not(windows))] let (_tx_holder, mut rx_clip_client) = mpsc::unbounded_channel::(); #[cfg(windows)] - let mut rx_clip_client = get_rx_clip_client().lock().await; + let mut rx_clip_client = clipboard::get_rx_clip_client().lock().await; let mut status_timer = time::interval(Duration::new(1, 0)); @@ -153,7 +153,7 @@ impl Remote { #[cfg(windows)] match _msg { Some((_, clip)) => { - allow_err!(peer.send(&clip_2_msg(clip)).await); + allow_err!(peer.send(&crate::clipboard_file::clip_2_msg(clip)).await); } None => { // unreachable!() @@ -357,7 +357,13 @@ impl Remote { to, job.files().len() ); - self.handler.update_folder_files(job.id(), job.files(), path,!is_remote, true); + self.handler.update_folder_files( + job.id(), + job.files(), + path, + !is_remote, + true, + ); #[cfg(not(windows))] let files = job.files().clone(); #[cfg(windows)] @@ -416,7 +422,13 @@ impl Remote { to, job.files().len() ); - self.handler.update_folder_files(job.id(), job.files(), path,!is_remote, true); + self.handler.update_folder_files( + job.id(), + job.files(), + path, + !is_remote, + true, + ); job.is_last_job = true; self.read_jobs.push(job); self.timer = time::interval(MILLI1); @@ -528,7 +540,13 @@ impl Remote { } else { match fs::get_recursive_files(&path, include_hidden) { Ok(entries) => { - self.handler.update_folder_files(id, &entries, path.clone(),!is_remote, false); + self.handler.update_folder_files( + id, + &entries, + path.clone(), + !is_remote, + false, + ); self.remove_jobs .insert(id, RemoveJob::new(entries, path, sep, is_remote)); } @@ -785,8 +803,8 @@ impl Remote { Some(message::Union::Cliprdr(clip)) => { if !self.handler.lc.read().unwrap().disable_clipboard { if let Some(context) = &mut self.clipboard_file_context { - if let Some(clip) = msg_2_clip(clip) { - server_clip_file(context, 0, clip); + if let Some(clip) = crate::clipboard_file::msg_2_clip(clip) { + clipboard::server_clip_file(context, 0, clip); } } } @@ -804,7 +822,13 @@ impl Remote { fs::transform_windows_path(&mut entries); } } - self.handler.update_folder_files(fd.id, &entries, fd.path, false, fd.id > 0); + self.handler.update_folder_files( + fd.id, + &entries, + fd.path, + false, + fd.id > 0, + ); if let Some(job) = fs::get_job(fd.id, &mut self.write_jobs) { log::info!("job set_files: {:?}", entries); job.set_files(entries); @@ -1132,7 +1156,7 @@ impl Remote { && self.handler.lc.read().unwrap().enable_file_transfer; if enabled == self.clipboard_file_context.is_none() { self.clipboard_file_context = if enabled { - match create_clipboard_file_context(true, false) { + match clipboard::create_cliprdr_context(true, false) { Ok(context) => { log::info!("clipboard context for file transfer created."); Some(context) diff --git a/src/flutter.rs b/src/flutter.rs index a3c7ea70f..1c4ed8869 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -396,13 +396,10 @@ pub mod connection_manager { #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn start_listen_ipc_thread() { - use crate::{ - ipc::start_pa, - ui_cm_interface::{start_ipc, ConnectionManager}, - }; + use crate::ui_cm_interface::{start_ipc, ConnectionManager}; #[cfg(target_os = "linux")] - std::thread::spawn(start_pa); + std::thread::spawn(crate::ipc::start_pa); let cm = ConnectionManager { ui_handler: FlutterHandler {}, @@ -411,7 +408,7 @@ pub mod connection_manager { } #[cfg(target_os = "android")] - use hbb_common::tokio::sync::mpsc::{UnboundedReceiver,UnboundedSender}; + use hbb_common::tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; #[cfg(target_os = "android")] pub fn start_channel( diff --git a/src/ui/remote.rs b/src/ui/remote.rs index f6b3acec6..08430110c 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1,10 +1,7 @@ use std::{ collections::HashMap, ops::{Deref, DerefMut}, - sync::{ - atomic::Ordering, - Arc, Mutex, - }, + sync::{atomic::Ordering, Arc, Mutex}, }; use sciter::{ @@ -40,9 +37,6 @@ lazy_static::lazy_static! { static ref VIDEO: Arc>> = Default::default(); } -#[cfg(windows)] -static mut IS_ALT_GR: bool = false; - /// SciterHandler /// * element /// * close_state for file path when close diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index b9045532b..d416fdd63 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -268,19 +268,18 @@ pub async fn start_ipc(cm: ConnectionManager) { Data::FS(fs) => { handle_fs(fs, &mut write_jobs, &tx).await; } - // TODO ClipbaordFile - // #[cfg(windows)] - // Data::ClipbaordFile(_clip) => { - // tx_file - // .send(ClipboardFileData::Clip((id, _clip))) - // .ok(); - // } - // #[cfg(windows)] - // Data::ClipboardFileEnabled(enabled) => { - // tx_file - // .send(ClipboardFileData::Enable((id, enabled))) - // .ok(); - // } + #[cfg(windows)] + Data::ClipbaordFile(_clip) => { + tx_file + .send(ClipboardFileData::Clip((conn_id, _clip))) + .ok(); + } + #[cfg(windows)] + Data::ClipboardFileEnabled(enabled) => { + tx_file + .send(ClipboardFileData::Enable((conn_id, enabled))) + .ok(); + } _ => { } @@ -316,7 +315,11 @@ pub async fn start_ipc(cm: ConnectionManager) { #[cfg(target_os = "android")] #[tokio::main(flavor = "current_thread")] -pub async fn start_listen(cm: ConnectionManager, mut rx: mpsc::UnboundedReceiver, tx: mpsc::UnboundedSender) { +pub async fn start_listen( + cm: ConnectionManager, + mut rx: mpsc::UnboundedReceiver, + tx: mpsc::UnboundedSender, +) { let mut current_id = 0; let mut write_jobs: Vec = Vec::new(); loop { @@ -596,19 +599,18 @@ fn send_raw(msg: Message, tx: &UnboundedSender) { #[cfg(windows)] #[tokio::main(flavor = "current_thread")] -pub async fn start_clipboard_file( - cm: ConnectionManager, +pub async fn start_clipboard_file( + cm: ConnectionManager, mut rx: mpsc::UnboundedReceiver, ) { let mut cliprdr_context = None; - let mut rx_clip_client = get_rx_clip_client().lock().await; + let mut rx_clip_client = clipboard::get_rx_clip_client().lock().await; loop { tokio::select! { clip_file = rx_clip_client.recv() => match clip_file { Some((conn_id, clip)) => { cmd_inner_send( - &cm, conn_id, Data::ClipbaordFile(clip) ); @@ -620,12 +622,12 @@ pub async fn start_clipboard_file( server_msg = rx.recv() => match server_msg { Some(ClipboardFileData::Clip((conn_id, clip))) => { if let Some(ctx) = cliprdr_context.as_mut() { - server_clip_file(ctx, conn_id, clip); + clipboard::server_clip_file(ctx, conn_id, clip); } } Some(ClipboardFileData::Enable((id, enabled))) => { if enabled && cliprdr_context.is_none() { - cliprdr_context = Some(match create_cliprdr_context(true, false) { + cliprdr_context = Some(match clipboard::create_cliprdr_context(true, false) { Ok(context) => { log::info!("clipboard context for file transfer created."); context @@ -639,10 +641,10 @@ pub async fn start_clipboard_file( } }); } - set_conn_enabled(id, enabled); + clipboard::set_conn_enabled(id, enabled); if !enabled { if let Some(ctx) = cliprdr_context.as_mut() { - empty_clipboard(ctx, id); + clipboard::empty_clipboard(ctx, id); } } } @@ -655,15 +657,15 @@ pub async fn start_clipboard_file( } #[cfg(windows)] -fn cmd_inner_send(cm: &ConnectionManager, id: i32, data: Data) { - let lock = cm.read().unwrap(); +fn cmd_inner_send(id: i32, data: Data) { + let lock = CLIENTS.read().unwrap(); if id != 0 { - if let Some(s) = lock.senders.get(&id) { - allow_err!(s.send(data)); + if let Some(s) = lock.get(&id) { + allow_err!(s.tx.send(data)); } } else { - for s in lock.senders.values() { - allow_err!(s.send(data.clone())); + for s in lock.values() { + allow_err!(s.tx.send(data.clone())); } } } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 9fca2dfbb..717963561 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -26,6 +26,9 @@ use std::sync::{Arc, Mutex, RwLock}; pub static IS_IN: AtomicBool = AtomicBool::new(false); static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); +#[cfg(windows)] +static mut IS_ALT_GR: bool = false; + #[derive(Clone, Default)] pub struct Session { pub cmd: String, From 235eb5415e7acfc62d0414a7e6833e58f83e05eb Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 6 Sep 2022 19:08:45 +0800 Subject: [PATCH 17/19] update file transfer and adjust icon size --- .../lib/desktop/pages/file_manager_page.dart | 51 ++++++++++++------- flutter/lib/models/file_model.dart | 8 +-- flutter/lib/models/model.dart | 15 ++++-- src/client.rs | 1 + src/client/io_loop.rs | 31 +---------- src/flutter_ffi.rs | 2 +- src/ui_session_interface.rs | 26 ++++++++++ 7 files changed, 76 insertions(+), 58 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index b13f40a5f..be0fedc5c 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -59,8 +59,11 @@ class _FileManagerPageState extends State super.initState(); _ffi = FFI(); _ffi.connect(widget.id, isFileTransfer: true); + WidgetsBinding.instance.addPostFrameCallback((_) { + _ffi.dialogManager + .showLoading(translate('Connecting...'), onCancel: closeConnection); + }); Get.put(_ffi, tag: 'ft_${widget.id}'); - // _ffi.ffiModel.updateEventListener(widget.id); if (!Platform.isLinux) { Wakelock.enable(); } @@ -117,7 +120,8 @@ class _FileManagerPageState extends State Widget menu({bool isLocal = false}) { return PopupMenuButton( - icon: Icon(Icons.more_vert), + icon: const Icon(Icons.more_vert), + splashRadius: 20, itemBuilder: (context) { return [ PopupMenuItem( @@ -413,6 +417,7 @@ class _FileManagerPageState extends State /// watch transfer status Widget statusList() { return PreferredSize( + preferredSize: const Size(200, double.infinity), child: Container( margin: const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0), padding: const EdgeInsets.all(8.0), @@ -429,8 +434,8 @@ class _FileManagerPageState extends State children: [ Transform.rotate( angle: item.isRemote ? pi : 0, - child: Icon(Icons.send)), - SizedBox( + child: const Icon(Icons.send)), + const SizedBox( width: 16.0, ), Expanded( @@ -441,7 +446,7 @@ class _FileManagerPageState extends State Tooltip( message: item.jobName, child: Text( - '${item.jobName}', + item.jobName, maxLines: 1, overflow: TextOverflow.ellipsis, )), @@ -455,7 +460,7 @@ class _FileManagerPageState extends State offstage: item.state != JobState.inProgress, child: Text( - '${readableFileSize(item.speed) + "/s"} ')), + '${"${readableFileSize(item.speed)}/s"} ')), Offstage( offstage: item.totalSize <= 0, child: Text( @@ -475,10 +480,12 @@ class _FileManagerPageState extends State onPressed: () { model.resumeJob(item.id); }, - icon: Icon(Icons.restart_alt_rounded)), + splashRadius: 20, + icon: const Icon(Icons.restart_alt_rounded)), ), IconButton( - icon: Icon(Icons.delete), + icon: const Icon(Icons.delete), + splashRadius: 20, onPressed: () { model.jobTable.removeAt(index); model.cancelJob(item.id); @@ -500,8 +507,7 @@ class _FileManagerPageState extends State itemCount: model.jobTable.length, ), ), - ), - preferredSize: Size(200, double.infinity)); + )); } goBack({bool? isLocal}) { @@ -551,12 +557,15 @@ class _FileManagerPageState extends State Row( children: [ IconButton( - onPressed: () { - model.goHome(isLocal: isLocal); - }, - icon: Icon(Icons.home_outlined)), + onPressed: () { + model.goHome(isLocal: isLocal); + }, + icon: const Icon(Icons.home_outlined), + splashRadius: 20, + ), IconButton( - icon: Icon(Icons.arrow_upward), + icon: const Icon(Icons.arrow_upward), + splashRadius: 20, onPressed: () { goBack(isLocal: isLocal); }, @@ -622,13 +631,15 @@ class _FileManagerPageState extends State ), )) ], - child: Icon(Icons.search), + splashRadius: 20, + child: const Icon(Icons.search), ), IconButton( onPressed: () { model.refresh(isLocal: isLocal); }, - icon: Icon(Icons.refresh)), + splashRadius: 20, + icon: const Icon(Icons.refresh)), ], ), Row( @@ -686,6 +697,7 @@ class _FileManagerPageState extends State ); }); }, + splashRadius: 20, icon: const Icon(Icons.create_new_folder_outlined)), IconButton( onPressed: () async { @@ -695,7 +707,8 @@ class _FileManagerPageState extends State await (model.removeAction(items, isLocal: isLocal)); items.clear(); }, - icon: Icon(Icons.delete_forever_outlined)), + splashRadius: 20, + icon: const Icon(Icons.delete_forever_outlined)), ], ), ), @@ -707,7 +720,7 @@ class _FileManagerPageState extends State }, icon: Transform.rotate( angle: isLocal ? 0 : pi, - child: Icon( + child: const Icon( Icons.send, ), ), diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index dedca5efa..3246346be 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -360,9 +360,9 @@ class FileModel extends ChangeNotifier { Future refresh({bool? isLocal}) async { if (isDesktop) { isLocal = isLocal ?? _isLocal; - await isLocal - ? openDirectory(currentLocalDir.path, isLocal: isLocal) - : openDirectory(currentRemoteDir.path, isLocal: isLocal); + isLocal + ? await openDirectory(currentLocalDir.path, isLocal: isLocal) + : await openDirectory(currentRemoteDir.path, isLocal: isLocal); } else { await openDirectory(currentDir.path); } @@ -393,7 +393,7 @@ class FileModel extends ChangeNotifier { } notifyListeners(); } catch (e) { - debugPrint("Failed to openDirectory ${path} :$e"); + debugPrint("Failed to openDirectory $path: $e"); } } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 1993145e7..7d8cdc203 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -326,8 +326,8 @@ class FfiModel with ChangeNotifier { await bind.sessionGetOption(id: peerId, arg: "touch-mode") != ''; } - if (evt['is_file_transfer'] == "true") { - // TODO is file transfer + if (parent.target != null && + parent.target!.connType == ConnType.fileTransfer) { parent.target?.fileModel.onReady(); } else { _pi.displays = []; @@ -916,6 +916,8 @@ extension ToString on MouseButtons { } } +enum ConnType { defaultConn, fileTransfer, portForward, rdp } + /// FFI class for communicating with the Rust core. class FFI { var id = ""; @@ -924,6 +926,7 @@ class FFI { var alt = false; var command = false; var version = ""; + var connType = ConnType.defaultConn; /// dialogManager use late to ensure init after main page binding [globalKey] late final dialogManager = OverlayDialogManager(); @@ -1055,9 +1058,11 @@ class FFI { double tabBarHeight = 0.0}) { assert(!(isFileTransfer && isPortForward), "more than one connect type"); if (isFileTransfer) { - id = 'ft_${id}'; + connType = ConnType.fileTransfer; + id = 'ft_$id'; } else if (isPortForward) { - id = 'pf_${id}'; + connType = ConnType.portForward; + id = 'pf_$id'; } else { chatModel.resetClientMode(); canvasModel.id = id; @@ -1086,7 +1091,7 @@ class FFI { // every instance will bind a stream this.id = id; if (isFileTransfer) { - this.fileModel.initFileFetcher(); + fileModel.initFileFetcher(); } } diff --git a/src/client.rs b/src/client.rs index 32c0003fd..a4a864846 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1665,6 +1665,7 @@ pub async fn handle_login_from_ui( /// Interface for client to send data and commands. #[async_trait] pub trait Interface: Send + Clone + 'static + Sized { + /// Send message data to remote peer. fn send(&self, data: Data); fn msgbox(&self, msgtype: &str, title: &str, text: &str); fn handle_login_error(&mut self, err: &str) -> bool; diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 552fea7a8..f33f740a3 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -269,33 +269,6 @@ impl Remote { Some(tx) } - // TODO - fn load_last_jobs(&mut self) { - self.handler.clear_all_jobs(); - let pc = self.handler.load_config(); - if pc.transfer.write_jobs.is_empty() && pc.transfer.read_jobs.is_empty() { - // no last jobs - return; - } - // TODO: can add a confirm dialog - let mut cnt = 1; - for job_str in pc.transfer.read_jobs.iter() { - if !job_str.is_empty() { - self.handler.load_last_job(cnt, job_str); - cnt += 1; - log::info!("restore read_job: {:?}", job_str); - } - } - for job_str in pc.transfer.write_jobs.iter() { - if !job_str.is_empty() { - self.handler.load_last_job(cnt, job_str); - cnt += 1; - log::info!("restore write_job: {:?}", job_str); - } - } - self.handler.update_transfer_list(); - } - async fn handle_msg_from_ui(&mut self, data: Data, peer: &mut Stream) -> bool { match data { Data::Close => { @@ -768,7 +741,7 @@ impl Remote { } if self.handler.is_file_transfer() { - self.load_last_jobs(); + self.handler.load_last_jobs(); } } _ => {} @@ -827,7 +800,7 @@ impl Remote { &entries, fd.path, false, - fd.id > 0, + false, ); if let Some(job) = fs::get_job(fd.id, &mut self.write_jobs) { log::info!("job set_files: {:?}", entries); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 5da94c3c1..ef2aaeaa1 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -366,7 +366,7 @@ pub fn session_get_platform(id: String, is_remote: bool) -> String { pub fn session_load_last_transfer_jobs(id: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - // return session.load_last_jobs(); + return session.load_last_jobs(); } else { // a tip for flutter dev eprintln!( diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 717963561..f1444f4c3 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -532,6 +532,32 @@ impl Session { pub fn close(&self) { self.send(Data::Close); } + + pub fn load_last_jobs(&self) { + self.clear_all_jobs(); + let pc = self.load_config(); + if pc.transfer.write_jobs.is_empty() && pc.transfer.read_jobs.is_empty() { + // no last jobs + return; + } + // TODO: can add a confirm dialog + let mut cnt = 1; + for job_str in pc.transfer.read_jobs.iter() { + if !job_str.is_empty() { + self.load_last_job(cnt, job_str); + cnt += 1; + log::info!("restore read_job: {:?}", job_str); + } + } + for job_str in pc.transfer.write_jobs.iter() { + if !job_str.is_empty() { + self.load_last_job(cnt, job_str); + cnt += 1; + log::info!("restore write_job: {:?}", job_str); + } + } + self.update_transfer_list(); + } } pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { From 05218ecabcdd019f1db42e082f800da727e7ebd5 Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 6 Sep 2022 19:56:35 +0800 Subject: [PATCH 18/19] fix sciter confirm_delete_files bug --- src/client/io_loop.rs | 1 - src/ui/remote.rs | 4 ---- 2 files changed, 5 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index f33f740a3..cc2ca1dae 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -452,7 +452,6 @@ impl Remote { file_num, job.files[i].name.clone(), ); - self.handler.confirm_delete_files(id, file_num); } } } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 08430110c..c6a36f5c6 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -709,18 +709,15 @@ impl SciterSession { } pub fn make_fd(id: i32, entries: &Vec, only_count: bool) -> Value { - log::debug!("make_fd"); let mut m = Value::map(); m.set_item("id", id); let mut a = Value::array(0); let mut n: u64 = 0; for entry in entries { - log::debug!("for"); n += entry.size; if only_count { continue; } - log::debug!("for1"); let mut e = Value::map(); e.set_item("name", entry.name.to_owned()); let tmp = entry.entry_type.value(); @@ -734,6 +731,5 @@ pub fn make_fd(id: i32, entries: &Vec, only_count: bool) -> Value { } m.set_item("num_entries", entries.len() as i32); m.set_item("total_size", n as f64); - log::debug!("make_fd end"); m } From 468527775eb24e184400e3a6a850adeadeded992 Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 6 Sep 2022 21:10:59 +0800 Subject: [PATCH 19/19] fix sciter can't update connect status bug --- src/ui.rs | 96 +----------------------------------------------- src/ui/index.tis | 1 + 2 files changed, 2 insertions(+), 95 deletions(-) diff --git a/src/ui.rs b/src/ui.rs index b8b136c45..96cf21f20 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -87,7 +87,7 @@ pub fn start(args: &mut [String]) { } #[cfg(windows)] if args.len() > 0 && args[0] == "--tray" { - let options = check_connect_status(false).1; + let options = crate::ui_interface::check_connect_status(false).1; crate::tray::start_tray(options); return; } @@ -664,79 +664,6 @@ pub fn check_zombie(childs: Childs) { } } -// notice: avoiding create ipc connecton repeatly, -// because windows named pipe has serious memory leak issue. -#[tokio::main(flavor = "current_thread")] -async fn check_connect_status_( - reconnect: bool, - status: Arc>, - options: Arc>>, - rx: mpsc::UnboundedReceiver, - password: Arc>, -) { - let mut key_confirmed = false; - let mut rx = rx; - let mut mouse_time = 0; - let mut id = "".to_owned(); - loop { - if let Ok(mut c) = ipc::connect(1000, "").await { - let mut timer = time::interval(time::Duration::from_secs(1)); - loop { - tokio::select! { - res = c.next() => { - match res { - Err(err) => { - log::error!("ipc connection closed: {}", err); - break; - } - Ok(Some(ipc::Data::MouseMoveTime(v))) => { - mouse_time = v; - status.lock().unwrap().2 = v; - } - Ok(Some(ipc::Data::Options(Some(v)))) => { - *options.lock().unwrap() = v - } - Ok(Some(ipc::Data::Config((name, Some(value))))) => { - if name == "id" { - id = value; - } else if name == "temporary-password" { - *password.lock().unwrap() = value; - } - } - Ok(Some(ipc::Data::OnlineStatus(Some((mut x, c))))) => { - if x > 0 { - x = 1 - } - key_confirmed = c; - *status.lock().unwrap() = (x as _, key_confirmed, mouse_time, id.clone()); - } - _ => {} - } - } - Some(data) = rx.recv() => { - allow_err!(c.send(&data).await); - } - _ = timer.tick() => { - c.send(&ipc::Data::OnlineStatus(None)).await.ok(); - c.send(&ipc::Data::Options(None)).await.ok(); - c.send(&ipc::Data::Config(("id".to_owned(), None))).await.ok(); - c.send(&ipc::Data::Config(("temporary-password".to_owned(), None))).await.ok(); - } - } - } - } - if !reconnect { - options - .lock() - .unwrap() - .insert("ipc-closed".to_owned(), "Y".to_owned()); - break; - } - *status.lock().unwrap() = (-1, key_confirmed, mouse_time, id.clone()); - sleep(1.).await; - } -} - #[cfg(not(target_os = "linux"))] fn get_sound_inputs() -> Vec { let mut out = Vec::new(); @@ -763,27 +690,6 @@ fn get_sound_inputs() -> Vec { .collect() } -fn check_connect_status( - reconnect: bool, -) -> ( - Arc>, - Arc>>, - mpsc::UnboundedSender, - Arc>, -) { - let status = Arc::new(Mutex::new((0, false, 0, "".to_owned()))); - let options = Arc::new(Mutex::new(Config::get_options())); - let cloned = status.clone(); - let cloned_options = options.clone(); - let (tx, rx) = mpsc::unbounded_channel::(); - let password = Arc::new(Mutex::new(String::default())); - let cloned_password = password.clone(); - std::thread::spawn(move || { - check_connect_status_(reconnect, cloned, cloned_options, rx, cloned_password) - }); - (status, options, tx, password) -} - const INVALID_FORMAT: &'static str = "Invalid format"; const UNKNOWN_ERROR: &'static str = "Unknown error"; diff --git a/src/ui/index.tis b/src/ui/index.tis index 256f00c44..d0a9d29a8 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -1055,6 +1055,7 @@ function showSettings() { } function checkConnectStatus() { + handler.check_mouse_time(); // trigger connection status updater self.timer(1s, function() { var tmp = !!handler.get_option("stop-service"); if (tmp != service_stopped) {