opt peer card multi select

Signed-off-by: 21pages <pages21@163.com>
This commit is contained in:
21pages 2023-08-09 22:00:15 +08:00
parent 5028b8a93d
commit 3fd58bb69d
7 changed files with 135 additions and 87 deletions

BIN
flutter/assets/checkbox.ttf Normal file

Binary file not shown.

View File

@ -90,6 +90,7 @@ class IconFont {
static const IconData roundClose = IconData(0xe6ed, fontFamily: _family2); static const IconData roundClose = IconData(0xe6ed, fontFamily: _family2);
static const IconData addressBook = static const IconData addressBook =
IconData(0xe602, fontFamily: "AddressBook"); IconData(0xe602, fontFamily: "AddressBook");
static const IconData checkbox = IconData(0xe7d6, fontFamily: "CheckBox");
} }
class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> { class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {

View File

@ -61,20 +61,19 @@ class _PeerCardState extends State<_PeerCard>
final name = final name =
'${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}'; '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
final PeerTabModel peerTabModel = Provider.of(context); final PeerTabModel peerTabModel = Provider.of(context);
final selected = peerTabModel.isPeerSelected(peer.id);
return Card( return Card(
margin: EdgeInsets.symmetric(horizontal: 2), margin: EdgeInsets.symmetric(horizontal: 2),
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
if (peerTabModel.multiSelectionMode) { if (peerTabModel.multiSelectionMode) {
peerTabModel.togglePeerSelect(peer); peerTabModel.select(peer);
} else { } else {
if (!isWebDesktop) connect(context, peer.id); if (!isWebDesktop) connect(context, peer.id);
} }
}, },
onDoubleTap: isWebDesktop ? () => connect(context, peer.id) : null, onDoubleTap: isWebDesktop ? () => connect(context, peer.id) : null,
onLongPress: () { onLongPress: () {
peerTabModel.togglePeerSelect(peer); peerTabModel.select(peer);
}, },
child: Container( child: Container(
padding: EdgeInsets.only(left: 12, top: 8, bottom: 8), padding: EdgeInsets.only(left: 12, top: 8, bottom: 8),
@ -103,23 +102,7 @@ class _PeerCardState extends State<_PeerCard>
], ],
).paddingOnly(left: 8.0), ).paddingOnly(left: 8.0),
), ),
selected checkBoxOrActionMoreMobile(peer),
? 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);
}),
], ],
), ),
))); )));
@ -159,15 +142,8 @@ class _PeerCardState extends State<_PeerCard>
onDoubleTap: peerTabModel.multiSelectionMode onDoubleTap: peerTabModel.multiSelectionMode
? null ? null
: () => widget.connect(context, peer.id), : () => widget.connect(context, peer.id),
onLongPress: () { onTap: () => peerTabModel.select(peer),
peerTabModel.togglePeerSelect(peer); onLongPress: () => peerTabModel.select(peer),
},
onSecondaryTapDown: (_) {
peerTabModel.togglePeerSelect(peer);
},
onTap: peerTabModel.multiSelectionMode
? () => peerTabModel.togglePeerSelect(peer)
: null,
child: Obx(() => peerCardUiType.value == PeerUiType.grid child: Obx(() => peerCardUiType.value == PeerUiType.grid
? _buildPeerCard(context, peer, deco) ? _buildPeerCard(context, peer, deco)
: _buildPeerTile(context, peer, deco))), : _buildPeerTile(context, peer, deco))),
@ -176,8 +152,6 @@ class _PeerCardState extends State<_PeerCard>
Widget _buildPeerTile( Widget _buildPeerTile(
BuildContext context, Peer peer, Rx<BoxDecoration?> deco) { BuildContext context, Peer peer, Rx<BoxDecoration?> deco) {
final PeerTabModel peerTabModel = Provider.of(context);
final selected = peerTabModel.isPeerSelected(peer.id);
final name = final name =
'${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}'; '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
final greyStyle = TextStyle( final greyStyle = TextStyle(
@ -237,7 +211,7 @@ class _PeerCardState extends State<_PeerCard>
], ],
).marginOnly(top: 2), ).marginOnly(top: 2),
), ),
selected ? checkBox() : _actionMore(peer), checkBoxOrActionMoreDesktop(peer),
], ],
).paddingOnly(left: 10.0, top: 3.0), ).paddingOnly(left: 10.0, top: 3.0),
), ),
@ -250,8 +224,6 @@ class _PeerCardState extends State<_PeerCard>
Widget _buildPeerCard( Widget _buildPeerCard(
BuildContext context, Peer peer, Rx<BoxDecoration?> deco) { BuildContext context, Peer peer, Rx<BoxDecoration?> deco) {
final PeerTabModel peerTabModel = Provider.of(context);
final selected = peerTabModel.isPeerSelected(peer.id);
final name = final name =
'${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}'; '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
return Card( return Card(
@ -321,7 +293,7 @@ class _PeerCardState extends State<_PeerCard>
style: Theme.of(context).textTheme.titleSmall, style: Theme.of(context).textTheme.titleSmall,
)), )),
]).paddingSymmetric(vertical: 8)), ]).paddingSymmetric(vertical: 8)),
selected ? checkBox() : _actionMore(peer), checkBoxOrActionMoreDesktop(peer),
], ],
).paddingSymmetric(horizontal: 12.0), ).paddingSymmetric(horizontal: 12.0),
) )
@ -333,11 +305,57 @@ class _PeerCardState extends State<_PeerCard>
); );
} }
Widget checkBox() { Widget checkBoxOrActionMoreMobile(Peer peer) {
return Icon( final PeerTabModel peerTabModel = Provider.of(context);
Icons.check_box, final selected = peerTabModel.isPeerSelected(peer.id);
color: MyTheme.accent, 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( Widget _actionMore(Peer peer) => Listener(

View File

@ -103,6 +103,7 @@ class _PeerTabPageState extends State<PeerTabPage>
Expanded(child: _createSwitchBar(context)), Expanded(child: _createSwitchBar(context)),
const PeerSearchBar().marginOnly(right: isMobile ? 0 : 13), const PeerSearchBar().marginOnly(right: isMobile ? 0 : 13),
_createRefresh(), _createRefresh(),
_createMultiSelection(),
Offstage( Offstage(
offstage: !isDesktop, offstage: !isDesktop,
child: _createPeerViewTypeSwitch(context)), child: _createPeerViewTypeSwitch(context)),
@ -258,6 +259,24 @@ class _PeerTabPageState extends State<PeerTabPage>
); );
} }
Widget _createMultiSelection() {
final textColor = Theme.of(context).textTheme.titleLarge?.color;
final model = Provider.of<PeerTabModel>(context);
return Container(
padding: EdgeInsets.all(4.0),
child: InkWell(
onTap: () {
model.setMultiSelectionMode(true);
},
child: Icon(
IconFont.checkbox,
size: 18,
color: textColor,
),
),
);
}
Widget createMultiSelectionBar() { Widget createMultiSelectionBar() {
final model = Provider.of<PeerTabModel>(context); final model = Provider.of<PeerTabModel>(context);
return Row( return Row(
@ -308,7 +327,7 @@ class _PeerTabPageState extends State<PeerTabPage>
default: default:
break; break;
} }
gFFI.peerTabModel.closeSelection(); gFFI.peerTabModel.setMultiSelectionMode(false);
showToast(translate('Successful')); showToast(translate('Successful'));
} }
@ -334,7 +353,7 @@ class _PeerTabPageState extends State<PeerTabPage>
} }
} }
await bind.mainStoreFav(favs: favs); await bind.mainStoreFav(favs: favs);
gFFI.peerTabModel.closeSelection(); model.setMultiSelectionMode(false);
showToast(translate('Successful')); showToast(translate('Successful'));
}, },
child: Tooltip( child: Tooltip(
@ -355,7 +374,7 @@ class _PeerTabPageState extends State<PeerTabPage>
final peers = model.selectedPeers; final peers = model.selectedPeers;
gFFI.abModel.addPeers(peers); gFFI.abModel.addPeers(peers);
gFFI.abModel.pushAb(); gFFI.abModel.pushAb();
gFFI.peerTabModel.closeSelection(); model.setMultiSelectionMode(false);
showToast(translate('Successful')); showToast(translate('Successful'));
}, },
child: Tooltip( child: Tooltip(
@ -379,7 +398,7 @@ class _PeerTabPageState extends State<PeerTabPage>
gFFI.abModel.changeTagForPeers( gFFI.abModel.changeTagForPeers(
peers.map((p) => p.id).toList(), selectedTags); peers.map((p) => p.id).toList(), selectedTags);
gFFI.abModel.pushAb(); gFFI.abModel.pushAb();
gFFI.peerTabModel.closeSelection(); model.setMultiSelectionMode(false);
showToast(translate('Successful')); showToast(translate('Successful'));
}); });
}, },
@ -416,7 +435,7 @@ class _PeerTabPageState extends State<PeerTabPage>
final model = Provider.of<PeerTabModel>(context); final model = Provider.of<PeerTabModel>(context);
return InkWell( return InkWell(
onTap: () { onTap: () {
model.closeSelection(); model.setMultiSelectionMode(false);
}, },
child: child:
Tooltip(message: translate('Close'), child: Icon(Icons.clear))) Tooltip(message: translate('Close'), child: Icon(Icons.clear)))

View File

@ -418,7 +418,7 @@ class _AppState extends State<App> {
: (context, child) { : (context, child) {
child = _keepScaleBuilder(context, child); child = _keepScaleBuilder(context, child);
child = botToastBuilder(context, child); child = botToastBuilder(context, child);
if (desktopType == DesktopType.main) { if (isDesktop && desktopType == DesktopType.main) {
child = keyListenerBuilder(context, child); child = keyListenerBuilder(context, child);
} }
return child; return child;
@ -465,9 +465,9 @@ Widget keyListenerBuilder(BuildContext context, Widget? child) {
onKey: (RawKeyEvent event) { onKey: (RawKeyEvent event) {
if (event.logicalKey == LogicalKeyboardKey.shiftLeft) { if (event.logicalKey == LogicalKeyboardKey.shiftLeft) {
if (event is RawKeyDownEvent) { if (event is RawKeyDownEvent) {
gFFI.peerTabModel.isShiftDown = true; gFFI.peerTabModel.setShiftDown(true);
} else if (event is RawKeyUpEvent) { } else if (event is RawKeyUpEvent) {
gFFI.peerTabModel.isShiftDown = false; gFFI.peerTabModel.setShiftDown(false);
} }
} }
}, },

View File

@ -39,11 +39,14 @@ class PeerTabModel with ChangeNotifier {
List<int> get indexs => List.generate(tabNames.length, (index) => index); List<int> get indexs => List.generate(tabNames.length, (index) => index);
List<Peer> _selectedPeers = List.empty(growable: true); List<Peer> _selectedPeers = List.empty(growable: true);
List<Peer> get selectedPeers => _selectedPeers; List<Peer> get selectedPeers => _selectedPeers;
bool get multiSelectionMode => _selectedPeers.isNotEmpty; bool _multiSelectionMode = false;
bool get multiSelectionMode => _multiSelectionMode;
List<Peer> _currentTabCachedPeers = List.empty(growable: true); List<Peer> _currentTabCachedPeers = List.empty(growable: true);
List<Peer> get currentTabCachedPeers => _currentTabCachedPeers; List<Peer> get currentTabCachedPeers => _currentTabCachedPeers;
bool isShiftDown = false; bool _isShiftDown = false;
String? _shiftAnchorId; bool get isShiftDown => _isShiftDown;
String _lastId = '';
String get lastId => _lastId;
PeerTabModel(this.parent) { PeerTabModel(this.parent) {
// init currentTab // init currentTab
@ -85,38 +88,39 @@ class PeerTabModel with ChangeNotifier {
return Icons.help; 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(); final cached = _currentTabCachedPeers.map((e) => e.id).toList();
int thisIndex = cached.indexOf(peer.id); int thisIndex = cached.indexOf(peer.id);
int closestIndex = -1; int lastIndex = cached.indexOf(_lastId);
String? closestId; if (_isShiftDown && thisIndex >= 0 && lastIndex >= 0) {
int smallestDiff = -1; int start = min(thisIndex, lastIndex);
for (var i = 0; i < cached.length; i++) { int end = max(thisIndex, lastIndex);
if (isPeerSelected(cached[i])) { bool remove = isPeerSelected(peer.id);
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();
for (var i = start; i <= end; i++) { for (var i = start; i <= end; i++) {
if (!isPeerSelected(cached[i])) { if (remove) {
_selectedPeers.add(_currentTabCachedPeers[i]); if (isPeerSelected(cached[i])) {
_selectedPeers.removeWhere((p) => p.id == cached[i]);
}
} else {
if (!isPeerSelected(cached[i])) {
_selectedPeers.add(_currentTabCachedPeers[i]);
}
} }
} }
} else { } else {
@ -125,14 +129,8 @@ class PeerTabModel with ChangeNotifier {
} else { } else {
_selectedPeers.add(peer); _selectedPeers.add(peer);
} }
_shiftAnchorId = null;
} }
notifyListeners(); _lastId = peer.id;
}
closeSelection() {
_selectedPeers.clear();
_shiftAnchorId = null;
notifyListeners(); notifyListeners();
} }
@ -151,4 +149,13 @@ class PeerTabModel with ChangeNotifier {
bool isPeerSelected(String id) { bool isPeerSelected(String id) {
return selectedPeers.firstWhereOrNull((p) => p.id == id) != null; return selectedPeers.firstWhereOrNull((p) => p.id == id) != null;
} }
setShiftDown(bool v) {
if (_isShiftDown != v) {
_isShiftDown = v;
if (_multiSelectionMode) {
notifyListeners();
}
}
}
} }

View File

@ -149,6 +149,9 @@ flutter:
- family: AddressBook - family: AddressBook
fonts: fonts:
- asset: assets/address_book.ttf - 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 # An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware. # https://flutter.dev/assets-and-images/#resolution-aware.