peer card batch operation

Signed-off-by: 21pages <pages21@163.com>
This commit is contained in:
21pages 2023-08-03 16:48:14 +08:00
parent bdc5cded22
commit 5a0865559c
8 changed files with 462 additions and 174 deletions

View File

@ -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<dynamic> currentTags, Function(List<dynamic>) 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,
);
});
}

View File

@ -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?>(
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<BoxDecoration?> 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<BoxDecoration?> 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<String> _removeAction(
String id, Future<void> Function() reloadFunc,
{bool isLan = false}) {
MenuEntryBase<String> _removeAction(String id) {
return MenuEntryButton<String>(
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<List<MenuEntryBase<String>>> _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<List<MenuEntryBase<String>>> _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<List<MenuEntryBase<String>>> _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<List<MenuEntryBase<String>>> _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<String> _removeAction(
String id, Future<void> Function() reloadFunc,
{bool isLan = false}) {
return MenuEntryButton<String>(
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<String> _editTagAction(String id) {
return MenuEntryButton<String>(
@ -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<List<MenuEntryBase<String>>> _buildMenuItems(

View File

@ -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<PeerTabPage>
@override
Widget build(BuildContext context) {
final model = Provider.of<PeerTabModel>(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<PeerTabPage>
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<PeerTabPage>
).marginOnly(left: 8),
),
],
),
)),
),
),
_createPeersView(),
@ -251,6 +257,167 @@ class _PeerTabPageState extends State<PeerTabPage>
),
);
}
Widget createMultiSelectionBar() {
final model = Provider.of<PeerTabModel>(context);
return Row(
children: [
deleteSelection(),
addSelectionToFav(),
addSelectionToAb(),
editSelectionTags(),
Expanded(child: Container()),
selectionCount(model.selectedPeers.length),
selectAll(),
closeSelection(),
],
);
}
Widget deleteSelection() {
final model = Provider.of<PeerTabModel>(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<PeerTabModel>(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<PeerTabModel>(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<PeerTabModel>(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<PeerTabModel>(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<PeerTabModel>(context);
return InkWell(
onTap: () {
model.closeSelection();
},
child:
Tooltip(message: translate('Close'), child: Icon(Icons.clear)))
.marginOnly(left: 10);
}
}
class PeerSearchBar extends StatefulWidget {

View File

@ -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 = <Widget>[];
for (final peer in peers) {
final visibilityChild = VisibilityDetector(

View File

@ -131,6 +131,12 @@ class AbModel {
peers.add(peer);
}
void addPeers(List<Peer> 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<String> ids, List<dynamic> tags) {
peers.map((e) {
if (ids.contains(e.id)) {
e.tags = tags;
}
}).toList();
}
Future<void> 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<String> ids) {
peers.removeWhere((e) => ids.contains(e.id));
}
void deleteTag(String tag) {
gFFI.abModel.selectedTags.remove(tag);
tags.removeWhere((element) => element == tag);

View File

@ -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<int> get indexs => List.generate(tabNames.length, (index) => index);
List<Peer> _selectedPeers = List.empty(growable: true);
List<Peer> get selectedPeers => _selectedPeers;
bool get multiSelectionMode => _selectedPeers.isNotEmpty;
List<Peer> _currentTabCachedPeers = List.empty(growable: true);
List<Peer> 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<Peer> 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;
}
}

View File

@ -15,6 +15,7 @@ bool refreshingUser = false;
class UserModel {
final RxString userName = ''.obs;
final RxBool isAdmin = false.obs;
bool get isLogin => userName.isNotEmpty;
WeakReference<FFI> parent;
UserModel(this.parent);

View File

@ -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()
}