From 4cfa84082223df106e0b137dba86be93fa140b6b Mon Sep 17 00:00:00 2001 From: kingtous Date: Mon, 25 Jul 2022 16:23:45 +0800 Subject: [PATCH] add: address book ui&getAb Signed-off-by: Kingtous --- flutter/lib/common.dart | 2 + .../lib/desktop/pages/connection_page.dart | 524 +++++++++++++----- flutter/lib/models/model.dart | 19 +- src/flutter_ffi.rs | 49 +- src/ipc.rs | 21 +- src/ui_interface.rs | 2 +- 6 files changed, 467 insertions(+), 150 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index e1315d233..b896fdf9f 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -325,4 +325,6 @@ Future initGlobalFFI() async { // after `put`, can also be globally found by Get.find(); Get.put(_globalFFI, permanent: true); await _globalFFI.ffiModel.init(); + // trigger connection status updater + await _globalFFI.bind.mainCheckConnectStatus(); } \ No newline at end of file diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 1e2939284..e29ab9b5f 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; @@ -5,6 +6,7 @@ import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:url_launcher/url_launcher_string.dart'; import '../../common.dart'; import '../../mobile/pages/home_page.dart'; @@ -12,11 +14,7 @@ import '../../mobile/pages/scan_page.dart'; import '../../mobile/pages/settings_page.dart'; import '../../models/model.dart'; -enum RemoteType { - recently, - favorite, - discovered -} +enum RemoteType { recently, favorite, discovered, addressBook } /// Connection page for connecting to a remote peer. class ConnectionPage extends StatefulWidget implements PageShape { @@ -44,18 +42,22 @@ class _ConnectionPageState extends State { var _updateUrl = ''; var _menuPos; + Timer? _updateTimer; + @override void initState() { super.initState(); + _updateTimer = Timer.periodic(Duration(seconds: 1), (timer) { + updateStatus(); + }); } @override Widget build(BuildContext context) { - Provider.of(context); if (_idController.text.isEmpty) _idController.text = gFFI.getId(); return Container( decoration: BoxDecoration( - color: MyTheme.grayBg + color: MyTheme.grayBg ), child: Column( mainAxisAlignment: MainAxisAlignment.start, @@ -77,25 +79,17 @@ class _ConnectionPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ TabBar( - labelColor: Colors.black87, - isScrollable: true, + labelColor: Colors.black87, + 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(future: getPeers(rType: RemoteType.recently), - builder: (context, snapshot){ - if (snapshot.hasData) { - return snapshot.data!; - } else { - return Offstage(); - } - }), - FutureBuilder(future: getPeers(rType: RemoteType.favorite), builder: (context, snapshot){ if (snapshot.hasData) { return snapshot.data!; @@ -103,12 +97,40 @@ class _ConnectionPageState extends State { return Offstage(); } }), - Container(), - Container(), - ]).paddingSymmetric(horizontal: 12.0,vertical: 4.0)) + FutureBuilder( + future: getPeers(rType: RemoteType.favorite), + builder: (context, snapshot) { + if (snapshot.hasData) { + return snapshot.data!; + } else { + return Offstage(); + } + }), + FutureBuilder( + future: getPeers(rType: RemoteType.discovered), + builder: (context, snapshot) { + if (snapshot.hasData) { + return snapshot.data!; + } else { + return Offstage(); + } + }), + FutureBuilder( + future: buildAddressBook(context), + builder: (context, snapshot) { + if (snapshot.hasData) { + return snapshot.data!; + } else { + return Offstage(); + } + }), + ]).paddingSymmetric(horizontal: 12.0, vertical: 4.0)) ], )), ), + Divider(), + SizedBox(height: 50, child: Obx(() => buildStatus())) + .paddingSymmetric(horizontal: 12.0) ]), ); } @@ -142,20 +164,20 @@ class _ConnectionPageState extends State { 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. @@ -267,6 +289,7 @@ class _ConnectionPageState extends State { @override void dispose() { _idController.dispose(); + _updateTimer?.cancel(); super.dispose(); } @@ -281,7 +304,6 @@ class _ConnectionPageState extends State { /// Get all the saved peers. Future getPeers({RemoteType rType = RemoteType.recently}) async { - final size = MediaQuery.of(context).size; final space = 8.0; final cards = []; var peers; @@ -305,104 +327,145 @@ class _ConnectionPageState extends State { }); break; case RemoteType.discovered: - // TODO: Handle this case. - peers = await gFFI.bind.mainGetLanPeers().then((peers_string){ - + peers = await gFFI.bind.mainGetLanPeers().then((peers_string) { + print(peers_string); + return []; }); break; + case RemoteType.addressBook: + await gFFI.abModel.getAb(); + peers = gFFI.abModel.peers.map((e) { + return Peer.fromJson(e['id'], e); + }).toList(); + break; } peers.forEach((p) { + var deco = Rx(BoxDecoration( + border: Border.all(color: Colors.transparent, width: 1.0), + borderRadius: BorderRadius.circular(20))); cards.add(Container( - width: 250, + width: 225, height: 150, child: Card( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), - child: GestureDetector( - onTap: !isWebDesktop ? () => connect('${p.id}') : null, - onDoubleTap: isWebDesktop ? () => connect('${p.id}') : null, - onLongPressStart: (details) { - final x = details.globalPosition.dx; - final y = details.globalPosition.dy; - _menuPos = RelativeRect.fromLTRB(x, y, x, y); - showPeerMenu(context, p.id); - }, - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: Container( - decoration: BoxDecoration( - color: str2color('${p.id}${p.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('${p.platform}'),), - Row( - children: [ - Expanded( - child: Text('${p.username}@${p.hostname}', style: TextStyle( - color: Colors.white70, - fontSize: 12 - ),textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ], - ).paddingAll(4.0), + 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: Obx( + () => Container( + decoration: deco.value, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + color: str2color('${p.id}${p.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('${p.platform}'), + ), + Row( + children: [ + Expanded( + child: Tooltip( + message: + '${p.username}@${p.hostname}', + child: Text( + '${p.username}@${p.hostname}', + style: TextStyle( + color: Colors.white70, + fontSize: 12), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ), + ], + ).paddingAll(4.0), + ), + ], + ), ), ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text("${p.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, p.id); - }), - ], - ).paddingSymmetric(vertical: 8.0,horizontal: 12.0) - ], - ))))); + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("${p.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, p.id, rType); + }), + ], + ).paddingSymmetric(vertical: 8.0, horizontal: 12.0) + ], + ), + ), + ), + )))); }); - return SingleChildScrollView(child: Wrap(children: cards, spacing: space, runSpacing: space)); + return SingleChildScrollView( + child: Wrap(children: cards, spacing: space, runSpacing: space)); } /// 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 { + void showPeerMenu(BuildContext context, String id, RemoteType rType) async { + var items = [ + PopupMenuItem( + child: Text(translate('Connect')), value: 'connect'), + PopupMenuItem( + child: Text(translate('Transfer File')), value: 'file'), + PopupMenuItem( + child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'), + PopupMenuItem(child: Text(translate('Rename')), value: 'rename'), + PopupMenuItem(child: Text(translate('Remove')), value: 'remove'), + PopupMenuItem( + child: Text(translate('Unremember Password')), + value: 'unremember-password'), + ]; + if (rType == RemoteType.favorite) { + items.add(PopupMenuItem( + child: Text(translate('Remove from Favorites')), + value: 'remove-fav')); + } else + items.add(PopupMenuItem( + child: Text(translate('Add to Favorites')), value: 'add-fav')); var value = await showMenu( context: context, position: this._menuPos, - items: [ - PopupMenuItem( - child: Text(translate('Remove')), value: 'remove') - ] + - ([ - PopupMenuItem( - child: Text(translate('Transfer File')), value: 'file') - ]), + items: items, elevation: 8, ); if (value == 'remove') { @@ -412,7 +475,200 @@ class _ConnectionPageState extends State { }(); } else if (value == 'file') { connect(id, isFileTransfer: true); + } else if (value == 'add-fav') {} + } + + var svcStopped = false.obs; + var svcStatusCode = 0.obs; + var svcIsUsingPublicServer = true.obs; + + Widget buildStatus() { + final light = Container( + height: 8, + width: 8, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Colors.green, + ), + ).paddingSymmetric(horizontal: 8.0); + if (svcStopped.value) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [light, Text(translate("Service is not running"))], + ); + } else { + if (svcStatusCode.value == 0) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [light, Text(translate("connecting_status"))], + ); + } else if (svcStatusCode.value == -1) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [light, Text(translate("not_ready_status"))], + ); + } } + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + light, + Text("${translate('Ready')}"), + svcIsUsingPublicServer.value + ? InkWell( + onTap: onUsePublicServerGuide, + child: Text( + ', ${translate('setup_server_tip')}', + style: TextStyle(decoration: TextDecoration.underline), + ), + ) + : Offstage() + ], + ); + } + + void onUsePublicServerGuide() { + final url = "https://rustdesk.com/blog/id-relay-set/"; + canLaunchUrlString(url).then((can) { + if (can) { + launchUrlString(url); + } + }); + } + + updateStatus() async { + svcStopped.value = gFFI.getOption("stop-service") == "Y"; + final status = jsonDecode(await gFFI.bind.mainGetConnectStatus()) + as Map; + svcStatusCode.value = status["status_num"]; + svcIsUsingPublicServer.value = await gFFI.bind.mainIsUsingPublicServer(); + } + + handleLogin() {} + + Future buildAddressBook(BuildContext context) async { + final token = await gFFI.getLocalOption('access_token'); + if (token.trim().isEmpty) { + return Center( + child: InkWell( + onTap: handleLogin, + child: Text( + translate("Login"), + style: TextStyle(decoration: TextDecoration.underline), + ), + ), + ); + } + final model = gFFI.abModel; + return FutureBuilder( + future: model.getAb(), + builder: (context, snapshot) { + if (snapshot.hasData) { + return _buildAddressBook(context); + } else { + if (model.abLoading) { + return Center( + child: CircularProgressIndicator(), + ); + } else if (model.abError.isNotEmpty) { + return Center( + child: CircularProgressIndicator(), + ); + } else { + return Offstage(); + } + } + }); + } + + Widget _buildAddressBook(BuildContext context) { + return Row( + children: [ + Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: BorderSide(color: MyTheme.grayBg)), + color: Colors.white, + child: Container( + width: 200, + height: double.infinity, + padding: EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(translate('Tags')), + InkWell( + child: PopupMenuButton( + itemBuilder: (context) => [], + child: Icon(Icons.more_vert_outlined)), + ) + ], + ), + Expanded( + child: Container( + width: double.infinity, + height: double.infinity, + decoration: BoxDecoration( + border: Border.all(color: MyTheme.darkGray)), + child: Wrap( + children: + gFFI.abModel.tags.map((e) => buildTag(e)).toList(), + ), + ).marginSymmetric(vertical: 8.0), + ) + ], + ), + ), + ).marginOnly(right: 8.0), + Column( + children: [ + FutureBuilder( + future: getPeers(rType: RemoteType.addressBook), + builder: (context, snapshot) { + if (snapshot.hasData) { + return snapshot.data!; + } else { + return Center(child: CircularProgressIndicator()); + } + }), + ], + ) + ], + ); + } + + Widget buildTag(String tagName) { + return Container( + decoration: BoxDecoration( + 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), + ); + } +} + +class AddressBookPage extends StatefulWidget { + const AddressBookPage({Key? key}) : super(key: key); + + @override + State createState() => _AddressBookPageState(); +} + +class _AddressBookPageState extends State { + @override + void initState() { + // TODO: implement initState + final ab = gFFI.abModel.getAb(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Container(); } } @@ -430,13 +686,13 @@ class _WebMenuState extends State { icon: Icon(Icons.more_vert), itemBuilder: (context) { return (isIOS - ? [ - PopupMenuItem( - child: Icon(Icons.qr_code_scanner, color: Colors.black), - value: "scan", - ) - ] - : >[]) + + ? [ + PopupMenuItem( + child: Icon(Icons.qr_code_scanner, color: Colors.black), + value: "scan", + ) + ] + : >[]) + [ PopupMenuItem( child: Text(translate('ID/Relay Server')), @@ -446,13 +702,13 @@ class _WebMenuState extends State { (getUrl().contains('admin.rustdesk.com') ? >[] : [ - 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/models/model.dart b/flutter/lib/models/model.dart index 9b0b7930a..c326dbc30 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -8,6 +8,7 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/generated_bridge.dart'; +import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/file_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; @@ -809,6 +810,7 @@ class FFI { late final ServerModel serverModel; late final ChatModel chatModel; late final FileModel fileModel; + late final AbModel abModel; FFI() { this.imageModel = ImageModel(WeakReference(this)); @@ -818,6 +820,7 @@ class FFI { this.serverModel = ServerModel(WeakReference(this)); // use global FFI this.chatModel = ChatModel(WeakReference(this)); this.fileModel = FileModel(WeakReference(this)); + this.abModel = AbModel(WeakReference(this)); } static FFI newFFI() { @@ -995,9 +998,17 @@ class FFI { return ffiModel.platformFFI.getByName("option", name); } + Future getLocalOption(String name) { + return bind.mainGetLocalOption(key: name); + } + + Future setLocalOption(String key, String value) { + return bind.mainSetLocalOption(key: key, value: value); + } + void setOption(String name, String value) { Map res = Map() - ..["name"] = name + ..["name"] = name ..["value"] = value; return ffiModel.platformFFI.setByName('option', jsonEncode(res)); } @@ -1087,9 +1098,13 @@ class FFI { return input; } - void setDefaultAudioInput(String input){ + void setDefaultAudioInput(String input) { setOption('audio-input', input); } + + Future> getHttpHeaders() async { + return {"Authorization": "Bearer " + await getLocalOption("access_token")}; + } } class Peer { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index edd368509..d38dd9529 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -5,7 +5,7 @@ use std::{ }; use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; -use serde_json::{Number, Value}; +use serde_json::{json, Number, Value}; use hbb_common::ResultType; use hbb_common::{ @@ -20,9 +20,11 @@ use crate::flutter::{self, Session, SESSIONS}; use crate::start_server; use crate::ui_interface; use crate::ui_interface::{ - change_id, get_app_name, get_async_job_status, get_fav, get_lan_peers, get_license, - get_options, get_peer, get_socks, get_sound_inputs, get_version, is_ok_change_id, set_options, - set_socks, store_fav, test_if_valid_server, + change_id, check_connect_status, get_api_server, get_app_name, get_async_job_status, + get_connect_status, get_fav, get_lan_peers, get_license, get_local_option, get_options, + get_peer, get_socks, get_sound_inputs, get_version, has_rendezvous_service, is_ok_change_id, + post_request, set_local_option, set_options, set_socks, store_fav, test_if_valid_server, + using_public_server, }; fn initialize(app_dir: &str) { @@ -447,6 +449,45 @@ pub fn main_get_lan_peers() -> String { get_lan_peers() } +pub fn main_get_connect_status() -> String { + let status = get_connect_status(); + // (status_num, key_confirmed, mouse_time, id) + let mut m = serde_json::Map::new(); + m.insert("status_num".to_string(), json!(status.0)); + m.insert("key_confirmed".to_string(), json!(status.1)); + m.insert("mouse_time".to_string(), json!(status.2)); + m.insert("id".to_string(), json!(status.3)); + serde_json::to_string(&m).unwrap_or("".to_string()) +} + +pub fn main_check_connect_status() { + check_connect_status(true); +} + +pub fn main_is_using_public_server() -> bool { + using_public_server() +} + +pub fn main_has_rendezvous_service() -> bool { + has_rendezvous_service() +} + +pub fn main_get_api_server() -> String { + get_api_server() +} + +pub fn main_post_request(url: String, body: String, header: String) { + post_request(url, body, header) +} + +pub fn main_get_local_option(key: String) -> String { + get_local_option(key) +} + +pub fn main_set_local_option(key: String, value: String) { + set_local_option(key, value) +} + /// FFI for **get** commands which are idempotent. /// Return result in c string. /// diff --git a/src/ipc.rs b/src/ipc.rs index 5eabbab66..c20864700 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -1,4 +1,12 @@ -use crate::rendezvous_mediator::RendezvousMediator; +use std::{collections::HashMap, sync::atomic::Ordering}; +#[cfg(not(windows))] +use std::{fs::File, io::prelude::*}; + +use parity_tokio_ipc::{ + Connection as Conn, ConnectionClient as ConnClient, Endpoint, Incoming, SecurityAttributes, +}; +use serde_derive::{Deserialize, Serialize}; + #[cfg(not(any(target_os = "android", target_os = "ios")))] pub use clipboard::ClipbaordFile; use hbb_common::{ @@ -12,13 +20,8 @@ use hbb_common::{ tokio_util::codec::Framed, ResultType, }; -use parity_tokio_ipc::{ - Connection as Conn, ConnectionClient as ConnClient, Endpoint, Incoming, SecurityAttributes, -}; -use serde_derive::{Deserialize, Serialize}; -use std::{collections::HashMap, sync::atomic::Ordering}; -#[cfg(not(windows))] -use std::{fs::File, io::prelude::*}; + +use crate::rendezvous_mediator::RendezvousMediator; // State with timestamp, because std::time::Instant cannot be serialized #[derive(Debug, Serialize, Deserialize, Copy, Clone)] @@ -73,7 +76,7 @@ pub enum FS { WriteOffset { id: i32, file_num: i32, - offset_blk: u32 + offset_blk: u32, }, CheckDigest { id: i32, diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 7eaf938d1..86b4e9e9a 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -630,7 +630,7 @@ pub fn check_zombie(childs: Childs) { } } -fn check_connect_status(reconnect: bool) -> mpsc::UnboundedSender { +pub(crate) fn check_connect_status(reconnect: bool) -> mpsc::UnboundedSender { let (tx, rx) = mpsc::unbounded_channel::(); std::thread::spawn(move || check_connect_status_(reconnect, rx)); tx