diff --git a/flutter/lib/common/shared_state.dart b/flutter/lib/common/shared_state.dart index 67752d888..9e741846f 100644 --- a/flutter/lib/common/shared_state.dart +++ b/flutter/lib/common/shared_state.dart @@ -1,7 +1,8 @@ import 'package:get/get.dart'; import '../consts.dart'; -import '../models/platform_model.dart'; + +// TODO: A lot of dup code. class PrivacyModeState { static String tag(String id) => 'privacy_mode_$id'; @@ -156,3 +157,25 @@ class KeyboardEnabledState { static RxBool find(String id) => Get.find(tag: tag(id)); } + +class RemoteCursorMovedState { + static String tag(String id) => 'remote_cursor_moved_$id'; + + static void init(String id) { + final key = tag(id); + if (!Get.isRegistered(tag: key)) { + // Server side, default true + 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)); +} diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index e4f6527ca..b113d8a56 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -22,10 +22,10 @@ import '../../models/platform_model.dart'; /// Connection page for connecting to a remote peer. class ConnectionPage extends StatefulWidget { - ConnectionPage({Key? key}) : super(key: key); + const ConnectionPage({Key? key}) : super(key: key); @override - _ConnectionPageState createState() => _ConnectionPageState(); + State createState() => _ConnectionPageState(); } /// State for the connection page. @@ -101,7 +101,7 @@ class _ConnectionPageState extends State { ], ).marginSymmetric(horizontal: 22), ), - Divider(), + const Divider(), SizedBox(height: 50, child: Obx(() => buildStatus())) .paddingSymmetric(horizontal: 12.0) ]), diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 3ce956c23..3cda8aa60 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -19,11 +19,18 @@ import 'package:tray_manager/tray_manager.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:window_manager/window_manager.dart'; +class _MenubarTheme { + static const Color commonColor = MyTheme.accent; + // kMinInteractiveDimension + static const double height = 25.0; + static const double dividerHeight = 12.0; +} + class DesktopHomePage extends StatefulWidget { - DesktopHomePage({Key? key}) : super(key: key); + const DesktopHomePage({Key? key}) : super(key: key); @override - State createState() => _DesktopHomePageState(); + State createState() => _DesktopHomePageState(); } const borderColor = Color(0xFF2F65BA); @@ -109,7 +116,7 @@ class _DesktopHomePageState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( + Container( height: 25, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -135,11 +142,11 @@ class _DesktopHomePageState extends State child: TextFormField( controller: model.serverId, readOnly: true, - decoration: const InputDecoration( + decoration: InputDecoration( border: InputBorder.none, contentPadding: EdgeInsets.only(bottom: 20), ), - style: const TextStyle( + style: TextStyle( fontSize: 22, ), ), @@ -322,18 +329,19 @@ class _DesktopHomePageState extends State onTap: () => bind.mainUpdateTemporaryPassword(), onHover: (value) => refreshHover.value = value, ), - FutureBuilder( - future: buildPasswordPopupMenu(context), - builder: (context, snapshot) { - if (snapshot.hasError) { - print("${snapshot.error}"); - } - if (snapshot.hasData) { - return snapshot.data!; - } else { - return Offstage(); - } - }) + const _PasswordPopupMenu(), + // FutureBuilder( + // future: buildPasswordPopupMenu(context), + // builder: (context, snapshot) { + // if (snapshot.hasError) { + // print("${snapshot.error}"); + // } + // if (snapshot.hasData) { + // return snapshot.data!; + // } else { + // return Offstage(); + // } + // }) ], ), ], @@ -366,7 +374,7 @@ class _DesktopHomePageState extends State ), ], ), - onTap: () => gFFI.serverModel.verificationMethod = value, + onTap: () => gFFI.serverModel.setVerificationMethod(value), ); final temporary_enabled = gFFI.serverModel.verificationMethod != kUsePermanentPassword; @@ -403,8 +411,11 @@ class _DesktopHomePageState extends State onTap: () { if (gFFI.serverModel.temporaryPasswordLength != e) { - gFFI.serverModel.temporaryPasswordLength = e; - bind.mainUpdateTemporaryPassword(); + () async { + await gFFI.serverModel + .setTemporaryPasswordLength(e); + await bind.mainUpdateTemporaryPassword(); + }(); } }, )) @@ -1035,3 +1046,120 @@ void setPasswordDialog() async { ); }); } + +class _PasswordPopupMenu extends StatefulWidget { + const _PasswordPopupMenu({Key? key}) : super(key: key); + + @override + State<_PasswordPopupMenu> createState() => _PasswordPopupMenuState(); +} + +class _PasswordPopupMenuState extends State<_PasswordPopupMenu> { + final RxBool _tempEnabled = true.obs; + final RxBool _permEnabled = true.obs; + + List> _buildMenus() { + return >[ + MenuEntryRadios( + text: translate('Password type'), + optionsGetter: () => [ + MenuEntryRadioOption( + text: translate('Use temporary password'), + value: kUseTemporaryPassword), + MenuEntryRadioOption( + text: translate('Use permanent password'), + value: kUsePermanentPassword), + MenuEntryRadioOption( + text: translate('Use both passwords'), + value: kUseBothPasswords), + ], + curOptionGetter: () async { + return gFFI.serverModel.verificationMethod; + }, + optionSetter: (String oldValue, String newValue) async { + await bind.mainSetOption( + key: "verification-method", value: newValue); + await gFFI.serverModel.updatePasswordModel(); + setState(() { + _tempEnabled.value = + gFFI.serverModel.verificationMethod != kUsePermanentPassword; + _permEnabled.value = + gFFI.serverModel.verificationMethod != kUseTemporaryPassword; + }); + }), + MenuEntryDivider(), + MenuEntryButton( + enabled: _permEnabled, + childBuilder: (TextStyle? style) => Text( + translate('Set permanent password'), + style: style, + ), + proc: () { + setPasswordDialog(); + }, + dismissOnClicked: true, + ), + MenuEntrySubMenu( + enabled: _tempEnabled, + text: translate('Set temporary password length'), + entries: [ + MenuEntryRadios( + enabled: _tempEnabled, + text: translate(''), + optionsGetter: () => [ + MenuEntryRadioOption( + text: translate('6'), + value: '6', + enabled: _tempEnabled, + ), + MenuEntryRadioOption( + text: translate('8'), + value: '8', + enabled: _tempEnabled, + ), + MenuEntryRadioOption( + text: translate('10'), + value: '10', + enabled: _tempEnabled, + ), + ], + curOptionGetter: () async { + return gFFI.serverModel.temporaryPasswordLength; + }, + optionSetter: (String oldValue, String newValue) async { + if (oldValue != newValue) { + await gFFI.serverModel.setTemporaryPasswordLength(newValue); + await gFFI.serverModel.updatePasswordModel(); + } + }), + ]) + ]; + } + + @override + Widget build(BuildContext context) { + final editHover = false.obs; + return mod_menu.PopupMenuButton( + padding: EdgeInsets.zero, + onHover: (v) => editHover.value = v, + tooltip: translate(''), + position: mod_menu.PopupMenuPosition.overSide, + itemBuilder: (BuildContext context) => _buildMenus() + .map((entry) => entry.build( + context, + const MenuConfig( + commonColor: _MenubarTheme.commonColor, + height: _MenubarTheme.height, + dividerHeight: _MenubarTheme.dividerHeight, + ))) + .expand((i) => i) + .toList(), + child: Obx(() => Icon(Icons.edit, + size: 22, + color: editHover.value + ? MyTheme.color(context).text + : const Color(0xFFDDDDDD)) + .marginOnly(bottom: 2)), + ); + } +} diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index b4bb00ab8..48fc0a5e7 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -314,8 +313,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { translate("Use permanent password"), translate("Use both passwords"), ]; - bool tmp_enabled = model.verificationMethod != kUsePermanentPassword; - bool perm_enabled = model.verificationMethod != kUseTemporaryPassword; + bool tmpEnabled = model.verificationMethod != kUsePermanentPassword; + bool permEnabled = model.verificationMethod != kUseTemporaryPassword; String currentValue = values[keys.indexOf(model.verificationMethod)]; List radios = values .map((value) => _Radio( @@ -324,16 +323,24 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { groupValue: currentValue, label: value, onChanged: ((value) { - model.verificationMethod = keys[values.indexOf(value)]; + () async { + await model + .setVerificationMethod(keys[values.indexOf(value)]); + await model.updatePasswordModel(); + }(); }), enabled: !locked, )) .toList(); - var onChanged = tmp_enabled && !locked + var onChanged = tmpEnabled && !locked ? (value) { - if (value != null) - model.temporaryPasswordLength = value.toString(); + if (value != null) { + () async { + await model.setTemporaryPasswordLength(value.toString()); + await model.updatePasswordModel(); + }(); + } } : null; List lengthRadios = ['6', '8', '10'] @@ -365,10 +372,10 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { ...lengthRadios, ], ), - enabled: tmp_enabled && !locked), + enabled: tmpEnabled && !locked), radios[1], _SubButton('Set permanent password', setPasswordDialog, - perm_enabled && !locked), + permEnabled && !locked), radios[2], ]); }))); diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index be0fedc5c..bd6e4cb63 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -17,7 +17,7 @@ import '../../models/platform_model.dart'; enum LocationStatus { bread, textField } class FileManagerPage extends StatefulWidget { - FileManagerPage({Key? key, required this.id}) : super(key: key); + const FileManagerPage({Key? key, required this.id}) : super(key: key); final String id; @override diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 18ea039a7..d6f01e55f 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -21,8 +21,8 @@ class FileManagerTabPage extends StatefulWidget { class _FileManagerTabPageState extends State { DesktopTabController get tabController => Get.find(); - static final IconData selectedIcon = Icons.file_copy_sharp; - static final IconData unselectedIcon = Icons.file_copy_outlined; + static const IconData selectedIcon = Icons.file_copy_sharp; + static const IconData unselectedIcon = Icons.file_copy_outlined; _FileManagerTabPageState(Map params) { Get.put(DesktopTabController(tabType: DesktopTabType.fileTransfer)); diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 23e4e4900..90c729abe 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/gestures.dart'; @@ -8,6 +9,7 @@ import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:wakelock/wakelock.dart'; +import 'package:flutter_custom_cursor/flutter_custom_cursor.dart'; // import 'package:window_manager/window_manager.dart'; @@ -22,7 +24,7 @@ import '../../common/shared_state.dart'; final initText = '\1' * 1024; class RemotePage extends StatefulWidget { - RemotePage({ + const RemotePage({ Key? key, required this.id, required this.tabBarHeight, @@ -32,7 +34,7 @@ class RemotePage extends StatefulWidget { final double tabBarHeight; @override - _RemotePageState createState() => _RemotePageState(); + State createState() => _RemotePageState(); } class _RemotePageState extends State @@ -41,6 +43,7 @@ class _RemotePageState extends State String _value = ''; final _cursorOverImage = false.obs; late RxBool _showRemoteCursor; + late RxBool _remoteCursorMoved; late RxBool _keyboardEnabled; final FocusNode _mobileFocusNode = FocusNode(); @@ -60,8 +63,10 @@ class _RemotePageState extends State CurrentDisplayState.init(id); KeyboardEnabledState.init(id); ShowRemoteCursorState.init(id); + RemoteCursorMovedState.init(id); _showRemoteCursor = ShowRemoteCursorState.find(id); _keyboardEnabled = KeyboardEnabledState.find(id); + _remoteCursorMoved = RemoteCursorMovedState.find(id); } void _removeStates(String id) { @@ -70,6 +75,7 @@ class _RemotePageState extends State CurrentDisplayState.delete(id); ShowRemoteCursorState.delete(id); KeyboardEnabledState.delete(id); + RemoteCursorMovedState.delete(id); } @override @@ -395,13 +401,14 @@ class _RemotePageState extends State id: widget.id, cursorOverImage: _cursorOverImage, keyboardEnabled: _keyboardEnabled, + remoteCursorMoved: _remoteCursorMoved, listenerBuilder: _buildImageListener, ); })) ]; paints.add(Obx(() => Visibility( - visible: _keyboardEnabled.isTrue || _showRemoteCursor.isTrue, + visible: _showRemoteCursor.isTrue && _remoteCursorMoved.isTrue, child: CursorPaint( id: widget.id, )))); @@ -459,6 +466,7 @@ class ImagePaint extends StatelessWidget { final String id; final Rx cursorOverImage; final Rx keyboardEnabled; + final Rx remoteCursorMoved; final Widget Function(Widget)? listenerBuilder; final ScrollController _horizontal = ScrollController(); final ScrollController _vertical = ScrollController(); @@ -468,6 +476,7 @@ class ImagePaint extends StatelessWidget { required this.id, required this.cursorOverImage, required this.keyboardEnabled, + required this.remoteCursorMoved, this.listenerBuilder}) : super(key: key); @@ -475,6 +484,7 @@ class ImagePaint extends StatelessWidget { Widget build(BuildContext context) { final m = Provider.of(context); var c = Provider.of(context); + final cursor = Provider.of(context); final s = c.scale; if (c.scrollStyle == ScrollStyle.scrollbar) { final imageWidget = SizedBox( @@ -483,6 +493,8 @@ class ImagePaint extends StatelessWidget { child: CustomPaint( painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s), )); + + Rx pos = Rx(const Offset(0.0, 0.0)); return Center( child: NotificationListener( onNotification: (notification) { @@ -498,9 +510,22 @@ class ImagePaint extends StatelessWidget { return false; }, child: Obx(() => MouseRegion( - cursor: (keyboardEnabled.isTrue && cursorOverImage.isTrue) - ? SystemMouseCursors.none + cursor: (cursorOverImage.isTrue && keyboardEnabled.isTrue) + ? (remoteCursorMoved.isTrue + ? SystemMouseCursors.none + : (cursor.pngData != null + ? FlutterCustomMemoryImageCursor( + pixbuf: cursor.pngData!, + hotx: cursor.hotx, + hoty: cursor.hoty, + imageWidth: (cursor.image!.width * s).toInt(), + imageHeight: (cursor.image!.height * s).toInt(), + ) + : MouseCursor.defer)) : MouseCursor.defer, + onHover: (evt) { + pos.value = evt.position; + }, child: _buildCrossScrollbar(_buildListener(imageWidget)))), ), ); diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index ac2fb7caa..08f3e5836 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -13,8 +13,10 @@ import '../../models/platform_model.dart'; import '../../models/server_model.dart'; class DesktopServerPage extends StatefulWidget { + const DesktopServerPage({Key? key}) : super(key: key); + @override - State createState() => _DesktopServerPageState(); + State createState() => _DesktopServerPageState(); } class _DesktopServerPageState extends State diff --git a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart index a9aec932b..8b0acba9a 100644 --- a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart +++ b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart @@ -1031,6 +1031,7 @@ class PopupMenuButton extends StatefulWidget { Key? key, required this.itemBuilder, this.initialValue, + this.onHover, this.onSelected, this.onCanceled, this.tooltip, @@ -1061,6 +1062,9 @@ class PopupMenuButton extends StatefulWidget { /// The value of the menu item, if any, that should be highlighted when the menu opens. final T? initialValue; + /// Called when the user hovers this button. + final ValueChanged? onHover; + /// Called when the user selects a value from the popup menu created by this button. /// /// If the popup menu is dismissed without selecting a value, [onCanceled] is @@ -1273,18 +1277,20 @@ class PopupMenuButtonState extends State> { assert(debugCheckHasMaterialLocalizations(context)); - if (widget.child != null) + if (widget.child != null) { return Tooltip( message: widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip, child: InkWell( onTap: widget.enabled ? showButtonMenu : null, + onHover: widget.onHover, canRequestFocus: _canRequestFocus, radius: widget.splashRadius, enableFeedback: enableFeedback, child: widget.child, ), ); + } return IconButton( icon: widget.icon ?? Icon(Icons.adaptive.more), diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index 13ab92ffe..fb1c59891 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -427,7 +427,7 @@ abstract class BasePeerCard extends StatelessWidget { alignment: Alignment.centerRight, child: IconButton( padding: EdgeInsets.zero, - icon: Icon(Icons.edit), + icon: const Icon(Icons.edit), onPressed: () => _rdpDialog(id), ), )) @@ -440,6 +440,20 @@ abstract class BasePeerCard extends StatelessWidget { ); } + @protected + MenuEntryBase _wolAction(String id) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('WOL'), + style: style, + ), + proc: () { + bind.mainWol(id: id); + }, + dismissOnClicked: true, + ); + } + @protected Future> _forceAlwaysRelayAction(String id) async { const option = 'force-always-relay'; @@ -620,11 +634,16 @@ class RecentPeerCard extends BasePeerCard { _transferFileAction(context, peer.id), _tcpTunnelingAction(context, peer.id), ]; + MenuEntryBase? rdpAction; if (peer.platform == 'Windows') { - menuItems.add(_rdpAction(context, peer.id)); + rdpAction = _rdpAction(context, peer.id); } - menuItems.add(MenuEntryDivider()); menuItems.add(await _forceAlwaysRelayAction(peer.id)); + if (rdpAction != null) { + menuItems.add(rdpAction); + } + menuItems.add(_wolAction(peer.id)); + menuItems.add(MenuEntryDivider()); menuItems.add(_renameAction(peer.id, false)); menuItems.add(_removeAction(peer.id, () async { await bind.mainLoadRecentPeers(); @@ -647,10 +666,16 @@ class FavoritePeerCard extends BasePeerCard { _transferFileAction(context, peer.id), _tcpTunnelingAction(context, peer.id), ]; + MenuEntryBase? rdpAction; if (peer.platform == 'Windows') { - menuItems.add(_rdpAction(context, peer.id)); + rdpAction = _rdpAction(context, peer.id); } menuItems.add(await _forceAlwaysRelayAction(peer.id)); + if (rdpAction != null) { + menuItems.add(rdpAction); + } + menuItems.add(_wolAction(peer.id)); + menuItems.add(MenuEntryDivider()); menuItems.add(_renameAction(peer.id, false)); menuItems.add(_removeAction(peer.id, () async { await bind.mainLoadFavPeers(); @@ -673,10 +698,16 @@ class DiscoveredPeerCard extends BasePeerCard { _transferFileAction(context, peer.id), _tcpTunnelingAction(context, peer.id), ]; + MenuEntryBase? rdpAction; if (peer.platform == 'Windows') { - menuItems.add(_rdpAction(context, peer.id)); + rdpAction = _rdpAction(context, peer.id); } menuItems.add(await _forceAlwaysRelayAction(peer.id)); + if (rdpAction != null) { + menuItems.add(rdpAction); + } + menuItems.add(_wolAction(peer.id)); + menuItems.add(MenuEntryDivider()); menuItems.add(_renameAction(peer.id, false)); menuItems.add(_removeAction(peer.id, () async { await bind.mainLoadLanPeers(); @@ -698,10 +729,16 @@ class AddressBookPeerCard extends BasePeerCard { _transferFileAction(context, peer.id), _tcpTunnelingAction(context, peer.id), ]; + MenuEntryBase? rdpAction; if (peer.platform == 'Windows') { - menuItems.add(_rdpAction(context, peer.id)); + rdpAction = _rdpAction(context, peer.id); } menuItems.add(await _forceAlwaysRelayAction(peer.id)); + if (rdpAction != null) { + menuItems.add(rdpAction); + } + menuItems.add(_wolAction(peer.id)); + menuItems.add(MenuEntryDivider()); menuItems.add(_renameAction(peer.id, false)); menuItems.add(_removeAction(peer.id, () async {})); menuItems.add(_unrememberPasswordAction(peer.id)); diff --git a/flutter/lib/desktop/widgets/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart index ea678673a..0d469043f 100644 --- a/flutter/lib/desktop/widgets/popup_menu.dart +++ b/flutter/lib/desktop/widgets/popup_menu.dart @@ -12,7 +12,7 @@ class PopupMenuChildrenItem extends mod_menu.PopupMenuEntry { key, this.height = kMinInteractiveDimension, this.padding, - this.enable = true, + this.enabled, this.textStyle, this.onTap, this.position = mod_menu.PopupMenuPosition.overSide, @@ -25,7 +25,7 @@ class PopupMenuChildrenItem extends mod_menu.PopupMenuEntry { final Offset offset; final TextStyle? textStyle; final EdgeInsets? padding; - final bool enable; + final RxBool? enabled; final void Function()? onTap; final List> Function(BuildContext) itemBuilder; final Widget child; @@ -56,25 +56,27 @@ class MyPopupMenuItemState> TextStyle style = widget.textStyle ?? popupMenuTheme.textStyle ?? theme.textTheme.subtitle1!; - - return mod_menu.PopupMenuButton( - enabled: widget.enable, - position: widget.position, - offset: widget.offset, - onSelected: handleTap, - itemBuilder: widget.itemBuilder, - padding: EdgeInsets.zero, - child: AnimatedDefaultTextStyle( - style: style, - duration: kThemeChangeDuration, - child: Container( - alignment: AlignmentDirectional.centerStart, - constraints: BoxConstraints(minHeight: widget.height), - padding: widget.padding ?? const EdgeInsets.symmetric(horizontal: 16), - child: widget.child, + return Obx(() { + return mod_menu.PopupMenuButton( + enabled: widget.enabled != null ? widget.enabled!.value : true, + position: widget.position, + offset: widget.offset, + onSelected: handleTap, + itemBuilder: widget.itemBuilder, + padding: EdgeInsets.zero, + child: AnimatedDefaultTextStyle( + style: style, + duration: kThemeChangeDuration, + child: Container( + alignment: AlignmentDirectional.centerStart, + constraints: BoxConstraints(minHeight: widget.height), + padding: + widget.padding ?? const EdgeInsets.symmetric(horizontal: 16), + child: widget.child, + ), ), - ), - ); + ); + }); } } @@ -98,8 +100,12 @@ class MenuConfig { abstract class MenuEntryBase { bool dismissOnClicked; + RxBool? enabled; - MenuEntryBase({this.dismissOnClicked = false}); + MenuEntryBase({ + this.dismissOnClicked = false, + this.enabled, + }); List> build(BuildContext context, MenuConfig conf); } @@ -119,9 +125,14 @@ class MenuEntryRadioOption { String text; String value; bool dismissOnClicked; + RxBool? enabled; - MenuEntryRadioOption( - {required this.text, required this.value, this.dismissOnClicked = false}); + MenuEntryRadioOption({ + required this.text, + required this.value, + this.dismissOnClicked = false, + this.enabled, + }); } typedef RadioOptionsGetter = List Function(); @@ -138,13 +149,14 @@ class MenuEntryRadios extends MenuEntryBase { final RadioOptionSetter optionSetter; final RxString _curOption = "".obs; - MenuEntryRadios( - {required this.text, - required this.optionsGetter, - required this.curOptionGetter, - required this.optionSetter, - dismissOnClicked = false}) - : super(dismissOnClicked: dismissOnClicked) { + MenuEntryRadios({ + required this.text, + required this.optionsGetter, + required this.curOptionGetter, + required this.optionSetter, + dismissOnClicked = false, + RxBool? enabled, + }) : super(dismissOnClicked: dismissOnClicked, enabled: enabled) { () async { _curOption.value = await curOptionGetter(); }(); @@ -220,13 +232,17 @@ class MenuEntrySubRadios extends MenuEntryBase { final RadioOptionSetter optionSetter; final RxString _curOption = "".obs; - MenuEntrySubRadios( - {required this.text, - required this.optionsGetter, - required this.curOptionGetter, - required this.optionSetter, - dismissOnClicked = false}) - : super(dismissOnClicked: dismissOnClicked) { + MenuEntrySubRadios({ + required this.text, + required this.optionsGetter, + required this.curOptionGetter, + required this.optionSetter, + dismissOnClicked = false, + RxBool? enabled, + }) : super( + dismissOnClicked: dismissOnClicked, + enabled: enabled, + ) { () async { _curOption.value = await curOptionGetter(); }(); @@ -293,6 +309,7 @@ class MenuEntrySubRadios extends MenuEntryBase { BuildContext context, MenuConfig conf) { return [ PopupMenuChildrenItem( + enabled: super.enabled, padding: EdgeInsets.zero, height: conf.height, itemBuilder: (BuildContext context) => @@ -325,9 +342,14 @@ typedef SwitchSetter = Future Function(bool); abstract class MenuEntrySwitchBase extends MenuEntryBase { final String text; + final Rx? textStyle; - MenuEntrySwitchBase({required this.text, required dismissOnClicked}) - : super(dismissOnClicked: dismissOnClicked); + MenuEntrySwitchBase({ + required this.text, + required dismissOnClicked, + this.textStyle, + RxBool? enabled, + }) : super(dismissOnClicked: dismissOnClicked, enabled: enabled); RxBool get curOption; Future setOption(bool option); @@ -344,14 +366,23 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { alignment: AlignmentDirectional.centerStart, height: conf.height, child: Row(children: [ - // const SizedBox(width: MenuConfig.midPadding), - Text( - text, - style: TextStyle( - color: MyTheme.color(context).text, - fontSize: MenuConfig.fontSize, - fontWeight: FontWeight.normal), - ), + () { + if (textStyle != null) { + final style = textStyle!; + return Obx(() => Text( + text, + style: style.value, + )); + } else { + return Text( + text, + style: const TextStyle( + color: Colors.black, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal), + ); + } + }(), Expanded( child: Align( alignment: Alignment.centerRight, @@ -384,12 +415,19 @@ class MenuEntrySwitch extends MenuEntrySwitchBase { final SwitchSetter setter; final RxBool _curOption = false.obs; - MenuEntrySwitch( - {required String text, - required this.getter, - required this.setter, - dismissOnClicked = false}) - : super(text: text, dismissOnClicked: dismissOnClicked) { + MenuEntrySwitch({ + required String text, + required this.getter, + required this.setter, + Rx? textStyle, + dismissOnClicked = false, + RxBool? enabled, + }) : super( + text: text, + textStyle: textStyle, + dismissOnClicked: dismissOnClicked, + enabled: enabled, + ) { () async { _curOption.value = await getter(); }(); @@ -414,12 +452,17 @@ class MenuEntrySwitch2 extends MenuEntrySwitchBase { final Switch2Getter getter; final SwitchSetter setter; - MenuEntrySwitch2( - {required String text, - required this.getter, - required this.setter, - dismissOnClicked = false}) - : super(text: text, dismissOnClicked: dismissOnClicked); + MenuEntrySwitch2({ + required String text, + required this.getter, + required this.setter, + Rx? textStyle, + dismissOnClicked = false, + RxBool? enabled, + }) : super( + text: text, + textStyle: textStyle, + dismissOnClicked: dismissOnClicked); @override RxBool get curOption => getter(); @@ -433,13 +476,18 @@ class MenuEntrySubMenu extends MenuEntryBase { final String text; final List> entries; - MenuEntrySubMenu({required this.text, required this.entries}); + MenuEntrySubMenu({ + required this.text, + required this.entries, + RxBool? enabled, + }) : super(enabled: enabled); @override List> build( BuildContext context, MenuConfig conf) { return [ PopupMenuChildrenItem( + enabled: super.enabled, height: conf.height, padding: EdgeInsets.zero, position: mod_menu.PopupMenuPosition.overSide, @@ -449,20 +497,24 @@ class MenuEntrySubMenu extends MenuEntryBase { .toList(), child: Row(children: [ const SizedBox(width: MenuConfig.midPadding), - Text( - text, - style: TextStyle( - color: MyTheme.color(context).text, - fontSize: MenuConfig.fontSize, - fontWeight: FontWeight.normal), - ), + Obx(() => Text( + text, + style: TextStyle( + color: (super.enabled != null ? super.enabled!.value : true) + ? Colors.black + : Colors.grey, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal), + )), Expanded( child: Align( alignment: Alignment.centerRight, - child: Icon( - Icons.keyboard_arrow_right, - color: conf.commonColor, - ), + child: Obx(() => Icon( + Icons.keyboard_arrow_right, + color: (super.enabled != null ? super.enabled!.value : true) + ? conf.commonColor + : Colors.grey, + )), )) ]), ) @@ -474,36 +526,57 @@ class MenuEntryButton extends MenuEntryBase { final Widget Function(TextStyle? style) childBuilder; Function() proc; - MenuEntryButton( - {required this.childBuilder, - required this.proc, - dismissOnClicked = false}) - : super(dismissOnClicked: dismissOnClicked); + MenuEntryButton({ + required this.childBuilder, + required this.proc, + dismissOnClicked = false, + RxBool? enabled, + }) : super( + dismissOnClicked: dismissOnClicked, + enabled: enabled, + ); + + Widget _buildChild(BuildContext context, MenuConfig conf) { + return Obx(() { + bool enabled = true; + if (super.enabled != null) { + enabled = super.enabled!.value; + } + const enabledStyle = TextStyle( + color: Colors.black, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal); + const disabledStyle = TextStyle( + color: Colors.grey, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal); + return TextButton( + onPressed: enabled + ? () { + if (super.dismissOnClicked && Navigator.canPop(context)) { + Navigator.pop(context); + } + proc(); + } + : null, + child: Container( + alignment: AlignmentDirectional.centerStart, + constraints: BoxConstraints(minHeight: conf.height), + child: childBuilder(enabled ? enabledStyle : disabledStyle), + ), + ); + }); + } @override List> build( BuildContext context, MenuConfig conf) { return [ mod_menu.PopupMenuItem( + enabled: super.enabled != null ? super.enabled!.value : true, padding: EdgeInsets.zero, height: conf.height, - child: TextButton( - child: Container( - alignment: AlignmentDirectional.centerStart, - constraints: BoxConstraints(minHeight: conf.height), - child: childBuilder( - TextStyle( - color: MyTheme.color(context).text, - fontSize: MenuConfig.fontSize, - fontWeight: FontWeight.normal), - )), - onPressed: () { - if (super.dismissOnClicked && Navigator.canPop(context)) { - Navigator.pop(context); - } - proc(); - }, - ), + child: _buildChild(context, conf), ) ]; } diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index c83f61a17..411c30e73 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -75,20 +75,20 @@ class _RemoteMenubarState extends State { final List menubarItems = []; if (!isWebDesktop) { menubarItems.add(_buildFullscreen(context)); - if (widget.ffi.ffiModel.isPeerAndroid) { - menubarItems.add(IconButton( - tooltip: translate('Mobile Actions'), - color: _MenubarTheme.commonColor, - icon: const Icon(Icons.build), - onPressed: () { - if (mobileActionsOverlayEntry == null) { - showMobileActionsOverlay(); - } else { - hideMobileActionsOverlay(); - } - }, - )); - } + //if (widget.ffi.ffiModel.isPeerAndroid) { + menubarItems.add(IconButton( + tooltip: translate('Mobile Actions'), + color: _MenubarTheme.commonColor, + icon: const Icon(Icons.build), + onPressed: () { + if (mobileActionsOverlayEntry == null) { + showMobileActionsOverlay(); + } else { + hideMobileActionsOverlay(); + } + }, + )); + //} } menubarItems.add(_buildMonitor(context)); menubarItems.add(_buildControl(context)); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 7d8cdc203..9f9899553 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; import 'dart:ui' as ui; +import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -54,7 +55,7 @@ class FfiModel with ChangeNotifier { bool get touchMode => _touchMode; - bool get isPeerAndroid => _pi.platform == "Android"; + bool get isPeerAndroid => _pi.platform == 'Android'; set inputBlocked(v) { _inputBlocked = v; @@ -116,7 +117,7 @@ class FfiModel with ChangeNotifier { return null; } else { final icon = - '${secure == true ? "secure" : "insecure"}${direct == true ? "" : "_relay"}'; + '${secure == true ? 'secure' : 'insecure'}${direct == true ? '' : '_relay'}'; return Image.asset('assets/$icon.png', width: 48, height: 48); } } @@ -143,17 +144,17 @@ class FfiModel with ChangeNotifier { } else if (name == 'cursor_id') { parent.target?.cursorModel.updateCursorId(evt); } else if (name == 'cursor_position') { - parent.target?.cursorModel.updateCursorPosition(evt); + parent.target?.cursorModel.updateCursorPosition(evt, peerId); } else if (name == 'clipboard') { Clipboard.setData(ClipboardData(text: evt['content'])); } else if (name == 'permission') { parent.target?.ffiModel.updatePermission(evt, peerId); } else if (name == 'chat_client_mode') { parent.target?.chatModel - .receive(ChatModel.clientModeID, evt['text'] ?? ""); + .receive(ChatModel.clientModeID, evt['text'] ?? ''); } else if (name == 'chat_server_mode') { parent.target?.chatModel - .receive(int.parse(evt['id'] as String), evt['text'] ?? ""); + .receive(int.parse(evt['id'] as String), evt['text'] ?? ''); } else if (name == 'file_dir') { parent.target?.fileModel.receiveFileDir(evt); } else if (name == 'job_progress') { @@ -184,61 +185,7 @@ class FfiModel with ChangeNotifier { /// Bind the event listener to receive events from the Rust core. void updateEventListener(String peerId) { - cb(evt) { - var name = evt['name']; - if (name == 'msgbox') { - handleMsgBox(evt, peerId); - } else if (name == 'peer_info') { - handlePeerInfo(evt, peerId); - } else if (name == 'connection_ready') { - parent.target?.ffiModel.setConnectionType( - peerId, evt['secure'] == 'true', evt['direct'] == 'true'); - } else if (name == 'switch_display') { - handleSwitchDisplay(evt); - } else if (name == 'cursor_data') { - parent.target?.cursorModel.updateCursorData(evt); - } else if (name == 'cursor_id') { - parent.target?.cursorModel.updateCursorId(evt); - } else if (name == 'cursor_position') { - parent.target?.cursorModel.updateCursorPosition(evt); - } else if (name == 'clipboard') { - Clipboard.setData(ClipboardData(text: evt['content'])); - } else if (name == 'permission') { - parent.target?.ffiModel.updatePermission(evt, peerId); - } else if (name == 'chat_client_mode') { - parent.target?.chatModel - .receive(ChatModel.clientModeID, evt['text'] ?? ""); - } else if (name == 'chat_server_mode') { - parent.target?.chatModel - .receive(int.parse(evt['id'] as String), evt['text'] ?? ""); - } else if (name == 'file_dir') { - parent.target?.fileModel.receiveFileDir(evt); - } else if (name == 'job_progress') { - parent.target?.fileModel.tryUpdateJobProgress(evt); - } else if (name == 'job_done') { - parent.target?.fileModel.jobDone(evt); - } else if (name == 'job_error') { - parent.target?.fileModel.jobError(evt); - } else if (name == 'override_file_confirm') { - parent.target?.fileModel.overrideFileConfirm(evt); - } else if (name == 'load_last_job') { - parent.target?.fileModel.loadLastJob(evt); - } else if (name == 'update_folder_files') { - parent.target?.fileModel.updateFolderFiles(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') { - parent.target?.qualityMonitorModel.updateQualityStatus(evt); - } else if (name == 'update_block_input_state') { - updateBlockInputState(evt, peerId); - } else if (name == 'update_privacy_mode') { - updatePrivacyMode(evt, peerId); - } - } - - platformFFI.setEventCallback(cb); + platformFFI.setEventCallback(startEventListener(peerId)); } void handleSwitchDisplay(Map evt) { @@ -249,8 +196,9 @@ class FfiModel with ChangeNotifier { _display.y = double.parse(evt['y']); _display.width = int.parse(evt['width']); _display.height = int.parse(evt['height']); - if (old != _pi.currentDisplay) + if (old != _pi.currentDisplay) { parent.target?.cursorModel.updateDisplayOrigin(_display.x, _display.y); + } // remote is mobile, and orientation changed if ((_display.width > _display.height) != oldOrientation) { @@ -307,7 +255,7 @@ class FfiModel with ChangeNotifier { _pi.username = evt['username']; _pi.hostname = evt['hostname']; _pi.platform = evt['platform']; - _pi.sasEnabled = evt['sas_enabled'] == "true"; + _pi.sasEnabled = evt['sas_enabled'] == 'true'; _pi.currentDisplay = int.parse(evt['current_display']); try { @@ -323,7 +271,7 @@ class FfiModel with ChangeNotifier { } } else { _touchMode = - await bind.sessionGetOption(id: peerId, arg: "touch-mode") != ''; + await bind.sessionGetOption(id: peerId, arg: 'touch-mode') != ''; } if (parent.target != null && @@ -381,7 +329,7 @@ class ImageModel with ChangeNotifier { ui.Image? get image => _image; - String _id = ""; + String _id = ''; WeakReference parent; @@ -426,7 +374,7 @@ class ImageModel with ChangeNotifier { } Future.delayed(Duration(milliseconds: 1), () { if (parent.target?.ffiModel.isPeerAndroid ?? false) { - bind.sessionPeerOption(id: _id, name: "view-style", value: "shrink"); + bind.sessionPeerOption(id: _id, name: 'view-style', value: 'shrink'); parent.target?.canvasModel.updateViewStyle(); } }); @@ -471,7 +419,7 @@ class CanvasModel with ChangeNotifier { // the tabbar over the image double tabBarHeight = 0.0; // TODO multi canvas model - String id = ""; + String id = ''; // scroll offset x percent double _scrollX = 0.0; // scroll offset y percent @@ -580,9 +528,16 @@ class CanvasModel with ChangeNotifier { } // If keyboard is not permitted, do not move cursor when mouse is moving. - if (parent.target != null) { - if (parent.target!.ffiModel.keyboard()) { + if (parent.target != null && parent.target!.ffiModel.keyboard()) { + // Draw cursor if is not desktop. + if (!isDesktop) { parent.target!.cursorModel.moveLocal(x, y); + } else { + try { + RemoteCursorMovedState.find(id).value = false; + } catch (e) { + // + } } } } @@ -642,16 +597,19 @@ class CanvasModel with ChangeNotifier { class CursorModel with ChangeNotifier { ui.Image? _image; final _images = >{}; + Uint8List? _pngData; + final _pngs = {}; double _x = -10000; double _y = -10000; double _hotx = 0; double _hoty = 0; double _displayOriginX = 0; double _displayOriginY = 0; - String id = ""; // TODO multi cursor model + String id = ''; // TODO multi cursor model WeakReference parent; ui.Image? get image => _image; + Uint8List? get pngData => _pngData; double get x => _x - _displayOriginX; @@ -801,19 +759,29 @@ class CursorModel with ChangeNotifier { var pid = parent.target?.id; ui.decodeImageFromPixels(rgba, width, height, ui.PixelFormat.rgba8888, (image) { - if (parent.target?.id != pid) return; - _image = image; - _images[id] = Tuple3(image, _hotx, _hoty); - try { - // my throw exception, because the listener maybe already dispose - notifyListeners(); - } catch (e) { - debugPrint('notify cursor: $e'); - } + () async { + if (parent.target?.id != pid) return; + _image = image; + _images[id] = Tuple3(image, _hotx, _hoty); + final data = await image.toByteData(format: ImageByteFormat.png); + if (data != null) { + _pngData = data.buffer.asUint8List(); + } else { + _pngData = null; + } + _pngs[id] = _pngData; + try { + // my throw exception, because the listener maybe already dispose + notifyListeners(); + } catch (e) { + debugPrint('notify cursor: $e'); + } + }(); }); } void updateCursorId(Map evt) { + _pngData = _pngs[int.parse(evt['id'])]; final tmp = _images[int.parse(evt['id'])]; if (tmp != null) { _image = tmp.item1; @@ -824,9 +792,14 @@ class CursorModel with ChangeNotifier { } /// Update the cursor position. - void updateCursorPosition(Map evt) { + void updateCursorPosition(Map evt, String id) { _x = double.parse(evt['x']); _y = double.parse(evt['y']); + try { + RemoteCursorMovedState.find(id).value = true; + } catch (e) { + // + } notifyListeners(); } @@ -888,13 +861,15 @@ class QualityMonitorModel with ChangeNotifier { updateQualityStatus(Map evt) { try { - if ((evt["speed"] as String).isNotEmpty) _data.speed = evt["speed"]; - if ((evt["fps"] as String).isNotEmpty) _data.fps = evt["fps"]; - if ((evt["delay"] as String).isNotEmpty) _data.delay = evt["delay"]; - if ((evt["target_bitrate"] as String).isNotEmpty) - _data.targetBitrate = evt["target_bitrate"]; - if ((evt["codec_format"] as String).isNotEmpty) - _data.codecFormat = evt["codec_format"]; + if ((evt['speed'] as String).isNotEmpty) _data.speed = evt['speed']; + if ((evt['fps'] as String).isNotEmpty) _data.fps = evt['fps']; + if ((evt['delay'] as String).isNotEmpty) _data.delay = evt['delay']; + if ((evt['target_bitrate'] as String).isNotEmpty) { + _data.targetBitrate = evt['target_bitrate']; + } + if ((evt['codec_format'] as String).isNotEmpty) { + _data.codecFormat = evt['codec_format']; + } notifyListeners(); } catch (e) {} } @@ -907,11 +882,11 @@ extension ToString on MouseButtons { String get value { switch (this) { case MouseButtons.left: - return "left"; + return 'left'; case MouseButtons.right: - return "right"; + return 'right'; case MouseButtons.wheel: - return "wheel"; + return 'wheel'; } } } @@ -920,12 +895,12 @@ enum ConnType { defaultConn, fileTransfer, portForward, rdp } /// FFI class for communicating with the Rust core. class FFI { - var id = ""; + var id = ''; var shift = false; var ctrl = false; var alt = false; var command = false; - var version = ""; + var version = ''; var connType = ConnType.defaultConn; /// dialogManager use late to ensure init after main page binding [globalKey] @@ -1006,11 +981,11 @@ class FFI { // out['name'] = name; // // default: down = false // if (down == true) { - // out['down'] = "true"; + // out['down'] = 'true'; // } // // default: press = true // if (press != false) { - // out['press'] = "true"; + // out['press'] = 'true'; // } // setByName('input_key', json.encode(modify(out))); // TODO id @@ -1038,7 +1013,7 @@ class FFI { Future> peers() async { try { var str = await bind.mainGetRecentPeers(); - if (str == "") return []; + if (str == '') return []; List peers = json.decode(str); return peers .map((s) => s as List) @@ -1046,7 +1021,7 @@ class FFI { Peer.fromJson(s[0] as String, s[1] as Map)) .toList(); } catch (e) { - print('peers(): $e'); + debugPrint('peers(): $e'); } return []; } @@ -1056,7 +1031,7 @@ class FFI { {bool isFileTransfer = false, bool isPortForward = false, double tabBarHeight = 0.0}) { - assert(!(isFileTransfer && isPortForward), "more than one connect type"); + assert(!(isFileTransfer && isPortForward), 'more than one connect type'); if (isFileTransfer) { connType = ConnType.fileTransfer; id = 'ft_$id'; @@ -1108,13 +1083,13 @@ class FFI { canvasModel.y, canvasModel.scale, ffiModel.pi.currentDisplay); } bind.sessionClose(id: id); - id = ""; + id = ''; imageModel.update(null, 0.0); cursorModel.clear(); ffiModel.clear(); canvasModel.clear(); resetModifiers(); - debugPrint("model $id closed"); + debugPrint('model $id closed'); } /// Send **get** command to the Rust core based on [name] and [arg]. @@ -1221,7 +1196,7 @@ class FFI { Future getDefaultAudioInput() async { final input = await bind.mainGetOption(key: 'audio-input'); if (input.isEmpty && Platform.isWindows) { - return "System Sound"; + return 'System Sound'; } return input; } @@ -1232,8 +1207,8 @@ class FFI { Future> getHttpHeaders() async { return { - "Authorization": - "Bearer " + await bind.mainGetLocalOption(key: "access_token") + 'Authorization': + 'Bearer ' + await bind.mainGetLocalOption(key: 'access_token') }; } } @@ -1246,10 +1221,10 @@ class Display { } class PeerInfo { - String version = ""; - String username = ""; - String hostname = ""; - String platform = ""; + String version = ''; + String username = ''; + String hostname = ''; + String platform = ''; bool sasEnabled = false; int currentDisplay = 0; List displays = []; diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 9d921ef48..33ce7e707 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -35,7 +35,7 @@ class ServerModel with ChangeNotifier { final tabController = DesktopTabController(tabType: DesktopTabType.cm); - List _clients = []; + final List _clients = []; bool get isStart => _isStart; @@ -61,8 +61,8 @@ class ServerModel with ChangeNotifier { return _verificationMethod; } - set verificationMethod(String method) { - bind.mainSetOption(key: "verification-method", value: method); + setVerificationMethod(String method) async { + await bind.mainSetOption(key: "verification-method", value: method); } String get temporaryPasswordLength { @@ -73,8 +73,8 @@ class ServerModel with ChangeNotifier { return _temporaryPasswordLength; } - set temporaryPasswordLength(String length) { - bind.mainSetOption(key: "temporary-password-length", value: length); + setTemporaryPasswordLength(String length) async { + await bind.mainSetOption(key: "temporary-password-length", value: length); } TextEditingController get serverId => _serverId; diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index f86c59755..1f4987e5c 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -68,6 +68,10 @@ dependencies: git: url: https://github.com/Kingtous/rustdesk_tray_manager ref: 3aa37c86e47ea748e7b5507cbe59f2c54ebdb23a + flutter_custom_cursor: + git: + url: https://github.com/Kingtous/rustdesk_flutter_custom_cursor + ref: 7fe78c139c711bafbae52d924e9caf18bd193e28 get: ^4.6.5 visibility_detector: ^0.3.3 contextmenu: ^3.0.0 diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index ef2aaeaa1..04d8619c1 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -789,6 +789,10 @@ pub fn main_get_mouse_time() -> f64 { get_mouse_time() } +pub fn main_wol(id: String) { + crate::lan::send_wol(id) +} + pub fn cm_send_chat(conn_id: i32, msg: String) { crate::ui_cm_interface::send_chat(conn_id, msg); }