diff --git a/.gitignore b/.gitignore
index 5b26711c5..9d152ac1d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+/build
 /target
 .vscode
 .idea
diff --git a/build.rs b/build.rs
index 7d6aac441..860ebae77 100644
--- a/build.rs
+++ b/build.rs
@@ -77,6 +77,10 @@ fn install_oboe() {
 }
 
 fn gen_flutter_rust_bridge() {
+    let llvm_path = match std::env::var("LLVM_HOME") {
+        Ok(path) => Some(vec![path]),
+        Err(_) => None,
+    };
     // Tell Cargo that if the given file changes, to rerun this build script.
     println!("cargo:rerun-if-changed=src/flutter_ffi.rs");
     // settings for fbr_codegen
@@ -88,6 +92,7 @@ fn gen_flutter_rust_bridge() {
         // Path of output generated C header
         c_output: Some(vec!["flutter/macos/Runner/bridge_generated.h".to_string()]),
         // for other options lets use default
+        llvm_path,
         ..Default::default()
     };
     // run fbr_codegen
diff --git a/flutter/.gitignore b/flutter/.gitignore
index ede37092d..e5db34d22 100644
--- a/flutter/.gitignore
+++ b/flutter/.gitignore
@@ -48,13 +48,11 @@ lib/generated_bridge.dart
 lib/generated_bridge.freezed.dart
 
 # Flutter Generated Files
-linux/flutter/generated_plugin_registrant.cc
-linux/flutter/generated_plugin_registrant.h
-linux/flutter/generated_plugins.cmake
-macos/Flutter/GeneratedPluginRegistrant.swift
-windows/flutter/generated_plugin_registrant.cc
-windows/flutter/generated_plugin_registrant.h
-windows/flutter/generated_plugins.cmake
+**/flutter/GeneratedPluginRegistrant.swift
+**/flutter/generated_plugin_registrant.cc
+**/flutter/generated_plugin_registrant.h
+**/flutter/generated_plugins.cmake
+**/Runner/bridge_generated.h
 flutter_export_environment.sh
 Flutter-Generated.xcconfig
 key.jks
diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart
index 14a1cc4e7..42c41f8b9 100644
--- a/flutter/lib/desktop/pages/connection_page.dart
+++ b/flutter/lib/desktop/pages/connection_page.dart
@@ -4,6 +4,7 @@ import 'dart:convert';
 import 'package:contextmenu/contextmenu.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
+import 'package:flutter_hbb/desktop/widgets/peer_widget.dart';
 import 'package:flutter_hbb/utils/multi_window_manager.dart';
 import 'package:get/get.dart';
 import 'package:provider/provider.dart';
@@ -15,6 +16,7 @@ import '../../mobile/pages/home_page.dart';
 import '../../mobile/pages/scan_page.dart';
 import '../../mobile/pages/settings_page.dart';
 import '../../models/model.dart';
+import '../../models/peer_model.dart';
 
 enum RemoteType { recently, favorite, discovered, addressBook }
 
@@ -58,9 +60,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
   Widget build(BuildContext context) {
     if (_idController.text.isEmpty) _idController.text = gFFI.getId();
     return Container(
-      decoration: BoxDecoration(
-          color: MyTheme.grayBg
-      ),
+      decoration: BoxDecoration(color: MyTheme.grayBg),
       child: Column(
           mainAxisAlignment: MainAxisAlignment.start,
           mainAxisSize: MainAxisSize.max,
@@ -73,7 +73,9 @@ class _ConnectionPageState extends State<ConnectionPage> {
               ],
             ).marginOnly(top: 16.0, left: 16.0),
             SizedBox(height: 12),
-            Divider(thickness: 1,),
+            Divider(
+              thickness: 1,
+            ),
             Expanded(
               child: DefaultTabController(
                   length: 4,
@@ -85,47 +87,61 @@ class _ConnectionPageState extends State<ConnectionPage> {
                           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")),),
+                            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: [
-                        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();
-                              }
-                            }),
+                      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))
                     ],
                   )),
@@ -166,20 +182,20 @@ class _ConnectionPageState extends State<ConnectionPage> {
     return _updateUrl.isEmpty
         ? SizedBox(height: 0)
         : InkWell(
-        onTap: () async {
-          final url = _updateUrl + '.apk';
-          if (await canLaunch(url)) {
-            await launch(url);
-          }
-        },
-        child: Container(
-            alignment: AlignmentDirectional.center,
-            width: double.infinity,
-            color: Colors.pinkAccent,
-            padding: EdgeInsets.symmetric(vertical: 12),
-            child: Text(translate('Download new version'),
-                style: TextStyle(
-                    color: Colors.white, fontWeight: FontWeight.bold))));
+            onTap: () async {
+              final url = _updateUrl + '.apk';
+              if (await canLaunch(url)) {
+                await launch(url);
+              }
+            },
+            child: Container(
+                alignment: AlignmentDirectional.center,
+                width: double.infinity,
+                color: Colors.pinkAccent,
+                padding: EdgeInsets.symmetric(vertical: 12),
+                child: Text(translate('Download new version'),
+                    style: TextStyle(
+                        color: Colors.white, fontWeight: FontWeight.bold))));
   }
 
   /// UI for the search bar.
@@ -214,8 +230,8 @@ class _ConnectionPageState extends State<ConnectionPage> {
                         labelText: translate('Control Remote Desktop'),
                         // hintText: 'Enter your remote ID',
                         // border: InputBorder.,
-                        border: OutlineInputBorder(
-                            borderRadius: BorderRadius.zero),
+                        border:
+                            OutlineInputBorder(borderRadius: BorderRadius.zero),
                         helperStyle: TextStyle(
                           fontWeight: FontWeight.bold,
                           fontSize: 16,
@@ -238,8 +254,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
               ],
             ),
             Padding(
-              padding: const EdgeInsets.only(
-                  top: 16.0),
+              padding: const EdgeInsets.only(top: 16.0),
               child: Row(
                 mainAxisAlignment: MainAxisAlignment.end,
                 children: [
@@ -996,13 +1011,13 @@ class _WebMenuState extends State<WebMenu> {
         icon: Icon(Icons.more_vert),
         itemBuilder: (context) {
           return (isIOS
-              ? [
-            PopupMenuItem(
-              child: Icon(Icons.qr_code_scanner, color: Colors.black),
-              value: "scan",
-            )
-          ]
-              : <PopupMenuItem<String>>[]) +
+                  ? [
+                      PopupMenuItem(
+                        child: Icon(Icons.qr_code_scanner, color: Colors.black),
+                        value: "scan",
+                      )
+                    ]
+                  : <PopupMenuItem<String>>[]) +
               [
                 PopupMenuItem(
                   child: Text(translate('ID/Relay Server')),
@@ -1012,13 +1027,13 @@ class _WebMenuState extends State<WebMenu> {
               (getUrl().contains('admin.rustdesk.com')
                   ? <PopupMenuItem<String>>[]
                   : [
-                PopupMenuItem(
-                  child: Text(username == null
-                      ? translate("Login")
-                      : translate("Logout") + ' ($username)'),
-                  value: "login",
-                )
-              ]) +
+                      PopupMenuItem(
+                        child: Text(username == null
+                            ? translate("Login")
+                            : translate("Logout") + ' ($username)'),
+                        value: "login",
+                      )
+                    ]) +
               [
                 PopupMenuItem(
                   child: Text(translate('About') + ' RustDesk'),
diff --git a/flutter/lib/desktop/widgets/peer_widget.dart b/flutter/lib/desktop/widgets/peer_widget.dart
new file mode 100644
index 000000000..e0c82bb30
--- /dev/null
+++ b/flutter/lib/desktop/widgets/peer_widget.dart
@@ -0,0 +1,244 @@
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/foundation.dart';
+import 'package:provider/provider.dart';
+import 'package:visibility_detector/visibility_detector.dart';
+import 'package:window_manager/window_manager.dart';
+
+import '../../models/peer_model.dart';
+import '../../common.dart';
+import 'peercard_widget.dart';
+
+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,
+      PeerCardWidgetFunc peerCardWidgetFunc,
+      {Key? key})
+      : super(key: key) {
+    _name = name;
+    _peers = peers;
+    _offstageFunc = offstageFunc;
+    _peerCardWidgetFunc = peerCardWidgetFunc;
+  }
+
+  @override
+  _PeerWidgetState createState() => _PeerWidgetState();
+}
+
+/// State for the peer widget.
+class _PeerWidgetState extends State<_PeerWidget> with WindowListener {
+  static const int _maxQueryCount = 3;
+
+  var _curPeers = Set<String>();
+  var _lastChangeTime = DateTime.now();
+  var _lastQueryPeers = Set<String>();
+  var _lastQueryTime = DateTime.now().subtract(Duration(hours: 1));
+  var _queryCoun = 0;
+  var _exit = false;
+
+  _PeerWidgetState() {
+    _startCheckOnlines();
+  }
+
+  @override
+  void initState() {
+    windowManager.addListener(this);
+    super.initState();
+  }
+
+  @override
+  void dispose() {
+    windowManager.removeListener(this);
+    _exit = true;
+    super.dispose();
+  }
+
+  @override
+  void onWindowFocus() {
+    _queryCoun = 0;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final space = 8.0;
+    return ChangeNotifierProvider<Peers>(
+      create: (context) => Peers(super.widget._name, super.widget._peers),
+      child: SingleChildScrollView(
+          child: Consumer<Peers>(
+              builder: (context, peers, child) => Wrap(
+                  children: () {
+                    final cards = <Widget>[];
+                    peers.peers.forEach((peer) {
+                      cards.add(Offstage(
+                          offstage: super.widget._offstageFunc(peer),
+                          child: Container(
+                            width: 225,
+                            height: 150,
+                            child: VisibilityDetector(
+                              key: Key('${peer.id}'),
+                              onVisibilityChanged: (info) {
+                                final peerId = (info.key as ValueKey).value;
+                                if (info.visibleFraction > 0.00001) {
+                                  _curPeers.add(peerId);
+                                } else {
+                                  _curPeers.remove(peerId);
+                                }
+                                _lastChangeTime = DateTime.now();
+                              },
+                              child: super.widget._peerCardWidgetFunc(peer),
+                            ),
+                          )));
+                    });
+                    return cards;
+                  }(),
+                  spacing: space,
+                  runSpacing: space))),
+    );
+  }
+
+  // ignore: todo
+  // TODO: variables walk through async tasks?
+  void _startCheckOnlines() {
+    () async {
+      while (!_exit) {
+        final now = DateTime.now();
+        if (!setEquals(_curPeers, _lastQueryPeers)) {
+          if (now.difference(_lastChangeTime) > Duration(seconds: 1)) {
+            gFFI.ffiModel.platformFFI.ffiBind
+                .queryOnlines(ids: _curPeers.toList(growable: false));
+            _lastQueryPeers = {..._curPeers};
+            _lastQueryTime = DateTime.now();
+            _queryCoun = 0;
+          }
+        } else {
+          if (_queryCoun < _maxQueryCount) {
+            if (now.difference(_lastQueryTime) > Duration(seconds: 20)) {
+              gFFI.ffiModel.platformFFI.ffiBind
+                  .queryOnlines(ids: _curPeers.toList(growable: false));
+              _lastQueryTime = DateTime.now();
+              _queryCoun += 1;
+            }
+          }
+        }
+        await Future.delayed(Duration(milliseconds: 300));
+      }
+    }();
+  }
+}
+
+abstract class BasePeerWidget extends StatelessWidget {
+  late final _name;
+  late final OffstageFunc _offstageFunc;
+  late final PeerCardWidgetFunc _peerCardWidgetFunc;
+
+  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();
+      }
+    });
+  }
+
+  @protected
+  Future<List<Peer>> _loadPeers();
+}
+
+class RecentPeerWidget extends BasePeerWidget {
+  RecentPeerWidget({Key? key}) : super(key: key) {
+    super._name = "recent peer";
+    super._offstageFunc = (Peer _peer) => false;
+    super._peerCardWidgetFunc = (Peer peer) => RecentPeerCard(peer: peer);
+  }
+
+  Future<List<Peer>> _loadPeers() async {
+    return gFFI.peers();
+  }
+}
+
+class FavoritePeerWidget extends BasePeerWidget {
+  FavoritePeerWidget({Key? key}) : super(key: key) {
+    super._name = "favorite peer";
+    super._offstageFunc = (Peer _peer) => false;
+    super._peerCardWidgetFunc = (Peer peer) => FavoritePeerCard(peer: peer);
+  }
+
+  @override
+  Future<List<Peer>> _loadPeers() async {
+    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;
+    });
+  }
+}
+
+class DiscoveredPeerWidget extends BasePeerWidget {
+  DiscoveredPeerWidget({Key? key}) : super(key: key) {
+    super._name = "discovered peer";
+    super._offstageFunc = (Peer _peer) => false;
+    super._peerCardWidgetFunc = (Peer peer) => DiscoveredPeerCard(peer: peer);
+  }
+
+  Future<List<Peer>> _loadPeers() async {
+    return await gFFI.bind.mainGetLanPeers().then((peers_string) {
+      debugPrint(peers_string);
+      return [];
+    });
+  }
+}
+
+class AddressBookPeerWidget extends BasePeerWidget {
+  AddressBookPeerWidget({Key? key}) : super(key: key) {
+    super._name = "address book peer";
+    super._offstageFunc =
+        (Peer peer) => !_hitTag(gFFI.abModel.selectedTags, peer.tags);
+    super._peerCardWidgetFunc = (Peer peer) => AddressBookPeerCard(peer: peer);
+  }
+
+  Future<List<Peer>> _loadPeers() async {
+    return gFFI.abModel.peers.map((e) {
+      return Peer.fromJson(e['id'], e);
+    }).toList();
+  }
+
+  bool _hitTag(List<dynamic> selectedTags, List<dynamic> idents) {
+    if (selectedTags.isEmpty) {
+      return true;
+    }
+    if (idents.isEmpty) {
+      return false;
+    }
+    for (final tag in selectedTags) {
+      if (!idents.contains(tag)) {
+        return false;
+      }
+    }
+    return true;
+  }
+}
diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart
new file mode 100644
index 000000000..b8c6d54de
--- /dev/null
+++ b/flutter/lib/desktop/widgets/peercard_widget.dart
@@ -0,0 +1,371 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hbb/utils/multi_window_manager.dart';
+import 'package:get/get.dart';
+import 'package:contextmenu/contextmenu.dart';
+
+import '../../common.dart';
+import '../../models/model.dart';
+import '../../models/peer_model.dart';
+
+class _PeerCard extends StatefulWidget {
+  final Peer peer;
+  final List<PopupMenuItem<String>> popupMenuItems;
+
+  _PeerCard({required this.peer, required this.popupMenuItems, Key? key})
+      : super(key: key);
+
+  @override
+  _PeerCardState createState() => _PeerCardState();
+}
+
+/// State for the connection page.
+class _PeerCardState extends State<_PeerCard> {
+  var _menuPos;
+
+  @override
+  Widget build(BuildContext context) {
+    final peer = super.widget.peer;
+    var deco = Rx<BoxDecoration?>(BoxDecoration(
+        border: Border.all(color: Colors.transparent, width: 1.0),
+        borderRadius: BorderRadius.circular(20)));
+    return Card(
+        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
+        child: MouseRegion(
+          onEnter: (evt) {
+            deco.value = BoxDecoration(
+                border: Border.all(color: Colors.blue, width: 1.0),
+                borderRadius: BorderRadius.circular(20));
+          },
+          onExit: (evt) {
+            deco.value = BoxDecoration(
+                border: Border.all(color: Colors.transparent, width: 1.0),
+                borderRadius: BorderRadius.circular(20));
+          },
+          child: _buildPeerTile(context, peer, deco),
+        ));
+  }
+
+  Widget _buildPeerTile(
+      BuildContext context, Peer peer, Rx<BoxDecoration?> deco) {
+    return Obx(
+      () => Container(
+        decoration: deco.value,
+        child: Column(
+          mainAxisSize: MainAxisSize.min,
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: [
+            Expanded(
+              child: Container(
+                decoration: BoxDecoration(
+                  color: str2color('${peer.id}${peer.platform}', 0x7f),
+                  borderRadius: BorderRadius.only(
+                    topLeft: Radius.circular(20),
+                    topRight: Radius.circular(20),
+                  ),
+                ),
+                child: Row(
+                  children: [
+                    Expanded(
+                      child: Column(
+                        crossAxisAlignment: CrossAxisAlignment.center,
+                        children: [
+                          Container(
+                            padding: const EdgeInsets.all(6),
+                            child: _getPlatformImage('${peer.platform}'),
+                          ),
+                          Row(
+                            children: [
+                              Expanded(
+                                child: Tooltip(
+                                  message: '${peer.username}@${peer.hostname}',
+                                  child: Text(
+                                    '${peer.username}@${peer.hostname}',
+                                    style: TextStyle(
+                                        color: Colors.white70, fontSize: 12),
+                                    textAlign: TextAlign.center,
+                                    overflow: TextOverflow.ellipsis,
+                                  ),
+                                ),
+                              ),
+                            ],
+                          ),
+                        ],
+                      ).paddingAll(4.0),
+                    ),
+                  ],
+                ),
+              ),
+            ),
+            Row(
+              mainAxisAlignment: MainAxisAlignment.spaceBetween,
+              children: [
+                Row(children: [
+                  Padding(
+                      padding: EdgeInsets.fromLTRB(0, 4, 8, 4),
+                      child: CircleAvatar(
+                          radius: 5,
+                          backgroundColor:
+                              peer.online ? Colors.green : Colors.yellow)),
+                  Text('${peer.id}')
+                ]),
+                InkWell(
+                    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(context, peer.id);
+                    }),
+              ],
+            ).paddingSymmetric(vertical: 8.0, horizontal: 12.0)
+          ],
+        ),
+      ),
+    );
+  }
+
+  /// Connect to a peer with [id].
+  /// If [isFileTransfer], starts a session only for file transfer.
+  void _connect(String id, {bool isFileTransfer = false}) async {
+    if (id == '') return;
+    id = id.replaceAll(' ', '');
+    if (isFileTransfer) {
+      await rustDeskWinManager.new_file_transfer(id);
+    } else {
+      await rustDeskWinManager.new_remote_desktop(id);
+    }
+    FocusScopeNode currentFocus = FocusScope.of(context);
+    if (!currentFocus.hasPrimaryFocus) {
+      currentFocus.unfocus();
+    }
+  }
+
+  /// 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) async {
+    var value = await showMenu(
+      context: context,
+      position: this._menuPos,
+      items: super.widget.popupMenuItems,
+      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);
+    }
+  }
+
+  Widget _buildTag(String tagName, RxList<dynamic> rxTags,
+      {Function()? onTap}) {
+    return ContextMenuArea(
+      width: 100,
+      builder: (context) => [
+        ListTile(
+          title: Text(translate("Delete")),
+          onTap: () {
+            gFFI.abModel.deleteTag(tagName);
+            gFFI.abModel.updateAb();
+            Future.delayed(Duration.zero, () => Get.back());
+          },
+        )
+      ],
+      child: GestureDetector(
+        onTap: onTap,
+        child: Obx(
+          () => Container(
+            decoration: BoxDecoration(
+                color: rxTags.contains(tagName) ? Colors.blue : null,
+                border: Border.all(color: MyTheme.darkGray),
+                borderRadius: BorderRadius.circular(10)),
+            margin: EdgeInsets.symmetric(horizontal: 4.0, vertical: 8.0),
+            padding: EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0),
+            child: Text(
+              tagName,
+              style: TextStyle(
+                  color: rxTags.contains(tagName) ? MyTheme.white : null),
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+
+  /// Get the image for the current [platform].
+  Widget _getPlatformImage(String platform) {
+    platform = platform.toLowerCase();
+    if (platform == 'mac os')
+      platform = 'mac';
+    else if (platform != 'linux' && platform != 'android') platform = 'win';
+    return Image.asset('assets/$platform.png', height: 50);
+  }
+
+  void _abEditTag(String id) {
+    var isInProgress = false;
+
+    final tags = List.of(gFFI.abModel.tags);
+    var selectedTag = gFFI.abModel.getPeerTags(id).obs;
+
+    DialogManager.show((setState, close) {
+      return CustomAlertDialog(
+        title: Text(translate("Edit Tag")),
+        content: Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            Container(
+              padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
+              child: Wrap(
+                children: tags
+                    .map((e) => _buildTag(e, selectedTag, onTap: () {
+                          if (selectedTag.contains(e)) {
+                            selectedTag.remove(e);
+                          } else {
+                            selectedTag.add(e);
+                          }
+                        }))
+                    .toList(growable: false),
+              ),
+            ),
+            Offstage(offstage: !isInProgress, child: LinearProgressIndicator())
+          ],
+        ),
+        actions: [
+          TextButton(
+              onPressed: () {
+                close();
+              },
+              child: Text(translate("Cancel"))),
+          TextButton(
+              onPressed: () async {
+                setState(() {
+                  isInProgress = true;
+                });
+                gFFI.abModel.changeTagForPeer(id, selectedTag);
+                await gFFI.abModel.updateAb();
+                close();
+              },
+              child: Text(translate("OK"))),
+        ],
+      );
+    });
+  }
+}
+
+abstract class BasePeerCard extends StatelessWidget {
+  final Peer peer;
+  BasePeerCard({required this.peer, Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return _PeerCard(peer: peer, popupMenuItems: _getPopupMenuItems());
+  }
+
+  @protected
+  List<PopupMenuItem<String>> _getPopupMenuItems();
+}
+
+class RecentPeerCard extends BasePeerCard {
+  RecentPeerCard({required Peer peer, Key? key}) : super(peer: peer, key: key);
+
+  List<PopupMenuItem<String>> _getPopupMenuItems() {
+    return [
+      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'),
+      PopupMenuItem<String>(child: Text(translate('Remove')), value: 'remove'),
+      PopupMenuItem<String>(
+          child: Text(translate('Unremember Password')),
+          value: 'unremember-password'),
+      PopupMenuItem<String>(
+          child: Text(translate('Edit Tag')), value: 'ab-edit-tag'),
+    ];
+  }
+}
+
+class FavoritePeerCard extends BasePeerCard {
+  FavoritePeerCard({required Peer peer, Key? key})
+      : super(peer: peer, key: key);
+
+  List<PopupMenuItem<String>> _getPopupMenuItems() {
+    return [
+      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'),
+      PopupMenuItem<String>(child: Text(translate('Remove')), value: 'remove'),
+      PopupMenuItem<String>(
+          child: Text(translate('Unremember Password')),
+          value: 'unremember-password'),
+      PopupMenuItem<String>(
+          child: Text(translate('Remove from Favorites')), value: 'remove-fav'),
+    ];
+  }
+}
+
+class DiscoveredPeerCard extends BasePeerCard {
+  DiscoveredPeerCard({required Peer peer, Key? key})
+      : super(peer: peer, key: key);
+
+  List<PopupMenuItem<String>> _getPopupMenuItems() {
+    return [
+      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'),
+      PopupMenuItem<String>(child: Text(translate('Remove')), value: 'remove'),
+      PopupMenuItem<String>(
+          child: Text(translate('Unremember Password')),
+          value: 'unremember-password'),
+      PopupMenuItem<String>(
+          child: Text(translate('Edit Tag')), value: 'ab-edit-tag'),
+    ];
+  }
+}
+
+class AddressBookPeerCard extends BasePeerCard {
+  AddressBookPeerCard({required Peer peer, Key? key})
+      : super(peer: peer, key: key);
+
+  List<PopupMenuItem<String>> _getPopupMenuItems() {
+    return [
+      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'),
+      PopupMenuItem<String>(
+          child: Text(translate('Remove')), value: 'ab-delete'),
+      PopupMenuItem<String>(
+          child: Text(translate('Unremember Password')),
+          value: 'unremember-password'),
+      PopupMenuItem<String>(
+          child: Text(translate('Add to Favorites')), value: 'add-fav'),
+    ];
+  }
+}
diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart
index fa8210618..bc64ff6f5 100644
--- a/flutter/lib/models/model.dart
+++ b/flutter/lib/models/model.dart
@@ -21,6 +21,7 @@ import '../common.dart';
 import '../mobile/widgets/dialog.dart';
 import '../mobile/widgets/overlay.dart';
 import 'native_model.dart' if (dart.library.html) 'web_model.dart';
+import 'peer_model.dart';
 
 typedef HandleMsgBox = void Function(Map<String, dynamic> evt, String id);
 bool _waitForImage = false;
@@ -1092,7 +1093,7 @@ class FFI {
   Future<List<String>> getAudioInputs() async {
     return await bind.mainGetSoundInputs();
   }
-  
+
   String getDefaultAudioInput() {
     final input = getOption('audio-input');
     if (input.isEmpty && Platform.isWindows) {
@@ -1110,21 +1111,6 @@ class FFI {
   }
 }
 
-class Peer {
-  final String id;
-  final String username;
-  final String hostname;
-  final String platform;
-  final List<dynamic> tags;
-
-  Peer.fromJson(String id, Map<String, dynamic> json)
-      : id = id,
-        username = json['username'] ?? '',
-        hostname = json['hostname'] ?? '',
-        platform = json['platform'] ?? '',
-        tags = json['tags'] ?? [];
-}
-
 class Display {
   double x = 0;
   double y = 0;
diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart
index c0fd4dfa1..511aa5ffe 100644
--- a/flutter/lib/models/native_model.dart
+++ b/flutter/lib/models/native_model.dart
@@ -6,6 +6,7 @@ import 'dart:typed_data';
 import 'package:device_info_plus/device_info_plus.dart';
 import 'package:external_path/external_path.dart';
 import 'package:ffi/ffi.dart';
+import 'package:flutter/foundation.dart';
 import 'package:flutter/services.dart';
 import 'package:package_info_plus/package_info_plus.dart';
 import 'package:path_provider/path_provider.dart';
@@ -21,6 +22,7 @@ class RgbaFrame extends Struct {
 
 typedef F2 = Pointer<Utf8> Function(Pointer<Utf8>, Pointer<Utf8>);
 typedef F3 = void Function(Pointer<Utf8>, Pointer<Utf8>);
+typedef HandleEvent = void Function(Map<String, dynamic> evt);
 
 /// FFI wrapper around the native Rust core.
 /// Hides the platform differences.
@@ -30,6 +32,7 @@ class PlatformFFI {
   String _homeDir = '';
   F2? _getByName;
   F3? _setByName;
+  var _eventHandlers = Map<String, Map<String, HandleEvent>>();
   late RustdeskImpl _ffiBind;
   void Function(Map<String, dynamic>)? _eventCallback;
 
@@ -40,6 +43,31 @@ class PlatformFFI {
     return packageInfo.version;
   }
 
+  bool registerEventHandler(
+      String event_name, String handler_name, HandleEvent handler) {
+    debugPrint('registerEventHandler $event_name $handler_name');
+    var handlers = _eventHandlers[event_name];
+    if (handlers == null) {
+      _eventHandlers[event_name] = {handler_name: handler};
+      return true;
+    } else {
+      if (handlers.containsKey(handler_name)) {
+        return false;
+      } else {
+        handlers[handler_name] = handler;
+        return true;
+      }
+    }
+  }
+
+  void unregisterEventHandler(String event_name, String handler_name) {
+    debugPrint('unregisterEventHandler $event_name $handler_name');
+    var handlers = _eventHandlers[event_name];
+    if (handlers != null) {
+      handlers.remove(handler_name);
+    }
+  }
+
   /// Send **get** command to the Rust core based on [name] and [arg].
   /// Return the result as a string.
   String getByName(String name, [String arg = '']) {
@@ -138,6 +166,22 @@ class PlatformFFI {
     version = await getVersion();
   }
 
+  bool _tryHandle(Map<String, dynamic> evt) {
+    final name = evt['name'];
+    if (name != null) {
+      final handlers = _eventHandlers[name];
+      if (handlers != null) {
+        if (handlers.isNotEmpty) {
+          handlers.values.forEach((handler) {
+            handler(evt);
+          });
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
   /// Start listening to the Rust core's events and frames.
   void _startListenEvent(RustdeskImpl rustdeskImpl) {
     () async {
@@ -145,7 +189,10 @@ class PlatformFFI {
         if (_eventCallback != null) {
           try {
             Map<String, dynamic> event = json.decode(message);
-            _eventCallback!(event);
+            // _tryHandle here may be more flexible than _eventCallback
+            if (!_tryHandle(event)) {
+              _eventCallback!(event);
+            }
           } catch (e) {
             print('json.decode fail(): $e');
           }
diff --git a/flutter/lib/models/peer_model.dart b/flutter/lib/models/peer_model.dart
new file mode 100644
index 000000000..939d16ede
--- /dev/null
+++ b/flutter/lib/models/peer_model.dart
@@ -0,0 +1,89 @@
+import 'package:flutter/foundation.dart';
+import '../../common.dart';
+
+class Peer {
+  final String id;
+  final String username;
+  final String hostname;
+  final String platform;
+  final List<dynamic> tags;
+  bool online = false;
+
+  Peer.fromJson(String id, Map<String, dynamic> json)
+      : id = id,
+        username = json['username'] ?? '',
+        hostname = json['hostname'] ?? '',
+        platform = json['platform'] ?? '',
+        tags = json['tags'] ?? [];
+
+  Peer({
+    required this.id,
+    required this.username,
+    required this.hostname,
+    required this.platform,
+    required this.tags,
+  });
+
+  Peer.loading()
+      : this(
+            id: '...',
+            username: '...',
+            hostname: '...',
+            platform: '...',
+            tags: []);
+}
+
+class Peers extends ChangeNotifier {
+  late String _name;
+  late var _peers;
+  static const cbQueryOnlines = 'callback_query_onlines';
+
+  Peers(String name, List<Peer> peers) {
+    _name = name;
+    _peers = peers;
+    gFFI.ffiModel.platformFFI.registerEventHandler(cbQueryOnlines, _name,
+        (evt) {
+      _updateOnlineState(evt);
+    });
+  }
+
+  List<Peer> get peers => _peers;
+
+  @override
+  void dispose() {
+    gFFI.ffiModel.platformFFI.unregisterEventHandler(cbQueryOnlines, _name);
+    super.dispose();
+  }
+
+  Peer getByIndex(int index) {
+    if (index < _peers.length) {
+      return _peers[index];
+    } else {
+      return Peer.loading();
+    }
+  }
+
+  int getPeersCount() {
+    return _peers.length;
+  }
+
+  void _updateOnlineState(Map<String, dynamic> evt) {
+    evt['onlines'].split(',').forEach((online) {
+      for (var i = 0; i < _peers.length; i++) {
+        if (_peers[i].id == online) {
+          _peers[i].online = true;
+        }
+      }
+    });
+
+    evt['offlines'].split(',').forEach((offline) {
+      for (var i = 0; i < _peers.length; i++) {
+        if (_peers[i].id == offline) {
+          _peers[i].online = false;
+        }
+      }
+    });
+
+    notifyListeners();
+  }
+}
diff --git a/flutter/macos/Runner/bridge_generated.h b/flutter/macos/Runner/bridge_generated.h
deleted file mode 100644
index 163ad91cd..000000000
--- a/flutter/macos/Runner/bridge_generated.h
+++ /dev/null
@@ -1,220 +0,0 @@
-#include <stdbool.h>
-#include <stdint.h>
-#include <stdlib.h>
-
-#define GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT 2
-
-#define GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS 4
-
-typedef struct wire_uint_8_list {
-  uint8_t *ptr;
-  int32_t len;
-} wire_uint_8_list;
-
-typedef struct WireSyncReturnStruct {
-  uint8_t *ptr;
-  int32_t len;
-  bool success;
-} WireSyncReturnStruct;
-
-typedef int64_t DartPort;
-
-typedef bool (*DartPostCObjectFnType)(DartPort port_id, void *message);
-
-void wire_rustdesk_core_main(int64_t port_);
-
-void wire_start_global_event_stream(int64_t port_);
-
-void wire_host_stop_system_key_propagate(int64_t port_, bool stopped);
-
-void wire_session_connect(int64_t port_, struct wire_uint_8_list *id, bool is_file_transfer);
-
-void wire_get_session_remember(int64_t port_, struct wire_uint_8_list *id);
-
-void wire_get_session_toggle_option(int64_t port_,
-                                    struct wire_uint_8_list *id,
-                                    struct wire_uint_8_list *arg);
-
-struct WireSyncReturnStruct wire_get_session_toggle_option_sync(struct wire_uint_8_list *id,
-                                                                struct wire_uint_8_list *arg);
-
-void wire_get_session_image_quality(int64_t port_, struct wire_uint_8_list *id);
-
-void wire_get_session_option(int64_t port_,
-                             struct wire_uint_8_list *id,
-                             struct wire_uint_8_list *arg);
-
-void wire_session_login(int64_t port_,
-                        struct wire_uint_8_list *id,
-                        struct wire_uint_8_list *password,
-                        bool remember);
-
-void wire_session_close(int64_t port_, struct wire_uint_8_list *id);
-
-void wire_session_refresh(int64_t port_, struct wire_uint_8_list *id);
-
-void wire_session_reconnect(int64_t port_, struct wire_uint_8_list *id);
-
-void wire_session_toggle_option(int64_t port_,
-                                struct wire_uint_8_list *id,
-                                struct wire_uint_8_list *value);
-
-void wire_session_set_image_quality(int64_t port_,
-                                    struct wire_uint_8_list *id,
-                                    struct wire_uint_8_list *value);
-
-void wire_session_lock_screen(int64_t port_, struct wire_uint_8_list *id);
-
-void wire_session_ctrl_alt_del(int64_t port_, struct wire_uint_8_list *id);
-
-void wire_session_switch_display(int64_t port_, struct wire_uint_8_list *id, int32_t value);
-
-void wire_session_input_key(int64_t port_,
-                            struct wire_uint_8_list *id,
-                            struct wire_uint_8_list *name,
-                            bool down,
-                            bool press,
-                            bool alt,
-                            bool ctrl,
-                            bool shift,
-                            bool command);
-
-void wire_session_input_string(int64_t port_,
-                               struct wire_uint_8_list *id,
-                               struct wire_uint_8_list *value);
-
-void wire_session_send_chat(int64_t port_,
-                            struct wire_uint_8_list *id,
-                            struct wire_uint_8_list *text);
-
-void wire_session_send_mouse(int64_t port_,
-                             struct wire_uint_8_list *id,
-                             int32_t mask,
-                             int32_t x,
-                             int32_t y,
-                             bool alt,
-                             bool ctrl,
-                             bool shift,
-                             bool command);
-
-void wire_session_peer_option(int64_t port_,
-                              struct wire_uint_8_list *id,
-                              struct wire_uint_8_list *name,
-                              struct wire_uint_8_list *value);
-
-void wire_session_get_peer_option(int64_t port_,
-                                  struct wire_uint_8_list *id,
-                                  struct wire_uint_8_list *name);
-
-void wire_session_input_os_password(int64_t port_,
-                                    struct wire_uint_8_list *id,
-                                    struct wire_uint_8_list *value);
-
-void wire_session_read_remote_dir(int64_t port_,
-                                  struct wire_uint_8_list *id,
-                                  struct wire_uint_8_list *path,
-                                  bool include_hidden);
-
-void wire_session_send_files(int64_t port_,
-                             struct wire_uint_8_list *id,
-                             int32_t act_id,
-                             struct wire_uint_8_list *path,
-                             struct wire_uint_8_list *to,
-                             int32_t file_num,
-                             bool include_hidden,
-                             bool is_remote);
-
-void wire_session_set_confirm_override_file(int64_t port_,
-                                            struct wire_uint_8_list *id,
-                                            int32_t act_id,
-                                            int32_t file_num,
-                                            bool need_override,
-                                            bool remember,
-                                            bool is_upload);
-
-void wire_session_remove_file(int64_t port_,
-                              struct wire_uint_8_list *id,
-                              int32_t act_id,
-                              struct wire_uint_8_list *path,
-                              int32_t file_num,
-                              bool is_remote);
-
-void wire_session_read_dir_recursive(int64_t port_,
-                                     struct wire_uint_8_list *id,
-                                     int32_t act_id,
-                                     struct wire_uint_8_list *path,
-                                     bool is_remote,
-                                     bool show_hidden);
-
-void wire_session_remove_all_empty_dirs(int64_t port_,
-                                        struct wire_uint_8_list *id,
-                                        int32_t act_id,
-                                        struct wire_uint_8_list *path,
-                                        bool is_remote);
-
-void wire_session_cancel_job(int64_t port_, struct wire_uint_8_list *id, int32_t act_id);
-
-void wire_session_create_dir(int64_t port_,
-                             struct wire_uint_8_list *id,
-                             int32_t act_id,
-                             struct wire_uint_8_list *path,
-                             bool is_remote);
-
-void wire_session_read_local_dir_sync(int64_t port_,
-                                      struct wire_uint_8_list *id,
-                                      struct wire_uint_8_list *path,
-                                      bool show_hidden);
-
-struct wire_uint_8_list *new_uint_8_list(int32_t len);
-
-void free_WireSyncReturnStruct(struct WireSyncReturnStruct val);
-
-void store_dart_post_cobject(DartPostCObjectFnType ptr);
-
-/**
- * FFI for rustdesk core's main entry.
- * Return true if the app should continue running with UI(possibly Flutter), false if the app should exit.
- */
-bool rustdesk_core_main(void);
-
-static int64_t dummy_method_to_enforce_bundling(void) {
-    int64_t dummy_var = 0;
-    dummy_var ^= ((int64_t) (void*) wire_rustdesk_core_main);
-    dummy_var ^= ((int64_t) (void*) wire_start_global_event_stream);
-    dummy_var ^= ((int64_t) (void*) wire_host_stop_system_key_propagate);
-    dummy_var ^= ((int64_t) (void*) wire_session_connect);
-    dummy_var ^= ((int64_t) (void*) wire_get_session_remember);
-    dummy_var ^= ((int64_t) (void*) wire_get_session_toggle_option);
-    dummy_var ^= ((int64_t) (void*) wire_get_session_toggle_option_sync);
-    dummy_var ^= ((int64_t) (void*) wire_get_session_image_quality);
-    dummy_var ^= ((int64_t) (void*) wire_get_session_option);
-    dummy_var ^= ((int64_t) (void*) wire_session_login);
-    dummy_var ^= ((int64_t) (void*) wire_session_close);
-    dummy_var ^= ((int64_t) (void*) wire_session_refresh);
-    dummy_var ^= ((int64_t) (void*) wire_session_reconnect);
-    dummy_var ^= ((int64_t) (void*) wire_session_toggle_option);
-    dummy_var ^= ((int64_t) (void*) wire_session_set_image_quality);
-    dummy_var ^= ((int64_t) (void*) wire_session_lock_screen);
-    dummy_var ^= ((int64_t) (void*) wire_session_ctrl_alt_del);
-    dummy_var ^= ((int64_t) (void*) wire_session_switch_display);
-    dummy_var ^= ((int64_t) (void*) wire_session_input_key);
-    dummy_var ^= ((int64_t) (void*) wire_session_input_string);
-    dummy_var ^= ((int64_t) (void*) wire_session_send_chat);
-    dummy_var ^= ((int64_t) (void*) wire_session_send_mouse);
-    dummy_var ^= ((int64_t) (void*) wire_session_peer_option);
-    dummy_var ^= ((int64_t) (void*) wire_session_get_peer_option);
-    dummy_var ^= ((int64_t) (void*) wire_session_input_os_password);
-    dummy_var ^= ((int64_t) (void*) wire_session_read_remote_dir);
-    dummy_var ^= ((int64_t) (void*) wire_session_send_files);
-    dummy_var ^= ((int64_t) (void*) wire_session_set_confirm_override_file);
-    dummy_var ^= ((int64_t) (void*) wire_session_remove_file);
-    dummy_var ^= ((int64_t) (void*) wire_session_read_dir_recursive);
-    dummy_var ^= ((int64_t) (void*) wire_session_remove_all_empty_dirs);
-    dummy_var ^= ((int64_t) (void*) wire_session_cancel_job);
-    dummy_var ^= ((int64_t) (void*) wire_session_create_dir);
-    dummy_var ^= ((int64_t) (void*) wire_session_read_local_dir_sync);
-    dummy_var ^= ((int64_t) (void*) new_uint_8_list);
-    dummy_var ^= ((int64_t) (void*) free_WireSyncReturnStruct);
-    dummy_var ^= ((int64_t) (void*) store_dart_post_cobject);
-    return dummy_var;
-}
\ No newline at end of file
diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock
index 364bad74d..127dcd523 100644
--- a/flutter/pubspec.lock
+++ b/flutter/pubspec.lock
@@ -771,6 +771,13 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "3.1.0"
+  screen_retriever:
+    dependency: transitive
+    description:
+      name: screen_retriever
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.1.2"
   settings_ui:
     dependency: "direct main"
     description:
@@ -1028,6 +1035,13 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.1.2"
+  visibility_detector:
+    dependency: "direct main"
+    description:
+      name: visibility_detector
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.3.3"
   wakelock:
     dependency: "direct main"
     description:
@@ -1084,6 +1098,13 @@ packages:
       url: "https://pub.flutter-io.cn"
     source: hosted
     version: "2.6.1"
+  window_manager:
+    dependency: "direct main"
+    description:
+      name: window_manager
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.2.5"
   xdg_directories:
     dependency: transitive
     description:
diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml
index 4a2b64043..76c2f7e12 100644
--- a/flutter/pubspec.yaml
+++ b/flutter/pubspec.yaml
@@ -58,7 +58,7 @@ dependencies:
             url: https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge
             ref: master
             path: frb_dart
-    # window_manager: ^0.2.5
+    window_manager: ^0.2.5
     desktop_multi_window:
         git:
             url: https://github.com/Kingtous/rustdesk_desktop_multi_window
@@ -67,6 +67,7 @@ dependencies:
     freezed_annotation: ^2.0.3
     tray_manager: 0.1.7
     get: ^4.6.5
+    visibility_detector: ^0.3.3
     contextmenu: ^3.0.0
 
 dev_dependencies:
diff --git a/libs/hbb_common/protos/rendezvous.proto b/libs/hbb_common/protos/rendezvous.proto
index 2c5f1b3ba..1ac60f3f3 100644
--- a/libs/hbb_common/protos/rendezvous.proto
+++ b/libs/hbb_common/protos/rendezvous.proto
@@ -148,6 +148,15 @@ message PeerDiscovery {
   string misc = 7;
 }
 
+message OnlineRequest {
+  string id = 1;
+  repeated string peers = 2;
+}
+
+message OnlineResponse {
+  bytes states = 1;
+}
+
 message RendezvousMessage {
   oneof union {
     RegisterPeer register_peer = 6;
@@ -167,5 +176,7 @@ message RendezvousMessage {
     TestNatRequest test_nat_request = 20;
     TestNatResponse test_nat_response = 21;
     PeerDiscovery peer_discovery = 22;
+    OnlineRequest online_request = 23;
+    OnlineResponse online_response = 24;
   }
 }
diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs
index 3d94f6cc7..57e7db87d 100644
--- a/src/flutter_ffi.rs
+++ b/src/flutter_ffi.rs
@@ -985,6 +985,21 @@ unsafe extern "C" fn set_by_name(name: *const c_char, value: *const c_char) {
     }
 }
 
+fn handle_query_onlines(onlines: Vec<String>, offlines: Vec<String>) {
+    if let Some(s) = flutter::GLOBAL_EVENT_STREAM.read().unwrap().as_ref() {
+        let data = HashMap::from([
+            ("name", "callback_query_onlines".to_owned()),
+            ("onlines", onlines.join(",")),
+            ("offlines", offlines.join(",")),
+        ]);
+        s.add(serde_json::ser::to_string(&data).unwrap_or("".to_owned()));
+    };
+}
+
+pub fn query_onlines(ids: Vec<String>) {
+    crate::rendezvous_mediator::query_online_states(ids, handle_query_onlines)
+}
+
 #[cfg(target_os = "android")]
 pub mod server_side {
     use jni::{
diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs
index a7f90b977..09500804b 100644
--- a/src/rendezvous_mediator.rs
+++ b/src/rendezvous_mediator.rs
@@ -8,6 +8,7 @@ use hbb_common::{
     protobuf::Message as _,
     rendezvous_proto::*,
     sleep, socket_client,
+    tcp::FramedStream,
     tokio::{
         self, select,
         time::{interval, Duration},
@@ -637,3 +638,139 @@ pub fn discover() -> ResultType<()> {
     config::LanPeers::store(serde_json::to_string(&peers)?);
     Ok(())
 }
+
+#[tokio::main(flavor = "current_thread")]
+pub async fn query_online_states<F: FnOnce(Vec<String>, Vec<String>)>(ids: Vec<String>, f: F) {
+    let test = false;
+    if test {
+        sleep(1.5).await;
+        let mut onlines = ids;
+        let offlines = onlines.drain((onlines.len() / 2)..).collect();
+        f(onlines, offlines)
+    } else {
+        let query_begin = Instant::now();
+        let query_timeout = std::time::Duration::from_millis(3_000);
+        loop {
+            if SHOULD_EXIT.load(Ordering::SeqCst) {
+                break;
+            }
+            match query_online_states_(&ids, query_timeout).await {
+                Ok((onlines, offlines)) => {
+                    f(onlines, offlines);
+                    break;
+                }
+                Err(e) => {
+                    log::debug!("{}", &e);
+                }
+            }
+
+            if query_begin.elapsed() > query_timeout {
+                log::debug!("query onlines timeout {:?}", query_timeout);
+                break;
+            }
+
+            sleep(1.5).await;
+        }
+    }
+}
+
+async fn create_online_stream() -> ResultType<FramedStream> {
+    let rendezvous_server = crate::get_rendezvous_server(1_000).await;
+    let tmp: Vec<&str> = rendezvous_server.split(":").collect();
+    if tmp.len() != 2 {
+        bail!("Invalid server address: {}", rendezvous_server);
+    }
+    let port: u16 = tmp[1].parse()?;
+    if port == 0 {
+        bail!("Invalid server address: {}", rendezvous_server);
+    }
+    let online_server = format!("{}:{}", tmp[0], port - 1);
+    let server_addr = socket_client::get_target_addr(&online_server)?;
+    socket_client::connect_tcp(
+        server_addr,
+        Config::get_any_listen_addr(),
+        RENDEZVOUS_TIMEOUT,
+    )
+    .await
+}
+
+async fn query_online_states_(
+    ids: &Vec<String>,
+    timeout: std::time::Duration,
+) -> ResultType<(Vec<String>, Vec<String>)> {
+    let query_begin = Instant::now();
+
+    let mut msg_out = RendezvousMessage::new();
+    msg_out.set_online_request(OnlineRequest {
+        id: Config::get_id(),
+        peers: ids.clone(),
+        ..Default::default()
+    });
+
+    loop {
+        if SHOULD_EXIT.load(Ordering::SeqCst) {
+            // No need to care about onlines
+            return Ok((Vec::new(), Vec::new()));
+        }
+
+        let mut socket = create_online_stream().await?;
+        socket.send(&msg_out).await?;
+        match socket.next_timeout(RENDEZVOUS_TIMEOUT).await {
+            Some(Ok(bytes)) => {
+                if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) {
+                    match msg_in.union {
+                        Some(rendezvous_message::Union::online_response(online_response)) => {
+                            let states = online_response.states;
+                            let mut onlines = Vec::new();
+                            let mut offlines = Vec::new();
+                            for i in 0..ids.len() {
+                                // bytes index from left to right
+                                let bit_value = 0x01 << (7 - i % 8);
+                                if (states[i / 8] & bit_value) == bit_value {
+                                    onlines.push(ids[i].clone());
+                                } else {
+                                    offlines.push(ids[i].clone());
+                                }
+                            }
+                            return Ok((onlines, offlines));
+                        }
+                        _ => {
+                            // ignore
+                        }
+                    }
+                }
+            }
+            Some(Err(e)) => {
+                log::error!("Failed to receive {e}");
+            }
+            None => {
+                // TODO: Make sure socket closed?
+                bail!("Online stream receives None");
+            }
+        }
+
+        if query_begin.elapsed() > timeout {
+            bail!("Try query onlines timeout {:?}", &timeout);
+        }
+
+        sleep(300.0).await;
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    #[test]
+    fn test_query_onlines() {
+        super::query_online_states(
+            vec![
+                "152183996".to_owned(),
+                "165782066".to_owned(),
+                "155323351".to_owned(),
+                "460952777".to_owned(),
+            ],
+            |onlines: Vec<String>, offlines: Vec<String>| {
+                println!("onlines: {:?}, offlines: {:?}", &onlines, &offlines);
+            },
+        );
+    }
+}