diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index 47a992bd3..49d2eaf04 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -1,9 +1,10 @@ -import 'package:contextmenu/contextmenu.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common/widgets/peer_card.dart'; import 'package:flutter_hbb/common/widgets/peers_view.dart'; -import 'package:flutter_hbb/models/ab_model.dart'; +import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; +import '../../consts.dart'; +import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; import 'package:get/get.dart'; -import 'package:provider/provider.dart'; import '../../common.dart'; import '../../desktop/pages/desktop_home_page.dart'; @@ -24,7 +25,7 @@ class _AddressBookState extends State { @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) => gFFI.abModel.getAb()); + WidgetsBinding.instance.addPostFrameCallback((_) => gFFI.abModel.pullAb()); } @override @@ -66,174 +67,136 @@ class _AddressBookState extends State { } final model = gFFI.abModel; return FutureBuilder( - future: model.getAb(), + future: model.pullAb(), builder: (context, snapshot) { if (snapshot.hasData) { return _buildAddressBook(context); } else if (snapshot.hasError) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(translate("${snapshot.error}")), - TextButton( - onPressed: () { - setState(() {}); - }, - child: Text(translate("Retry"))) - ], - ); + return _buildShowError(snapshot.error.toString()); } else { - if (model.abLoading) { - return const Center( - child: CircularProgressIndicator(), - ); - } else if (model.abError.isNotEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(translate(model.abError)), - TextButton( - onPressed: () { - setState(() {}); - }, - child: Text(translate("Retry"))) - ], - ), - ); - } else { - return const Offstage(); - } + return Obx(() { + if (model.abLoading.value) { + return const Center( + child: CircularProgressIndicator(), + ); + } else if (model.abError.isNotEmpty) { + return _buildShowError(model.abError.value); + } else { + return const Offstage(); + } + }); } }); } - Widget _buildAddressBook(BuildContext context) { - return Consumer( - builder: (context, model, child) => Row( - children: [ - Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - side: BorderSide( - color: Theme.of(context).scaffoldBackgroundColor)), - child: Container( - width: 200, - height: double.infinity, - padding: const EdgeInsets.symmetric( - horizontal: 12.0, vertical: 8.0), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(translate('Tags')), - InkWell( - child: PopupMenuButton( - itemBuilder: (context) => [ - PopupMenuItem( - value: 'add-id', - child: Text(translate("Add ID")), - ), - PopupMenuItem( - value: 'add-tag', - child: Text(translate("Add Tag")), - ), - PopupMenuItem( - value: 'unset-all-tag', - child: Text( - translate("Unselect all tags")), - ), - ], - onSelected: handleAbOp, - child: const Icon(Icons.more_vert_outlined)), - ) - ], - ), - Expanded( - child: Container( - width: double.infinity, - height: double.infinity, - decoration: BoxDecoration( - border: Border.all(color: MyTheme.darkGray)), - child: Obx( - () => Wrap( - children: gFFI.abModel.tags - .map((e) => - buildTag(e, gFFI.abModel.selectedTags, - onTap: () { - // - if (gFFI.abModel.selectedTags - .contains(e)) { - gFFI.abModel.selectedTags.remove(e); - } else { - gFFI.abModel.selectedTags.add(e); - } - })) - .toList(), - ), - ), - ).marginSymmetric(vertical: 8.0), - ) - ], - ), - ), - ).marginOnly(right: 8.0), - Expanded( - child: Align( - alignment: Alignment.topLeft, - child: AddressBookPeersView( - menuPadding: widget.menuPadding, - )), - ) - ], - )); + Widget _buildShowError(String error) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(translate(error)), + TextButton( + onPressed: () { + setState(() {}); + }, + child: Text(translate("Retry"))) + ], + )); } - Widget buildTag(String tagName, RxList rxTags, {Function()? onTap}) { - return ContextMenuArea( - width: 100, - builder: (context) => [ - ListTile( - title: Text(translate("Delete")), - onTap: () { - gFFI.abModel.deleteTag(tagName); - gFFI.abModel.updateAb(); - Future.delayed(Duration.zero, () => Get.back()); - }, - ) - ], - child: GestureDetector( - onTap: onTap, - child: Obx( - () => Container( - decoration: BoxDecoration( - color: rxTags.contains(tagName) ? Colors.blue : null, - border: Border.all(color: MyTheme.darkGray), - borderRadius: BorderRadius.circular(10)), - margin: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 8.0), - padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0), - child: Text( - tagName, - style: TextStyle( - color: - rxTags.contains(tagName) ? Colors.white : null), // TODO + Widget _buildAddressBook(BuildContext context) { + var pos = RelativeRect.fill; + return Row( + children: [ + Card( + margin: EdgeInsets.symmetric(horizontal: 4.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: + BorderSide(color: Theme.of(context).scaffoldBackgroundColor)), + child: Container( + width: 200, + height: double.infinity, + padding: + const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(translate('Tags')), + GestureDetector( + onTapDown: (e) { + final x = e.globalPosition.dx; + final y = e.globalPosition.dy; + pos = RelativeRect.fromLTRB(x, y, x, y); + }, + onTap: () => _showMenu(pos), + child: ActionMore()), + ], + ), + Expanded( + child: Container( + width: double.infinity, + height: double.infinity, + decoration: BoxDecoration( + border: Border.all(color: MyTheme.darkGray), + borderRadius: BorderRadius.circular(2)), + child: Obx( + () => Wrap( + children: gFFI.abModel.tags + .map((e) => AddressBookTag( + name: e, + tags: gFFI.abModel.selectedTags, + onTap: () { + if (gFFI.abModel.selectedTags.contains(e)) { + gFFI.abModel.selectedTags.remove(e); + } else { + gFFI.abModel.selectedTags.add(e); + } + })) + .toList(), + ), + ), + ).marginSymmetric(vertical: 8.0), + ) + ], ), ), - ), - ), + ).marginOnly(right: 8.0), + Expanded( + child: Align( + alignment: Alignment.topLeft, + child: Obx(() => AddressBookPeersView( + menuPadding: widget.menuPadding, + initPeers: gFFI.abModel.peers.value, + ))), + ) + ], ); } - /// tag operation - void handleAbOp(String value) { - if (value == 'add-id') { - abAddId(); - } else if (value == 'add-tag') { - abAddTag(); - } else if (value == 'unset-all-tag') { - gFFI.abModel.unsetSelectedTags(); - } + void _showMenu(RelativeRect pos) { + final items = [ + getEntry(translate("Add ID"), abAddId), + getEntry(translate("Add Tag"), abAddTag), + getEntry(translate("Unselect all tags"), gFFI.abModel.unsetSelectedTags), + ]; + + mod_menu.showMenu( + context: context, + position: pos, + items: items + .map((e) => e.build( + context, + MenuConfig( + commonColor: CustomPopupMenuTheme.commonColor, + height: CustomPopupMenuTheme.height, + dividerHeight: CustomPopupMenuTheme.dividerHeight))) + .expand((i) => i) + .toList(), + elevation: 8, + ); } void abAddId() async { @@ -260,7 +223,7 @@ class _AddressBookState extends State { } gFFI.abModel.addId(newId); } - await gFFI.abModel.updateAb(); + await gFFI.abModel.pushAb(); this.setState(() {}); // final currentPeers } @@ -327,7 +290,7 @@ class _AddressBookState extends State { for (final tag in tags) { gFFI.abModel.addTag(tag); } - await gFFI.abModel.updateAb(); + await gFFI.abModel.pushAb(); // final currentPeers } close(); @@ -373,54 +336,88 @@ class _AddressBookState extends State { ); }); } +} - void abEditTag(String id) { - var isInProgress = false; +class AddressBookTag extends StatelessWidget { + final String name; + final RxList tags; + final Function()? onTap; + final bool showActionMenu; - final tags = List.of(gFFI.abModel.tags); - var selectedTag = gFFI.abModel.getPeerTags(id).obs; + const AddressBookTag( + {Key? key, + required this.name, + required this.tags, + this.onTap, + this.showActionMenu = true}) + : super(key: key); - gFFI.dialogManager.show((setState, close) { - submit() async { - setState(() { - isInProgress = true; - }); - gFFI.abModel.changeTagForPeer(id, selectedTag); - await gFFI.abModel.updateAb(); - close(); - } + @override + Widget build(BuildContext context) { + var pos = RelativeRect.fill; - return CustomAlertDialog( - title: Text(translate("Edit Tag")), - content: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: - const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - child: Wrap( - children: tags - .map((e) => buildTag(e, selectedTag, onTap: () { - if (selectedTag.contains(e)) { - selectedTag.remove(e); - } else { - selectedTag.add(e); - } - })) - .toList(growable: false), - ), - ), - Offstage( - offstage: !isInProgress, child: const LinearProgressIndicator()) - ], + void setPosition(TapDownDetails e) { + final x = e.globalPosition.dx; + final y = e.globalPosition.dy; + pos = RelativeRect.fromLTRB(x, y, x, y); + } + + return GestureDetector( + onTap: onTap, + onTapDown: showActionMenu ? setPosition : null, + onSecondaryTapDown: showActionMenu ? setPosition : null, + onSecondaryTap: showActionMenu ? () => _showMenu(context, pos) : null, + onLongPress: showActionMenu ? () => _showMenu(context, pos) : null, + child: Obx( + () => Container( + decoration: BoxDecoration( + color: tags.contains(name) ? Colors.blue : null, + border: Border.all(color: MyTheme.darkGray), + borderRadius: BorderRadius.circular(6)), + margin: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 8.0), + padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0), + child: Text(name, + style: + TextStyle(color: tags.contains(name) ? Colors.white : null)), ), - actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton(onPressed: submit, child: Text(translate("OK"))), - ], - onSubmit: submit, - onCancel: close, - ); - }); + ), + ); + } + + void _showMenu(BuildContext context, RelativeRect pos) { + final items = [ + getEntry(translate("Delete"), () { + gFFI.abModel.deleteTag(name); + gFFI.abModel.pushAb(); + Future.delayed(Duration.zero, () => Get.back()); + }), + ]; + + mod_menu.showMenu( + context: context, + position: pos, + items: items + .map((e) => e.build( + context, + MenuConfig( + commonColor: CustomPopupMenuTheme.commonColor, + height: CustomPopupMenuTheme.height, + dividerHeight: CustomPopupMenuTheme.dividerHeight))) + .expand((i) => i) + .toList(), + elevation: 8, + ); } } + +MenuEntryButton getEntry(String title, VoidCallback proc) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + title, + style: style, + ), + proc: proc, + padding: kDesktopMenuPadding, + dismissOnClicked: true, + ); +} diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index e3ab82cf5..19d28513c 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -1,6 +1,6 @@ -import 'package:contextmenu/contextmenu.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common/widgets/address_book.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:get/get.dart'; @@ -12,7 +12,7 @@ import '../../models/platform_model.dart'; import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; import '../../desktop/widgets/popup_menu.dart'; -class _PopupMenuTheme { +class CustomPopupMenuTheme { static const Color commonColor = MyTheme.accent; // kMinInteractiveDimension static const double height = 20.0; @@ -46,9 +46,8 @@ class _PeerCard extends StatefulWidget { class _PeerCardState extends State<_PeerCard> with AutomaticKeepAliveClientMixin { var _menuPos = RelativeRect.fill; - final double _cardRadis = 16; + final double _cardRadius = 16; final double _borderWidth = 2; - final RxBool _iconMoreHover = false.obs; @override Widget build(BuildContext context) { @@ -122,23 +121,23 @@ class _PeerCardState extends State<_PeerCard> var deco = Rx(BoxDecoration( border: Border.all(color: Colors.transparent, width: _borderWidth), borderRadius: peerCardUiType.value == PeerUiType.grid - ? BorderRadius.circular(_cardRadis) + ? BorderRadius.circular(_cardRadius) : null)); return MouseRegion( onEnter: (evt) { deco.value = BoxDecoration( border: Border.all( - color: Theme.of(context).colorScheme.secondary, + color: Theme.of(context).colorScheme.primary, width: _borderWidth), borderRadius: peerCardUiType.value == PeerUiType.grid - ? BorderRadius.circular(_cardRadis) + ? BorderRadius.circular(_cardRadius) : null); }, onExit: (evt) { deco.value = BoxDecoration( border: Border.all(color: Colors.transparent, width: _borderWidth), borderRadius: peerCardUiType.value == PeerUiType.grid - ? BorderRadius.circular(_cardRadis) + ? BorderRadius.circular(_cardRadius) : null); }, child: GestureDetector( @@ -221,7 +220,7 @@ class _PeerCardState extends State<_PeerCard> () => Container( foregroundDecoration: deco.value, child: ClipRRect( - borderRadius: BorderRadius.circular(_cardRadis - _borderWidth), + borderRadius: BorderRadius.circular(_cardRadius - _borderWidth), child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, @@ -299,27 +298,7 @@ class _PeerCardState extends State<_PeerCard> _menuPos = RelativeRect.fromLTRB(x, y, x, y); }, onPointerUp: (_) => _showPeerMenu(peer.id), - child: MouseRegion( - onEnter: (_) => _iconMoreHover.value = true, - onExit: (_) => _iconMoreHover.value = false, - child: CircleAvatar( - radius: 14, - backgroundColor: _iconMoreHover.value - ? Theme.of(context).scaffoldBackgroundColor - : Theme.of(context).backgroundColor, - // ? Theme.of(context).scaffoldBackgroundColor! - // : Theme.of(context).backgroundColor!, - child: Icon(Icons.more_vert, - size: 18, - color: _iconMoreHover.value - ? Theme.of(context).textTheme.titleLarge?.color - : Theme.of(context) - .textTheme - .titleLarge - ?.color - ?.withOpacity(0.5))))); - // ? MyTheme.color(context).text - // : MyTheme.color(context).lightText)))); + child: ActionMore()); /// Show the peer menu and handle user's choice. /// User might remove the peer or send a file to the peer. @@ -358,9 +337,9 @@ abstract class BasePeerCard extends StatelessWidget { .map((e) => e.build( context, const MenuConfig( - commonColor: _PopupMenuTheme.commonColor, - height: _PopupMenuTheme.height, - dividerHeight: _PopupMenuTheme.dividerHeight))) + commonColor: CustomPopupMenuTheme.commonColor, + height: CustomPopupMenuTheme.height, + dividerHeight: CustomPopupMenuTheme.dividerHeight))) .expand((i) => i) .toList(); @@ -426,7 +405,7 @@ abstract class BasePeerCard extends StatelessWidget { return MenuEntryButton( childBuilder: (TextStyle? style) => Container( alignment: AlignmentDirectional.center, - height: _PopupMenuTheme.height, + height: CustomPopupMenuTheme.height, child: Row( children: [ Text( @@ -601,11 +580,11 @@ abstract class BasePeerCard extends StatelessWidget { var name = peer.alias; var controller = TextEditingController(text: name); if (isAddressBook) { - final peer = gFFI.abModel.peers.firstWhere((p) => id == p['id']); + final peer = gFFI.abModel.peers.firstWhereOrNull((p) => id == p.id); if (peer == null) { // this should not happen } else { - name = peer['alias'] ?? ''; + name = peer.alias; } } gFFI.dialogManager.show((setState, close) { @@ -614,11 +593,11 @@ abstract class BasePeerCard extends StatelessWidget { name = controller.text; await bind.mainSetPeerOption(id: id, key: 'alias', value: name); if (isAddressBook) { - gFFI.abModel.setPeerOption(id, 'alias', name); - await gFFI.abModel.updateAb(); + gFFI.abModel.setPeerAlias(id, name); + await gFFI.abModel.pushAb(); } if (isAddressBook) { - gFFI.abModel.getAb(); + gFFI.abModel.pullAb(); } else { bind.mainLoadRecentPeers(); bind.mainLoadFavPeers(); @@ -774,7 +753,9 @@ class AddressBookPeerCard extends BasePeerCard { if (await bind.mainPeerHasPassword(id: peer.id)) { menuItems.add(_unrememberPasswordAction(peer.id)); } - menuItems.add(_editTagAction(peer.id)); + if (gFFI.abModel.tags.isNotEmpty) { + menuItems.add(_editTagAction(peer.id)); + } return menuItems; } @@ -791,7 +772,7 @@ class AddressBookPeerCard extends BasePeerCard { proc: () { () async { gFFI.abModel.deletePeer(id); - await gFFI.abModel.updateAb(); + await gFFI.abModel.pushAb(); }(); }, padding: super.menuPadding, @@ -826,7 +807,7 @@ class AddressBookPeerCard extends BasePeerCard { isInProgress = true; }); gFFI.abModel.changeTagForPeer(id, selectedTag); - await gFFI.abModel.updateAb(); + await gFFI.abModel.pushAb(); close(); } @@ -836,17 +817,20 @@ class AddressBookPeerCard extends BasePeerCard { crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - padding: - const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + padding: const EdgeInsets.symmetric(vertical: 8.0), child: Wrap( children: tags - .map((e) => _buildTag(e, selectedTag, onTap: () { + .map((e) => AddressBookTag( + name: e, + tags: selectedTag, + onTap: () { if (selectedTag.contains(e)) { selectedTag.remove(e); } else { selectedTag.add(e); } - })) + }, + showActionMenu: false)) .toList(growable: false), ), ), @@ -863,41 +847,6 @@ class AddressBookPeerCard extends BasePeerCard { ); }); } - - Widget _buildTag(String tagName, RxList rxTags, - {Function()? onTap}) { - return ContextMenuArea( - width: 100, - builder: (context) => [ - ListTile( - title: Text(translate("Delete")), - onTap: () { - gFFI.abModel.deleteTag(tagName); - gFFI.abModel.updateAb(); - Future.delayed(Duration.zero, () => Get.back()); - }, - ) - ], - child: GestureDetector( - onTap: onTap, - child: Obx( - () => Container( - decoration: BoxDecoration( - color: rxTags.contains(tagName) ? Colors.blue : null, - border: Border.all(color: MyTheme.darkGray), - borderRadius: BorderRadius.circular(10)), - margin: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 8.0), - padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0), - child: Text( - tagName, - style: TextStyle( - color: rxTags.contains(tagName) ? Colors.white : null), - ), - ), - ), - ), - ); - } } void _rdpDialog(String id) async { @@ -905,7 +854,7 @@ void _rdpDialog(String id) async { text: await bind.mainGetPeerOption(id: id, key: 'rdp_port')); final userController = TextEditingController( text: await bind.mainGetPeerOption(id: id, key: 'rdp_username')); - final passwordContorller = TextEditingController( + final passwordController = TextEditingController( text: await bind.mainGetPeerOption(id: id, key: 'rdp_password')); RxBool secure = true.obs; @@ -916,7 +865,7 @@ void _rdpDialog(String id) async { await bind.mainSetPeerOption( id: id, key: 'rdp_username', value: userController.text); await bind.mainSetPeerOption( - id: id, key: 'rdp_password', value: passwordContorller.text); + id: id, key: 'rdp_password', value: passwordController.text); close(); } @@ -1000,7 +949,7 @@ void _rdpDialog(String id) async { icon: Icon(secure.value ? Icons.visibility_off : Icons.visibility))), - controller: passwordContorller, + controller: passwordController, )), ), ], @@ -1027,3 +976,28 @@ Widget getOnline(double rightPadding, bool online) { child: CircleAvatar( radius: 3, backgroundColor: online ? Colors.green : kColorWarn))); } + +class ActionMore extends StatelessWidget { + final RxBool _iconMoreHover = false.obs; + + @override + Widget build(BuildContext context) { + return MouseRegion( + onEnter: (_) => _iconMoreHover.value = true, + onExit: (_) => _iconMoreHover.value = false, + child: Obx(() => CircleAvatar( + radius: 14, + backgroundColor: _iconMoreHover.value + ? Theme.of(context).scaffoldBackgroundColor + : Theme.of(context).backgroundColor, + child: Icon(Icons.more_vert, + size: 18, + color: _iconMoreHover.value + ? Theme.of(context).textTheme.titleLarge?.color + : Theme.of(context) + .textTheme + .titleLarge + ?.color + ?.withOpacity(0.5))))); + } +} diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index c91b01ca8..9a5503e26 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -54,7 +54,7 @@ class _PeerTabPageState extends State bind.mainDiscover(); break; case 3: - gFFI.abModel.getAb(); + gFFI.abModel.pullAb(); break; } } diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart index 9316c792b..03a2436f2 100644 --- a/flutter/lib/common/widgets/peers_view.dart +++ b/flutter/lib/common/widgets/peers_view.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/consts.dart'; -import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:visibility_detector/visibility_detector.dart'; @@ -14,8 +13,7 @@ import '../../models/peer_model.dart'; import '../../models/platform_model.dart'; import 'peer_card.dart'; -typedef OffstageFunc = bool Function(Peer peer); -typedef PeerCardBuilder = BasePeerCard Function(Peer peer); +typedef PeerCardBuilder = Widget Function(Peer peer); /// for peer search text, global obs value final peerSearchText = "".obs; @@ -24,16 +22,10 @@ final peerSearchTextController = class _PeersView extends StatefulWidget { final Peers peers; - final OffstageFunc offstageFunc; final PeerCardBuilder peerCardBuilder; - final ScrollController? scrollController; const _PeersView( - {required this.peers, - required this.offstageFunc, - required this.peerCardBuilder, - Key? key, - this.scrollController}) + {required this.peers, required this.peerCardBuilder, Key? key}) : super(key: key); @override @@ -124,20 +116,16 @@ class _PeersViewState extends State<_PeersView> with WindowListener { }, child: widget.peerCardBuilder(peer), ); - cards.add(Offstage( - key: ValueKey("off${peer.id}"), - offstage: widget.offstageFunc(peer), - child: isDesktop - ? Obx( - () => SizedBox( - width: 220, - height: peerCardUiType.value == PeerUiType.grid - ? 140 - : 42, - child: visibilityChild, - ), - ) - : SizedBox(width: mobileWidth, child: visibilityChild))); + cards.add(isDesktop + ? Obx( + () => SizedBox( + width: 220, + height: + peerCardUiType.value == PeerUiType.grid ? 140 : 42, + child: visibilityChild, + ), + ) + : SizedBox(width: mobileWidth, child: visibilityChild)); } return Wrap(spacing: space, runSpacing: space, children: cards); } else { @@ -190,7 +178,6 @@ class _PeersViewState extends State<_PeersView> with WindowListener { abstract class BasePeersView extends StatelessWidget { final String name; final String loadEvent; - final OffstageFunc offstageFunc; final PeerCardBuilder peerCardBuilder; final List initPeers; @@ -198,7 +185,6 @@ abstract class BasePeersView extends StatelessWidget { Key? key, required this.name, required this.loadEvent, - required this.offstageFunc, required this.peerCardBuilder, required this.initPeers, }) : super(key: key); @@ -207,7 +193,6 @@ abstract class BasePeersView extends StatelessWidget { Widget build(BuildContext context) { return _PeersView( peers: Peers(name: name, loadEvent: loadEvent, peers: initPeers), - offstageFunc: offstageFunc, peerCardBuilder: peerCardBuilder); } } @@ -219,7 +204,6 @@ class RecentPeersView extends BasePeersView { key: key, name: 'recent peer', loadEvent: 'load_recent_peers', - offstageFunc: (Peer peer) => false, peerCardBuilder: (Peer peer) => RecentPeerCard( peer: peer, menuPadding: menuPadding, @@ -242,7 +226,6 @@ class FavoritePeersView extends BasePeersView { key: key, name: 'favorite peer', loadEvent: 'load_fav_peers', - offstageFunc: (Peer peer) => false, peerCardBuilder: (Peer peer) => FavoritePeerCard( peer: peer, menuPadding: menuPadding, @@ -265,7 +248,6 @@ class DiscoveredPeersView extends BasePeersView { key: key, name: 'discovered peer', loadEvent: 'load_lan_peers', - offstageFunc: (Peer peer) => false, peerCardBuilder: (Peer peer) => DiscoveredPeerCard( peer: peer, menuPadding: menuPadding, @@ -283,27 +265,24 @@ class DiscoveredPeersView extends BasePeersView { class AddressBookPeersView extends BasePeersView { AddressBookPeersView( - {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController}) + {Key? key, + EdgeInsets? menuPadding, + ScrollController? scrollController, + required List initPeers}) : super( key: key, name: 'address book peer', loadEvent: 'load_address_book_peers', - offstageFunc: (Peer peer) => - !_hitTag(gFFI.abModel.selectedTags, peer.tags), - peerCardBuilder: (Peer peer) => AddressBookPeerCard( - peer: peer, - menuPadding: menuPadding, - ), - initPeers: _loadPeers(), + peerCardBuilder: (Peer peer) => Obx(() => Offstage( + key: ValueKey("off${peer.id}"), + offstage: !_hitTag(gFFI.abModel.selectedTags, peer.tags), + child: AddressBookPeerCard( + peer: peer, + menuPadding: menuPadding, + ))), + initPeers: initPeers, ); - static List _loadPeers() { - debugPrint("_loadPeers : ${gFFI.abModel.peers.toString()}"); - return gFFI.abModel.peers.map((e) { - return Peer.fromJson(e); - }).toList(); - } - static bool _hitTag(List selectedTags, List idents) { if (selectedTags.isEmpty) { return true; @@ -318,11 +297,4 @@ class AddressBookPeersView extends BasePeersView { } return true; } - - @override - Widget build(BuildContext context) { - final widget = super.build(context); - // gFFI.abModel.updateAb(); - return widget; - } } diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index c8dcbbfba..9e46db0d2 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -32,6 +32,7 @@ const kDefaultMouseWheelThrottleDuration = Duration(milliseconds: 50); const kFullScreenEdgeSize = 0.0; var kWindowEdgeSize = Platform.isWindows ? 1.0 : 5.0; const kWindowBorderWidth = 1.0; +const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0); const kInvalidValueStr = "InvalidValueStr"; diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 627cd23e9..9a606d122 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -86,16 +86,16 @@ class _ConnectionPageState extends State ], children: [ RecentPeersView( - menuPadding: EdgeInsets.only(left: 12.0, right: 3.0), + menuPadding: kDesktopMenuPadding, ), FavoritePeersView( - menuPadding: EdgeInsets.only(left: 12.0, right: 3.0), + menuPadding: kDesktopMenuPadding, ), DiscoveredPeersView( - menuPadding: EdgeInsets.only(left: 12.0, right: 3.0), + menuPadding: kDesktopMenuPadding, ), const AddressBook( - menuPadding: EdgeInsets.only(left: 12.0, right: 3.0), + menuPadding: kDesktopMenuPadding, ), ], ).paddingOnly(right: 12.0), @@ -288,17 +288,23 @@ class _ConnectionPageState extends State children: [ light, Text(translate('Ready'), style: textStyle), - Text(', ', style: textStyle), - svcIsUsingPublicServer.value - ? InkWell( - onTap: onUsePublicServerGuide, - child: Text( - translate('setup_server_tip'), - style: TextStyle( - decoration: TextDecoration.underline, fontSize: fontSize), - ), - ) - : Offstage() + Offstage( + offstage: !svcIsUsingPublicServer.value, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(', ', style: textStyle), + InkWell( + onTap: onUsePublicServerGuide, + child: Text( + translate('setup_server_tip'), + style: TextStyle( + decoration: TextDecoration.underline, + fontSize: fontSize), + ), + ) + ], + )) ], ); } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 9ba69a476..230199431 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -267,7 +267,6 @@ class _AppState extends State { ChangeNotifierProvider.value(value: gFFI.imageModel), ChangeNotifierProvider.value(value: gFFI.cursorModel), ChangeNotifierProvider.value(value: gFFI.canvasModel), - ChangeNotifierProvider.value(value: gFFI.abModel), ChangeNotifierProvider.value(value: gFFI.userModel), ], child: GetMaterialApp( diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index b4de861e9..ae41e07e6 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -2,17 +2,18 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/models/model.dart'; +import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; import '../common.dart'; -class AbModel with ChangeNotifier { - var abLoading = false; - var abError = ""; +class AbModel { + var abLoading = false.obs; + var abError = "".obs; var tags = [].obs; - var peers = [].obs; + var peers = List.empty(growable: true).obs; var selectedTags = List.empty(growable: true).obs; @@ -22,11 +23,10 @@ class AbModel with ChangeNotifier { FFI? get _ffi => parent.target; - Future getAb() async { - abLoading = true; - notifyListeners(); + Future pullAb() async { + abLoading.value = true; // request - final api = "${await getApiServer()}/api/ab/get"; + final api = "${await bind.mainGetApiServer()}/api/ab/get"; try { final resp = await http.post(Uri.parse(api), headers: await getHttpHeaders()); @@ -37,38 +37,34 @@ class AbModel with ChangeNotifier { } else if (json.containsKey('data')) { final data = jsonDecode(json['data']); tags.value = data['tags']; - peers.value = data['peers']; + peers.clear(); + for (final peer in data['peers']) { + peers.add(Peer.fromJson(peer)); + } } - notifyListeners(); return resp.body; } else { return ""; } } catch (err) { - abError = err.toString(); + err.printError(); + abError.value = err.toString(); } finally { - abLoading = false; - notifyListeners(); + abLoading.value = false; } return null; } - Future getApiServer() async { - return await bind.mainGetApiServer(); - } - void reset() { tags.clear(); peers.clear(); - notifyListeners(); } void addId(String id) async { if (idContainBy(id)) { return; } - peers.add({"id": id}); - notifyListeners(); + peers.add(Peer.fromJson({"id": id})); } void addTag(String tag) async { @@ -76,42 +72,40 @@ class AbModel with ChangeNotifier { return; } tags.add(tag); - notifyListeners(); } void changeTagForPeer(String id, List tags) { - final it = peers.where((element) => element['id'] == id); + final it = peers.where((element) => element.id == id); if (it.isEmpty) { return; } - it.first['tags'] = tags; + it.first.tags = tags; } - Future updateAb() async { - abLoading = true; - notifyListeners(); - final api = "${await getApiServer()}/api/ab"; + Future pushAb() async { + abLoading.value = true; + final api = "${await bind.mainGetApiServer()}/api/ab"; var authHeaders = await getHttpHeaders(); authHeaders['Content-Type'] = "application/json"; + final peersJsonData = peers.map((e) => e.toJson()).toList(); final body = jsonEncode({ - "data": jsonEncode({"tags": tags, "peers": peers}) + "data": jsonEncode({"tags": tags, "peers": peersJsonData}) }); try { final resp = await http.post(Uri.parse(api), headers: authHeaders, body: body); - abError = ""; - await getAb(); + abError.value = ""; + await pullAb(); debugPrint("resp: ${resp.body}"); } catch (e) { - abError = e.toString(); + abError.value = e.toString(); } finally { - abLoading = false; + abLoading.value = false; } - notifyListeners(); } bool idContainBy(String id) { - return peers.where((element) => element['id'] == id).isNotEmpty; + return peers.where((element) => element.id == id).isNotEmpty; } bool tagContainBy(String tag) { @@ -119,50 +113,47 @@ class AbModel with ChangeNotifier { } void deletePeer(String id) { - peers.removeWhere((element) => element['id'] == id); - notifyListeners(); + peers.removeWhere((element) => element.id == id); } void deleteTag(String tag) { + gFFI.abModel.selectedTags.remove(tag); tags.removeWhere((element) => element == tag); for (var peer in peers) { - if (peer['tags'] == null) { + if (peer.tags.isEmpty) { continue; } - if (((peer['tags']) as List).contains(tag)) { - ((peer['tags']) as List).remove(tag); + if (peer.tags.contains(tag)) { + ((peer.tags)).remove(tag); } } - notifyListeners(); } void unsetSelectedTags() { selectedTags.clear(); - notifyListeners(); } List getPeerTags(String id) { - final it = peers.where((p0) => p0['id'] == id); + final it = peers.where((p0) => p0.id == id); if (it.isEmpty) { return []; } else { - return it.first['tags'] ?? []; + return it.first.tags; } } - void setPeerOption(String id, String key, String value) { - final it = peers.where((p0) => p0['id'] == id); + void setPeerAlias(String id, String value) { + final it = peers.where((p0) => p0.id == id); if (it.isEmpty) { - debugPrint("${id} is not exists"); + debugPrint("$id is not exists"); return; } else { - it.first[key] = value; + it.first.alias = value; } } void clear() { peers.clear(); tags.clear(); - notifyListeners(); } } diff --git a/flutter/lib/models/peer_model.dart b/flutter/lib/models/peer_model.dart index c68ca26df..6dd94bcf4 100644 --- a/flutter/lib/models/peer_model.dart +++ b/flutter/lib/models/peer_model.dart @@ -7,8 +7,8 @@ class Peer { final String username; final String hostname; final String platform; - final String alias; - final List tags; + String alias; + List tags; bool online = false; Peer.fromJson(Map json) @@ -19,6 +19,17 @@ class Peer { alias = json['alias'] ?? '', tags = json['tags'] ?? []; + Map toJson() { + return { + "id": id, + "username": username, + "hostname": hostname, + "platform": platform, + "alias": alias, + "tags": tags, + }; + } + Peer({ required this.id, required this.username,