diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 06af89fa6..2d4084851 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -822,7 +822,11 @@ class OverlayDialogManager { close([res]) { _dialogs.remove(dialogTag); - dialog.complete(res); + try { + dialog.complete(res); + } catch (e) { + debugPrint("Dialog complete catch error: $e"); + } BackButtonInterceptor.removeByName(dialogTag); } diff --git a/flutter/lib/common/widgets/autocomplete.dart b/flutter/lib/common/widgets/autocomplete.dart index d7c713648..1ea6d47be 100644 --- a/flutter/lib/common/widgets/autocomplete.dart +++ b/flutter/lib/common/widgets/autocomplete.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/formatter/id_formatter.dart'; import '../../../models/platform_model.dart'; @@ -8,87 +7,56 @@ import 'package:flutter_hbb/common/widgets/peer_card.dart'; class AllPeersLoader { List peers = []; - bool hasMoreRecentPeers = false; bool isPeersLoading = false; - bool _isPartialPeersLoaded = false; - bool _isPeersLoaded = false; + bool isPeersLoaded = false; + + final String _listenerKey = 'AllPeersLoader'; + + late void Function(VoidCallback) setState; AllPeersLoader(); - bool get isLoaded => _isPartialPeersLoaded || _isPeersLoaded; - - void reset() { - peers.clear(); - hasMoreRecentPeers = false; - _isPartialPeersLoaded = false; - _isPeersLoaded = false; + 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); } - Future getAllPeers(void Function(VoidCallback) setState) async { - if (isPeersLoading) { + void clear() { + gFFI.recentPeersModel.removeListener(_mergeAllPeers); + gFFI.lanPeersModel.removeListener(_mergeAllPeers); + gFFI.abModel.removePeerUpdateListener(_listenerKey); + gFFI.groupModel.removePeerUpdateListener(_listenerKey); + } + + Future getAllPeers() async { + if (isPeersLoaded || isPeersLoading) { return; } - reset(); 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(); - 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; - }); + _mergeAllPeers(); + final diffTime = DateTime.now().difference(startTime).inMilliseconds; + if (diffTime < 100) { + await Future.delayed(Duration(milliseconds: diffTime)); } } - Future _getAllPeers(bool getAllRecentPeers) async { - Map recentPeers = - jsonDecode(await bind.mainGetRecentPeers(getAll: getAllRecentPeers)); - Map lanPeers = jsonDecode(bind.mainLoadLanPeersSync()); + void _mergeAllPeers() { Map combinedPeers = {}; - - void mergePeers(Map 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(); @@ -101,27 +69,36 @@ class AllPeersLoader { } List parsedPeers = []; - for (var peer in combinedPeers.values) { parsedPeers.add(Peer.fromJson(peer)); } - try { - final List 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; - } + 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); } - } catch (e) { - debugPrint("Error parsing more peer ids: $e"); } peers = parsedPeers; + setState(() { + isPeersLoading = false; + isPeersLoaded = true; + }); } } diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index b4bca12a9..6d00edc8d 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -716,18 +716,18 @@ abstract class BasePeerCard extends StatelessWidget { switch (tab) { case PeerTabIndex.recent: await bind.mainRemovePeer(id: id); - await bind.mainLoadRecentPeers(); + bind.mainLoadRecentPeers(); break; case PeerTabIndex.fav: final favs = (await bind.mainGetFav()).toList(); if (favs.remove(id)) { await bind.mainStoreFav(favs: favs); - await bind.mainLoadFavPeers(); + bind.mainLoadFavPeers(); } break; case PeerTabIndex.lan: await bind.mainRemoveDiscovered(id: id); - await bind.mainLoadLanPeers(); + bind.mainLoadLanPeers(); break; case PeerTabIndex.ab: await gFFI.abModel.deletePeers([id]); diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 9d21ec6cd..53c9dce06 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -404,7 +404,7 @@ class _PeerTabPageState extends State for (var p in peers) { await bind.mainRemovePeer(id: p.id); } - await bind.mainLoadRecentPeers(); + bind.mainLoadRecentPeers(); break; case 1: final favs = (await bind.mainGetFav()).toList(); @@ -412,13 +412,13 @@ class _PeerTabPageState extends State favs.remove(p.id); }).toList(); await bind.mainStoreFav(favs: favs); - await bind.mainLoadFavPeers(); + bind.mainLoadFavPeers(); break; case 2: for (var p in peers) { await bind.mainRemoveDiscovered(id: p.id); } - await bind.mainLoadLanPeers(); + bind.mainLoadLanPeers(); break; case 3: await gFFI.abModel.deletePeers(peers.map((p) => p.id).toList()); diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 131337165..2dc387067 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -205,7 +205,7 @@ class _ConnectionPageState extends State bool isWindowMinimized = false; - AllPeersLoader allPeersLoader = AllPeersLoader(); + final AllPeersLoader _allPeersLoader = AllPeersLoader(); // https://github.com/flutter/flutter/issues/157244 Iterable _autocompleteOpts = []; @@ -213,6 +213,7 @@ class _ConnectionPageState extends State @override void initState() { super.initState(); + _allPeersLoader.init(setState); _idFocusNode.addListener(onFocusChanged); if (_idController.text.isEmpty) { WidgetsBinding.instance.addPostFrameCallback((_) async { @@ -232,6 +233,7 @@ class _ConnectionPageState extends State void dispose() { _idController.dispose(); windowManager.removeListener(this); + _allPeersLoader.clear(); _idFocusNode.removeListener(onFocusChanged); _idFocusNode.dispose(); _idEditingController.dispose(); @@ -280,8 +282,8 @@ class _ConnectionPageState extends State void onFocusChanged() { _idInputFocused.value = _idFocusNode.hasFocus; - if (_idFocusNode.hasFocus && !allPeersLoader.isPeersLoading) { - allPeersLoader.getAllPeers(setState); + if (_idFocusNode.hasFocus && !_allPeersLoader.isPeersLoading) { + _allPeersLoader.getAllPeers(); } } @@ -336,8 +338,8 @@ class _ConnectionPageState extends State optionsBuilder: (TextEditingValue textEditingValue) { if (textEditingValue.text == '') { _autocompleteOpts = const Iterable.empty(); - } else if (allPeersLoader.peers.isEmpty && - !allPeersLoader.isLoaded) { + } else if (_allPeersLoader.peers.isEmpty && + !_allPeersLoader.isPeersLoaded) { Peer emptyPeer = Peer( id: '', username: '', @@ -364,7 +366,7 @@ class _ConnectionPageState extends State ); } String textToFind = textEditingValue.text.toLowerCase(); - _autocompleteOpts = allPeersLoader.peers + _autocompleteOpts = _allPeersLoader.peers .where((peer) => peer.id.toLowerCase().contains(textToFind) || peer.username @@ -471,8 +473,8 @@ class _ConnectionPageState extends State maxHeight: maxHeight, maxWidth: 319, ), - child: allPeersLoader.peers.isEmpty && - !allPeersLoader.isLoaded + child: _allPeersLoader.peers.isEmpty && + !_allPeersLoader.isPeersLoaded ? Container( height: 80, child: Center( diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 54056a00d..e550200bf 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -44,7 +44,7 @@ class _ConnectionPageState extends State { final FocusNode _idFocusNode = FocusNode(); final TextEditingController _idEditingController = TextEditingController(); - AllPeersLoader allPeersLoader = AllPeersLoader(); + final AllPeersLoader _allPeersLoader = AllPeersLoader(); StreamSubscription? _uniLinksSubscription; @@ -62,6 +62,7 @@ class _ConnectionPageState extends State { @override void initState() { super.initState(); + _allPeersLoader.init(setState); _idFocusNode.addListener(onFocusChanged); if (_idController.text.isEmpty) { WidgetsBinding.instance.addPostFrameCallback((_) async { @@ -103,8 +104,8 @@ class _ConnectionPageState extends State { void onFocusChanged() { _idEmpty.value = _idEditingController.text.isEmpty; - if (_idFocusNode.hasFocus && !allPeersLoader.isPeersLoading) { - allPeersLoader.getAllPeers(setState); + if (_idFocusNode.hasFocus && !_allPeersLoader.isPeersLoading) { + _allPeersLoader.getAllPeers(); } } @@ -157,8 +158,8 @@ class _ConnectionPageState extends State { optionsBuilder: (TextEditingValue textEditingValue) { if (textEditingValue.text == '') { _autocompleteOpts = const Iterable.empty(); - } else if (allPeersLoader.peers.isEmpty && - !allPeersLoader.isLoaded) { + } else if (_allPeersLoader.peers.isEmpty && + !_allPeersLoader.isPeersLoaded) { Peer emptyPeer = Peer( id: '', username: '', @@ -186,7 +187,7 @@ class _ConnectionPageState extends State { } String textToFind = textEditingValue.text.toLowerCase(); - _autocompleteOpts = allPeersLoader.peers + _autocompleteOpts = _allPeersLoader.peers .where((peer) => peer.id.toLowerCase().contains(textToFind) || peer.username @@ -296,8 +297,9 @@ class _ConnectionPageState extends State { maxHeight: maxHeight, maxWidth: 320, ), - child: allPeersLoader.peers.isEmpty && - !allPeersLoader.isLoaded + child: _allPeersLoader + .peers.isEmpty && + !_allPeersLoader.isPeersLoaded ? Container( height: 80, child: Center( @@ -361,6 +363,7 @@ class _ConnectionPageState extends State { _uniLinksSubscription?.cancel(); _idController.dispose(); _idFocusNode.removeListener(onFocusChanged); + _allPeersLoader.clear(); _idFocusNode.dispose(); _idEditingController.dispose(); if (Get.isRegistered()) { diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index 3aa722a5a..5efbd9e5f 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -58,6 +58,9 @@ class AbModel { String? _personalAbGuid; RxBool legacyMode = false.obs; + // Only handles peers add/remove + final Map _peerIdUpdateListeners = {}; + final sortTags = shouldSortTags().obs; final filterByIntersection = filterAbTagByIntersection().obs; @@ -188,6 +191,7 @@ class AbModel { debugPrint("pull current Ab error: $e"); } } + _callbackPeerUpdate(); if (listInitialized && current.initialized) { _saveCache(); } @@ -419,6 +423,7 @@ class AbModel { } }); } + _callbackPeerUpdate(); return ret; } @@ -620,6 +625,9 @@ class AbModel { } } } + if (abEntries.isNotEmpty) { + _callbackPeerUpdate(); + } } } @@ -742,6 +750,20 @@ class AbModel { } } + void _callbackPeerUpdate() { + for (var listener in _peerIdUpdateListeners.values) { + listener(); + } + } + + void addPeerUpdateListener(String key, VoidCallback listener) { + _peerIdUpdateListeners[key] = listener; + } + + void removePeerUpdateListener(String key) { + _peerIdUpdateListeners.remove(key); + } + // #endregion } diff --git a/flutter/lib/models/group_model.dart b/flutter/lib/models/group_model.dart index 155bf99f6..67947f5a8 100644 --- a/flutter/lib/models/group_model.dart +++ b/flutter/lib/models/group_model.dart @@ -23,6 +23,8 @@ class GroupModel { var _cacheLoadOnceFlag = false; var _statusCode = 200; + final Map _peerIdUpdateListeners = {}; + bool get emtpy => deviceGroups.isEmpty && users.isEmpty && peers.isEmpty; late final Peers peersModel; @@ -92,6 +94,7 @@ class GroupModel { .map((e) => e.online = true) .toList(); groupLoadError.value = ''; + _callbackPeerUpdate(); } Future _getDeviceGroups( @@ -329,6 +332,7 @@ class GroupModel { for (final peer in data['peers']) { peers.add(Peer.fromJson(peer)); } + _callbackPeerUpdate(); } } catch (e) { debugPrint("load group cache: $e"); @@ -343,4 +347,18 @@ class GroupModel { selectedAccessibleItemName.value = ''; await bind.mainClearGroup(); } + + void _callbackPeerUpdate() { + for (var listener in _peerIdUpdateListeners.values) { + listener(); + } + } + + void addPeerUpdateListener(String key, VoidCallback listener) { + _peerIdUpdateListeners[key] = listener; + } + + void removePeerUpdateListener(String key) { + _peerIdUpdateListeners.remove(key); + } } diff --git a/flutter/lib/models/peer_model.dart b/flutter/lib/models/peer_model.dart index d2a38c68b..35236dd4c 100644 --- a/flutter/lib/models/peer_model.dart +++ b/flutter/lib/models/peer_model.dart @@ -165,6 +165,11 @@ class Peers extends ChangeNotifier { final String name; final String loadEvent; List peers = List.empty(growable: true); + // Part of the peers that are not in the rest peers list. + // When there're too many peers, we may want to load the front 100 peers first, + // so we can see peers in UI quickly. `restPeerIds` is the rest peers' ids. + // And then load all peers later. + List restPeerIds = List.empty(growable: true); final GetInitPeers? getInitPeers; UpdateEvent event = UpdateEvent.load; static const _cbQueryOnlines = 'callback_query_onlines'; @@ -238,6 +243,12 @@ class Peers extends ChangeNotifier { } else { peers = _decodePeers(evt['peers']); } + + restPeerIds = []; + if (evt['ids'] != null) { + restPeerIds = (evt['ids'] as String).split(','); + } + for (var peer in peers) { final state = onlineStates[peer.id]; peer.online = state != null && state != false; diff --git a/flutter/lib/plugin/utils/dialogs.dart b/flutter/lib/plugin/utils/dialogs.dart index c01dc368f..6fdb86ab4 100644 --- a/flutter/lib/plugin/utils/dialogs.dart +++ b/flutter/lib/plugin/utils/dialogs.dart @@ -2,14 +2,15 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; -import 'package:flutter_hbb/models/platform_model.dart'; void showPeerSelectionDialog( {bool singleSelection = false, required Function(List) onPeersCallback}) async { - final peers = await bind.mainGetRecentPeers(getAll: true); + // load recent peers, we can directly use the peers in `gFFI.recentPeersModel`. + // The plugin is not used for now, so just left it empty here. + final peers = ''; if (peers.isEmpty) { - debugPrint("load recent peers failed."); + // debugPrint("load recent peers failed."); return; } diff --git a/libs/hbb_common b/libs/hbb_common index f94753bdd..16900b9b0 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit f94753bddb11a6de52e4fe7f9e490fcf8df2275a +Subproject commit 16900b9b064067e28f6e685b29a94c16350ffc36 diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 1a65658e2..fed038233 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1127,84 +1127,48 @@ pub fn main_load_recent_peers() { return; } - let push_to_flutter = |peers| { - let data = HashMap::from([("name", "load_recent_peers".to_owned()), ("peers", peers)]); + let push_to_flutter = |peers, ids| { + let mut data = + HashMap::from([("name", "load_recent_peers".to_owned()), ("peers", peers)]); + if let Some(ids) = ids { + data.insert("ids", ids); + } let _res = flutter::push_global_event( flutter::APP_TYPE_MAIN, serde_json::ser::to_string(&data).unwrap_or("".to_owned()), ); }; - let load_two_times = - vec_id_modified_time_path.len() > PeerConfig::BATCH_LOADING_COUNT && cfg!(target_os = "windows"); + let load_two_times = vec_id_modified_time_path.len() > PeerConfig::BATCH_LOADING_COUNT + && cfg!(target_os = "windows"); let mut all_peers = vec![]; if load_two_times { let next_from = load_recent_peers(&vec_id_modified_time_path, false, &mut all_peers, 0); - push_to_flutter(serde_json::ser::to_string(&all_peers).unwrap_or("".to_owned())); + let rest_ids = if next_from < vec_id_modified_time_path.len() { + Some( + vec_id_modified_time_path[next_from..] + .iter() + .map(|(id, _, _)| id.clone()) + .collect::>() + .join(", "), + ) + } else { + None + }; + push_to_flutter( + serde_json::ser::to_string(&all_peers).unwrap_or("".to_owned()), + rest_ids, + ); let _ = load_recent_peers(&vec_id_modified_time_path, true, &mut all_peers, next_from); } else { let _ = load_recent_peers(&vec_id_modified_time_path, true, &mut all_peers, 0); } - push_to_flutter(serde_json::ser::to_string(&all_peers).unwrap_or("".to_owned())); - } -} - -fn get_partial_recent_peers( - vec_id_modified_time_path: Vec<(String, SystemTime, std::path::PathBuf)>, - count: usize, -) -> String { - let (peers, next_from) = PeerConfig::batch_peers( - &vec_id_modified_time_path, - 0, - Some(count.min(vec_id_modified_time_path.len())), - ); - let peer_maps: Vec<_> = peers - .into_iter() - .map(|(id, _, p)| peer_to_map(id, p)) - .collect(); - let mut data = HashMap::from([( - "peers", - serde_json::ser::to_string(&peer_maps).unwrap_or("[]".to_owned()), - )]); - if next_from < vec_id_modified_time_path.len() { - let ids: Vec<_> = vec_id_modified_time_path[next_from..] - .iter() - .map(|(id, _, _)| id.clone()) - .collect(); - data.insert( - "ids", - serde_json::ser::to_string(&ids).unwrap_or("[]".to_owned()), + // Don't check if `all_peers` is empty, because we need this message to update the state in the flutter side. + push_to_flutter( + serde_json::ser::to_string(&all_peers).unwrap_or("".to_owned()), + None, ); } - return serde_json::ser::to_string(&data).unwrap_or("".to_owned()); -} - -pub fn main_get_recent_peers(get_all: bool) -> String { - if !config::APP_DIR.read().unwrap().is_empty() { - let vec_id_modified_time_path = PeerConfig::get_vec_id_modified_time_path(&None); - - let load_two_times = !get_all - && vec_id_modified_time_path.len() > PeerConfig::BATCH_LOADING_COUNT - && cfg!(target_os = "windows"); - let load_count = if load_two_times { - PeerConfig::BATCH_LOADING_COUNT - } else { - vec_id_modified_time_path.len() - }; - return get_partial_recent_peers(vec_id_modified_time_path, load_count); - } - "".to_string() -} - -pub fn main_load_lan_peers_sync() -> SyncReturn { - let data = HashMap::from([ - ("name", "load_lan_peers".to_owned()), - ( - "peers", - serde_json::to_string(&get_lan_peers()).unwrap_or_default(), - ), - ]); - return SyncReturn(serde_json::ser::to_string(&data).unwrap_or("".to_owned())); } pub fn main_load_recent_peers_for_ab(filter: String) -> String {