import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/formatter/id_formatter.dart'; import '../../../models/platform_model.dart'; import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common/widgets/peer_card.dart'; class AllPeersLoader { List peers = []; bool _isPeersLoading = false; bool _isPeersLoaded = false; final String _listenerKey = 'AllPeersLoader'; late void Function(VoidCallback) setState; bool get needLoad => !_isPeersLoaded && !_isPeersLoading; bool get isPeersLoaded => _isPeersLoaded; AllPeersLoader(); void init(void Function(VoidCallback) setState) { this.setState = setState; gFFI.recentPeersModel.addListener(_mergeAllPeers); gFFI.lanPeersModel.addListener(_mergeAllPeers); gFFI.abModel.addPeerUpdateListener(_listenerKey, _mergeAllPeers); gFFI.groupModel.addPeerUpdateListener(_listenerKey, _mergeAllPeers); } void clear() { gFFI.recentPeersModel.removeListener(_mergeAllPeers); gFFI.lanPeersModel.removeListener(_mergeAllPeers); gFFI.abModel.removePeerUpdateListener(_listenerKey); gFFI.groupModel.removePeerUpdateListener(_listenerKey); } Future getAllPeers() async { if (!needLoad) { return; } _isPeersLoading = true; if (gFFI.recentPeersModel.peers.isEmpty) { bind.mainLoadRecentPeers(); } if (gFFI.lanPeersModel.peers.isEmpty) { bind.mainLoadLanPeers(); } // No need to care about peers from abModel, and group model. // Because they will pull data in `refreshCurrentUser()` on startup. final startTime = DateTime.now(); _mergeAllPeers(); final diffTime = DateTime.now().difference(startTime).inMilliseconds; if (diffTime < 100) { await Future.delayed(Duration(milliseconds: diffTime)); } } void _mergeAllPeers() { Map combinedPeers = {}; for (var p in gFFI.abModel.allPeers()) { if (!combinedPeers.containsKey(p.id)) { combinedPeers[p.id] = p.toJson(); } } for (var p in gFFI.groupModel.peers.map((e) => Peer.copy(e)).toList()) { if (!combinedPeers.containsKey(p.id)) { combinedPeers[p.id] = p.toJson(); } } List parsedPeers = []; for (var peer in combinedPeers.values) { parsedPeers.add(Peer.fromJson(peer)); } Set peerIds = combinedPeers.keys.toSet(); for (final peer in gFFI.lanPeersModel.peers) { if (!peerIds.contains(peer.id)) { parsedPeers.add(peer); peerIds.add(peer.id); } } for (final peer in gFFI.recentPeersModel.peers) { if (!peerIds.contains(peer.id)) { parsedPeers.add(peer); peerIds.add(peer.id); } } for (final id in gFFI.recentPeersModel.restPeerIds) { if (!peerIds.contains(id)) { parsedPeers.add(Peer.fromJson({'id': id})); peerIds.add(id); } } peers = parsedPeers; setState(() { _isPeersLoading = false; _isPeersLoaded = true; }); } } class AutocompletePeerTile extends StatefulWidget { final VoidCallback onSelect; final Peer peer; const AutocompletePeerTile({ Key? key, required this.onSelect, required this.peer, }) : super(key: key); @override AutocompletePeerTileState createState() => AutocompletePeerTileState(); } class AutocompletePeerTileState extends State { List _frontN(List list, int n) { if (list.length <= n) { return list; } else { return list.sublist(0, n); } } @override Widget build(BuildContext context) { final double tileRadius = 5; final name = '${widget.peer.username}${widget.peer.username.isNotEmpty && widget.peer.hostname.isNotEmpty ? '@' : ''}${widget.peer.hostname}'; final greyStyle = TextStyle( fontSize: 11, color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6)); final child = GestureDetector( onTap: () => widget.onSelect(), child: Padding( padding: EdgeInsets.only(left: 5, right: 5), child: Container( height: 42, margin: EdgeInsets.only(bottom: 5), child: Row( mainAxisSize: MainAxisSize.max, children: [ Container( decoration: BoxDecoration( color: str2color( '${widget.peer.id}${widget.peer.platform}', 0x7f), borderRadius: BorderRadius.only( topLeft: Radius.circular(tileRadius), bottomLeft: Radius.circular(tileRadius), ), ), alignment: Alignment.center, width: 42, height: null, child: Padding( padding: EdgeInsets.all(6), child: getPlatformImage(widget.peer.platform, size: 30))), Expanded( child: Container( padding: EdgeInsets.only(left: 10), decoration: BoxDecoration( color: Theme.of(context).colorScheme.background, borderRadius: BorderRadius.only( topRight: Radius.circular(tileRadius), bottomRight: Radius.circular(tileRadius), ), ), child: Row( children: [ Expanded( child: Container( margin: EdgeInsets.only(top: 2), child: Container( margin: EdgeInsets.only(top: 2), child: Column( children: [ Container( margin: EdgeInsets.only(top: 2), child: Row(children: [ getOnline( 8, widget.peer.online), Expanded( child: Text( widget.peer.alias.isEmpty ? formatID( widget.peer.id) : widget.peer.alias, overflow: TextOverflow.ellipsis, style: Theme.of(context) .textTheme .titleSmall, )), widget.peer.alias.isNotEmpty ? Padding( padding: const EdgeInsets .only( left: 5, right: 5), child: Text( "(${widget.peer.id})", style: greyStyle, overflow: TextOverflow .ellipsis, )) : Container(), ])), Align( alignment: Alignment.centerLeft, child: Text( name, style: greyStyle, textAlign: TextAlign.start, overflow: TextOverflow.ellipsis, ), ), ], )))), ], )), ) ], )))); final colors = _frontN(widget.peer.tags, 25) .map((e) => gFFI.abModel.getCurrentAbTagColor(e)) .toList(); return Tooltip( message: !(isDesktop || isWebDesktop) ? '' : widget.peer.tags.isNotEmpty ? '${translate('Tags')}: ${widget.peer.tags.join(', ')}' : '', child: Stack(children: [ child, if (colors.isNotEmpty) Positioned( top: 5, right: 10, child: CustomPaint( painter: TagPainter(radius: 3, colors: colors), ), ) ]), ); } }