From 70c472676632144305164f1b9dfb54a26f5b9d8a Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 6 Sep 2022 21:20:53 -0700 Subject: [PATCH] flutter_desktop: password menu Signed-off-by: fufesou --- flutter/lib/common/shared_state.dart | 25 +- .../lib/desktop/pages/desktop_home_page.dart | 427 +++++++++--------- .../desktop/pages/desktop_setting_page.dart | 24 +- flutter/lib/desktop/pages/remote_page.dart | 24 +- .../widgets/material_mod_popup_menu.dart | 8 +- flutter/lib/desktop/widgets/popup_menu.dart | 236 ++++++---- flutter/lib/models/model.dart | 165 +++---- flutter/lib/models/server_model.dart | 10 +- 8 files changed, 501 insertions(+), 418 deletions(-) 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/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 545e9165c..3cda8aa60 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -19,18 +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 _PopupMenuTheme { +class _MenubarTheme { static const Color commonColor = MyTheme.accent; // kMinInteractiveDimension static const double height = 25.0; - static const double dividerHeight = 3.0; + static const double dividerHeight = 12.0; } class DesktopHomePage extends StatefulWidget { const DesktopHomePage({Key? key}) : super(key: key); @override - State createState() => _DesktopHomePageState(); + State createState() => _DesktopHomePageState(); } const borderColor = Color(0xFF2F65BA); @@ -93,7 +93,7 @@ class _DesktopHomePageState extends State buildServerBoard(BuildContext context) { return Container( color: MyTheme.color(context).grayBg, - child: const ConnectionPage(), + child: ConnectionPage(), ); } @@ -116,7 +116,7 @@ class _DesktopHomePageState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( + Container( height: 25, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -142,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, ), ), @@ -161,190 +161,8 @@ class _DesktopHomePageState extends State ); } - // Future> _genSwitchEntry( - // String label, String key) async { - - // final v = await bind.mainGetOption(key: key); - // bool enable; - // if (key == "stop-service") { - // enable = v != "Y"; - // } else if (key.startsWith("allow-")) { - // enable = v == "Y"; - // } else { - // enable = v != "N"; - // } - - // return PopupMenuItem( - // child: Row( - // children: [ - // Icon(Icons.check, - // color: enable ? null : MyTheme.accent.withAlpha(00)), - // Text( - // label, - // style: genTextStyle(enable), - // ), - // ], - // ), - // value: key, - // ); - // } - - _popupMenu(BuildContext context, RelativeRect position) async { - TextStyle styleEnabled = const TextStyle( - color: Colors.black, - fontSize: MenuConfig.fontSize, - fontWeight: FontWeight.normal); - TextStyle styleDisabled = const TextStyle( - color: Colors.redAccent, - fontSize: MenuConfig.fontSize, - fontWeight: FontWeight.normal, - decoration: TextDecoration.lineThrough); - - enabledEntry(String label, String key) { - Rx textStyle = styleEnabled.obs; - return MenuEntrySwitch( - text: translate(label), - textStyle: textStyle, - getter: () async { - final opt = await bind.mainGetOption(key: key); - bool enabled; - if (key == 'stop-service') { - enabled = opt != 'Y'; - } else if (key.startsWith("allow-")) { - enabled = opt == 'Y'; - } else { - enabled = opt != 'N'; - } - textStyle.value = enabled ? styleEnabled : styleDisabled; - return enabled; - }, - setter: (bool v) async { - String opt; - if (key == 'stop-service') { - opt = v ? 'Y' : ''; - } else if (key.startsWith("allow-")) { - opt = v ? 'Y' : ''; - } else { - opt = v ? '' : 'N'; - } - await bind.mainSetOption(key: key, value: opt); - if (key == 'allow-darktheme') { - changeTheme(opt); - } - }, - dismissOnClicked: false, - ); - } - - final userName = await gFFI.userModel.getUserName(); - final enabledInput = await bind.mainGetOption(key: 'enable-audio'); - final defaultInput = await gFFI.getDefaultAudioInput(); - - final List> menu = >[ - enabledEntry('Enable Keyboard/Mouse', 'enable-keyboard'), - enabledEntry('Enable Clipboard', 'enable-clipboard'), - enabledEntry('Enable File Transfer', 'enable-file-transfer'), - enabledEntry('Enable TCP Tunneling', 'enable-tunnel'), - // TODO: audio sub menu? - // genAudioInputPopupMenuItem(enabledInput != "N", defaultInput), - MenuEntryDivider(), - MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('ID/Relay Server'), - style: style, - ), - proc: () { - changeServer(); - }, - dismissOnClicked: true, - ), - MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('IP Whitelisting'), - style: style, - ), - proc: () { - changeWhiteList(); - }, - dismissOnClicked: true, - ), - MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Socks5 Proxy'), - style: style, - ), - proc: () { - changeSocks5Proxy(); - }, - dismissOnClicked: true, - ), - MenuEntryDivider(), - enabledEntry('Enable Service', 'stop-service'), - enabledEntry('Always connected via relay', 'allow-always-relay'), - // FIXME: is this option correct? - enabledEntry('Start ID/relay service', 'stop-rendezvous-service'), - MenuEntryDivider(), - userName.isEmpty - ? MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Login'), - style: style, - ), - proc: () { - login(); - }, - dismissOnClicked: true, - ) - : MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Logout'), - style: style, - ), - proc: () { - logOut(); - }, - dismissOnClicked: true, - ), - MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Change ID'), - style: style, - ), - proc: () { - changeId(); - }, - dismissOnClicked: true, - ), - MenuEntryDivider(), - enabledEntry('Dark Theme', 'allow-darktheme'), - MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('About'), - style: style, - ), - proc: () { - about(); - }, - dismissOnClicked: true, - ), - ]; - - await mod_menu.showMenu( - context: context, - position: position, - items: menu - .map((e) => e.build( - context, - const MenuConfig( - commonColor: _PopupMenuTheme.commonColor, - height: _PopupMenuTheme.height, - dividerHeight: _PopupMenuTheme.dividerHeight))) - .expand((i) => i) - .toList()); - } - Widget buildPopupMenu(BuildContext context) { - RelativeRect position = const RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0); + var position; RxBool hover = false.obs; return InkWell( onTapDown: (detail) { @@ -353,7 +171,83 @@ class _DesktopHomePageState extends State position = RelativeRect.fromLTRB(x, y, x, y); }, onTap: () async { - await _popupMenu(context, position); + final userName = await gFFI.userModel.getUserName(); + final enabledInput = await bind.mainGetOption(key: 'enable-audio'); + final defaultInput = await gFFI.getDefaultAudioInput(); + var menu = [ + await genEnablePopupMenuItem( + translate("Enable Keyboard/Mouse"), + 'enable-keyboard', + ), + await genEnablePopupMenuItem( + translate("Enable Clipboard"), + 'enable-clipboard', + ), + await genEnablePopupMenuItem( + translate("Enable File Transfer"), + 'enable-file-transfer', + ), + await genEnablePopupMenuItem( + translate("Enable TCP Tunneling"), + 'enable-tunnel', + ), + genAudioInputPopupMenuItem(enabledInput != "N", defaultInput), + PopupMenuDivider(), + PopupMenuItem( + child: Text(translate("ID/Relay Server")), + value: 'custom-server', + ), + PopupMenuItem( + child: Text(translate("IP Whitelisting")), + value: 'whitelist', + ), + PopupMenuItem( + child: Text(translate("Socks5 Proxy")), + value: 'socks5-proxy', + ), + PopupMenuDivider(), + await genEnablePopupMenuItem( + translate("Enable Service"), + 'stop-service', + ), + // TODO: direct server + await genEnablePopupMenuItem( + translate("Always connected via relay"), + 'allow-always-relay', + ), + await genEnablePopupMenuItem( + translate("Start ID/relay service"), + 'stop-rendezvous-service', + ), + PopupMenuDivider(), + userName.isEmpty + ? PopupMenuItem( + child: Text(translate("Login")), + value: 'login', + ) + : PopupMenuItem( + child: Text("${translate("Logout")} $userName"), + value: 'logout', + ), + PopupMenuItem( + child: Text(translate("Change ID")), + value: 'change-id', + ), + PopupMenuDivider(), + await genEnablePopupMenuItem( + translate("Dark Theme"), + 'allow-darktheme', + ), + PopupMenuItem( + child: Text(translate("About")), + value: 'about', + ), + ]; + final v = + await showMenu(context: context, position: position, items: menu); + if (v != null) { + onSelectMenu(v); + } }, child: Obx( () => CircleAvatar( @@ -435,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(); + // } + // }) ], ), ], @@ -479,7 +374,7 @@ class _DesktopHomePageState extends State ), ], ), - onTap: () => gFFI.serverModel.verificationMethod = value, + onTap: () => gFFI.serverModel.setVerificationMethod(value), ); final temporary_enabled = gFFI.serverModel.verificationMethod != kUsePermanentPassword; @@ -516,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(); + }(); } }, )) @@ -1148,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 35383a7e0..48fc0a5e7 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -313,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( @@ -323,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'] @@ -364,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/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 9b0ce66c2..3a34b44ef 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -42,6 +42,7 @@ class _RemotePageState extends State String _value = ''; final _cursorOverImage = false.obs; late RxBool _showRemoteCursor; + late RxBool _remoteCursorMoved; late RxBool _keyboardEnabled; final FocusNode _mobileFocusNode = FocusNode(); @@ -61,8 +62,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) { @@ -71,6 +74,7 @@ class _RemotePageState extends State CurrentDisplayState.delete(id); ShowRemoteCursorState.delete(id); KeyboardEnabledState.delete(id); + RemoteCursorMovedState.delete(id); } @override @@ -396,6 +400,7 @@ class _RemotePageState extends State id: widget.id, cursorOverImage: _cursorOverImage, keyboardEnabled: _keyboardEnabled, + remoteCursorMoved: _remoteCursorMoved, listenerBuilder: _buildImageListener, ); })) @@ -460,6 +465,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(); @@ -469,6 +475,7 @@ class ImagePaint extends StatelessWidget { required this.id, required this.cursorOverImage, required this.keyboardEnabled, + required this.remoteCursorMoved, this.listenerBuilder}) : super(key: key); @@ -476,6 +483,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( @@ -501,12 +509,16 @@ class ImagePaint extends StatelessWidget { return false; }, child: Obx(() => MouseRegion( - // cursor: (keyboardEnabled.isTrue && cursorOverImage.isTrue) - // ? SystemMouseCursors.none - // : MouseCursor.defer, - /// cursor: MouseCursor.defer, - cursor: FlutterCustomCursor( - path: "assets/pencil.png", x: 1.0, y: 8.0), + cursor: (cursorOverImage.isTrue && keyboardEnabled.isTrue) + ? (remoteCursorMoved.isTrue + ? SystemMouseCursors.none + : FlutterCustomMemoryImageCursor( + pixbuf: cursor.rgba!, + hotx: cursor.hotx, + hoty: cursor.hoty, + imageWidth: (cursor.image!.width * s).toInt(), + imageHeight: (cursor.image!.height * s).toInt())) + : MouseCursor.defer, onHover: (evt) { pos.value = evt.position; }, 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/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart index 02e3512df..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) => @@ -327,9 +344,12 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { final String text; final Rx? textStyle; - MenuEntrySwitchBase( - {required this.text, required dismissOnClicked, this.textStyle}) - : 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); @@ -395,16 +415,19 @@ class MenuEntrySwitch extends MenuEntrySwitchBase { final SwitchSetter setter; final RxBool _curOption = false.obs; - MenuEntrySwitch( - {required String text, - required this.getter, - required this.setter, - Rx? textStyle, - dismissOnClicked = false}) - : super( - text: text, - textStyle: textStyle, - 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(); }(); @@ -429,13 +452,14 @@ class MenuEntrySwitch2 extends MenuEntrySwitchBase { final Switch2Getter getter; final SwitchSetter setter; - MenuEntrySwitch2( - {required String text, - required this.getter, - required this.setter, - Rx? textStyle, - dismissOnClicked = false}) - : super( + MenuEntrySwitch2({ + required String text, + required this.getter, + required this.setter, + Rx? textStyle, + dismissOnClicked = false, + RxBool? enabled, + }) : super( text: text, textStyle: textStyle, dismissOnClicked: dismissOnClicked); @@ -452,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, @@ -468,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, + )), )) ]), ) @@ -493,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/models/model.dart b/flutter/lib/models/model.dart index 7d8cdc203..51c0b4225 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -54,7 +54,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 +116,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 +143,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 +184,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 +195,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 +254,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 +270,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 +328,7 @@ class ImageModel with ChangeNotifier { ui.Image? get image => _image; - String _id = ""; + String _id = ''; WeakReference parent; @@ -426,7 +373,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 +418,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 +527,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) { + // + } } } } @@ -641,17 +595,19 @@ class CanvasModel with ChangeNotifier { class CursorModel with ChangeNotifier { ui.Image? _image; - final _images = >{}; + Uint8List? _rgba; + final _images = >{}; 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 rgba => _rgba; double get x => _x - _displayOriginX; @@ -803,7 +759,8 @@ class CursorModel with ChangeNotifier { (image) { if (parent.target?.id != pid) return; _image = image; - _images[id] = Tuple3(image, _hotx, _hoty); + _rgba = rgba; + _images[id] = Tuple4(rgba, image, _hotx, _hoty); try { // my throw exception, because the listener maybe already dispose notifyListeners(); @@ -816,17 +773,23 @@ class CursorModel with ChangeNotifier { void updateCursorId(Map evt) { final tmp = _images[int.parse(evt['id'])]; if (tmp != null) { - _image = tmp.item1; - _hotx = tmp.item2; - _hoty = tmp.item3; + _rgba = tmp.item1; + _image = tmp.item2; + _hotx = tmp.item3; + _hoty = tmp.item4; notifyListeners(); } } /// 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 = false; + } catch (e) { + // + } notifyListeners(); } @@ -888,13 +851,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 +872,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 +885,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 +971,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 +1003,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) @@ -1056,7 +1021,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 +1073,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 +1186,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 +1197,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 +1211,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;