refact: optimize, loading recent peers (#10847)

Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
fufesou 2025-02-20 11:53:36 +08:00 committed by GitHub
parent 2e89a33210
commit 055b351164
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 265 additions and 139 deletions

View file

@ -6,56 +6,123 @@ import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/widgets/peer_card.dart';
Future<List<Peer>> getAllPeers() async {
Map<String, dynamic> recentPeers = jsonDecode(bind.mainLoadRecentPeersSync());
Map<String, dynamic> lanPeers = jsonDecode(bind.mainLoadLanPeersSync());
Map<String, dynamic> combinedPeers = {};
class AllPeersLoader {
List<Peer> peers = [];
bool hasMoreRecentPeers = false;
void mergePeers(Map<String, dynamic> peers) {
if (peers.containsKey("peers")) {
dynamic peerData = peers["peers"];
bool isPeersLoading = false;
bool _isPartialPeersLoaded = false;
bool _isPeersLoaded = false;
if (peerData is String) {
try {
peerData = jsonDecode(peerData);
} catch (e) {
print("Error decoding peers: $e");
return;
}
AllPeersLoader();
bool get isLoaded => _isPartialPeersLoaded || _isPeersLoaded;
void reset() {
peers.clear();
hasMoreRecentPeers = false;
_isPartialPeersLoaded = false;
_isPeersLoaded = false;
}
Future<void> getAllPeers(void Function(VoidCallback) setState) async {
if (isPeersLoading) {
return;
}
reset();
isPeersLoading = true;
final startTime = DateTime.now();
await _getAllPeers(false);
if (!hasMoreRecentPeers) {
final diffTime = DateTime.now().difference(startTime).inMilliseconds;
if (diffTime < 100) {
await Future.delayed(Duration(milliseconds: diffTime));
}
setState(() {
isPeersLoading = false;
_isPeersLoaded = true;
});
} else {
setState(() {
_isPartialPeersLoaded = true;
});
await _getAllPeers(true);
setState(() {
isPeersLoading = false;
_isPeersLoaded = true;
});
}
}
if (peerData is List) {
for (var peer in peerData) {
if (peer is Map && peer.containsKey("id")) {
String id = peer["id"];
if (!combinedPeers.containsKey(id)) {
combinedPeers[id] = peer;
Future<void> _getAllPeers(bool getAllRecentPeers) async {
Map<String, dynamic> recentPeers =
jsonDecode(await bind.mainGetRecentPeers(getAll: getAllRecentPeers));
Map<String, dynamic> lanPeers = jsonDecode(bind.mainLoadLanPeersSync());
Map<String, dynamic> combinedPeers = {};
void mergePeers(Map<String, dynamic> peers) {
if (peers.containsKey("peers")) {
dynamic peerData = peers["peers"];
if (peerData is String) {
try {
peerData = jsonDecode(peerData);
} catch (e) {
print("Error decoding peers: $e");
return;
}
}
if (peerData is List) {
for (var peer in peerData) {
if (peer is Map && peer.containsKey("id")) {
String id = peer["id"];
if (!combinedPeers.containsKey(id)) {
combinedPeers[id] = peer;
}
}
}
}
}
}
}
mergePeers(recentPeers);
mergePeers(lanPeers);
for (var p in gFFI.abModel.allPeers()) {
if (!combinedPeers.containsKey(p.id)) {
combinedPeers[p.id] = p.toJson();
mergePeers(recentPeers);
mergePeers(lanPeers);
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();
for (var p in gFFI.groupModel.peers.map((e) => Peer.copy(e)).toList()) {
if (!combinedPeers.containsKey(p.id)) {
combinedPeers[p.id] = p.toJson();
}
}
}
List<Peer> parsedPeers = [];
List<Peer> parsedPeers = [];
for (var peer in combinedPeers.values) {
parsedPeers.add(Peer.fromJson(peer));
for (var peer in combinedPeers.values) {
parsedPeers.add(Peer.fromJson(peer));
}
try {
final List<dynamic> moreRecentPeerIds =
jsonDecode(recentPeers["ids"] ?? '[]');
hasMoreRecentPeers = false;
for (final id in moreRecentPeerIds) {
final sid = id.toString();
if (!parsedPeers.any((element) => element.id == sid)) {
parsedPeers.add(Peer.fromJson({'id': sid}));
hasMoreRecentPeers = true;
}
}
} catch (e) {
debugPrint("Error parsing more peer ids: $e");
}
peers = parsedPeers;
}
return parsedPeers;
}
class AutocompletePeerTile extends StatefulWidget {

View file

@ -200,18 +200,20 @@ class _ConnectionPageState extends State<ConnectionPage>
final _idController = IDTextEditingController();
final RxBool _idInputFocused = false.obs;
final FocusNode _idFocusNode = FocusNode();
final TextEditingController _idEditingController = TextEditingController();
bool isWindowMinimized = false;
List<Peer> peers = [];
bool isPeersLoading = false;
bool isPeersLoaded = false;
AllPeersLoader allPeersLoader = AllPeersLoader();
// https://github.com/flutter/flutter/issues/157244
Iterable<Peer> _autocompleteOpts = [];
@override
void initState() {
super.initState();
_idFocusNode.addListener(onFocusChanged);
if (_idController.text.isEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
final lastRemoteId = await bind.mainGetLastRemoteId();
@ -230,6 +232,9 @@ class _ConnectionPageState extends State<ConnectionPage>
void dispose() {
_idController.dispose();
windowManager.removeListener(this);
_idFocusNode.removeListener(onFocusChanged);
_idFocusNode.dispose();
_idEditingController.dispose();
if (Get.isRegistered<IDTextEditingController>()) {
Get.delete<IDTextEditingController>();
}
@ -273,6 +278,13 @@ class _ConnectionPageState extends State<ConnectionPage>
bind.mainOnMainWindowClose();
}
void onFocusChanged() {
_idInputFocused.value = _idFocusNode.hasFocus;
if (_idFocusNode.hasFocus && !allPeersLoader.isPeersLoading) {
allPeersLoader.getAllPeers(setState);
}
}
@override
Widget build(BuildContext context) {
final isOutgoingOnly = bind.isOutgoingOnly();
@ -304,18 +316,6 @@ class _ConnectionPageState extends State<ConnectionPage>
connect(context, id, isFileTransfer: isFileTransfer);
}
Future<void> _fetchPeers() async {
setState(() {
isPeersLoading = true;
});
await Future.delayed(Duration(milliseconds: 100));
peers = await getAllPeers();
setState(() {
isPeersLoading = false;
isPeersLoaded = true;
});
}
/// UI for the remote ID TextField.
/// Search for a peer.
Widget _buildRemoteIDTextField(BuildContext context) {
@ -332,11 +332,12 @@ class _ConnectionPageState extends State<ConnectionPage>
Row(
children: [
Expanded(
child: Autocomplete<Peer>(
child: RawAutocomplete<Peer>(
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text == '') {
_autocompleteOpts = const Iterable<Peer>.empty();
} else if (peers.isEmpty && !isPeersLoaded) {
} else if (allPeersLoader.peers.isEmpty &&
!allPeersLoader.isLoaded) {
Peer emptyPeer = Peer(
id: '',
username: '',
@ -363,7 +364,7 @@ class _ConnectionPageState extends State<ConnectionPage>
);
}
String textToFind = textEditingValue.text.toLowerCase();
_autocompleteOpts = peers
_autocompleteOpts = allPeersLoader.peers
.where((peer) =>
peer.id.toLowerCase().contains(textToFind) ||
peer.username
@ -377,6 +378,8 @@ class _ConnectionPageState extends State<ConnectionPage>
}
return _autocompleteOpts;
},
focusNode: _idFocusNode,
textEditingController: _idEditingController,
fieldViewBuilder: (
BuildContext context,
TextEditingController fieldTextEditingController,
@ -385,17 +388,17 @@ class _ConnectionPageState extends State<ConnectionPage>
) {
fieldTextEditingController.text = _idController.text;
Get.put<TextEditingController>(fieldTextEditingController);
fieldFocusNode.addListener(() async {
_idInputFocused.value = fieldFocusNode.hasFocus;
if (fieldFocusNode.hasFocus && !isPeersLoading) {
_fetchPeers();
}
});
final textLength =
fieldTextEditingController.value.text.length;
// select all to facilitate removing text, just following the behavior of address input of chrome
fieldTextEditingController.selection =
TextSelection(baseOffset: 0, extentOffset: textLength);
// The listener will be added multiple times when the widget is rebuilt.
// We may need to use the `RawAutocomplete` to get the focus node.
// Temporarily remove Selection because Selection can cause users to accidentally delete previously entered content during input.
// final textLength =
// fieldTextEditingController.value.text.length;
// // Select all to facilitate removing text, just following the behavior of address input of chrome.
// fieldTextEditingController.selection =
// TextSelection(baseOffset: 0, extentOffset: textLength);
return Obx(() => TextField(
autocorrect: false,
enableSuggestions: false,
@ -468,7 +471,8 @@ class _ConnectionPageState extends State<ConnectionPage>
maxHeight: maxHeight,
maxWidth: 319,
),
child: peers.isEmpty && isPeersLoading
child: allPeersLoader.peers.isEmpty &&
!allPeersLoader.isLoaded
? Container(
height: 80,
child: Center(

View file

@ -41,10 +41,11 @@ class _ConnectionPageState extends State<ConnectionPage> {
final _idController = IDTextEditingController();
final RxBool _idEmpty = true.obs;
List<Peer> peers = [];
final FocusNode _idFocusNode = FocusNode();
final TextEditingController _idEditingController = TextEditingController();
AllPeersLoader allPeersLoader = AllPeersLoader();
bool isPeersLoading = false;
bool isPeersLoaded = false;
StreamSubscription? _uniLinksSubscription;
// https://github.com/flutter/flutter/issues/157244
@ -61,6 +62,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
@override
void initState() {
super.initState();
_idFocusNode.addListener(onFocusChanged);
if (_idController.text.isEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
final lastRemoteId = await bind.mainGetLastRemoteId();
@ -99,6 +101,13 @@ class _ConnectionPageState extends State<ConnectionPage> {
connect(context, id);
}
void onFocusChanged() {
_idEmpty.value = _idEditingController.text.isEmpty;
if (_idFocusNode.hasFocus && !allPeersLoader.isPeersLoading) {
allPeersLoader.getAllPeers(setState);
}
}
/// UI for software update.
/// If _updateUrl] is not empty, shows a button to update the software.
Widget _buildUpdateUI(String updateUrl) {
@ -127,18 +136,6 @@ class _ConnectionPageState extends State<ConnectionPage> {
color: Colors.white, fontWeight: FontWeight.bold))));
}
Future<void> _fetchPeers() async {
setState(() {
isPeersLoading = true;
});
await Future.delayed(Duration(milliseconds: 100));
peers = await getAllPeers();
setState(() {
isPeersLoading = false;
isPeersLoaded = true;
});
}
/// UI for the remote ID TextField.
/// Search for a peer and connect to it if the id exists.
Widget _buildRemoteIDTextField() {
@ -156,11 +153,12 @@ class _ConnectionPageState extends State<ConnectionPage> {
Expanded(
child: Container(
padding: const EdgeInsets.only(left: 16, right: 16),
child: Autocomplete<Peer>(
child: RawAutocomplete<Peer>(
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text == '') {
_autocompleteOpts = const Iterable<Peer>.empty();
} else if (peers.isEmpty && !isPeersLoaded) {
} else if (allPeersLoader.peers.isEmpty &&
!allPeersLoader.isLoaded) {
Peer emptyPeer = Peer(
id: '',
username: '',
@ -188,7 +186,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
}
String textToFind = textEditingValue.text.toLowerCase();
_autocompleteOpts = peers
_autocompleteOpts = allPeersLoader.peers
.where((peer) =>
peer.id.toLowerCase().contains(textToFind) ||
peer.username
@ -202,6 +200,8 @@ class _ConnectionPageState extends State<ConnectionPage> {
}
return _autocompleteOpts;
},
focusNode: _idFocusNode,
textEditingController: _idEditingController,
fieldViewBuilder: (BuildContext context,
TextEditingController fieldTextEditingController,
FocusNode fieldFocusNode,
@ -209,18 +209,14 @@ class _ConnectionPageState extends State<ConnectionPage> {
fieldTextEditingController.text = _idController.text;
Get.put<TextEditingController>(
fieldTextEditingController);
fieldFocusNode.addListener(() async {
_idEmpty.value =
fieldTextEditingController.text.isEmpty;
if (fieldFocusNode.hasFocus && !isPeersLoading) {
_fetchPeers();
}
});
final textLength =
fieldTextEditingController.value.text.length;
// select all to facilitate removing text, just following the behavior of address input of chrome
fieldTextEditingController.selection = TextSelection(
baseOffset: 0, extentOffset: textLength);
// Temporarily remove Selection because Selection can cause users to accidentally delete previously entered content during input.
// final textLength =
// fieldTextEditingController.value.text.length;
// // select all to facilitate removing text, just following the behavior of address input of chrome
// fieldTextEditingController.selection = TextSelection(
// baseOffset: 0, extentOffset: textLength);
return AutoSizeTextField(
controller: fieldTextEditingController,
focusNode: fieldFocusNode,
@ -300,7 +296,8 @@ class _ConnectionPageState extends State<ConnectionPage> {
maxHeight: maxHeight,
maxWidth: 320,
),
child: peers.isEmpty && isPeersLoading
child: allPeersLoader.peers.isEmpty &&
!allPeersLoader.isLoaded
? Container(
height: 80,
child: Center(
@ -363,6 +360,9 @@ class _ConnectionPageState extends State<ConnectionPage> {
void dispose() {
_uniLinksSubscription?.cancel();
_idController.dispose();
_idFocusNode.removeListener(onFocusChanged);
_idFocusNode.dispose();
_idEditingController.dispose();
if (Get.isRegistered<IDTextEditingController>()) {
Get.delete<IDTextEditingController>();
}

View file

@ -6,12 +6,13 @@ import 'package:flutter_hbb/models/platform_model.dart';
void showPeerSelectionDialog(
{bool singleSelection = false,
required Function(List<String>) onPeersCallback}) {
final peers = bind.mainLoadRecentPeersSync();
required Function(List<String>) onPeersCallback}) async {
final peers = await bind.mainGetRecentPeers(getAll: true);
if (peers.isEmpty) {
debugPrint("load recent peers sync failed.");
debugPrint("load recent peers failed.");
return;
}
Map<String, dynamic> map = jsonDecode(peers);
List<dynamic> peersList = map['peers'] ?? [];
final selected = List<String>.empty(growable: true);