diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart index 6f9248c1d..05a105e68 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -9,6 +9,7 @@ import 'package:get/get.dart'; import '../../common.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; +import 'address_book.dart'; void clientClose(SessionID sessionId, OverlayDialogManager dialogManager) { msgBox(sessionId, 'info', 'Close', 'Are you sure to close the connection?', @@ -1350,3 +1351,98 @@ customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async { ); msgBoxCommon(ffi.dialogManager, 'Custom Image Quality', content, [btnClose]); } + +void deletePeerConfirmDialog(Function onSubmit) async { + gFFI.dialogManager.show( + (setState, close, context) { + submit() async { + await onSubmit(); + close(); + } + + return CustomAlertDialog( + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.delete_rounded, + color: Colors.red, + ), + Text(translate('Delete')).paddingOnly( + left: 10, + ), + ], + ), + content: SizedBox.shrink(), + actions: [ + dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: close, + isOutline: true, + ), + dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: submit, + ), + ], + onSubmit: submit, + onCancel: close, + ); + }, + ); +} + +void editAbTagDialog( + List currentTags, Function(List) onSubmit) { + var isInProgress = false; + + final tags = List.of(gFFI.abModel.tags); + var selectedTag = currentTags.obs; + + gFFI.dialogManager.show((setState, close, context) { + submit() async { + setState(() { + isInProgress = true; + }); + await onSubmit(selectedTag); + close(); + } + + return CustomAlertDialog( + title: Text(translate("Edit Tag")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Wrap( + children: tags + .map((e) => AddressBookTag( + name: e, + tags: selectedTag, + onTap: () { + if (selectedTag.contains(e)) { + selectedTag.remove(e); + } else { + selectedTag.add(e); + } + }, + showActionMenu: false)) + .toList(growable: false), + ), + ), + Offstage( + offstage: !isInProgress, child: const LinearProgressIndicator()) + ], + ), + actions: [ + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 9da472fb4..a69b302f5 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -3,8 +3,11 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/common/widgets/address_book.dart'; +import 'package:flutter_hbb/common/widgets/dialog.dart'; import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:get/get.dart'; +import 'package:provider/provider.dart'; import '../../common.dart'; import '../../common/formatter/id_formatter.dart'; @@ -58,17 +61,21 @@ class _PeerCardState extends State<_PeerCard> final peer = super.widget.peer; final name = '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}'; - + final PeerTabModel peerTabModel = Provider.of(context); + final selected = peerTabModel.isPeerSelected(peer.id); return Card( margin: EdgeInsets.symmetric(horizontal: 2), child: GestureDetector( - onTap: !isWebDesktop ? () => connect(context, peer.id) : null, + onTap: () { + if (peerTabModel.multiSelectionMode) { + peerTabModel.onPeerCardTap(peer); + } else { + if (!isWebDesktop) connect(context, peer.id); + } + }, onDoubleTap: isWebDesktop ? () => connect(context, peer.id) : null, - onLongPressStart: (details) { - final x = details.globalPosition.dx; - final y = details.globalPosition.dy; - _menuPos = RelativeRect.fromLTRB(x, y, x, y); - _showPeerMenu(peer.id); + onLongPress: () { + peerTabModel.togglePeerSelect(peer); }, child: Container( padding: EdgeInsets.only(left: 12, top: 8, bottom: 8), @@ -97,24 +104,30 @@ class _PeerCardState extends State<_PeerCard> ], ).paddingOnly(left: 8.0), ), - InkWell( - child: const Padding( - padding: EdgeInsets.all(12), - child: Icon(Icons.more_vert)), - onTapDown: (e) { - final x = e.globalPosition.dx; - final y = e.globalPosition.dy; - _menuPos = RelativeRect.fromLTRB(x, y, x, y); - }, - onTap: () { - _showPeerMenu(peer.id); - }) + selected + ? Padding( + padding: const EdgeInsets.all(12), + child: checkBox(), + ) + : InkWell( + child: const Padding( + padding: EdgeInsets.all(12), + child: Icon(Icons.more_vert)), + onTapDown: (e) { + final x = e.globalPosition.dx; + final y = e.globalPosition.dy; + _menuPos = RelativeRect.fromLTRB(x, y, x, y); + }, + onTap: () { + _showPeerMenu(peer.id); + }), ], ), ))); } Widget _buildDesktop() { + final PeerTabModel peerTabModel = Provider.of(context); final peer = super.widget.peer; var deco = Rx( BoxDecoration( @@ -144,7 +157,18 @@ class _PeerCardState extends State<_PeerCard> ); }, child: GestureDetector( - onDoubleTap: () => widget.connect(context, peer.id), + onDoubleTap: peerTabModel.multiSelectionMode + ? null + : () => widget.connect(context, peer.id), + onLongPress: () { + peerTabModel.togglePeerSelect(peer); + }, + onSecondaryTapDown: (_) { + peerTabModel.togglePeerSelect(peer); + }, + onTap: peerTabModel.multiSelectionMode + ? () => peerTabModel.onPeerCardTap(peer) + : null, child: Obx(() => peerCardUiType.value == PeerUiType.grid ? _buildPeerCard(context, peer, deco) : _buildPeerTile(context, peer, deco))), @@ -153,6 +177,8 @@ class _PeerCardState extends State<_PeerCard> Widget _buildPeerTile( BuildContext context, Peer peer, Rx deco) { + final PeerTabModel peerTabModel = Provider.of(context); + final selected = peerTabModel.isPeerSelected(peer.id); final name = '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}'; final greyStyle = TextStyle( @@ -212,7 +238,7 @@ class _PeerCardState extends State<_PeerCard> ], ).marginOnly(top: 2), ), - _actionMore(peer), + selected ? checkBox() : _actionMore(peer), ], ).paddingOnly(left: 10.0, top: 3.0), ), @@ -225,6 +251,8 @@ class _PeerCardState extends State<_PeerCard> Widget _buildPeerCard( BuildContext context, Peer peer, Rx deco) { + final PeerTabModel peerTabModel = Provider.of(context); + final selected = peerTabModel.isPeerSelected(peer.id); final name = '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}'; return Card( @@ -294,7 +322,7 @@ class _PeerCardState extends State<_PeerCard> style: Theme.of(context).textTheme.titleSmall, )), ]).paddingSymmetric(vertical: 8)), - _actionMore(peer), + selected ? checkBox() : _actionMore(peer), ], ).paddingSymmetric(horizontal: 12.0), ) @@ -306,6 +334,13 @@ class _PeerCardState extends State<_PeerCard> ); } + Widget checkBox() { + return Icon( + Icons.check_box, + color: MyTheme.accent, + ); + } + Widget _actionMore(Peer peer) => Listener( onPointerDown: (e) { final x = e.position.dx; @@ -332,9 +367,11 @@ class _PeerCardState extends State<_PeerCard> abstract class BasePeerCard extends StatelessWidget { final Peer peer; + final PeerTabIndex tab; final EdgeInsets? menuPadding; - BasePeerCard({required this.peer, this.menuPadding, Key? key}) + BasePeerCard( + {required this.peer, required this.tab, this.menuPadding, Key? key}) : super(key: key); @override @@ -524,9 +561,7 @@ abstract class BasePeerCard extends StatelessWidget { } @protected - MenuEntryBase _removeAction( - String id, Future Function() reloadFunc, - {bool isLan = false}) { + MenuEntryBase _removeAction(String id) { return MenuEntryButton( childBuilder: (TextStyle? style) => Row( children: [ @@ -545,7 +580,33 @@ abstract class BasePeerCard extends StatelessWidget { ], ), proc: () { - _delete(id, isLan, reloadFunc); + onSubmit() async { + switch (tab) { + case PeerTabIndex.recent: + await bind.mainRemovePeer(id: id); + await bind.mainLoadRecentPeers(); + break; + case PeerTabIndex.fav: + final favs = (await bind.mainGetFav()).toList(); + if (favs.remove(id)) { + await bind.mainStoreFav(favs: favs); + await bind.mainLoadFavPeers(); + } + break; + case PeerTabIndex.lan: + await bind.mainRemoveDiscovered(id: id); + await bind.mainLoadLanPeers(); + break; + case PeerTabIndex.ab: + gFFI.abModel.deletePeer(id); + await gFFI.abModel.pushAb(); + break; + case PeerTabIndex.group: + break; + } + } + + deletePeerConfirmDialog(onSubmit); }, padding: menuPadding, dismissOnClicked: true, @@ -721,62 +782,15 @@ abstract class BasePeerCard extends StatelessWidget { @protected void _update(); - - void _delete(String id, bool isLan, Function reloadFunc) async { - gFFI.dialogManager.show( - (setState, close, context) { - submit() async { - if (isLan) { - await bind.mainRemoveDiscovered(id: id); - } else { - final favs = (await bind.mainGetFav()).toList(); - if (favs.remove(id)) { - await bind.mainStoreFav(favs: favs); - } - await bind.mainRemovePeer(id: id); - } - await reloadFunc(); - close(); - } - - return CustomAlertDialog( - title: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.delete_rounded, - color: Colors.red, - ), - Text(translate('Delete')).paddingOnly( - left: 10, - ), - ], - ), - content: SizedBox.shrink(), - actions: [ - dialogButton( - "Cancel", - icon: Icon(Icons.close_rounded), - onPressed: close, - isOutline: true, - ), - dialogButton( - "OK", - icon: Icon(Icons.done_rounded), - onPressed: submit, - ), - ], - onSubmit: submit, - onCancel: close, - ); - }, - ); - } } class RecentPeerCard extends BasePeerCard { RecentPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) - : super(peer: peer, menuPadding: menuPadding, key: key); + : super( + peer: peer, + tab: PeerTabIndex.recent, + menuPadding: menuPadding, + key: key); @override Future>> _buildMenuItems( @@ -817,9 +831,7 @@ class RecentPeerCard extends BasePeerCard { } menuItems.add(MenuEntryDivider()); - menuItems.add(_removeAction(peer.id, () async { - await bind.mainLoadRecentPeers(); - })); + menuItems.add(_removeAction(peer.id)); return menuItems; } @@ -830,7 +842,11 @@ class RecentPeerCard extends BasePeerCard { class FavoritePeerCard extends BasePeerCard { FavoritePeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) - : super(peer: peer, menuPadding: menuPadding, key: key); + : super( + peer: peer, + tab: PeerTabIndex.fav, + menuPadding: menuPadding, + key: key); @override Future>> _buildMenuItems( @@ -865,9 +881,7 @@ class FavoritePeerCard extends BasePeerCard { } menuItems.add(MenuEntryDivider()); - menuItems.add(_removeAction(peer.id, () async { - await bind.mainLoadFavPeers(); - })); + menuItems.add(_removeAction(peer.id)); return menuItems; } @@ -878,7 +892,11 @@ class FavoritePeerCard extends BasePeerCard { class DiscoveredPeerCard extends BasePeerCard { DiscoveredPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) - : super(peer: peer, menuPadding: menuPadding, key: key); + : super( + peer: peer, + tab: PeerTabIndex.lan, + menuPadding: menuPadding, + key: key); @override Future>> _buildMenuItems( @@ -915,11 +933,7 @@ class DiscoveredPeerCard extends BasePeerCard { } menuItems.add(MenuEntryDivider()); - menuItems.add( - _removeAction(peer.id, () async { - await bind.mainLoadLanPeers(); - }, isLan: true), - ); + menuItems.add(_removeAction(peer.id)); return menuItems; } @@ -930,7 +944,11 @@ class DiscoveredPeerCard extends BasePeerCard { class AddressBookPeerCard extends BasePeerCard { AddressBookPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) - : super(peer: peer, menuPadding: menuPadding, key: key); + : super( + peer: peer, + tab: PeerTabIndex.ab, + menuPadding: menuPadding, + key: key); @override Future>> _buildMenuItems( @@ -959,7 +977,7 @@ class AddressBookPeerCard extends BasePeerCard { } menuItems.add(MenuEntryDivider()); - menuItems.add(_removeAction(peer.id, () async {})); + menuItems.add(_removeAction(peer.id)); return menuItems; } @@ -967,27 +985,6 @@ class AddressBookPeerCard extends BasePeerCard { @override void _update() => gFFI.abModel.pullAb(); - @protected - @override - MenuEntryBase _removeAction( - String id, Future Function() reloadFunc, - {bool isLan = false}) { - return MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Remove'), - style: style, - ), - proc: () { - () async { - gFFI.abModel.deletePeer(id); - await gFFI.abModel.pushAb(); - }(); - }, - padding: super.menuPadding, - dismissOnClicked: true, - ); - } - @protected MenuEntryBase _editTagAction(String id) { return MenuEntryButton( @@ -996,70 +993,24 @@ class AddressBookPeerCard extends BasePeerCard { style: style, ), proc: () { - _abEditTag(id); + editAbTagDialog(gFFI.abModel.getPeerTags(id), (selectedTag) async { + gFFI.abModel.changeTagForPeer(id, selectedTag); + await gFFI.abModel.pushAb(); + }); }, padding: super.menuPadding, dismissOnClicked: true, ); } - - void _abEditTag(String id) { - var isInProgress = false; - - final tags = List.of(gFFI.abModel.tags); - var selectedTag = gFFI.abModel.getPeerTags(id).obs; - - gFFI.dialogManager.show((setState, close, context) { - submit() async { - setState(() { - isInProgress = true; - }); - gFFI.abModel.changeTagForPeer(id, selectedTag); - await gFFI.abModel.pushAb(); - close(); - } - - return CustomAlertDialog( - title: Text(translate("Edit Tag")), - content: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Wrap( - children: tags - .map((e) => AddressBookTag( - name: e, - tags: selectedTag, - onTap: () { - if (selectedTag.contains(e)) { - selectedTag.remove(e); - } else { - selectedTag.add(e); - } - }, - showActionMenu: false)) - .toList(growable: false), - ), - ), - Offstage( - offstage: !isInProgress, child: const LinearProgressIndicator()) - ], - ), - actions: [ - dialogButton("Cancel", onPressed: close, isOutline: true), - dialogButton("OK", onPressed: submit), - ], - onSubmit: submit, - onCancel: close, - ); - }); - } } class MyGroupPeerCard extends BasePeerCard { MyGroupPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) - : super(peer: peer, menuPadding: menuPadding, key: key); + : super( + peer: peer, + tab: PeerTabIndex.group, + menuPadding: menuPadding, + key: key); @override Future>> _buildMenuItems( diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index ab85b2960..56b5bc72c 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/widgets/address_book.dart'; +import 'package:flutter_hbb/common/widgets/dialog.dart'; import 'package:flutter_hbb/common/widgets/my_group.dart'; import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/common/widgets/peer_card.dart'; @@ -83,6 +84,11 @@ class _PeerTabPageState extends State @override Widget build(BuildContext context) { + final model = Provider.of(context); + Widget selectionWrap(Widget widget) { + return model.multiSelectionMode ? createMultiSelectionBar() : widget; + } + return Column( textBaseline: TextBaseline.ideographic, crossAxisAlignment: CrossAxisAlignment.start, @@ -91,7 +97,7 @@ class _PeerTabPageState extends State height: 32, child: Container( padding: isDesktop ? null : EdgeInsets.symmetric(horizontal: 2), - child: Row( + child: selectionWrap(Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded(child: _createSwitchBar(context)), @@ -127,7 +133,7 @@ class _PeerTabPageState extends State ).marginOnly(left: 8), ), ], - ), + )), ), ), _createPeersView(), @@ -251,6 +257,167 @@ class _PeerTabPageState extends State ), ); } + + Widget createMultiSelectionBar() { + final model = Provider.of(context); + return Row( + children: [ + deleteSelection(), + addSelectionToFav(), + addSelectionToAb(), + editSelectionTags(), + Expanded(child: Container()), + selectionCount(model.selectedPeers.length), + selectAll(), + closeSelection(), + ], + ); + } + + Widget deleteSelection() { + final model = Provider.of(context); + return InkWell( + onTap: () { + onSubmit() async { + final peers = model.selectedPeers; + switch (model.currentTab) { + case 0: + peers.map((p) async { + await bind.mainRemovePeer(id: p.id); + }).toList(); + await bind.mainLoadRecentPeers(); + break; + case 1: + final favs = (await bind.mainGetFav()).toList(); + peers.map((p) { + favs.remove(p.id); + }).toList(); + await bind.mainStoreFav(favs: favs); + await bind.mainLoadFavPeers(); + break; + case 2: + peers.map((p) async { + await bind.mainRemoveDiscovered(id: p.id); + }).toList(); + await bind.mainLoadLanPeers(); + break; + case 3: + gFFI.abModel.deletePeers(peers.map((p) => p.id).toList()); + await gFFI.abModel.pushAb(); + break; + default: + break; + } + gFFI.peerTabModel.closeSelection(); + } + + deletePeerConfirmDialog(onSubmit); + }, + child: Tooltip( + message: translate('Delete'), + child: Icon(Icons.delete, color: Colors.red))); + } + + Widget addSelectionToFav() { + final model = Provider.of(context); + return Offstage( + offstage: + model.currentTab != PeerTabIndex.recent.index, // show based on recent + child: InkWell( + onTap: () async { + final peers = model.selectedPeers; + final favs = (await bind.mainGetFav()).toList(); + for (var p in peers) { + if (!favs.contains(p.id)) { + favs.add(p.id); + } + } + await bind.mainStoreFav(favs: favs); + gFFI.peerTabModel.closeSelection(); + }, + child: Tooltip( + message: translate('Add to Favorites'), + child: Icon(model.icons[PeerTabIndex.fav.index])) + .marginOnly(left: isMobile ? 15 : 10), + ), + ); + } + + Widget addSelectionToAb() { + final model = Provider.of(context); + return Offstage( + offstage: + !gFFI.userModel.isLogin || model.currentTab == PeerTabIndex.ab.index, + child: InkWell( + onTap: () { + final peers = model.selectedPeers; + gFFI.abModel.addPeers(peers); + gFFI.abModel.pushAb(); + gFFI.peerTabModel.closeSelection(); + }, + child: Tooltip( + message: translate('Add to Address Book'), + child: Icon(model.icons[PeerTabIndex.ab.index])) + .marginOnly(left: isMobile ? 15 : 10), + ), + ); + } + + Widget editSelectionTags() { + final model = Provider.of(context); + return Offstage( + offstage: !gFFI.userModel.isLogin || + model.currentTab != PeerTabIndex.ab.index || + gFFI.abModel.tags.isEmpty, + child: InkWell( + onTap: () { + editAbTagDialog(List.empty(), (selectedTags) async { + final peers = model.selectedPeers; + gFFI.abModel.changeTagForPeers( + peers.map((p) => p.id).toList(), selectedTags); + gFFI.abModel.pushAb(); + gFFI.peerTabModel.closeSelection(); + }); + }, + child: Tooltip( + message: translate('Edit Tag'), child: Icon(Icons.tag))) + .marginOnly(left: isMobile ? 15 : 10), + ); + } + + Widget selectionCount(int count) { + return Align( + alignment: Alignment.center, + child: Text('$count selected'), + ); + } + + Widget selectAll() { + final model = Provider.of(context); + return Offstage( + offstage: + model.selectedPeers.length >= model.currentTabCachedPeers.length, + child: InkWell( + onTap: () { + model.selectAll(); + }, + child: Tooltip( + message: translate('Select All'), child: Icon(Icons.select_all)) + .marginOnly(left: 10), + ), + ); + } + + Widget closeSelection() { + final model = Provider.of(context); + return InkWell( + onTap: () { + model.closeSelection(); + }, + child: + Tooltip(message: translate('Close'), child: Icon(Icons.clear))) + .marginOnly(left: 10); + } } class PeerSearchBar extends StatefulWidget { diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart index 95099bcc8..e9af1c6e3 100644 --- a/flutter/lib/common/widgets/peers_view.dart +++ b/flutter/lib/common/widgets/peers_view.dart @@ -172,6 +172,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener { builder: (context, snapshot) { if (snapshot.hasData) { final peers = snapshot.data!; + gFFI.peerTabModel.setCurrentTabCachedPeers(peers); final cards = []; for (final peer in peers) { final visibilityChild = VisibilityDetector( diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index 2562a0b0e..9d24de94c 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -131,6 +131,12 @@ class AbModel { peers.add(peer); } + void addPeers(List ps) { + for (var p in ps) { + addPeer(p); + } + } + void addTag(String tag) async { if (tagContainBy(tag)) { return; @@ -146,6 +152,14 @@ class AbModel { it.first.tags = tags; } + void changeTagForPeers(List ids, List tags) { + peers.map((e) { + if (ids.contains(e.id)) { + e.tags = tags; + } + }).toList(); + } + Future pushAb() async { debugPrint("pushAb"); final api = "${await bind.mainGetApiServer()}/api/ab"; @@ -192,6 +206,10 @@ class AbModel { peers.removeWhere((element) => element.id == id); } + void deletePeers(List ids) { + peers.removeWhere((e) => ids.contains(e.id)); + } + void deleteTag(String tag) { gFFI.abModel.selectedTags.remove(tag); tags.removeWhere((element) => element == tag); diff --git a/flutter/lib/models/peer_tab_model.dart b/flutter/lib/models/peer_tab_model.dart index 5c3c6b01e..31d1855e8 100644 --- a/flutter/lib/models/peer_tab_model.dart +++ b/flutter/lib/models/peer_tab_model.dart @@ -1,10 +1,19 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get.dart'; import '../common.dart'; import 'model.dart'; -const int groupTabIndex = 4; +enum PeerTabIndex { + recent, + fav, + lan, + ab, + group, +} + const String defaultGroupTabname = 'Group'; class PeerTabModel with ChangeNotifier { @@ -26,6 +35,11 @@ class PeerTabModel with ChangeNotifier { Icons.group, ]; List get indexs => List.generate(tabNames.length, (index) => index); + List _selectedPeers = List.empty(growable: true); + List get selectedPeers => _selectedPeers; + bool get multiSelectionMode => _selectedPeers.isNotEmpty; + List _currentTabCachedPeers = List.empty(growable: true); + List get currentTabCachedPeers => _currentTabCachedPeers; PeerTabModel(this.parent) { // init currentTab @@ -45,7 +59,7 @@ class PeerTabModel with ChangeNotifier { String tabTooltip(int index, String groupName) { if (index >= 0 && index < tabNames.length) { - if (index == groupTabIndex) { + if (index == PeerTabIndex.group.index) { if (gFFI.userModel.isAdmin.value || groupName.isEmpty) { return translate(defaultGroupTabname); } else { @@ -66,4 +80,39 @@ class PeerTabModel with ChangeNotifier { assert(false); return Icons.help; } + + togglePeerSelect(Peer peer) { + if (_selectedPeers.firstWhereOrNull((p) => p.id == peer.id) != null) { + _selectedPeers.removeWhere((p) => p.id == peer.id); + } else { + _selectedPeers.add(peer); + } + notifyListeners(); + } + + onPeerCardTap(Peer peer) { + if (!multiSelectionMode) return; + togglePeerSelect(peer); + } + + closeSelection() { + _selectedPeers.clear(); + notifyListeners(); + } + + setCurrentTabCachedPeers(List peers) { + Future.delayed(Duration.zero, () { + _currentTabCachedPeers = peers; + notifyListeners(); + }); + } + + selectAll() { + _selectedPeers = _currentTabCachedPeers.toList(); + notifyListeners(); + } + + bool isPeerSelected(String id) { + return selectedPeers.firstWhereOrNull((p) => p.id == id) != null; + } } diff --git a/flutter/lib/models/user_model.dart b/flutter/lib/models/user_model.dart index 5730f5054..83df2e632 100644 --- a/flutter/lib/models/user_model.dart +++ b/flutter/lib/models/user_model.dart @@ -15,6 +15,7 @@ bool refreshingUser = false; class UserModel { final RxString userName = ''.obs; final RxBool isAdmin = false.obs; + bool get isLogin => userName.isNotEmpty; WeakReference parent; UserModel(this.parent); diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 36709d8c5..8a801b42a 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -978,6 +978,11 @@ impl PeerConfig { config } Err(err) => { + if let confy::ConfyError::GeneralLoadError(err) = &err { + if err.kind() == std::io::ErrorKind::NotFound { + return Default::default(); + } + } log::error!("Failed to load peer config '{}': {}", id, err); Default::default() }