From 3fd58bb69d6002e355376ee102257e393762c5a7 Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 9 Aug 2023 22:00:15 +0800 Subject: [PATCH] opt peer card multi select Signed-off-by: 21pages --- flutter/assets/checkbox.ttf | Bin 0 -> 1728 bytes flutter/lib/common.dart | 1 + flutter/lib/common/widgets/peer_card.dart | 98 +++++++++++------- flutter/lib/common/widgets/peer_tab_page.dart | 29 +++++- flutter/lib/main.dart | 6 +- flutter/lib/models/peer_tab_model.dart | 85 ++++++++------- flutter/pubspec.yaml | 3 + 7 files changed, 135 insertions(+), 87 deletions(-) create mode 100644 flutter/assets/checkbox.ttf diff --git a/flutter/assets/checkbox.ttf b/flutter/assets/checkbox.ttf new file mode 100644 index 0000000000000000000000000000000000000000..70ddde69842369c9634906a967604c5bfcff2e86 GIT binary patch literal 1728 zcmd^9OK%%h6#nj=vEz)vZ5~9(MP=$VUO=%^0|E6BMJ-Jv1QfL)LI?>aOf2+2X z>)!(mpZ=Q-&#BH${QN3$pZ`LG#Mq8;gZKyjCmXGJGef}KyHpuJbRE2@Y@wcrFM#z+D|0+oToSF8(;9IO`?beV6IRW#YHjm zq?lS1$HkaPkEKo&O5*v6=N84YCo0FqV-wRkAqr`ZGb9%1Db9*-b5>_uPUpwp`YUs6 zT4wg|TRGwH^=8i&3TJ2KQZnv+o-Y>ja!Ki4wrk}lrBxLtEjf|5_Ip{;KM0*oQVkUI zd%aOo()A>EbpOByHqH&u1d5tYRjY}G5S+T8d zpE=HXinyV%z)^gou|y8HG&acpq_Ii-tHvqJU{~Wb`8|y-tcgc8&Y~#Xi;?HVUe#XT zvYT!g)WRShjaR(-hVMipb2P4bQKuOO_UY36h+6RiFB+BUT(4K+xMtU)uw^f^p6C0a z-HyU*o*S1Maoj#rE~|MIUvk41pSlPh9K_UB*jUFFbrUW^1gIrCe#pMU=sGswGc$U~ zdpLWI(Gfa~Q#EazMv2eX;iwfx1xe)LZaTQm*(&TYW=A&vh?QDI%ep_0o%qBy4^a88 zv5w2=5*kTXn@@F_-)KFf_)@aY*6?*z*G;{z|0PmL!Si+-p8MW { diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index a88bb4930..f12c15160 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -61,20 +61,19 @@ class _PeerCardState extends State<_PeerCard> 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: () { if (peerTabModel.multiSelectionMode) { - peerTabModel.togglePeerSelect(peer); + peerTabModel.select(peer); } else { if (!isWebDesktop) connect(context, peer.id); } }, onDoubleTap: isWebDesktop ? () => connect(context, peer.id) : null, onLongPress: () { - peerTabModel.togglePeerSelect(peer); + peerTabModel.select(peer); }, child: Container( padding: EdgeInsets.only(left: 12, top: 8, bottom: 8), @@ -103,23 +102,7 @@ class _PeerCardState extends State<_PeerCard> ], ).paddingOnly(left: 8.0), ), - 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); - }), + checkBoxOrActionMoreMobile(peer), ], ), ))); @@ -159,15 +142,8 @@ class _PeerCardState extends State<_PeerCard> onDoubleTap: peerTabModel.multiSelectionMode ? null : () => widget.connect(context, peer.id), - onLongPress: () { - peerTabModel.togglePeerSelect(peer); - }, - onSecondaryTapDown: (_) { - peerTabModel.togglePeerSelect(peer); - }, - onTap: peerTabModel.multiSelectionMode - ? () => peerTabModel.togglePeerSelect(peer) - : null, + onTap: () => peerTabModel.select(peer), + onLongPress: () => peerTabModel.select(peer), child: Obx(() => peerCardUiType.value == PeerUiType.grid ? _buildPeerCard(context, peer, deco) : _buildPeerTile(context, peer, deco))), @@ -176,8 +152,6 @@ 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( @@ -237,7 +211,7 @@ class _PeerCardState extends State<_PeerCard> ], ).marginOnly(top: 2), ), - selected ? checkBox() : _actionMore(peer), + checkBoxOrActionMoreDesktop(peer), ], ).paddingOnly(left: 10.0, top: 3.0), ), @@ -250,8 +224,6 @@ 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( @@ -321,7 +293,7 @@ class _PeerCardState extends State<_PeerCard> style: Theme.of(context).textTheme.titleSmall, )), ]).paddingSymmetric(vertical: 8)), - selected ? checkBox() : _actionMore(peer), + checkBoxOrActionMoreDesktop(peer), ], ).paddingSymmetric(horizontal: 12.0), ) @@ -333,11 +305,57 @@ class _PeerCardState extends State<_PeerCard> ); } - Widget checkBox() { - return Icon( - Icons.check_box, - color: MyTheme.accent, - ); + Widget checkBoxOrActionMoreMobile(Peer peer) { + final PeerTabModel peerTabModel = Provider.of(context); + final selected = peerTabModel.isPeerSelected(peer.id); + if (peerTabModel.multiSelectionMode) { + return Padding( + padding: const EdgeInsets.all(12), + child: selected + ? Icon( + Icons.check_box, + color: MyTheme.accent, + ) + : Icon(Icons.check_box_outline_blank), + ); + } else { + return 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 checkBoxOrActionMoreDesktop(Peer peer) { + final PeerTabModel peerTabModel = Provider.of(context); + final selected = peerTabModel.isPeerSelected(peer.id); + if (peerTabModel.multiSelectionMode) { + final icon = selected + ? Icon( + Icons.check_box, + color: MyTheme.accent, + ) + : Icon(Icons.check_box_outline_blank); + bool last = peerTabModel.isShiftDown && peer.id == peerTabModel.lastId; + if (last) { + return Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.accent, width: 1)), + child: icon, + ); + } else { + return icon; + } + } else { + return _actionMore(peer); + } } Widget _actionMore(Peer peer) => Listener( diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index e8cbd2619..a034ec13d 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -103,6 +103,7 @@ class _PeerTabPageState extends State Expanded(child: _createSwitchBar(context)), const PeerSearchBar().marginOnly(right: isMobile ? 0 : 13), _createRefresh(), + _createMultiSelection(), Offstage( offstage: !isDesktop, child: _createPeerViewTypeSwitch(context)), @@ -258,6 +259,24 @@ class _PeerTabPageState extends State ); } + Widget _createMultiSelection() { + final textColor = Theme.of(context).textTheme.titleLarge?.color; + final model = Provider.of(context); + return Container( + padding: EdgeInsets.all(4.0), + child: InkWell( + onTap: () { + model.setMultiSelectionMode(true); + }, + child: Icon( + IconFont.checkbox, + size: 18, + color: textColor, + ), + ), + ); + } + Widget createMultiSelectionBar() { final model = Provider.of(context); return Row( @@ -308,7 +327,7 @@ class _PeerTabPageState extends State default: break; } - gFFI.peerTabModel.closeSelection(); + gFFI.peerTabModel.setMultiSelectionMode(false); showToast(translate('Successful')); } @@ -334,7 +353,7 @@ class _PeerTabPageState extends State } } await bind.mainStoreFav(favs: favs); - gFFI.peerTabModel.closeSelection(); + model.setMultiSelectionMode(false); showToast(translate('Successful')); }, child: Tooltip( @@ -355,7 +374,7 @@ class _PeerTabPageState extends State final peers = model.selectedPeers; gFFI.abModel.addPeers(peers); gFFI.abModel.pushAb(); - gFFI.peerTabModel.closeSelection(); + model.setMultiSelectionMode(false); showToast(translate('Successful')); }, child: Tooltip( @@ -379,7 +398,7 @@ class _PeerTabPageState extends State gFFI.abModel.changeTagForPeers( peers.map((p) => p.id).toList(), selectedTags); gFFI.abModel.pushAb(); - gFFI.peerTabModel.closeSelection(); + model.setMultiSelectionMode(false); showToast(translate('Successful')); }); }, @@ -416,7 +435,7 @@ class _PeerTabPageState extends State final model = Provider.of(context); return InkWell( onTap: () { - model.closeSelection(); + model.setMultiSelectionMode(false); }, child: Tooltip(message: translate('Close'), child: Icon(Icons.clear))) diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 14db061bd..ef3862aac 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -418,7 +418,7 @@ class _AppState extends State { : (context, child) { child = _keepScaleBuilder(context, child); child = botToastBuilder(context, child); - if (desktopType == DesktopType.main) { + if (isDesktop && desktopType == DesktopType.main) { child = keyListenerBuilder(context, child); } return child; @@ -465,9 +465,9 @@ Widget keyListenerBuilder(BuildContext context, Widget? child) { onKey: (RawKeyEvent event) { if (event.logicalKey == LogicalKeyboardKey.shiftLeft) { if (event is RawKeyDownEvent) { - gFFI.peerTabModel.isShiftDown = true; + gFFI.peerTabModel.setShiftDown(true); } else if (event is RawKeyUpEvent) { - gFFI.peerTabModel.isShiftDown = false; + gFFI.peerTabModel.setShiftDown(false); } } }, diff --git a/flutter/lib/models/peer_tab_model.dart b/flutter/lib/models/peer_tab_model.dart index 024158ccc..e7f4192cd 100644 --- a/flutter/lib/models/peer_tab_model.dart +++ b/flutter/lib/models/peer_tab_model.dart @@ -39,11 +39,14 @@ class PeerTabModel with ChangeNotifier { List get indexs => List.generate(tabNames.length, (index) => index); List _selectedPeers = List.empty(growable: true); List get selectedPeers => _selectedPeers; - bool get multiSelectionMode => _selectedPeers.isNotEmpty; + bool _multiSelectionMode = false; + bool get multiSelectionMode => _multiSelectionMode; List _currentTabCachedPeers = List.empty(growable: true); List get currentTabCachedPeers => _currentTabCachedPeers; - bool isShiftDown = false; - String? _shiftAnchorId; + bool _isShiftDown = false; + bool get isShiftDown => _isShiftDown; + String _lastId = ''; + String get lastId => _lastId; PeerTabModel(this.parent) { // init currentTab @@ -85,38 +88,39 @@ class PeerTabModel with ChangeNotifier { return Icons.help; } - togglePeerSelect(Peer peer) { + setMultiSelectionMode(bool mode) { + _multiSelectionMode = mode; + if (!mode) { + _selectedPeers.clear(); + _lastId = ''; + } + notifyListeners(); + } + + select(Peer peer) { + if (!_multiSelectionMode) { + // https://github.com/flutter/flutter/issues/101275#issuecomment-1604541700 + // After onTap, the shift key should be pressed for a while when not in multiselection mode, + // because onTap is delayed when onDoubleTap is not null + if (isDesktop && !_isShiftDown) return; + _multiSelectionMode = true; + } final cached = _currentTabCachedPeers.map((e) => e.id).toList(); int thisIndex = cached.indexOf(peer.id); - int closestIndex = -1; - String? closestId; - int smallestDiff = -1; - for (var i = 0; i < cached.length; i++) { - if (isPeerSelected(cached[i])) { - int diff = (i - thisIndex).abs(); - if (smallestDiff == -1 || diff < smallestDiff) { - closestIndex = i; - closestId = cached[i]; - smallestDiff = diff; - } - } - } - if (isShiftDown && - thisIndex >= 0 && - closestIndex >= 0 && - closestId != null) { - int shiftAnchorIndex = cached.indexOf(_shiftAnchorId ?? ''); - if (shiftAnchorIndex < 0) { - // use closest as shift anchor, rather than focused which we don't have - shiftAnchorIndex = closestIndex; - _shiftAnchorId = closestId; - } - int start = min(shiftAnchorIndex, thisIndex); - int end = max(shiftAnchorIndex, thisIndex); - _selectedPeers.clear(); + int lastIndex = cached.indexOf(_lastId); + if (_isShiftDown && thisIndex >= 0 && lastIndex >= 0) { + int start = min(thisIndex, lastIndex); + int end = max(thisIndex, lastIndex); + bool remove = isPeerSelected(peer.id); for (var i = start; i <= end; i++) { - if (!isPeerSelected(cached[i])) { - _selectedPeers.add(_currentTabCachedPeers[i]); + if (remove) { + if (isPeerSelected(cached[i])) { + _selectedPeers.removeWhere((p) => p.id == cached[i]); + } + } else { + if (!isPeerSelected(cached[i])) { + _selectedPeers.add(_currentTabCachedPeers[i]); + } } } } else { @@ -125,14 +129,8 @@ class PeerTabModel with ChangeNotifier { } else { _selectedPeers.add(peer); } - _shiftAnchorId = null; } - notifyListeners(); - } - - closeSelection() { - _selectedPeers.clear(); - _shiftAnchorId = null; + _lastId = peer.id; notifyListeners(); } @@ -151,4 +149,13 @@ class PeerTabModel with ChangeNotifier { bool isPeerSelected(String id) { return selectedPeers.firstWhereOrNull((p) => p.id == id) != null; } + + setShiftDown(bool v) { + if (_isShiftDown != v) { + _isShiftDown = v; + if (_multiSelectionMode) { + notifyListeners(); + } + } + } } diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index e7bc54b4b..9ae0016b6 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -149,6 +149,9 @@ flutter: - family: AddressBook fonts: - asset: assets/address_book.ttf + - family: CheckBox + fonts: + - asset: assets/checkbox.ttf # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware.