flutter_desktop_connection_2: debug lan

Signed-off-by: fufesou <shuanglongchen@yeah.net>
This commit is contained in:
fufesou
2022-08-02 13:10:09 +08:00
parent 90515ea588
commit 74a2929bc9
12 changed files with 740 additions and 609 deletions

View File

@@ -17,7 +17,7 @@ import '../../mobile/pages/scan_page.dart';
import '../../mobile/pages/settings_page.dart';
import '../../models/model.dart';
enum RemoteType { recently, favorite, discovered, addressBook }
// enum RemoteType { recently, favorite, discovered, addressBook }
/// Connection page for connecting to a remote peer.
class ConnectionPage extends StatefulWidget implements PageShape {
@@ -76,74 +76,57 @@ class _ConnectionPageState extends State<ConnectionPage> {
thickness: 1,
),
Expanded(
child: DefaultTabController(
length: 4,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TabBar(
isScrollable: true,
indicatorSize: TabBarIndicatorSize.label,
tabs: [
Tab(
child: Text(translate("Recent Sessions")),
),
Tab(
child: Text(translate("Favorites")),
),
Tab(
child: Text(translate("Discovered")),
),
Tab(
child: Text(translate("Address Book")),
),
]),
Expanded(
child: TabBarView(children: [
RecentPeerWidget(),
FavoritePeerWidget(),
DiscoveredPeerWidget(),
// AddressBookPeerWidget(),
// FutureBuilder<Widget>(
// future: getPeers(rType: RemoteType.recently),
// builder: (context, snapshot) {
// if (snapshot.hasData) {
// return snapshot.data!;
// } else {
// return Offstage();
// }
// }),
// FutureBuilder<Widget>(
// future: getPeers(rType: RemoteType.favorite),
// builder: (context, snapshot) {
// if (snapshot.hasData) {
// return snapshot.data!;
// } else {
// return Offstage();
// }
// }),
// FutureBuilder<Widget>(
// future: getPeers(rType: RemoteType.discovered),
// builder: (context, snapshot) {
// if (snapshot.hasData) {
// return snapshot.data!;
// } else {
// return Offstage();
// }
// }),
FutureBuilder<Widget>(
future: buildAddressBook(context),
builder: (context, snapshot) {
if (snapshot.hasData) {
return snapshot.data!;
} else {
return Offstage();
}
}),
]).paddingSymmetric(horizontal: 12.0, vertical: 4.0))
],
)),
),
// TODO: move all tab info into _PeerTabbedPage
child: _PeerTabbedPage(
tabs: [
translate('Recent Sessions'),
translate('Favorites'),
translate('Discovered'),
translate('Address Book')
],
children: [
RecentPeerWidget(),
FavoritePeerWidget(),
DiscoveredPeerWidget(),
// AddressBookPeerWidget(),
// FutureBuilder<Widget>(
// future: getPeers(rType: RemoteType.recently),
// builder: (context, snapshot) {
// if (snapshot.hasData) {
// return snapshot.data!;
// } else {
// return Offstage();
// }
// }),
// FutureBuilder<Widget>(
// future: getPeers(rType: RemoteType.favorite),
// builder: (context, snapshot) {
// if (snapshot.hasData) {
// return snapshot.data!;
// } else {
// return Offstage();
// }
// }),
// FutureBuilder<Widget>(
// future: getPeers(rType: RemoteType.discovered),
// builder: (context, snapshot) {
// if (snapshot.hasData) {
// return snapshot.data!;
// } else {
// return Offstage();
// }
// }),
FutureBuilder<Widget>(
future: buildAddressBook(context),
builder: (context, snapshot) {
if (snapshot.hasData) {
return snapshot.data!;
} else {
return Offstage();
}
}),
],
)),
Divider(),
SizedBox(height: 50, child: Obx(() => buildStatus()))
.paddingSymmetric(horizontal: 12.0)
@@ -329,61 +312,61 @@ class _ConnectionPageState extends State<ConnectionPage> {
return true;
}
/// Show the peer menu and handle user's choice.
/// User might remove the peer or send a file to the peer.
void showPeerMenu(BuildContext context, String id, RemoteType rType) async {
var items = [
PopupMenuItem<String>(
child: Text(translate('Connect')), value: 'connect'),
PopupMenuItem<String>(
child: Text(translate('Transfer File')), value: 'file'),
PopupMenuItem<String>(
child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'),
PopupMenuItem<String>(child: Text(translate('Rename')), value: 'rename'),
rType == RemoteType.addressBook
? PopupMenuItem<String>(
child: Text(translate('Remove')), value: 'ab-delete')
: PopupMenuItem<String>(
child: Text(translate('Remove')), value: 'remove'),
PopupMenuItem<String>(
child: Text(translate('Unremember Password')),
value: 'unremember-password'),
];
if (rType == RemoteType.favorite) {
items.add(PopupMenuItem<String>(
child: Text(translate('Remove from Favorites')),
value: 'remove-fav'));
} else if (rType != RemoteType.addressBook) {
items.add(PopupMenuItem<String>(
child: Text(translate('Add to Favorites')), value: 'add-fav'));
} else {
items.add(PopupMenuItem<String>(
child: Text(translate('Edit Tag')), value: 'ab-edit-tag'));
}
var value = await showMenu(
context: context,
position: this._menuPos,
items: items,
elevation: 8,
);
if (value == 'remove') {
setState(() => gFFI.setByName('remove', '$id'));
() async {
removePreference(id);
}();
} else if (value == 'file') {
connect(id, isFileTransfer: true);
} else if (value == 'add-fav') {
} else if (value == 'connect') {
connect(id, isFileTransfer: false);
} else if (value == 'ab-delete') {
gFFI.abModel.deletePeer(id);
await gFFI.abModel.updateAb();
setState(() {});
} else if (value == 'ab-edit-tag') {
abEditTag(id);
}
}
// /// Show the peer menu and handle user's choice.
// /// User might remove the peer or send a file to the peer.
// void showPeerMenu(BuildContext context, String id, RemoteType rType) async {
// var items = [
// PopupMenuItem<String>(
// child: Text(translate('Connect')), value: 'connect'),
// PopupMenuItem<String>(
// child: Text(translate('Transfer File')), value: 'file'),
// PopupMenuItem<String>(
// child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'),
// PopupMenuItem<String>(child: Text(translate('Rename')), value: 'rename'),
// rType == RemoteType.addressBook
// ? PopupMenuItem<String>(
// child: Text(translate('Remove')), value: 'ab-delete')
// : PopupMenuItem<String>(
// child: Text(translate('Remove')), value: 'remove'),
// PopupMenuItem<String>(
// child: Text(translate('Unremember Password')),
// value: 'unremember-password'),
// ];
// if (rType == RemoteType.favorite) {
// items.add(PopupMenuItem<String>(
// child: Text(translate('Remove from Favorites')),
// value: 'remove-fav'));
// } else if (rType != RemoteType.addressBook) {
// items.add(PopupMenuItem<String>(
// child: Text(translate('Add to Favorites')), value: 'add-fav'));
// } else {
// items.add(PopupMenuItem<String>(
// child: Text(translate('Edit Tag')), value: 'ab-edit-tag'));
// }
// var value = await showMenu(
// context: context,
// position: this._menuPos,
// items: items,
// elevation: 8,
// );
// if (value == 'remove') {
// setState(() => gFFI.setByName('remove', '$id'));
// () async {
// removePreference(id);
// }();
// } else if (value == 'file') {
// connect(id, isFileTransfer: true);
// } else if (value == 'add-fav') {
// } else if (value == 'connect') {
// connect(id, isFileTransfer: false);
// } else if (value == 'ab-delete') {
// gFFI.abModel.deletePeer(id);
// await gFFI.abModel.updateAb();
// setState(() {});
// } else if (value == 'ab-edit-tag') {
// abEditTag(id);
// }
// }
var svcStopped = false.obs;
var svcStatusCode = 0.obs;
@@ -896,3 +879,86 @@ class _WebMenuState extends State<WebMenu> {
});
}
}
class _PeerTabbedPage extends StatefulWidget {
final List<String> tabs;
final List<Widget> children;
const _PeerTabbedPage({required this.tabs, required this.children, Key? key})
: super(key: key);
@override
_PeerTabbedPageState createState() => _PeerTabbedPageState();
}
class _PeerTabbedPageState extends State<_PeerTabbedPage>
with SingleTickerProviderStateMixin {
late TabController _tabController;
@override
void initState() {
super.initState();
_tabController =
TabController(vsync: this, length: super.widget.tabs.length);
_tabController.addListener(_handleTabSelection);
}
// hard code for now
void _handleTabSelection() {
if (_tabController.indexIsChanging) {
switch (_tabController.index) {
case 0:
break;
case 1:
break;
case 2:
gFFI.bind.mainDiscover();
break;
case 3:
break;
}
}
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// return DefaultTabController(
// length: 4,
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// _createTabBar(),
// _createTabBarView(),
// ],
// ));
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_createTabBar(),
_createTabBarView(),
],
);
}
Widget _createTabBar() {
return TabBar(
isScrollable: true,
indicatorSize: TabBarIndicatorSize.label,
controller: _tabController,
tabs: super.widget.tabs.map((t) {
return Tab(child: Text(t));
}).toList());
}
Widget _createTabBarView() {
return Expanded(
child: TabBarView(
controller: _tabController, children: super.widget.children)
.paddingSymmetric(horizontal: 12.0, vertical: 4.0));
}
}

View File

@@ -296,6 +296,7 @@ class _RemotePageState extends State<RemotePage>
Widget getRawPointerAndKeyBody(bool keyboard, Widget child) {
return Listener(
onPointerHover: (e) {
debugPrint("onPointerHover ${e}");
if (e.kind != ui.PointerDeviceKind.mouse) return;
if (!_isPhysicalMouse) {
setState(() {
@@ -307,6 +308,7 @@ class _RemotePageState extends State<RemotePage>
}
},
onPointerDown: (e) {
debugPrint("onPointerDown ${e}");
if (e.kind != ui.PointerDeviceKind.mouse) {
if (_isPhysicalMouse) {
setState(() {
@@ -319,18 +321,21 @@ class _RemotePageState extends State<RemotePage>
}
},
onPointerUp: (e) {
debugPrint("onPointerUp ${e}");
if (e.kind != ui.PointerDeviceKind.mouse) return;
if (_isPhysicalMouse) {
_ffi.handleMouse(getEvent(e, 'mouseup'));
}
},
onPointerMove: (e) {
debugPrint("onPointerMove ${e}");
if (e.kind != ui.PointerDeviceKind.mouse) return;
if (_isPhysicalMouse) {
_ffi.handleMouse(getEvent(e, 'mousemove'));
}
},
onPointerSignal: (e) {
debugPrint("onPointerSignal ${e}");
if (e is PointerScrollEvent) {
var dx = e.scrollDelta.dx;
var dy = e.scrollDelta.dy;

View File

@@ -15,15 +15,13 @@ typedef OffstageFunc = bool Function(Peer peer);
typedef PeerCardWidgetFunc = Widget Function(Peer peer);
class _PeerWidget extends StatefulWidget {
late final _name;
late final _peers;
late final OffstageFunc _offstageFunc;
late final PeerCardWidgetFunc _peerCardWidgetFunc;
_PeerWidget(String name, List<Peer> peers, OffstageFunc offstageFunc,
_PeerWidget(Peers peers, OffstageFunc offstageFunc,
PeerCardWidgetFunc peerCardWidgetFunc,
{Key? key})
: super(key: key) {
_name = name;
_peers = peers;
_offstageFunc = offstageFunc;
_peerCardWidgetFunc = peerCardWidgetFunc;
@@ -70,7 +68,7 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener {
Widget build(BuildContext context) {
final space = 8.0;
return ChangeNotifierProvider<Peers>(
create: (context) => Peers(super.widget._name, super.widget._peers),
create: (context) => super.widget._peers,
child: SingleChildScrollView(
child: Consumer<Peers>(
builder: (context, peers, child) => Wrap(
@@ -136,83 +134,69 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener {
abstract class BasePeerWidget extends StatelessWidget {
late final _name;
late final _loadEvent;
late final OffstageFunc _offstageFunc;
late final PeerCardWidgetFunc _peerCardWidgetFunc;
late final List<Peer> _initPeers;
BasePeerWidget({Key? key}) : super(key: key) {}
@override
Widget build(BuildContext context) {
return FutureBuilder<Widget>(future: () async {
return _PeerWidget(
_name, await _loadPeers(), _offstageFunc, _peerCardWidgetFunc);
}(), builder: (context, snapshot) {
if (snapshot.hasData) {
return snapshot.data!;
} else {
return Offstage();
}
});
return _PeerWidget(Peers(_name, _loadEvent, _initPeers), _offstageFunc,
_peerCardWidgetFunc);
}
@protected
Future<List<Peer>> _loadPeers();
}
class RecentPeerWidget extends BasePeerWidget {
RecentPeerWidget({Key? key}) : super(key: key) {
super._name = "recent peer";
super._loadEvent = "load_recent_peers";
super._offstageFunc = (Peer _peer) => false;
super._peerCardWidgetFunc = (Peer peer) => RecentPeerCard(peer: peer);
super._initPeers = [];
}
Future<List<Peer>> _loadPeers() async {
debugPrint("call RecentPeerWidget _loadPeers");
return gFFI.peers();
@override
Widget build(BuildContext context) {
final widget = super.build(context);
gFFI.bind.mainLoadRecentPeers();
return widget;
}
}
class FavoritePeerWidget extends BasePeerWidget {
FavoritePeerWidget({Key? key}) : super(key: key) {
super._name = "favorite peer";
super._loadEvent = "load_fav_peers";
super._offstageFunc = (Peer _peer) => false;
super._peerCardWidgetFunc = (Peer peer) => FavoritePeerCard(peer: peer);
super._initPeers = [];
}
@override
Future<List<Peer>> _loadPeers() async {
debugPrint("call FavoritePeerWidget _loadPeers");
return await gFFI.bind.mainGetFav().then((peers) async {
final peersEntities = await Future.wait(peers
.map((id) => gFFI.bind.mainGetPeers(id: id))
.toList(growable: false))
.then((peers_str) {
final len = peers_str.length;
final ps = List<Peer>.empty(growable: true);
for (var i = 0; i < len; i++) {
print("${peers[i]}: ${peers_str[i]}");
ps.add(Peer.fromJson(peers[i], jsonDecode(peers_str[i])['info']));
}
return ps;
});
return peersEntities;
});
Widget build(BuildContext context) {
final widget = super.build(context);
gFFI.bind.mainLoadFavPeers();
return widget;
}
}
class DiscoveredPeerWidget extends BasePeerWidget {
DiscoveredPeerWidget({Key? key}) : super(key: key) {
super._name = "discovered peer";
super._loadEvent = "load_lan_peers";
super._offstageFunc = (Peer _peer) => false;
super._peerCardWidgetFunc = (Peer peer) => DiscoveredPeerCard(peer: peer);
super._initPeers = [];
}
Future<List<Peer>> _loadPeers() async {
debugPrint("call DiscoveredPeerWidget _loadPeers");
return await gFFI.bind.mainGetLanPeers().then((peers_string) {
debugPrint(peers_string);
return [];
});
@override
Widget build(BuildContext context) {
debugPrint("DiscoveredPeerWidget build");
final widget = super.build(context);
gFFI.bind.mainLoadLanPeers();
return widget;
}
}
@@ -222,10 +206,10 @@ class AddressBookPeerWidget extends BasePeerWidget {
super._offstageFunc =
(Peer peer) => !_hitTag(gFFI.abModel.selectedTags, peer.tags);
super._peerCardWidgetFunc = (Peer peer) => AddressBookPeerCard(peer: peer);
super._initPeers = _loadPeers();
}
Future<List<Peer>> _loadPeers() async {
debugPrint("call AddressBookPeerWidget _loadPeers");
List<Peer> _loadPeers() {
return gFFI.abModel.peers.map((e) {
return Peer.fromJson(e['id'], e);
}).toList();

View File

@@ -125,10 +125,10 @@ class _RemotePageState extends State<RemotePage> {
oldValue = oldValue.substring(j + 1);
var common = 0;
for (;
common < oldValue.length &&
common < newValue.length &&
newValue[common] == oldValue[common];
++common) {}
common < oldValue.length &&
common < newValue.length &&
newValue[common] == oldValue[common];
++common) {}
for (i = 0; i < oldValue.length - common; ++i) {
gFFI.inputKey('VK_BACK');
}
@@ -235,13 +235,13 @@ class _RemotePageState extends State<RemotePage> {
floatingActionButton: !showActionButton
? null
: FloatingActionButton(
mini: !hideKeyboard,
child: Icon(
hideKeyboard ? Icons.expand_more : Icons.expand_less),
backgroundColor: MyTheme.accent,
onPressed: () {
setState(() {
if (hideKeyboard) {
mini: !hideKeyboard,
child: Icon(
hideKeyboard ? Icons.expand_more : Icons.expand_less),
backgroundColor: MyTheme.accent,
onPressed: () {
setState(() {
if (hideKeyboard) {
_showEdit = false;
gFFI.invokeMethod("enable_soft_keyboard", false);
_mobileFocusNode.unfocus();
@@ -250,7 +250,7 @@ class _RemotePageState extends State<RemotePage> {
_showBar = !_showBar;
}
});
}),
}),
bottomNavigationBar: _showBar && pi.displays.length > 0
? getBottomAppBar(keyboard)
: null,
@@ -262,7 +262,7 @@ class _RemotePageState extends State<RemotePage> {
child: isWebDesktop
? getBodyForDesktopWithListener(keyboard)
: SafeArea(child:
OrientationBuilder(builder: (ctx, orientation) {
OrientationBuilder(builder: (ctx, orientation) {
if (_currentOrientation != orientation) {
Timer(Duration(milliseconds: 200), () {
resetMobileActionsOverlay();
@@ -271,10 +271,10 @@ class _RemotePageState extends State<RemotePage> {
});
}
return Container(
color: MyTheme.canvasColor,
child: _isPhysicalMouse
? getBodyForMobile()
: getBodyForMobileWithGesture());
color: MyTheme.canvasColor,
child: _isPhysicalMouse
? getBodyForMobile()
: getBodyForMobileWithGesture());
})));
})
],
@@ -395,14 +395,14 @@ class _RemotePageState extends State<RemotePage> {
children: <Widget>[
Row(
children: <Widget>[
IconButton(
color: Colors.white,
icon: Icon(Icons.clear),
onPressed: () {
clientClose();
},
)
] +
IconButton(
color: Colors.white,
icon: Icon(Icons.clear),
onPressed: () {
clientClose();
},
)
] +
<Widget>[
IconButton(
color: Colors.white,
@@ -441,20 +441,20 @@ class _RemotePageState extends State<RemotePage> {
: Icons.mouse),
onPressed: changeTouchMode,
),
]) +
]) +
(isWeb
? []
: <Widget>[
IconButton(
color: Colors.white,
icon: Icon(Icons.message),
onPressed: () {
IconButton(
color: Colors.white,
icon: Icon(Icons.message),
onPressed: () {
gFFI.chatModel
.changeCurrentID(ChatModel.clientModeID);
toggleChatOverlay();
},
)
]) +
)
]) +
[
IconButton(
color: Colors.white,
@@ -602,17 +602,17 @@ class _RemotePageState extends State<RemotePage> {
child: !_showEdit
? Container()
: TextFormField(
textInputAction: TextInputAction.newline,
autocorrect: false,
enableSuggestions: false,
autofocus: true,
focusNode: _mobileFocusNode,
maxLines: null,
initialValue: _value,
// trick way to make backspace work always
keyboardType: TextInputType.multiline,
onChanged: handleInput,
),
textInputAction: TextInputAction.newline,
autocorrect: false,
enableSuggestions: false,
autofocus: true,
focusNode: _mobileFocusNode,
maxLines: null,
initialValue: _value,
// trick way to make backspace work always
keyboardType: TextInputType.multiline,
onChanged: handleInput,
),
),
]));
}
@@ -697,7 +697,7 @@ class _RemotePageState extends State<RemotePage> {
value: 'block-input'));
}
}
() async {
() async {
var value = await showMenu(
context: context,
position: RelativeRect.fromLTRB(x, y, x, y),
@@ -715,7 +715,7 @@ class _RemotePageState extends State<RemotePage> {
} else if (value == 'refresh') {
gFFI.setByName('refresh');
} else if (value == 'paste') {
() async {
() async {
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
if (data != null && data.text != null) {
gFFI.setByName('input_string', '${data.text}');
@@ -803,25 +803,25 @@ class _RemotePageState extends State<RemotePage> {
final keys = <Widget>[
wrap(
' Fn ',
() => setState(
() => setState(
() {
_fn = !_fn;
if (_fn) {
_more = false;
}
},
),
_fn = !_fn;
if (_fn) {
_more = false;
}
},
),
_fn),
wrap(
' ... ',
() => setState(
() => setState(
() {
_more = !_more;
if (_more) {
_fn = false;
}
},
),
_more = !_more;
if (_more) {
_fn = false;
}
},
),
_more),
];
final fn = <Widget>[
@@ -952,7 +952,8 @@ class ImagePainter extends CustomPainter {
}
}
CheckboxListTile getToggle(void Function(void Function()) setState, option, name) {
CheckboxListTile getToggle(
void Function(void Function()) setState, option, name) {
return CheckboxListTile(
value: gFFI.getByName('toggle_option', option) == 'true',
onChanged: (v) {

View File

@@ -1028,6 +1028,7 @@ class FFI {
RustdeskImpl get bind => ffiModel.platformFFI.ffiBind;
handleMouse(Map<String, dynamic> evt) {
debugPrint("mouse ${evt.toString()}");
var type = '';
var isMove = false;
switch (evt['type']) {
@@ -1045,7 +1046,7 @@ class FFI {
}
evt['type'] = type;
var x = evt['x'];
var y = evt['y'];
var y = max(0.0, (evt['y'] as double) - 50.0);
if (isMove) {
canvasModel.moveDesktopMouse(x, y);
}

View File

@@ -1,3 +1,4 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import '../../common.dart';
@@ -35,23 +36,29 @@ class Peer {
class Peers extends ChangeNotifier {
late String _name;
late var _peers;
static const cbQueryOnlines = 'callback_query_onlines';
late List<Peer> _peers;
late final _loadEvent;
static const _cbQueryOnlines = 'callback_query_onlines';
Peers(String name, List<Peer> peers) {
Peers(String name, String loadEvent, List<Peer> _initPeers) {
_name = name;
_peers = peers;
gFFI.ffiModel.platformFFI.registerEventHandler(cbQueryOnlines, _name,
_loadEvent = loadEvent;
_peers = _initPeers;
gFFI.ffiModel.platformFFI.registerEventHandler(_cbQueryOnlines, _name,
(evt) {
_updateOnlineState(evt);
});
gFFI.ffiModel.platformFFI.registerEventHandler(_loadEvent, _name, (evt) {
_updatePeers(evt);
});
}
List<Peer> get peers => _peers;
@override
void dispose() {
gFFI.ffiModel.platformFFI.unregisterEventHandler(cbQueryOnlines, _name);
gFFI.ffiModel.platformFFI.unregisterEventHandler(_cbQueryOnlines, _name);
gFFI.ffiModel.platformFFI.unregisterEventHandler(_loadEvent, _name);
super.dispose();
}
@@ -86,4 +93,37 @@ class Peers extends ChangeNotifier {
notifyListeners();
}
void _updatePeers(Map<String, dynamic> evt) {
final onlineStates = _getOnlineStates();
_peers = _decodePeers(evt['peers']);
_peers.forEach((peer) {
final state = onlineStates[peer.id];
peer.online = state != null && state != false;
});
notifyListeners();
}
Map<String, bool> _getOnlineStates() {
var onlineStates = new Map<String, bool>();
_peers.forEach((peer) {
onlineStates[peer.id] = peer.online;
});
return onlineStates;
}
List<Peer> _decodePeers(String peersStr) {
try {
if (peersStr == "") return [];
List<dynamic> peers = json.decode(peersStr);
return peers
.map((s) => s as List<dynamic>)
.map((s) =>
Peer.fromJson(s[0] as String, s[1] as Map<String, dynamic>))
.toList();
} catch (e) {
print('peers(): $e');
}
return [];
}
}