diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart new file mode 100644 index 000000000..4fb65c2e3 --- /dev/null +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -0,0 +1,355 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/mobile/pages/file_manager_page.dart'; +import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'dart:async'; +import '../../common.dart'; +import '../../models/model.dart'; +import '../../mobile/pages/home_page.dart'; +import '../../mobile/pages/remote_page.dart'; +import '../../mobile/pages/settings_page.dart'; +import '../../mobile/pages/scan_page.dart'; +import '../../models/server_model.dart'; + +/// Connection page for connecting to a remote peer. +class ConnectionPage extends StatefulWidget implements PageShape { + ConnectionPage({Key? key}) : super(key: key); + + @override + final icon = Icon(Icons.connected_tv); + + @override + final title = translate("Connection"); + + @override + final appBarActions = !isAndroid ? [WebMenu()] : []; + + @override + _ConnectionPageState createState() => _ConnectionPageState(); +} + +/// State for the connection page. +class _ConnectionPageState extends State { + /// Controller for the id input bar. + final _idController = TextEditingController(); + + /// Update url. If it's not null, means an update is available. + var _updateUrl = ''; + var _menuPos; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + Provider.of(context); + if (_idController.text.isEmpty) _idController.text = FFI.getId(); + FFI.serverModel.startService(); + return SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + getUpdateUI(), + getSearchBarUI(), + Container(height: 12), + getPeers(), + ]), + ); + } + + /// Callback for the connect button. + /// Connects to the selected peer. + void onConnect() { + var id = _idController.text.trim(); + connect(id); + } + + /// 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) { + if (!await PermissionManager.check("file")) { + if (!await PermissionManager.request("file")) { + return; + } + } + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => FileManagerPage(id: id), + ), + ); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => RemotePage(id: id), + ), + ); + } + FocusScopeNode currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) { + currentFocus.unfocus(); + } + } + + /// UI for software update. + /// If [_updateUrl] is not empty, shows a button to update the software. + Widget getUpdateUI() { + 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)))); + } + + /// UI for the search bar. + /// Search for a peer and connect to it if the id exists. + Widget getSearchBarUI() { + var w = Padding( + padding: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 0.0), + child: Container( + height: 84, + child: Padding( + padding: const EdgeInsets.only(top: 8, bottom: 8), + child: Ink( + decoration: BoxDecoration( + color: MyTheme.white, + borderRadius: const BorderRadius.all(Radius.circular(13)), + ), + child: Row( + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.only(left: 16, right: 16), + child: TextField( + autocorrect: false, + enableSuggestions: false, + keyboardType: TextInputType.visiblePassword, + // keyboardType: TextInputType.number, + style: TextStyle( + fontFamily: 'WorkSans', + fontWeight: FontWeight.bold, + fontSize: 30, + color: MyTheme.idColor, + ), + decoration: InputDecoration( + labelText: translate('Remote ID'), + // hintText: 'Enter your remote ID', + border: InputBorder.none, + helperStyle: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: MyTheme.darkGray, + ), + labelStyle: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + letterSpacing: 0.2, + color: MyTheme.darkGray, + ), + ), + controller: _idController, + ), + ), + ), + SizedBox( + width: 60, + height: 60, + child: IconButton( + icon: Icon(Icons.arrow_forward, + color: MyTheme.darkGray, size: 45), + onPressed: onConnect, + ), + ), + ], + ), + ), + ), + ), + ); + return Center( + child: Container(constraints: BoxConstraints(maxWidth: 600), child: w)); + } + + @override + void dispose() { + _idController.dispose(); + super.dispose(); + } + + /// 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', width: 24, height: 24); + } + + /// Get all the saved peers. + Widget getPeers() { + final size = MediaQuery.of(context).size; + final space = 8.0; + var width = size.width - 2 * space; + final minWidth = 320.0; + if (size.width > minWidth + 2 * space) { + final n = (size.width / (minWidth + 2 * space)).floor(); + width = size.width / n - 2 * space; + } + final cards = []; + var peers = FFI.peers(); + peers.forEach((p) { + cards.add(Container( + width: width, + child: Card( + 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: ListTile( + contentPadding: const EdgeInsets.only(left: 12), + subtitle: Text('${p.username}@${p.hostname}'), + title: Text('${p.id}'), + leading: Container( + padding: const EdgeInsets.all(6), + child: getPlatformImage('${p.platform}'), + color: str2color('${p.id}${p.platform}', 0x7f)), + trailing: InkWell( + child: Padding( + padding: const EdgeInsets.all(12), + 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); + }), + ))))); + }); + return 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 { + var value = await showMenu( + context: context, + position: this._menuPos, + items: [ + PopupMenuItem( + child: Text(translate('Remove')), value: 'remove') + ] + + (!isAndroid + ? [] + : [ + PopupMenuItem( + child: Text(translate('File transfer')), value: 'file') + ]), + elevation: 8, + ); + if (value == 'remove') { + setState(() => FFI.setByName('remove', '$id')); + () async { + removePreference(id); + }(); + } else if (value == 'file') { + connect(id, isFileTransfer: true); + } + } +} + +class WebMenu extends StatefulWidget { + @override + _WebMenuState createState() => _WebMenuState(); +} + +class _WebMenuState extends State { + @override + Widget build(BuildContext context) { + Provider.of(context); + final username = getUsername(); + return PopupMenuButton( + icon: Icon(Icons.more_vert), + itemBuilder: (context) { + return (isIOS + ? [ + PopupMenuItem( + child: Icon(Icons.qr_code_scanner, color: Colors.black), + value: "scan", + ) + ] + : >[]) + + [ + PopupMenuItem( + child: Text(translate('ID/Relay Server')), + value: "server", + ) + ] + + (getUrl().contains('admin.rustdesk.com') + ? >[] + : [ + PopupMenuItem( + child: Text(username == null + ? translate("Login") + : translate("Logout") + ' ($username)'), + value: "login", + ) + ]) + + [ + PopupMenuItem( + child: Text(translate('About') + ' RustDesk'), + value: "about", + ) + ]; + }, + onSelected: (value) { + if (value == 'server') { + showServerSettings(); + } + if (value == 'about') { + showAbout(); + } + if (value == 'login') { + if (username == null) { + showLogin(); + } else { + logout(); + } + } + if (value == 'scan') { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => ScanPage(), + ), + ); + } + }); + } +} diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 00b071ec5..467f85cc1 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; -import 'package:flutter_hbb/mobile/pages/connection_page.dart'; +import 'package:flutter_hbb/desktop/pages/connection_page.dart'; import 'package:flutter_hbb/models/model.dart'; import 'package:provider/provider.dart'; @@ -76,6 +76,14 @@ class _DesktopHomePageState extends State { TextFormField( controller: model.serverId, ), + Text( + translate("Password"), + style: + TextStyle(fontSize: 18, fontWeight: FontWeight.w500), + ), + TextField( + controller: model.serverPasswd, + ) ], ), ), diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index f9135a06c..a8803a8f8 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -102,7 +102,7 @@ class PlatformFFI { name = '${androidInfo.brand}-${androidInfo.model}'; id = androidInfo.id.hashCode.toString(); androidVersion = androidInfo.version.sdkInt; - } else { + } else if (Platform.isIOS) { IosDeviceInfo iosInfo = await deviceInfo.iosInfo; name = iosInfo.utsname.machine; id = iosInfo.identifierForVendor.hashCode.toString(); diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 681ff3c25..68d3d2391 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -150,6 +150,7 @@ class ServerModel with ChangeNotifier { } } + /// Toggle the screen sharing service. toggleService() async { if (_isStart) { final res = @@ -198,6 +199,7 @@ class ServerModel with ChangeNotifier { } } + /// Start the screen sharing service. Future startService() async { _isStart = true; notifyListeners(); @@ -212,6 +214,7 @@ class ServerModel with ChangeNotifier { } } + /// Stop the screen sharing service. Future stopService() async { _isStart = false; FFI.serverModel.closeAll(); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 98fac8242..8344cae99 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,6 +1,7 @@ use crate::client::file_trait::FileManager; use crate::flutter::connection_manager::{self, get_clients_length, get_clients_state}; use crate::flutter::{self, make_fd_to_json, Session}; +use crate::start_server; use crate::ui_interface; use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; use hbb_common::ResultType; @@ -49,7 +50,7 @@ pub fn start_rgba_stream(s: StreamSink>>) -> ResultType<( /// FFI for **get** commands which are idempotent. /// Return result in c string. -/// +/// /// # Arguments /// /// * `name` - name of the command @@ -515,10 +516,9 @@ unsafe extern "C" fn set_by_name(name: *const c_char, value: *const c_char) { Config::set_option("stop-service".into(), "Y".into()); crate::rendezvous_mediator::RendezvousMediator::restart(); } - #[cfg(target_os = "android")] "start_service" => { Config::set_option("stop-service".into(), "".into()); - crate::rendezvous_mediator::RendezvousMediator::restart(); + start_server(false); } #[cfg(target_os = "android")] "close_conn" => { diff --git a/src/server.rs b/src/server.rs index f4758e3fb..0782c7231 100644 --- a/src/server.rs +++ b/src/server.rs @@ -287,12 +287,26 @@ pub fn check_zombie() { }); } +/// Start the host server that allows the remote peer to control the current machine. +/// +/// # Arguments +/// +/// * `is_server` - Whether the current client is definitely the server. +/// If true, the server will be started. +/// Otherwise, client will check if there's already a server and start one if not. #[cfg(any(target_os = "android", target_os = "ios"))] #[tokio::main] pub async fn start_server(is_server: bool) { crate::RendezvousMediator::start_all().await; } +/// Start the host server that allows the remote peer to control the current machine. +/// +/// # Arguments +/// +/// * `is_server` - Whether the current client is definitely the server. +/// If true, the server will be started. +/// Otherwise, client will check if there's already a server and start one if not. #[cfg(not(any(target_os = "android", target_os = "ios")))] #[tokio::main] pub async fn start_server(is_server: bool) {