From 722a4d3de7683de72e854a6d2bbc60269c494ab1 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 1 Sep 2022 22:36:40 -0700 Subject: [PATCH] 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',