From a5f647196c2b64e55d1c36ab1a0a978c1b56218d Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 6 Nov 2022 21:21:57 +0800 Subject: [PATCH 01/27] add missing cm close window handler Signed-off-by: 21pages --- flutter/lib/desktop/pages/server_page.dart | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 884ac6441..e344575c7 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -134,6 +134,7 @@ class ConnectionManagerState extends State { showMaximize: false, showMinimize: true, showClose: true, + onWindowCloseButton: handleWindowCloseButton, controller: serverModel.tabController, maxLabelWidth: 100, tail: buildScrollJumper(), @@ -206,6 +207,27 @@ class ConnectionManagerState extends State { ], )); } + + Future handleWindowCloseButton() async { + var tabController = gFFI.serverModel.tabController; + final connLength = tabController.length; + if (connLength <= 1) { + windowManager.close(); + return true; + } else { + final opt = "enable-confirm-closing-tabs"; + final bool res; + if (!option2bool(opt, await bind.mainGetOption(key: opt))) { + res = true; + } else { + res = await closeConfirmDialog(); + } + if (res) { + windowManager.close(); + } + return res; + } + } } Widget buildConnectionCard(Client client) { From 3454454bd569c7b993bfd55b8f92ad4536edf9e5 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 19 Oct 2022 22:48:51 +0800 Subject: [PATCH 02/27] account oidc init rs Signed-off-by: fufesou --- Cargo.lock | 2 + Cargo.toml | 1 + src/hbbs_http.rs | 42 +++++++ src/hbbs_http/account.rs | 242 +++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 + 5 files changed, 289 insertions(+) create mode 100644 src/hbbs_http.rs create mode 100644 src/hbbs_http/account.rs diff --git a/Cargo.lock b/Cargo.lock index 26ed9455f..3fddb5c37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4419,6 +4419,7 @@ dependencies = [ "system_shutdown", "tray-item", "trayicon", + "url", "uuid", "virtual_display", "whoami", @@ -5492,6 +5493,7 @@ dependencies = [ "idna", "matches", "percent-encoding", + "serde 1.0.144", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c950c8723..9516d8526 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,6 +64,7 @@ wol-rs = "0.9.1" flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge", optional = true } errno = "0.2.8" rdev = { git = "https://github.com/asur4s/rdev" } +url = { version = "2.1", features = ["serde"] } [target.'cfg(not(target_os = "linux"))'.dependencies] reqwest = { version = "0.11", features = ["json", "rustls-tls"], default-features=false } diff --git a/src/hbbs_http.rs b/src/hbbs_http.rs new file mode 100644 index 000000000..b0e8cdbab --- /dev/null +++ b/src/hbbs_http.rs @@ -0,0 +1,42 @@ +use hbb_common::{ + anyhow::{self, bail}, + tokio, ResultType, +}; +use reqwest::Response; +use serde_derive::Deserialize; +use serde_json::{Map, Value}; +use serde::de::DeserializeOwned; + +pub mod account; + +pub enum HbbHttpResponse { + ErrorFormat, + Error(String), + DataTypeFormat, + Data(T), +} + +#[tokio::main(flavor = "current_thread")] +async fn resp_to_serde_map(resp: Response) -> reqwest::Result> { + resp.json().await +} + +impl TryFrom for HbbHttpResponse { + type Error = reqwest::Error; + + fn try_from(resp: Response) -> Result>::Error> { + let map = resp_to_serde_map(resp)?; + if let Some(error) = map.get("error") { + if let Some(err) = error.as_str() { + Ok(Self::Error(err.to_owned())) + } else { + Ok(Self::ErrorFormat) + } + } else { + match serde_json::from_value(Value::Object(map)) { + Ok(v) => Ok(Self::Data(v)), + Err(_) => Ok(Self::DataTypeFormat), + } + } + } +} diff --git a/src/hbbs_http/account.rs b/src/hbbs_http/account.rs new file mode 100644 index 000000000..85b2f7e82 --- /dev/null +++ b/src/hbbs_http/account.rs @@ -0,0 +1,242 @@ +use super::HbbHttpResponse; +use hbb_common::{config::Config, log, sleep, tokio, tokio::sync::RwLock, ResultType}; +use serde_derive::Deserialize; +use std::{ + collections::HashMap, + sync::Arc, + time::{Duration, Instant}, +}; +use url::Url; + +lazy_static::lazy_static! { + static ref API_SERVER: String = crate::get_api_server( + Config::get_option("api-server"), Config::get_option("custom-rendezvous-server")); + static ref OIDC_SESSION: Arc> = Arc::new(RwLock::new(OidcSession::new())); +} + +const QUERY_INTERVAL_SECS: f32 = 1.0; +const QUERY_TIMEOUT_SECS: u64 = 60; + +#[derive(Deserialize, Clone)] +pub struct OidcAuthUrl { + code: String, + url: Url, +} + +#[derive(Debug, Deserialize, Default, Clone)] +pub struct UserPayload { + pub id: String, + pub name: String, + pub email: Option, + pub note: Option, + pub status: Option, + pub grp: Option, + pub is_admin: Option, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct AuthBody { + access_token: String, + token_type: String, + user: UserPayload, +} + +#[derive(Copy, Clone)] +pub enum OidcState { + // initial request + OidcRequest = 1, + // initial request failed + OidcRequestFailed = 2, + // request succeeded, loop querying + OidcQuerying = 11, + // loop querying failed + OidcQueryFailed = 12, + // query sucess before + OidcNotExists = 13, + // query timeout + OidcQueryTimeout = 14, + // already login + OidcLogin = 21, +} + +pub struct OidcSession { + client: reqwest::Client, + state: OidcState, + failed_msg: String, + code_url: Option, + auth_body: Option, + keep_querying: bool, + running: bool, + query_timeout: Duration, +} + +impl OidcSession { + fn new() -> Self { + Self { + client: reqwest::Client::new(), + state: OidcState::OidcRequest, + failed_msg: "".to_owned(), + code_url: None, + auth_body: None, + keep_querying: false, + running: false, + query_timeout: Duration::from_secs(QUERY_TIMEOUT_SECS), + } + } + + async fn auth(op: &str, id: &str, uuid: &str) -> ResultType> { + Ok(OIDC_SESSION + .read() + .await + .client + .post(format!("{}/api/oidc/auth", *API_SERVER)) + .json(&HashMap::from([("op", op), ("id", id), ("uuid", uuid)])) + .send() + .await? + .try_into()?) + } + + async fn query(code: &str, id: &str, uuid: &str) -> ResultType> { + let url = reqwest::Url::parse_with_params( + &format!("{}/api/oidc/auth-query", *API_SERVER), + &[("code", code), ("id", id), ("uuid", uuid)], + )?; + Ok(OIDC_SESSION + .read() + .await + .client + .get(url) + .send() + .await? + .try_into()?) + } + + fn reset(&mut self) { + self.state = OidcState::OidcRequest; + self.failed_msg = "".to_owned(); + self.keep_querying = true; + self.running = false; + self.code_url = None; + self.auth_body = None; + } + + async fn before_task(&mut self) { + self.reset(); + self.running = true; + } + + async fn after_task(&mut self) { + self.running = false; + } + + async fn auth_task(op: String, id: String, uuid: String) { + let code_url = match Self::auth(&op, &id, &uuid).await { + Ok(HbbHttpResponse::<_>::Data(code_url)) => code_url, + Ok(HbbHttpResponse::<_>::Error(err)) => { + OIDC_SESSION + .write() + .await + .set_state(OidcState::OidcRequestFailed, err); + return; + } + Ok(_) => { + OIDC_SESSION.write().await.set_state( + OidcState::OidcRequestFailed, + "Invalid auth response".to_owned(), + ); + return; + } + Err(err) => { + OIDC_SESSION + .write() + .await + .set_state(OidcState::OidcRequestFailed, err.to_string()); + return; + } + }; + + OIDC_SESSION + .write() + .await + .set_state(OidcState::OidcQuerying, "".to_owned()); + OIDC_SESSION.write().await.code_url = Some(code_url.clone()); + + let begin = Instant::now(); + let query_timeout = OIDC_SESSION.read().await.query_timeout; + while OIDC_SESSION.read().await.keep_querying && begin.elapsed() < query_timeout { + match Self::query(&code_url.code, &id, &uuid).await { + Ok(HbbHttpResponse::<_>::Data(auth_body)) => { + OIDC_SESSION + .write() + .await + .set_state(OidcState::OidcLogin, "".to_owned()); + OIDC_SESSION.write().await.auth_body = Some(auth_body); + return; + // to-do, set access-token + } + Ok(HbbHttpResponse::<_>::Error(err)) => { + if err.contains("No authed oidc is found") { + // ignore, keep querying + } else { + OIDC_SESSION + .write() + .await + .set_state(OidcState::OidcQueryFailed, err); + return; + } + } + Ok(_) => { + // ignore + } + Err(err) => { + log::trace!("Failed query oidc {}", err); + // ignore + } + } + sleep(QUERY_INTERVAL_SECS).await; + } + + if begin.elapsed() >= query_timeout { + OIDC_SESSION + .write() + .await + .set_state(OidcState::OidcQueryTimeout, "timeout".to_owned()); + } + + // no need to handle "keep_querying == false" + } + + fn set_state(&mut self, state: OidcState, failed_msg: String) { + self.state = state; + self.failed_msg = failed_msg; + } + + pub async fn account_auth(op: String, id: String, uuid: String) { + if OIDC_SESSION.read().await.running { + OIDC_SESSION.write().await.keep_querying = false; + } + let wait_secs = 0.3; + sleep(wait_secs).await; + while OIDC_SESSION.read().await.running { + sleep(wait_secs).await; + } + + tokio::spawn(async move { + OIDC_SESSION.write().await.before_task().await; + Self::auth_task(op, id, uuid).await; + OIDC_SESSION.write().await.after_task().await; + }); + } + + fn get_result_(&self) -> (u8, String, Option) { + ( + self.state as u8, + self.failed_msg.clone(), + self.auth_body.clone(), + ) + } + + pub async fn get_result() -> (u8, String, Option) { + OIDC_SESSION.read().await.get_result_() + } +} diff --git a/src/lib.rs b/src/lib.rs index 58dc50b04..eb8a876ec 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,6 +48,8 @@ mod ui_cm_interface; mod ui_interface; mod ui_session_interface; +mod hbbs_http; + #[cfg(windows)] pub mod clipboard_file; From 87e53501e3bbaed4c3a9475b70500f1357463ad2 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 20 Oct 2022 23:03:54 +0800 Subject: [PATCH 03/27] feat_account: mid commit Signed-off-by: fufesou --- flutter/lib/common/widgets/address_book.dart | 1 + .../lib/desktop/pages/desktop_home_page.dart | 2 +- .../desktop/pages/desktop_setting_page.dart | 1 + flutter/lib/desktop/widgets/login.dart | 469 ++++++++++++++++++ flutter/lib/models/user_model.dart | 4 +- src/flutter_ffi.rs | 10 + src/hbbs_http/account.rs | 86 ++-- src/ui_interface.rs | 13 +- 8 files changed, 536 insertions(+), 50 deletions(-) create mode 100644 flutter/lib/desktop/widgets/login.dart diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index 570ff6e95..c96dc115a 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/widgets/peer_card.dart'; import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; +import 'package:flutter_hbb/desktop/widgets/login.dart'; import '../../consts.dart'; import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; import 'package:get/get.dart'; diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index ad0aa4316..ef38bf443 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -472,7 +472,7 @@ class _DesktopHomePageState extends State /// common login dialog for desktop /// call this directly -Future loginDialog() async { +Future loginDialog2() async { String userName = ""; var userNameMsg = ""; String pass = ""; diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index b063c3c15..12bb935e9 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -7,6 +7,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; +import 'package:flutter_hbb/desktop/widgets/login.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:get/get.dart'; diff --git a/flutter/lib/desktop/widgets/login.dart b/flutter/lib/desktop/widgets/login.dart new file mode 100644 index 000000000..dec304cda --- /dev/null +++ b/flutter/lib/desktop/widgets/login.dart @@ -0,0 +1,469 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +import '../../common.dart'; +import '../widgets/button.dart'; + +class _IconOP extends StatelessWidget { + final String icon; + final double iconWidth; + const _IconOP({Key? key, required this.icon, required this.iconWidth}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 4.0), + child: SvgPicture.asset( + 'assets/$icon.svg', + width: iconWidth, + ), + ); + } +} + +class ButtonOP extends StatelessWidget { + final String op; + final RxString curOP; + final double iconWidth; + final Color primaryColor; + final double height; + final Function() onTap; + + const ButtonOP({ + Key? key, + required this.op, + required this.curOP, + required this.iconWidth, + required this.primaryColor, + required this.height, + required this.onTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row(children: [ + Expanded( + child: Container( + height: height, + padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), + child: Obx(() => ElevatedButton( + style: ElevatedButton.styleFrom( + primary: curOP.value.isEmpty || curOP.value == op + ? primaryColor + : Colors.grey, + ).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)), + onPressed: + curOP.value.isEmpty || curOP.value == op ? onTap : null, + child: Stack(children: [ + // to-do: translate + Center(child: Text('Continue with $op')), + Align( + alignment: Alignment.centerLeft, + child: SizedBox( + width: 120, + child: _IconOP( + icon: op, + iconWidth: iconWidth, + )), + ), + ]), + )), + ), + ) + ]); + } +} + +class ConfigOP { + final String op; + final double iconWidth; + ConfigOP({required this.op, required this.iconWidth}); +} + +class WidgetOP extends StatefulWidget { + final ConfigOP config; + final RxString curOP; + const WidgetOP({ + Key? key, + required this.config, + required this.curOP, + }) : super(key: key); + + @override + State createState() { + return _WidgetOPState(); + } +} + +class _WidgetOPState extends State { + Timer? _updateTimer; + String _stateMsg = ''; + String _stateFailedMsg = ''; + String _url = ''; + String _username = ''; + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + _updateTimer?.cancel(); + } + + _beginQueryState() { + _updateTimer = Timer.periodic(Duration(seconds: 1), (timer) { + _updateState(); + }); + } + + _updateState() { + bind.mainAccountAuthResult().then((result) { + if (result.isEmpty) { + return; + } + final resultMap = jsonDecode(result); + if (resultMap == null) { + return; + } + final String stateMsg = resultMap['state_msg']; + final String failedMsg = resultMap['failed_msg']; + // to-do: test null url + final String url = resultMap['url']; + if (_stateMsg != stateMsg) { + if (_url.isEmpty && url.isNotEmpty) { + launchUrl(Uri.parse(url)); + _url = url; + } + setState(() { + _stateMsg = stateMsg; + _stateFailedMsg = failedMsg; + }); + } + }); + } + + _resetState() { + _stateMsg = ''; + _stateFailedMsg = ''; + _url = ''; + _username = ''; + } + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(minWidth: 500), + child: Column( + children: [ + ButtonOP( + op: widget.config.op, + curOP: widget.curOP, + iconWidth: widget.config.iconWidth, + primaryColor: str2color(widget.config.op, 0x7f), + height: 40, + onTap: () { + widget.curOP.value = widget.config.op; + bind.mainAccountAuth(op: widget.config.op); + _beginQueryState(); + }, + ), + Obx(() => Offstage( + offstage: widget.curOP.value != widget.config.op, + child: Text( + _stateMsg, + style: TextStyle(fontSize: 12), + ))), + Obx( + () => Offstage( + offstage: widget.curOP.value != widget.config.op, + child: const SizedBox( + height: 5.0, + ), + ), + ), + Obx( + () => Offstage( + offstage: widget.curOP.value != widget.config.op, + child: ConstrainedBox( + constraints: BoxConstraints(maxHeight: 20), + child: ElevatedButton( + onPressed: () { + widget.curOP.value = ''; + _updateTimer?.cancel(); + _resetState(); + }, + child: Text( + translate('Cancel'), + style: TextStyle(fontSize: 15), + ), + ), + ), + ), + ), + ], + )); + } +} + +class LoginWidgetOP extends StatelessWidget { + final List ops; + final RxString curOP = ''.obs; + + LoginWidgetOP({ + Key? key, + required this.ops, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + var children = ops + .map((op) => [ + WidgetOP( + config: op, + curOP: curOP, + ), + const Divider() + ]) + .expand((i) => i) + .toList(); + if (children.isNotEmpty) { + children.removeLast(); + } + return SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: children, + )); + } +} + +class LoginWidgetUserPass extends StatelessWidget { + final String username; + final String pass; + final String usernameMsg; + final String passMsg; + final bool isInProgress; + final Function(String, String) onLogin; + const LoginWidgetUserPass({ + Key? key, + required this.username, + required this.pass, + required this.usernameMsg, + required this.passMsg, + required this.isInProgress, + required this.onLogin, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + var userController = TextEditingController(text: username); + var pwdController = TextEditingController(text: pass); + return ConstrainedBox( + constraints: const BoxConstraints(minWidth: 500), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 100), + child: Text( + '${translate("Username")}:', + textAlign: TextAlign.start, + ).marginOnly(bottom: 16.0)), + const SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + decoration: InputDecoration( + border: const OutlineInputBorder(), + errorText: usernameMsg.isNotEmpty ? usernameMsg : null), + controller: userController, + focusNode: FocusNode()..requestFocus(), + ), + ), + ], + ), + const SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 100), + child: Text('${translate("Password")}:') + .marginOnly(bottom: 16.0)), + const SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + obscureText: true, + decoration: InputDecoration( + border: const OutlineInputBorder(), + errorText: passMsg.isNotEmpty ? passMsg : null), + controller: pwdController, + ), + ), + ], + ), + const SizedBox( + height: 4.0, + ), + Offstage( + offstage: !isInProgress, child: const LinearProgressIndicator()), + const SizedBox( + height: 12.0, + ), + Row(children: [ + Expanded( + child: Container( + height: 50, + padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), + child: ElevatedButton( + child: const Text( + 'Login', + style: TextStyle(fontSize: 18), + ), + onPressed: () { + onLogin(userController.text, pwdController.text); + }, + ), + ), + ), + ]), + ], + ), + ); + } +} + +/// common login dialog for desktop +/// call this directly +Future loginDialog() async { + String username = ''; + var usernameMsg = ''; + String pass = ''; + var passMsg = ''; + var isInProgress = false; + var completer = Completer(); + + gFFI.dialogManager.show((setState, close) { + cancel() { + isInProgress = false; + completer.complete(false); + close(); + } + + onLogin(String username0, String pass0) async { + setState(() { + usernameMsg = ''; + passMsg = ''; + isInProgress = true; + }); + cancel() { + if (isInProgress) { + setState(() { + isInProgress = false; + }); + } + } + + username = username0; + pass = pass0; + if (username.isEmpty) { + usernameMsg = translate('Username missed'); + debugPrint('REMOVE ME ====================== username empty'); + cancel(); + return; + } + if (pass.isEmpty) { + passMsg = translate('Password missed'); + debugPrint('REMOVE ME ====================== password empty'); + cancel(); + return; + } + try { + final resp = await gFFI.userModel.login(username, pass); + if (resp.containsKey('error')) { + passMsg = resp['error']; + debugPrint('REMOVE ME ====================== password error'); + cancel(); + return; + } + // {access_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJndWlkIjoiMDFkZjQ2ZjgtZjg3OS00MDE0LTk5Y2QtMGMwYzM2MmViZGJlIiwiZXhwIjoxNjYxNDg2NzYwfQ.GZpe1oI8TfM5yTYNrpcwbI599P4Z_-b2GmnwNl2Lr-w, + // token_type: Bearer, user: {id: , name: admin, email: null, note: null, status: null, grp: null, is_admin: true}} + debugPrint('$resp'); + completer.complete(true); + } catch (err) { + debugPrint(err.toString()); + debugPrint( + 'REMOVE ME ====================== login error ${err.toString()}'); + cancel(); + return; + } + close(); + } + + return CustomAlertDialog( + title: Text(translate('Login')), + content: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 500), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 8.0, + ), + LoginWidgetUserPass( + username: username, + pass: pass, + usernameMsg: usernameMsg, + passMsg: passMsg, + isInProgress: isInProgress, + onLogin: onLogin, + ), + const SizedBox( + height: 8.0, + ), + const Center( + child: Text( + 'or', + style: TextStyle(fontSize: 16), + )), + const SizedBox( + height: 8.0, + ), + LoginWidgetOP(ops: [ + ConfigOP(op: 'Github', iconWidth: 24), + ConfigOP(op: 'Google', iconWidth: 24), + ConfigOP(op: 'Okta', iconWidth: 46), + ]), + ], + ), + ), + actions: [ + TextButton(onPressed: cancel, child: Text(translate('Cancel'))), + ], + onCancel: cancel, + ); + }); + return completer.future; +} diff --git a/flutter/lib/models/user_model.dart b/flutter/lib/models/user_model.dart index 721aac5b5..cfbe71cfe 100644 --- a/flutter/lib/models/user_model.dart +++ b/flutter/lib/models/user_model.dart @@ -77,7 +77,9 @@ class UserModel { return ""; } final m = jsonDecode(userInfo); - if (m != null) { + if (m == null) { + userName.value = ''; + } else { userName.value = m['name'] ?? ''; } return userName.value; diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index f20eb6153..937c108ed 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -13,6 +13,8 @@ use hbb_common::{ fs, log, }; +// use crate::hbbs_http::account::AuthResult; + use crate::flutter::{self, SESSIONS}; #[cfg(target_os = "android")] use crate::start_server; @@ -1082,6 +1084,14 @@ pub fn install_install_path() -> SyncReturn { SyncReturn(install_path()) } +pub fn main_account_auth(op: String) { + account_auth(op); +} + +pub fn main_account_auth_result() -> String { + account_auth_result() +} + #[cfg(target_os = "android")] pub mod server_side { use jni::{ diff --git a/src/hbbs_http/account.rs b/src/hbbs_http/account.rs index 85b2f7e82..4a2e365f4 100644 --- a/src/hbbs_http/account.rs +++ b/src/hbbs_http/account.rs @@ -1,6 +1,6 @@ use super::HbbHttpResponse; use hbb_common::{config::Config, log, sleep, tokio, tokio::sync::RwLock, ResultType}; -use serde_derive::Deserialize; +use serde_derive::{Deserialize, Serialize}; use std::{ collections::HashMap, sync::Arc, @@ -16,6 +16,9 @@ lazy_static::lazy_static! { const QUERY_INTERVAL_SECS: f32 = 1.0; const QUERY_TIMEOUT_SECS: u64 = 60; +const REQUESTING_ACCOUNT_AUTH: &str = "Requesting account auth"; +const WAITING_ACCOUNT_AUTH: &str = "Waiting account auth"; +const LOGIN_ACCOUNT_AUTH: &str = "Login account auth"; #[derive(Deserialize, Clone)] pub struct OidcAuthUrl { @@ -23,7 +26,7 @@ pub struct OidcAuthUrl { url: Url, } -#[derive(Debug, Deserialize, Default, Clone)] +#[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct UserPayload { pub id: String, pub name: String, @@ -34,34 +37,16 @@ pub struct UserPayload { pub is_admin: Option, } -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct AuthBody { - access_token: String, - token_type: String, - user: UserPayload, -} - -#[derive(Copy, Clone)] -pub enum OidcState { - // initial request - OidcRequest = 1, - // initial request failed - OidcRequestFailed = 2, - // request succeeded, loop querying - OidcQuerying = 11, - // loop querying failed - OidcQueryFailed = 12, - // query sucess before - OidcNotExists = 13, - // query timeout - OidcQueryTimeout = 14, - // already login - OidcLogin = 21, + pub access_token: String, + pub token_type: String, + pub user: UserPayload, } pub struct OidcSession { client: reqwest::Client, - state: OidcState, + state_msg: &'static str, failed_msg: String, code_url: Option, auth_body: Option, @@ -70,11 +55,19 @@ pub struct OidcSession { query_timeout: Duration, } +#[derive(Serialize)] +pub struct AuthResult { + pub state_msg: String, + pub failed_msg: String, + pub url: Option, + pub auth_body: Option, +} + impl OidcSession { fn new() -> Self { Self { client: reqwest::Client::new(), - state: OidcState::OidcRequest, + state_msg: REQUESTING_ACCOUNT_AUTH, failed_msg: "".to_owned(), code_url: None, auth_body: None, @@ -112,7 +105,7 @@ impl OidcSession { } fn reset(&mut self) { - self.state = OidcState::OidcRequest; + self.state_msg = REQUESTING_ACCOUNT_AUTH; self.failed_msg = "".to_owned(); self.keep_querying = true; self.running = false; @@ -136,21 +129,21 @@ impl OidcSession { OIDC_SESSION .write() .await - .set_state(OidcState::OidcRequestFailed, err); + .set_state(REQUESTING_ACCOUNT_AUTH, err); return; } Ok(_) => { - OIDC_SESSION.write().await.set_state( - OidcState::OidcRequestFailed, - "Invalid auth response".to_owned(), - ); + OIDC_SESSION + .write() + .await + .set_state(REQUESTING_ACCOUNT_AUTH, "Invalid auth response".to_owned()); return; } Err(err) => { OIDC_SESSION .write() .await - .set_state(OidcState::OidcRequestFailed, err.to_string()); + .set_state(REQUESTING_ACCOUNT_AUTH, err.to_string()); return; } }; @@ -158,7 +151,7 @@ impl OidcSession { OIDC_SESSION .write() .await - .set_state(OidcState::OidcQuerying, "".to_owned()); + .set_state(WAITING_ACCOUNT_AUTH, "".to_owned()); OIDC_SESSION.write().await.code_url = Some(code_url.clone()); let begin = Instant::now(); @@ -169,7 +162,7 @@ impl OidcSession { OIDC_SESSION .write() .await - .set_state(OidcState::OidcLogin, "".to_owned()); + .set_state(LOGIN_ACCOUNT_AUTH, "".to_owned()); OIDC_SESSION.write().await.auth_body = Some(auth_body); return; // to-do, set access-token @@ -181,7 +174,7 @@ impl OidcSession { OIDC_SESSION .write() .await - .set_state(OidcState::OidcQueryFailed, err); + .set_state(WAITING_ACCOUNT_AUTH, err); return; } } @@ -200,14 +193,14 @@ impl OidcSession { OIDC_SESSION .write() .await - .set_state(OidcState::OidcQueryTimeout, "timeout".to_owned()); + .set_state(WAITING_ACCOUNT_AUTH, "timeout".to_owned()); } // no need to handle "keep_querying == false" } - fn set_state(&mut self, state: OidcState, failed_msg: String) { - self.state = state; + fn set_state(&mut self, state_msg: &'static str, failed_msg: String) { + self.state_msg = state_msg; self.failed_msg = failed_msg; } @@ -228,15 +221,16 @@ impl OidcSession { }); } - fn get_result_(&self) -> (u8, String, Option) { - ( - self.state as u8, - self.failed_msg.clone(), - self.auth_body.clone(), - ) + fn get_result_(&self) -> AuthResult { + AuthResult { + state_msg: self.state_msg.to_string(), + failed_msg: self.failed_msg.clone(), + url: self.code_url.as_ref().map(|x| x.url.to_string()), + auth_body: self.auth_body.clone(), + } } - pub async fn get_result() -> (u8, String, Option) { + pub async fn get_result() -> AuthResult { OIDC_SESSION.read().await.get_result_() } } diff --git a/src/ui_interface.rs b/src/ui_interface.rs index cb2c178d6..78d9433b1 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -20,8 +20,7 @@ use hbb_common::{ tokio::{self, sync::mpsc, time}, }; -use crate::ipc; -use crate::{common::SOFTWARE_UPDATE_URL, platform}; +use crate::{common::SOFTWARE_UPDATE_URL, hbbs_http::account, ipc, platform}; type Message = RendezvousMessage; @@ -843,6 +842,16 @@ pub(crate) fn check_connect_status(reconnect: bool) -> mpsc::UnboundedSender String { + serde_json::to_string(&account::OidcSession::get_result().await).unwrap_or_default() +} + // notice: avoiding create ipc connecton repeatly, // because windows named pipe has serious memory leak issue. #[tokio::main(flavor = "current_thread")] From a84ee7a6ec9299b709683d1ea6eb9b959b721d6b Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 22 Oct 2022 22:19:14 +0800 Subject: [PATCH 04/27] oidc: init debug Signed-off-by: fufesou --- Cargo.toml | 3 +- flutter/lib/desktop/widgets/login.dart | 338 +++++++++++--------- flutter/lib/mobile/pages/settings_page.dart | 6 +- flutter/lib/models/user_model.dart | 51 +-- src/flutter_ffi.rs | 8 +- src/hbbs_http.rs | 12 +- src/hbbs_http/account.rs | 109 ++++--- src/main.rs | 15 +- src/server.rs | 13 +- src/ui_interface.rs | 14 +- 10 files changed, 322 insertions(+), 247 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9516d8526..1eb92c4e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,8 +66,7 @@ errno = "0.2.8" rdev = { git = "https://github.com/asur4s/rdev" } url = { version = "2.1", features = ["serde"] } -[target.'cfg(not(target_os = "linux"))'.dependencies] -reqwest = { version = "0.11", features = ["json", "rustls-tls"], default-features=false } +reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false } [target.'cfg(not(any(target_os = "android", target_os = "linux")))'.dependencies] cpal = "0.13.5" diff --git a/flutter/lib/desktop/widgets/login.dart b/flutter/lib/desktop/widgets/login.dart index dec304cda..5f849d822 100644 --- a/flutter/lib/desktop/widgets/login.dart +++ b/flutter/lib/desktop/widgets/login.dart @@ -2,15 +2,12 @@ import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:get/get.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'package:url_launcher/url_launcher_string.dart'; import '../../common.dart'; -import '../widgets/button.dart'; class _IconOP extends StatelessWidget { final String icon; @@ -92,10 +89,12 @@ class ConfigOP { class WidgetOP extends StatefulWidget { final ConfigOP config; final RxString curOP; + final Function(String) cbLogin; const WidgetOP({ Key? key, required this.config, required this.curOP, + required this.cbLogin, }) : super(key: key); @override @@ -107,9 +106,8 @@ class WidgetOP extends StatefulWidget { class _WidgetOPState extends State { Timer? _updateTimer; String _stateMsg = ''; - String _stateFailedMsg = ''; + String _FailedMsg = ''; String _url = ''; - String _username = ''; @override void initState() { @@ -138,17 +136,28 @@ class _WidgetOPState extends State { return; } final String stateMsg = resultMap['state_msg']; - final String failedMsg = resultMap['failed_msg']; - // to-do: test null url - final String url = resultMap['url']; - if (_stateMsg != stateMsg) { - if (_url.isEmpty && url.isNotEmpty) { + String failedMsg = resultMap['failed_msg']; + final String? url = resultMap['url']; + final authBody = resultMap['auth_body']; + if (_stateMsg != stateMsg || _FailedMsg != failedMsg) { + if (_url.isEmpty && url != null && url.isNotEmpty) { launchUrl(Uri.parse(url)); _url = url; } + if (authBody != null) { + _updateTimer?.cancel(); + final String username = authBody['user']['name']; + widget.curOP.value = ''; + widget.cbLogin(username); + } + setState(() { _stateMsg = stateMsg; - _stateFailedMsg = failedMsg; + _FailedMsg = failedMsg; + if (failedMsg.isNotEmpty) { + widget.curOP.value = ''; + _updateTimer?.cancel(); + } }); } }); @@ -156,74 +165,95 @@ class _WidgetOPState extends State { _resetState() { _stateMsg = ''; - _stateFailedMsg = ''; + _FailedMsg = ''; _url = ''; - _username = ''; } @override Widget build(BuildContext context) { - return ConstrainedBox( - constraints: const BoxConstraints(minWidth: 500), - child: Column( - children: [ - ButtonOP( - op: widget.config.op, - curOP: widget.curOP, - iconWidth: widget.config.iconWidth, - primaryColor: str2color(widget.config.op, 0x7f), - height: 40, - onTap: () { - widget.curOP.value = widget.config.op; - bind.mainAccountAuth(op: widget.config.op); - _beginQueryState(); - }, - ), - Obx(() => Offstage( - offstage: widget.curOP.value != widget.config.op, - child: Text( - _stateMsg, - style: TextStyle(fontSize: 12), - ))), - Obx( - () => Offstage( - offstage: widget.curOP.value != widget.config.op, - child: const SizedBox( - height: 5.0, - ), - ), - ), - Obx( - () => Offstage( - offstage: widget.curOP.value != widget.config.op, - child: ConstrainedBox( - constraints: BoxConstraints(maxHeight: 20), - child: ElevatedButton( - onPressed: () { - widget.curOP.value = ''; - _updateTimer?.cancel(); - _resetState(); - }, - child: Text( - translate('Cancel'), - style: TextStyle(fontSize: 15), + return Column( + children: [ + ButtonOP( + op: widget.config.op, + curOP: widget.curOP, + iconWidth: widget.config.iconWidth, + primaryColor: str2color(widget.config.op, 0x7f), + height: 40, + onTap: () async { + _resetState(); + widget.curOP.value = widget.config.op; + await bind.mainAccountAuth(op: widget.config.op); + _beginQueryState(); + }, + ), + Obx(() { + if (widget.curOP.isNotEmpty && + widget.curOP.value != widget.config.op) { + _FailedMsg = ''; + } + return Offstage( + offstage: + _FailedMsg.isEmpty && widget.curOP.value != widget.config.op, + child: Row( + children: [ + Text( + _stateMsg, + style: TextStyle(fontSize: 12), + ), + SizedBox(width: 8), + Text( + _FailedMsg, + style: TextStyle( + fontSize: 14, + color: Colors.red, ), ), + ], + )); + }), + Obx( + () => Offstage( + offstage: widget.curOP.value != widget.config.op, + child: const SizedBox( + height: 5.0, + ), + ), + ), + Obx( + () => Offstage( + offstage: widget.curOP.value != widget.config.op, + child: ConstrainedBox( + constraints: BoxConstraints(maxHeight: 20), + child: ElevatedButton( + onPressed: () { + widget.curOP.value = ''; + _updateTimer?.cancel(); + _resetState(); + bind.mainAccountAuthCancel(); + }, + child: Text( + translate('Cancel'), + style: TextStyle(fontSize: 15), ), ), ), - ], - )); + ), + ), + ], + ); } } class LoginWidgetOP extends StatelessWidget { final List ops; - final RxString curOP = ''.obs; + final RxString curOP; + final Function(String) cbLogin; LoginWidgetOP({ Key? key, required this.ops, + required this.curOP, + required this.cbLogin, }) : super(key: key); @override @@ -233,6 +263,7 @@ class LoginWidgetOP extends StatelessWidget { WidgetOP( config: op, curOP: curOP, + cbLogin: cbLogin, ), const Divider() ]) @@ -256,6 +287,7 @@ class LoginWidgetUserPass extends StatelessWidget { final String usernameMsg; final String passMsg; final bool isInProgress; + final RxString curOP; final Function(String, String) onLogin; const LoginWidgetUserPass({ Key? key, @@ -264,6 +296,7 @@ class LoginWidgetUserPass extends StatelessWidget { required this.usernameMsg, required this.passMsg, required this.isInProgress, + required this.curOP, required this.onLogin, }) : super(key: key); @@ -271,86 +304,90 @@ class LoginWidgetUserPass extends StatelessWidget { Widget build(BuildContext context) { var userController = TextEditingController(text: username); var pwdController = TextEditingController(text: pass); - return ConstrainedBox( - constraints: const BoxConstraints(minWidth: 500), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 8.0, - ), - Row( - children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text( - '${translate("Username")}:', - textAlign: TextAlign.start, - ).marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), - Expanded( - child: TextField( - decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: usernameMsg.isNotEmpty ? usernameMsg : null), - controller: userController, - focusNode: FocusNode()..requestFocus(), - ), - ), - ], - ), - const SizedBox( - height: 8.0, - ), - Row( - children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text('${translate("Password")}:') - .marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), - Expanded( - child: TextField( - obscureText: true, - decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: passMsg.isNotEmpty ? passMsg : null), - controller: pwdController, - ), - ), - ], - ), - const SizedBox( - height: 4.0, - ), - Offstage( - offstage: !isInProgress, child: const LinearProgressIndicator()), - const SizedBox( - height: 12.0, - ), - Row(children: [ + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 100), + child: Text( + '${translate("Username")}:', + textAlign: TextAlign.start, + ).marginOnly(bottom: 16.0)), + const SizedBox( + width: 24.0, + ), Expanded( - child: Container( - height: 50, - padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), - child: ElevatedButton( - child: const Text( - 'Login', - style: TextStyle(fontSize: 18), - ), - onPressed: () { - onLogin(userController.text, pwdController.text); - }, - ), + child: TextField( + decoration: InputDecoration( + border: const OutlineInputBorder(), + errorText: usernameMsg.isNotEmpty ? usernameMsg : null), + controller: userController, + focusNode: FocusNode()..requestFocus(), ), ), - ]), - ], - ), + ], + ), + const SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 100), + child: + Text('${translate("Password")}:').marginOnly(bottom: 16.0)), + const SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + obscureText: true, + decoration: InputDecoration( + border: const OutlineInputBorder(), + errorText: passMsg.isNotEmpty ? passMsg : null), + controller: pwdController, + ), + ), + ], + ), + const SizedBox( + height: 4.0, + ), + Offstage( + offstage: !isInProgress, child: const LinearProgressIndicator()), + const SizedBox( + height: 12.0, + ), + Row(children: [ + Expanded( + child: Container( + height: 50, + padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), + child: Obx(() => ElevatedButton( + style: curOP.value.isEmpty || curOP.value == 'rustdesk' + ? null + : ElevatedButton.styleFrom( + primary: Colors.grey, + ), + child: const Text( + 'Login', + style: TextStyle(fontSize: 18), + ), + onPressed: curOP.value.isEmpty || curOP.value == 'rustdesk' + ? () { + onLogin(userController.text, pwdController.text); + } + : null, + )), + ), + ), + ]), + ], ); } } @@ -364,6 +401,7 @@ Future loginDialog() async { var passMsg = ''; var isInProgress = false; var completer = Completer(); + final RxString curOP = ''.obs; gFFI.dialogManager.show((setState, close) { cancel() { @@ -379,6 +417,7 @@ Future loginDialog() async { isInProgress = true; }); cancel() { + curOP.value = ''; if (isInProgress) { setState(() { isInProgress = false; @@ -386,17 +425,16 @@ Future loginDialog() async { } } + curOP.value = 'rustdesk'; username = username0; pass = pass0; if (username.isEmpty) { usernameMsg = translate('Username missed'); - debugPrint('REMOVE ME ====================== username empty'); cancel(); return; } if (pass.isEmpty) { passMsg = translate('Password missed'); - debugPrint('REMOVE ME ====================== password empty'); cancel(); return; } @@ -404,7 +442,6 @@ Future loginDialog() async { final resp = await gFFI.userModel.login(username, pass); if (resp.containsKey('error')) { passMsg = resp['error']; - debugPrint('REMOVE ME ====================== password error'); cancel(); return; } @@ -414,8 +451,6 @@ Future loginDialog() async { completer.complete(true); } catch (err) { debugPrint(err.toString()); - debugPrint( - 'REMOVE ME ====================== login error ${err.toString()}'); cancel(); return; } @@ -438,6 +473,7 @@ Future loginDialog() async { usernameMsg: usernameMsg, passMsg: passMsg, isInProgress: isInProgress, + curOP: curOP, onLogin: onLogin, ), const SizedBox( @@ -451,11 +487,19 @@ Future loginDialog() async { const SizedBox( height: 8.0, ), - LoginWidgetOP(ops: [ - ConfigOP(op: 'Github', iconWidth: 24), - ConfigOP(op: 'Google', iconWidth: 24), - ConfigOP(op: 'Okta', iconWidth: 46), - ]), + LoginWidgetOP( + ops: [ + ConfigOP(op: 'Github', iconWidth: 24), + ConfigOP(op: 'Google', iconWidth: 24), + ConfigOP(op: 'Okta', iconWidth: 46), + ], + curOP: curOP, + cbLogin: (String username) { + gFFI.userModel.userName.value = username; + completer.complete(true); + close(); + }, + ), ], ), ), diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 7a82bcdd8..269439b1d 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -291,12 +291,12 @@ class _SettingsState extends State with WidgetsBindingObserver { return SettingsList( sections: [ SettingsSection( - title: Text(translate("Account")), + title: Text(translate('Account')), tiles: [ SettingsTile.navigation( title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty - ? translate("Login") - : '${translate("Logout")} (${gFFI.userModel.userName.value})')), + ? translate('Login') + : '${translate('Logout')} (${gFFI.userModel.userName.value})')), leading: Icon(Icons.person), onPressed: (context) { if (gFFI.userModel.userName.value.isEmpty) { diff --git a/flutter/lib/models/user_model.dart b/flutter/lib/models/user_model.dart index cfbe71cfe..d2e83990b 100644 --- a/flutter/lib/models/user_model.dart +++ b/flutter/lib/models/user_model.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; +import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; @@ -9,7 +10,7 @@ import 'model.dart'; import 'platform_model.dart'; class UserModel { - var userName = "".obs; + var userName = ''.obs; WeakReference parent; UserModel(this.parent) { @@ -18,7 +19,7 @@ class UserModel { void refreshCurrentUser() async { await getUserName(); - final token = await bind.mainGetLocalOption(key: "access_token"); + final token = await bind.mainGetLocalOption(key: 'access_token'); if (token == '') return; final url = await bind.mainGetApiServer(); final body = { @@ -28,8 +29,8 @@ class UserModel { try { final response = await http.post(Uri.parse('$url/api/currentUser'), headers: { - "Content-Type": "application/json", - "Authorization": "Bearer $token" + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token' }, body: json.encode(body)); final status = response.statusCode; @@ -44,9 +45,9 @@ class UserModel { } void resetToken() async { - await bind.mainSetLocalOption(key: "access_token", value: ""); - await bind.mainSetLocalOption(key: "user_info", value: ""); - userName.value = ""; + await bind.mainSetLocalOption(key: 'access_token', value: ''); + await bind.mainSetLocalOption(key: 'user_info', value: ''); + userName.value = ''; } Future _parseResp(String body) async { @@ -57,13 +58,13 @@ class UserModel { } final token = data['access_token']; if (token != null) { - await bind.mainSetLocalOption(key: "access_token", value: token); + await bind.mainSetLocalOption(key: 'access_token', value: token); } final info = data['user']; if (info != null) { final value = json.encode(info); - await bind.mainSetOption(key: "user_info", value: value); - userName.value = info["name"]; + await bind.mainSetOption(key: 'user_info', value: value); + userName.value = info['name']; } return ''; } @@ -74,7 +75,7 @@ class UserModel { } final userInfo = await bind.mainGetLocalOption(key: 'user_info'); if (userInfo.trim().isEmpty) { - return ""; + return ''; } final m = jsonDecode(userInfo); if (m == null) { @@ -88,10 +89,10 @@ class UserModel { Future logOut() async { final tag = gFFI.dialogManager.showLoading(translate('Waiting')); final url = await bind.mainGetApiServer(); - final _ = await http.post(Uri.parse("$url/api/logout"), + final _ = await http.post(Uri.parse('$url/api/logout'), body: { - "id": await bind.mainGetMyId(), - "uuid": await bind.mainGetUuid(), + 'id': await bind.mainGetMyId(), + 'uuid': await bind.mainGetUuid(), }, headers: await getHttpHeaders()); await Future.wait([ @@ -100,30 +101,30 @@ class UserModel { bind.mainSetLocalOption(key: 'selected-tags', value: ''), ]); parent.target?.abModel.clear(); - userName.value = ""; + userName.value = ''; gFFI.dialogManager.dismissByTag(tag); } Future> login(String userName, String pass) async { final url = await bind.mainGetApiServer(); try { - final resp = await http.post(Uri.parse("$url/api/login"), - headers: {"Content-Type": "application/json"}, + final resp = await http.post(Uri.parse('$url/api/login'), + headers: {'Content-Type': 'application/json'}, body: jsonEncode({ - "username": userName, - "password": pass, - "id": await bind.mainGetMyId(), - "uuid": await bind.mainGetUuid() + 'username': userName, + 'password': pass, + 'id': await bind.mainGetMyId(), + 'uuid': await bind.mainGetUuid() })); final body = jsonDecode(resp.body); bind.mainSetLocalOption( - key: "access_token", value: body['access_token'] ?? ""); + key: 'access_token', value: body['access_token'] ?? ''); bind.mainSetLocalOption( - key: "user_info", value: jsonEncode(body['user'])); - this.userName.value = body['user']?['name'] ?? ""; + key: 'user_info', value: jsonEncode(body['user'])); + this.userName.value = body['user']?['name'] ?? ''; return body; } catch (err) { - return {"error": "$err"}; + return {'error': '$err'}; } } } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 937c108ed..9552bd36e 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1085,7 +1085,13 @@ pub fn install_install_path() -> SyncReturn { } pub fn main_account_auth(op: String) { - account_auth(op); + let id = get_id(); + let uuid = get_uuid(); + account_auth(op, id, uuid); +} + +pub fn main_account_auth_cancel() { + account_auth_cancel() } pub fn main_account_auth_result() -> String { diff --git a/src/hbbs_http.rs b/src/hbbs_http.rs index b0e8cdbab..4360b6e8c 100644 --- a/src/hbbs_http.rs +++ b/src/hbbs_http.rs @@ -2,13 +2,14 @@ use hbb_common::{ anyhow::{self, bail}, tokio, ResultType, }; -use reqwest::Response; +use reqwest::blocking::Response; +use serde::de::DeserializeOwned; use serde_derive::Deserialize; use serde_json::{Map, Value}; -use serde::de::DeserializeOwned; pub mod account; +#[derive(Debug)] pub enum HbbHttpResponse { ErrorFormat, Error(String), @@ -16,16 +17,11 @@ pub enum HbbHttpResponse { Data(T), } -#[tokio::main(flavor = "current_thread")] -async fn resp_to_serde_map(resp: Response) -> reqwest::Result> { - resp.json().await -} - impl TryFrom for HbbHttpResponse { type Error = reqwest::Error; fn try_from(resp: Response) -> Result>::Error> { - let map = resp_to_serde_map(resp)?; + let map = resp.json::>()?; if let Some(error) = map.get("error") { if let Some(err) = error.as_str() { Ok(Self::Error(err.to_owned())) diff --git a/src/hbbs_http/account.rs b/src/hbbs_http/account.rs index 4a2e365f4..cdf724971 100644 --- a/src/hbbs_http/account.rs +++ b/src/hbbs_http/account.rs @@ -1,9 +1,13 @@ use super::HbbHttpResponse; -use hbb_common::{config::Config, log, sleep, tokio, tokio::sync::RwLock, ResultType}; +use hbb_common::{ + config::{Config, LocalConfig}, + log, sleep, tokio, ResultType, +}; +use reqwest::blocking::Client; use serde_derive::{Deserialize, Serialize}; use std::{ collections::HashMap, - sync::Arc, + sync::{Arc, RwLock}, time::{Duration, Instant}, }; use url::Url; @@ -15,12 +19,12 @@ lazy_static::lazy_static! { } const QUERY_INTERVAL_SECS: f32 = 1.0; -const QUERY_TIMEOUT_SECS: u64 = 60; +const QUERY_TIMEOUT_SECS: u64 = 60 * 3; const REQUESTING_ACCOUNT_AUTH: &str = "Requesting account auth"; const WAITING_ACCOUNT_AUTH: &str = "Waiting account auth"; const LOGIN_ACCOUNT_AUTH: &str = "Login account auth"; -#[derive(Deserialize, Clone)] +#[derive(Deserialize, Clone, Debug)] pub struct OidcAuthUrl { code: String, url: Url, @@ -45,7 +49,7 @@ pub struct AuthBody { } pub struct OidcSession { - client: reqwest::Client, + client: Client, state_msg: &'static str, failed_msg: String, code_url: Option, @@ -66,7 +70,7 @@ pub struct AuthResult { impl OidcSession { fn new() -> Self { Self { - client: reqwest::Client::new(), + client: Client::new(), state_msg: REQUESTING_ACCOUNT_AUTH, failed_msg: "".to_owned(), code_url: None, @@ -77,30 +81,28 @@ impl OidcSession { } } - async fn auth(op: &str, id: &str, uuid: &str) -> ResultType> { + fn auth(op: &str, id: &str, uuid: &str) -> ResultType> { Ok(OIDC_SESSION .read() - .await + .unwrap() .client .post(format!("{}/api/oidc/auth", *API_SERVER)) .json(&HashMap::from([("op", op), ("id", id), ("uuid", uuid)])) - .send() - .await? + .send()? .try_into()?) } - async fn query(code: &str, id: &str, uuid: &str) -> ResultType> { + fn query(code: &str, id: &str, uuid: &str) -> ResultType> { let url = reqwest::Url::parse_with_params( &format!("{}/api/oidc/auth-query", *API_SERVER), &[("code", code), ("id", id), ("uuid", uuid)], )?; Ok(OIDC_SESSION .read() - .await + .unwrap() .client .get(url) - .send() - .await? + .send()? .try_into()?) } @@ -113,36 +115,42 @@ impl OidcSession { self.auth_body = None; } - async fn before_task(&mut self) { + fn before_task(&mut self) { self.reset(); self.running = true; } - async fn after_task(&mut self) { + fn after_task(&mut self) { self.running = false; } - async fn auth_task(op: String, id: String, uuid: String) { - let code_url = match Self::auth(&op, &id, &uuid).await { + fn sleep(secs: f32) { + std::thread::sleep(std::time::Duration::from_secs_f32(secs)); + } + + fn auth_task(op: String, id: String, uuid: String) { + let auth_request_res = Self::auth(&op, &id, &uuid); + log::info!("Request oidc auth result: {:?}", &auth_request_res); + let code_url = match auth_request_res { Ok(HbbHttpResponse::<_>::Data(code_url)) => code_url, Ok(HbbHttpResponse::<_>::Error(err)) => { OIDC_SESSION .write() - .await + .unwrap() .set_state(REQUESTING_ACCOUNT_AUTH, err); return; } Ok(_) => { OIDC_SESSION .write() - .await + .unwrap() .set_state(REQUESTING_ACCOUNT_AUTH, "Invalid auth response".to_owned()); return; } Err(err) => { OIDC_SESSION .write() - .await + .unwrap() .set_state(REQUESTING_ACCOUNT_AUTH, err.to_string()); return; } @@ -150,22 +158,29 @@ impl OidcSession { OIDC_SESSION .write() - .await + .unwrap() .set_state(WAITING_ACCOUNT_AUTH, "".to_owned()); - OIDC_SESSION.write().await.code_url = Some(code_url.clone()); + OIDC_SESSION.write().unwrap().code_url = Some(code_url.clone()); let begin = Instant::now(); - let query_timeout = OIDC_SESSION.read().await.query_timeout; - while OIDC_SESSION.read().await.keep_querying && begin.elapsed() < query_timeout { - match Self::query(&code_url.code, &id, &uuid).await { + let query_timeout = OIDC_SESSION.read().unwrap().query_timeout; + while OIDC_SESSION.read().unwrap().keep_querying && begin.elapsed() < query_timeout { + match Self::query(&code_url.code, &id, &uuid) { Ok(HbbHttpResponse::<_>::Data(auth_body)) => { + LocalConfig::set_option( + "access_token".to_owned(), + auth_body.access_token.clone(), + ); + LocalConfig::set_option( + "user_info".to_owned(), + serde_json::to_string(&auth_body.user).unwrap_or_default(), + ); OIDC_SESSION .write() - .await + .unwrap() .set_state(LOGIN_ACCOUNT_AUTH, "".to_owned()); - OIDC_SESSION.write().await.auth_body = Some(auth_body); + OIDC_SESSION.write().unwrap().auth_body = Some(auth_body); return; - // to-do, set access-token } Ok(HbbHttpResponse::<_>::Error(err)) => { if err.contains("No authed oidc is found") { @@ -173,7 +188,7 @@ impl OidcSession { } else { OIDC_SESSION .write() - .await + .unwrap() .set_state(WAITING_ACCOUNT_AUTH, err); return; } @@ -186,13 +201,13 @@ impl OidcSession { // ignore } } - sleep(QUERY_INTERVAL_SECS).await; + Self::sleep(QUERY_INTERVAL_SECS); } if begin.elapsed() >= query_timeout { OIDC_SESSION .write() - .await + .unwrap() .set_state(WAITING_ACCOUNT_AUTH, "timeout".to_owned()); } @@ -204,20 +219,20 @@ impl OidcSession { self.failed_msg = failed_msg; } - pub async fn account_auth(op: String, id: String, uuid: String) { - if OIDC_SESSION.read().await.running { - OIDC_SESSION.write().await.keep_querying = false; - } + fn wait_stop_querying() { let wait_secs = 0.3; - sleep(wait_secs).await; - while OIDC_SESSION.read().await.running { - sleep(wait_secs).await; + while OIDC_SESSION.read().unwrap().running { + Self::sleep(wait_secs); } + } - tokio::spawn(async move { - OIDC_SESSION.write().await.before_task().await; - Self::auth_task(op, id, uuid).await; - OIDC_SESSION.write().await.after_task().await; + pub fn account_auth(op: String, id: String, uuid: String) { + Self::auth_cancel(); + Self::wait_stop_querying(); + OIDC_SESSION.write().unwrap().before_task(); + std::thread::spawn(|| { + Self::auth_task(op, id, uuid); + OIDC_SESSION.write().unwrap().after_task(); }); } @@ -230,7 +245,11 @@ impl OidcSession { } } - pub async fn get_result() -> AuthResult { - OIDC_SESSION.read().await.get_result_() + pub fn auth_cancel() { + OIDC_SESSION.write().unwrap().keep_querying = false; + } + + pub fn get_result() -> AuthResult { + OIDC_SESSION.read().unwrap().get_result_() } } diff --git a/src/main.rs b/src/main.rs index ac8fd5219..9c7170309 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,8 +32,8 @@ fn main() { if !common::global_init() { return; } - use hbb_common::log; use clap::App; + use hbb_common::log; let args = format!( "-p, --port-forward=[PORT-FORWARD-OPTIONS] 'Format: remote-id:local-port:remote-port[:remote-host]' -k, --key=[KEY] '' @@ -45,7 +45,7 @@ fn main() { .about("RustDesk command line tool") .args_from_usage(&args) .get_matches(); - use hbb_common::{env_logger::*, config::LocalConfig}; + use hbb_common::{config::LocalConfig, env_logger::*}; init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info")); if let Some(p) = matches.value_of("port-forward") { let options: Vec = p.split(":").map(|x| x.to_owned()).collect(); @@ -73,7 +73,14 @@ fn main() { } let key = matches.value_of("key").unwrap_or("").to_owned(); let token = LocalConfig::get_option("access_token"); - cli::start_one_port_forward(options[0].clone(), port, remote_host, remote_port, key, token); + cli::start_one_port_forward( + options[0].clone(), + port, + remote_host, + remote_port, + key, + token, + ); } common::global_clean(); -} \ No newline at end of file +} diff --git a/src/server.rs b/src/server.rs index 58aab8fd1..04814db42 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,4 +1,5 @@ use crate::ipc::Data; +use bytes::Bytes; pub use connection::*; use hbb_common::{ allow_err, @@ -20,7 +21,6 @@ use std::{ sync::{Arc, Mutex, RwLock, Weak}, time::Duration, }; -use bytes::Bytes; pub mod audio_service; cfg_if::cfg_if! { @@ -140,7 +140,8 @@ pub async fn create_tcp_connection( .write_to_bytes() .unwrap_or_default(), &sk, - ).into(), + ) + .into(), ..Default::default() }); timeout(CONNECT_TIMEOUT, stream.send(&msg_out)).await??; @@ -310,9 +311,9 @@ 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. @@ -323,9 +324,9 @@ pub async fn start_server(is_server: bool) { } /// 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. diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 78d9433b1..04ba90cb2 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -842,14 +842,16 @@ pub(crate) fn check_connect_status(reconnect: bool) -> mpsc::UnboundedSender String { - serde_json::to_string(&account::OidcSession::get_result().await).unwrap_or_default() +pub fn account_auth_cancel() { + account::OidcSession::auth_cancel(); +} + +pub fn account_auth_result() -> String { + serde_json::to_string(&account::OidcSession::get_result()).unwrap_or_default() } // notice: avoiding create ipc connecton repeatly, From dbd3df370a95a908ba09e718b41e774d94ce3238 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 7 Nov 2022 17:43:22 +0800 Subject: [PATCH 05/27] feat_account: update ui Signed-off-by: fufesou --- .../lib/desktop/pages/desktop_home_page.dart | 135 ------------------ flutter/lib/desktop/widgets/login.dart | 120 ++++++++-------- src/lang/cn.rs | 2 + src/lang/cs.rs | 2 + src/lang/da.rs | 2 + src/lang/de.rs | 2 + src/lang/en.rs | 2 + src/lang/eo.rs | 2 + src/lang/es.rs | 2 + src/lang/fr.rs | 2 + src/lang/hu.rs | 2 + src/lang/id.rs | 2 + src/lang/it.rs | 2 + src/lang/ja.rs | 2 + src/lang/ko.rs | 2 + src/lang/kz.rs | 2 + src/lang/pl.rs | 2 + src/lang/pt_PT.rs | 2 + src/lang/ptbr.rs | 2 + src/lang/ru.rs | 2 + src/lang/sk.rs | 2 + src/lang/template.rs | 2 + src/lang/tr.rs | 2 + src/lang/tw.rs | 2 + src/lang/ua.rs | 2 + src/lang/vn.rs | 2 + 26 files changed, 112 insertions(+), 191 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index ef38bf443..e7d6f50e8 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -470,141 +470,6 @@ class _DesktopHomePageState extends State } } -/// common login dialog for desktop -/// call this directly -Future loginDialog2() async { - String userName = ""; - var userNameMsg = ""; - String pass = ""; - var passMsg = ""; - var userController = TextEditingController(text: userName); - var pwdController = TextEditingController(text: pass); - - var isInProgress = false; - var completer = Completer(); - gFFI.dialogManager.show((setState, close) { - submit() async { - setState(() { - userNameMsg = ""; - passMsg = ""; - isInProgress = true; - }); - cancel() { - setState(() { - isInProgress = false; - }); - } - - userName = userController.text; - pass = pwdController.text; - if (userName.isEmpty) { - userNameMsg = translate("Username missed"); - cancel(); - return; - } - if (pass.isEmpty) { - passMsg = translate("Password missed"); - cancel(); - return; - } - try { - final resp = await gFFI.userModel.login(userName, pass); - if (resp.containsKey('error')) { - passMsg = resp['error']; - cancel(); - return; - } - // {access_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJndWlkIjoiMDFkZjQ2ZjgtZjg3OS00MDE0LTk5Y2QtMGMwYzM2MmViZGJlIiwiZXhwIjoxNjYxNDg2NzYwfQ.GZpe1oI8TfM5yTYNrpcwbI599P4Z_-b2GmnwNl2Lr-w, - // token_type: Bearer, user: {id: , name: admin, email: null, note: null, status: null, grp: null, is_admin: true}} - debugPrint("$resp"); - completer.complete(true); - } catch (err) { - debugPrint(err.toString()); - cancel(); - return; - } - close(); - } - - cancel() { - completer.complete(false); - close(); - } - - return CustomAlertDialog( - title: Text(translate("Login")), - content: ConstrainedBox( - constraints: const BoxConstraints(minWidth: 500), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 8.0, - ), - Row( - children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text( - "${translate('Username')}:", - textAlign: TextAlign.start, - ).marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), - Expanded( - child: TextField( - decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: userNameMsg.isNotEmpty ? userNameMsg : null), - controller: userController, - focusNode: FocusNode()..requestFocus(), - ), - ), - ], - ), - const SizedBox( - height: 8.0, - ), - Row( - children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text("${translate('Password')}:") - .marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), - Expanded( - child: TextField( - obscureText: true, - decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: passMsg.isNotEmpty ? passMsg : null), - controller: pwdController, - ), - ), - ], - ), - const SizedBox( - height: 4.0, - ), - Offstage( - offstage: !isInProgress, child: const LinearProgressIndicator()) - ], - ), - ), - actions: [ - TextButton(onPressed: cancel, child: Text(translate("Cancel"))), - TextButton(onPressed: submit, child: Text(translate("OK"))), - ], - onSubmit: submit, - onCancel: cancel, - ); - }); - return completer.future; -} - void setPasswordDialog() async { final pw = await bind.mainGetPermanentPassword(); final p0 = TextEditingController(text: pw); diff --git a/flutter/lib/desktop/widgets/login.dart b/flutter/lib/desktop/widgets/login.dart index 5f849d822..3e58a6de2 100644 --- a/flutter/lib/desktop/widgets/login.dart +++ b/flutter/lib/desktop/widgets/login.dart @@ -9,6 +9,8 @@ import 'package:url_launcher/url_launcher.dart'; import '../../common.dart'; +final kMidButtonPadding = const EdgeInsets.fromLTRB(15, 0, 15, 0); + class _IconOP extends StatelessWidget { final String icon; final double iconWidth; @@ -51,7 +53,7 @@ class ButtonOP extends StatelessWidget { Expanded( child: Container( height: height, - padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), + padding: kMidButtonPadding, child: Obx(() => ElevatedButton( style: ElevatedButton.styleFrom( primary: curOP.value.isEmpty || curOP.value == op @@ -61,8 +63,7 @@ class ButtonOP extends StatelessWidget { onPressed: curOP.value.isEmpty || curOP.value == op ? onTap : null, child: Stack(children: [ - // to-do: translate - Center(child: Text('Continue with $op')), + Center(child: Text('${translate("Continue with")} $op')), Align( alignment: Alignment.centerLeft, child: SizedBox( @@ -178,7 +179,7 @@ class _WidgetOPState extends State { curOP: widget.curOP, iconWidth: widget.config.iconWidth, primaryColor: str2color(widget.config.op, 0x7f), - height: 40, + height: 36, onTap: () async { _resetState(); widget.curOP.value = widget.config.op; @@ -265,7 +266,10 @@ class LoginWidgetOP extends StatelessWidget { curOP: curOP, cbLogin: cbLogin, ), - const Divider() + const Divider( + indent: 5, + endIndent: 5, + ) ]) .expand((i) => i) .toList(); @@ -310,50 +314,56 @@ class LoginWidgetUserPass extends StatelessWidget { const SizedBox( height: 8.0, ), - Row( - children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text( - '${translate("Username")}:', - textAlign: TextAlign.start, - ).marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), - Expanded( - child: TextField( - decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: usernameMsg.isNotEmpty ? usernameMsg : null), - controller: userController, - focusNode: FocusNode()..requestFocus(), + Container( + padding: kMidButtonPadding, + child: Row( + children: [ + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 100), + child: Text( + '${translate("Username")}:', + textAlign: TextAlign.start, + ).marginOnly(bottom: 16.0)), + const SizedBox( + width: 24.0, ), - ), - ], + Expanded( + child: TextField( + decoration: InputDecoration( + border: const OutlineInputBorder(), + errorText: usernameMsg.isNotEmpty ? usernameMsg : null), + controller: userController, + focusNode: FocusNode()..requestFocus(), + ), + ), + ], + ), ), const SizedBox( height: 8.0, ), - Row( - children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: - Text('${translate("Password")}:').marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), - Expanded( - child: TextField( - obscureText: true, - decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: passMsg.isNotEmpty ? passMsg : null), - controller: pwdController, + Container( + padding: kMidButtonPadding, + child: Row( + children: [ + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 100), + child: Text('${translate("Password")}:') + .marginOnly(bottom: 16.0)), + const SizedBox( + width: 24.0, ), - ), - ], + Expanded( + child: TextField( + obscureText: true, + decoration: InputDecoration( + border: const OutlineInputBorder(), + errorText: passMsg.isNotEmpty ? passMsg : null), + controller: pwdController, + ), + ), + ], + ), ), const SizedBox( height: 4.0, @@ -366,17 +376,17 @@ class LoginWidgetUserPass extends StatelessWidget { Row(children: [ Expanded( child: Container( - height: 50, - padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 38, + padding: kMidButtonPadding, child: Obx(() => ElevatedButton( style: curOP.value.isEmpty || curOP.value == 'rustdesk' ? null : ElevatedButton.styleFrom( primary: Colors.grey, ), - child: const Text( - 'Login', - style: TextStyle(fontSize: 18), + child: Text( + translate('Login'), + style: TextStyle(fontSize: 16), ), onPressed: curOP.value.isEmpty || curOP.value == 'rustdesk' ? () { @@ -479,9 +489,9 @@ Future loginDialog() async { const SizedBox( height: 8.0, ), - const Center( + Center( child: Text( - 'or', + translate('or'), style: TextStyle(fontSize: 16), )), const SizedBox( @@ -489,9 +499,9 @@ Future loginDialog() async { ), LoginWidgetOP( ops: [ - ConfigOP(op: 'Github', iconWidth: 24), - ConfigOP(op: 'Google', iconWidth: 24), - ConfigOP(op: 'Okta', iconWidth: 46), + ConfigOP(op: 'Github', iconWidth: 20), + ConfigOP(op: 'Google', iconWidth: 20), + ConfigOP(op: 'Okta', iconWidth: 38), ], curOP: curOP, cbLogin: (String username) { @@ -503,9 +513,7 @@ Future loginDialog() async { ], ), ), - actions: [ - TextButton(onPressed: cancel, child: Text(translate('Cancel'))), - ], + actions: [msgBoxButton(translate('Close'), cancel)], onCancel: cancel, ); }); diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 3ee4735e3..7240f2a91 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "请选择要分享的画面(对端操作)。"), ("Show RustDesk", "显示rustdesk"), ("This PC", "此电脑"), + ("or", "或"), + ("Continue with", "使用"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index e0998a7bb..b51cb69e9 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Vyberte prosím obrazovku, kterou chcete sdílet (Ovládejte na straně protějšku)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 9aa4f00b9..c4d633b9b 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Vælg venligst den skærm, der skal deles (Betjen på peer-siden)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 4ce6cbd56..9eb90ebcd 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Bitte wählen Sie den Bildschirm aus, der freigegeben werden soll (auf der Peer-Seite arbeiten)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 72aa45853..3415fa463 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -35,5 +35,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_foreground_window_warning", "Temporarily unable to use the mouse and keyboard, because the current window of the remote desktop requires higher privilege to operate, you can request the remote user to minimize the current window. To avoid this problem, it is recommended to install the software on the remote device or run it with administrator privileges."), ("JumpLink", "View"), ("Stop service", "Stop Service"), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 8fb58cf83..e7a35d937 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Bonvolu Elekti la ekranon por esti dividita (Funkciu ĉe la sama flanko)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index fa6aba297..4064d0fd7 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Seleccione la pantalla que se compartirá (Operar en el lado del par)."), ("Show RustDesk", "Mostrar RustDesk"), ("This PC", "Este PC"), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index f1119e166..a64fd6028 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Veuillez sélectionner l'écran à partager (opérer du côté pair)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 262574c43..c449c393d 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Kérjük, válassza ki a megosztani kívánt képernyőt (a társoldalon működjön)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 2d2ab9b1a..6f328f127 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Silakan Pilih layar yang akan dibagikan (Operasi di sisi rekan)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 6965e6610..75e7859ed 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Seleziona lo schermo da condividere (opera sul lato peer)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 12dc9ebaf..0e6931379 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "共有する画面を選択してください(ピア側で操作)。"), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 406de6cef..601db354d 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "공유할 화면을 선택하십시오(피어 측에서 작동)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index df1237bfb..359e14f55 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Бөлісетін экранды таңдаңыз (бірдей жағынан жұмыс жасаңыз)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 201a60811..382b254f0 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Wybierz ekran do udostępnienia (działaj po stronie równorzędnej)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index a9189fc14..b99cb9db0 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Por favor, selecione a tela a ser compartilhada (operar no lado do peer)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index d6a7ccec0..600506979 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", ""), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index dfe1f7e8f..4eae0d153 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Пожалуйста, выберите экран для совместного использования (работайте на одноранговой стороне)."), ("Show RustDesk", "Показать RustDesk"), ("This PC", "Этот компьютер"), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 38196c011..4dfd8b02e 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Vyberte obrazovku, ktorú chcete zdieľať (Ovládajte na strane partnera)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index d5130d66b..7c1f18df3 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", ""), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index fe9b5d2fc..f856182f3 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Lütfen paylaşılacak ekranı seçiniz (Ekran tarafında çalıştırın)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 0f986ffa1..6a196feb7 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "請選擇要分享的畫面(在對端操作)。"), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 1c34d0825..95d19b26b 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Будь ласка, виберіть екран, до якого потрібно надати доступ (працюйте на стороні однорангового пристрою)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 255b60def..c95498ca8 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Vui lòng Chọn màn hình để chia sẻ (Hoạt động ở phía ngang hàng)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } From 5139aee6eb1a9b61a9fe5d90cda5c5c4faece969 Mon Sep 17 00:00:00 2001 From: Vinicius Becker Date: Mon, 7 Nov 2022 10:01:22 -0300 Subject: [PATCH 06/27] Update ptbr.rs ### What does this PR do? This PR updates several translation fields in Portuguese (Brazil) language. --- src/lang/ptbr.rs | 294 +++++++++++++++++++++++------------------------ 1 file changed, 147 insertions(+), 147 deletions(-) diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 600506979..3a60dc313 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -2,8 +2,8 @@ lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Status"), - ("Your Desktop", "Seu Desktop"), - ("desk_tip", "Seu desktop pode ser acessado com este ID e senha."), + ("Your Desktop", "Seu Computador"), + ("desk_tip", "Seu computador pode ser acessado com este ID e senha."), ("Password", "Senha"), ("Ready", "Pronto"), ("Established", "Estabelecido"), @@ -13,37 +13,37 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Service is running", "Serviço está em execução"), ("Service is not running", "Serviço não está em execução"), ("not_ready_status", "Não está pronto. Por favor verifique sua conexão"), - ("Control Remote Desktop", "Controle o Desktop à distância"), + ("Control Remote Desktop", "Controle um Computador Remoto"), ("Transfer File", "Transferir Arquivo"), ("Connect", "Conectar"), - ("Recent Sessions", "Sessões recentes"), + ("Recent Sessions", "Sessões Recentes"), ("Address Book", "Lista de Endereços"), ("Confirmation", "Confirmação"), ("TCP Tunneling", "Tunelamento TCP"), ("Remove", "Remover"), ("Refresh random password", "Atualizar senha aleatória"), ("Set your own password", "Configure sua própria senha"), - ("Enable Keyboard/Mouse", "Habilitar Teclado/Mouse"), + ("Enable Keyboard/Mouse", "Habilitar teclado/mouse"), ("Enable Clipboard", "Habilitar Área de Transferência"), ("Enable File Transfer", "Habilitar Transferência de Arquivos"), ("Enable TCP Tunneling", "Habilitar Tunelamento TCP"), - ("IP Whitelisting", "Whitelist de IP"), + ("IP Whitelisting", "Lista de IPs Confiáveis"), ("ID/Relay Server", "Servidor ID/Relay"), ("Import Server Config", "Importar Configuração do Servidor"), - ("Export Server Config", ""), + ("Export Server Config", "Exportar Configuração do Servidor"), ("Import server configuration successfully", "Configuração do servidor importada com sucesso"), - ("Export server configuration successfully", ""), + ("Export server configuration successfully", "Configuração do servidor exportada com sucesso"), ("Invalid server configuration", "Configuração do servidor inválida"), ("Clipboard is empty", "A área de transferência está vazia"), ("Stop service", "Parar serviço"), ("Change ID", "Alterar ID"), ("Website", "Website"), ("About", "Sobre"), - ("Mute", "Emudecer"), + ("Mute", "Desativar som"), ("Audio Input", "Entrada de Áudio"), - ("Enhancements", ""), - ("Hardware Codec", ""), - ("Adaptive Bitrate", ""), + ("Enhancements", "Melhorias"), + ("Hardware Codec", "Codec de hardware"), + ("Adaptive Bitrate", "Taxa de bits adaptável"), ("ID Server", "Servidor de ID"), ("Relay Server", "Servidor de Relay"), ("API Server", "Servidor da API"), @@ -59,18 +59,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Close", "Fechar"), ("Retry", "Tentar novamente"), ("OK", "OK"), - ("Password Required", "Senha Necessária"), + ("Password Required", "Senha necessária"), ("Please enter your password", "Por favor informe sua senha"), ("Remember password", "Lembrar senha"), - ("Wrong Password", "Senha Incorreta"), - ("Do you want to enter again?", "Você quer entrar novamente?"), - ("Connection Error", "Erro de Conexão"), + ("Wrong Password", "Senha incorreta"), + ("Do you want to enter again?", "Você deseja conectar novamente?"), + ("Connection Error", "Erro de conexão"), ("Error", "Erro"), - ("Reset by the peer", "Reiniciado pelo par"), + ("Reset by the peer", "Reiniciado pelo parceiro"), ("Connecting...", "Conectando..."), ("Connection in progress. Please wait.", "Conexão em progresso. Aguarde por favor."), ("Please try 1 minute later", "Por favor tente após 1 minuto"), - ("Login Error", "Erro de Login"), + ("Login Error", "Erro de login"), ("Successful", "Sucesso"), ("Connected, waiting for image...", "Conectado. Aguardando pela imagem..."), ("Name", "Nome"), @@ -88,10 +88,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Confirm Delete", "Confirmar Apagar"), ("Delete", "Apagar"), ("Properties", "Propriedades"), - ("Multi Select", "Seleção Múltipla"), - ("Select All", ""), - ("Unselect All", ""), - ("Empty Directory", "Diretório Vazio"), + ("Multi Select", "Seleção múltipla"), + ("Select All", "Selecionar tudo"), + ("Unselect All", "Desmarcar tudo"), + ("Empty Directory", "Diretório vazio"), ("Not an empty directory", "Diretório não está vazio"), ("Are you sure you want to delete this file?", "Tem certeza que deseja apagar este arquivo?"), ("Are you sure you want to delete this empty directory?", "Tem certeza que deseja apagar este diretório vazio?"), @@ -116,18 +116,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Qualidade visual boa"), ("Balanced", "Balanceada"), ("Optimize reaction time", "Otimizar tempo de reação"), - ("Custom", ""), + ("Custom", "Personalizado"), ("Show remote cursor", "Mostrar cursor remoto"), - ("Show quality monitor", ""), + ("Show quality monitor", "Exibir monitor de qualidade"), ("Disable clipboard", "Desabilitar área de transferência"), ("Lock after session end", "Bloquear após o fim da sessão"), ("Insert", "Inserir"), - ("Insert Lock", "Inserir Trava"), + ("Insert Lock", "Bloquear computador"), ("Refresh", "Atualizar"), ("ID does not exist", "ID não existe"), ("Failed to connect to rendezvous server", "Falha ao conectar ao servidor de rendezvous"), ("Please try later", "Por favor tente mais tarde"), - ("Remote desktop is offline", "Desktop remoto está offline"), + ("Remote desktop is offline", "O computador remoto está offline"), ("Key mismatch", "Chaves incompatíveis"), ("Timeout", "Tempo esgotado"), ("Failed to connect to relay server", "Falha ao conectar ao servidor de relay"), @@ -141,14 +141,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Click to download", "Clique para baixar"), ("Click to update", "Clique para fazer o update"), ("Configure", "Configurar"), - ("config_acc", "Para controlar seu Desktop remotamente, você precisa conceder ao RustDesk permissões de \"Acessibilidade\"."), - ("config_screen", "Para acessar seu Desktop remotamente, você precisa conceder ao RustDesk permissões de \"Gravar a Tela\"/"), + ("config_acc", "Para controlar seu computador remotamente, você precisa conceder ao RustDesk permissões de \"Acessibilidade\"."), + ("config_screen", "Para acessar seu computador remotamente, você precisa conceder ao RustDesk permissões de \"Gravar a Tela\"/"), ("Installing ...", "Instalando ..."), ("Install", "Instalar"), ("Installation", "Instalação"), ("Installation Path", "Caminho da Instalação"), - ("Create start menu shortcuts", "Criar atalhos no menu iniciar"), - ("Create desktop icon", "Criar ícone na área de trabalho"), + ("Create start menu shortcuts", "Criar atalhos no Menu Iniciar"), + ("Create desktop icon", "Criar ícone na Área de Trabalho"), ("agreement_tip", "Ao iniciar a instalação, você concorda com o acordo de licença."), ("Accept and Install", "Aceitar e Instalar"), ("End-user license agreement", "Acordo de licença do usuário final"), @@ -161,8 +161,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Action", "Ação"), ("Add", "Adicionar"), ("Local Port", "Porta Local"), - ("Local Address", ""), - ("Change Local Port", ""), + ("Local Address", "Endereço Local"), + ("Change Local Port", "Alterar Porta Local"), ("setup_server_tip", "Para uma conexão mais rápida, por favor configure seu próprio servidor"), ("Too short, at least 6 characters.", "Muito curto, pelo menos 6 caracteres."), ("The confirmation is not identical.", "A confirmação não é idêntica."), @@ -173,7 +173,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Allow using keyboard and mouse", "Permitir o uso de teclado e mouse"), ("Allow using clipboard", "Permitir o uso da área de transferência"), ("Allow hearing sound", "Permitir escutar som"), - ("Allow file copy and paste", "Permitir copiar e pegar arquivos"), + ("Allow file copy and paste", "Permitir copiar e colar arquivos"), ("Connected", "Conectado"), ("Direct and encrypted connection", "Conexão direta e criptografada"), ("Relayed and encrypted connection", "Conexão via relay e criptografada"), @@ -186,39 +186,39 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto Login", "Login Automático (Somente válido se você habilitou \"Bloquear após o fim da sessão\")"), ("Enable Direct IP Access", "Habilitar Acesso IP Direto"), ("Rename", "Renomear"), - ("Space", "Espaõ"), + ("Space", "Espaço"), ("Create Desktop Shortcut", "Criar Atalho na Área de Trabalho"), ("Change Path", "Alterar Caminho"), ("Create Folder", "Criar Diretório"), ("Please enter the folder name", "Por favor informe o nome do diretório"), - ("Fix it", "Conserte"), - ("Warning", "Aguardando"), + ("Fix it", "Corrigir"), + ("Warning", "Aviso"), ("Login screen using Wayland is not supported", "Tela de Login utilizando Wayland não é suportada"), ("Reboot required", "Reinicialização necessária"), ("Unsupported display server ", "Servidor de display não suportado"), ("x11 expected", "x11 esperado"), - ("Port", ""), + ("Port", "Porta"), ("Settings", "Configurações"), ("Username", "Nome de usuário"), ("Invalid port", "Porta inválida"), - ("Closed manually by the peer", "Fechada manualmente pelo par"), + ("Closed manually by the peer", "Fechada manualmente pelo parceiro"), ("Enable remote configuration modification", "Habilitar modificações de configuração remotas"), ("Run without install", "Executar sem instalar"), ("Always connected via relay", "Sempre conectado via relay"), ("Always connect via relay", "Sempre conectar via relay"), - ("whitelist_tip", "Somente IPs na whitelist podem me acessar"), + ("whitelist_tip", "Somente IPs confiáveis podem me acessar"), ("Login", "Login"), ("Logout", "Sair"), ("Tags", "Tags"), - ("Search ID", "Buscar ID"), + ("Search ID", "Pesquisar ID"), ("Current Wayland display server is not supported", "Servidor de display Wayland atual não é suportado"), ("whitelist_sep", "Separado por vírcula, ponto-e-vírgula, espaços ou nova linha"), ("Add ID", "Adicionar ID"), ("Add Tag", "Adicionar Tag"), - ("Unselect all tags", "Desselecionar todas as tags"), + ("Unselect all tags", "Desmarcar todas as tags"), ("Network error", "Erro de rede"), - ("Username missed", "Nome de usuário faltante"), - ("Password missed", "Senha faltante"), + ("Username missed", "Nome de usuário requerido"), + ("Password missed", "Senha requerida"), ("Wrong credentials", "Nome de usuário ou senha incorretos"), ("Edit Tag", "Editar Tag"), ("Unremember Password", "Esquecer Senha"), @@ -250,10 +250,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Mouse Wheel", "Roda do Mouse"), ("Two-Finger Move", "Mover com dois dedos"), ("Canvas Move", "Mover Tela"), - ("Pinch to Zoom", "Beliscar para Zoom"), - ("Canvas Zoom", "Zoom na Tela"), + ("Pinch to Zoom", "Pinçar para Zoom"), + ("Canvas Zoom", "Zoom na tela"), ("Reset canvas", "Reiniciar tela"), - ("No permission of file transfer", "Sem permissões de transferência de arquivo"), + ("No permission of file transfer", "Sem permissão para transferência de arquivo"), ("Note", "Nota"), ("Connection", "Conexão"), ("Share Screen", "Compartilhar Tela"), @@ -276,118 +276,118 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_new_connection_tip", "Nova requisição de controle recebida, solicita o controle de seu dispositivo atual."), ("android_service_will_start_tip", "Habilitar a Captura de Tela irá automaticamente inicalizar o serviço, permitindo que outros dispositivos solicitem uma conexão deste dispositivo."), ("android_stop_service_tip", "Fechar o serviço irá automaticamente fechar todas as conexões estabelecidas."), - ("android_version_audio_tip", "A versão atual do Android não suporta captura de áudio, por favor atualize para o Android 10 ou maior."), + ("android_version_audio_tip", "A versão atual do Android não suporta captura de áudio, por favor atualize para o Android 10 ou superior."), ("android_start_service_tip", "Toque [Iniciar Serviço] ou abra a permissão [Captura de Tela] para iniciar o serviço de compartilhamento de tela."), - ("Account", ""), + ("Account", "Conta"), ("Overwrite", "Substituir"), ("This file exists, skip or overwrite this file?", "Este arquivo existe, pular ou substituir este arquivo?"), - ("Quit", "Saída"), + ("Quit", "Sair"), ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), ("Help", "Ajuda"), ("Failed", "Falhou"), - ("Succeeded", "Conseguiu"), - ("Someone turns on privacy mode, exit", "Alguém liga o modo de privacidade, saia"), - ("Unsupported", "Sem suporte"), - ("Peer denied", "Par negado"), + ("Succeeded", "Sucesso"), + ("Someone turns on privacy mode, exit", "Alguém habilitou o modo de privacidade, sair"), + ("Unsupported", "Não suportado"), + ("Peer denied", "Parceiro negou"), ("Please install plugins", "Por favor instale plugins"), - ("Peer exit", "Saída de pares"), + ("Peer exit", "Parceiro saiu"), ("Failed to turn off", "Falha ao desligar"), ("Turned off", "Desligado"), ("In privacy mode", "No modo de privacidade"), ("Out privacy mode", "Fora do modo de privacidade"), - ("Language", ""), - ("Keep RustDesk background service", ""), - ("Ignore Battery Optimizations", ""), - ("android_open_battery_optimizations_tip", ""), - ("Connection not allowed", ""), - ("Legacy mode", ""), - ("Map mode", ""), - ("Translate mode", ""), - ("Use temporary password", ""), - ("Use permanent password", ""), - ("Use both passwords", ""), - ("Set permanent password", ""), - ("Set temporary password length", ""), - ("Enable Remote Restart", ""), - ("Allow remote restart", ""), - ("Restart Remote Device", ""), - ("Are you sure you want to restart", ""), - ("Restarting Remote Device", ""), + ("Language", "Idioma"), + ("Keep RustDesk background service", "Manter o serviço do RustDesk executando em segundo plano"), + ("Ignore Battery Optimizations", "Ignorar otimizações de bateria"), + ("android_open_battery_optimizations_tip", "Abrir otimizações de bateria"), + ("Connection not allowed", "Conexão não permitida"), + ("Legacy mode", "Modo legado"), + ("Map mode", "Modo mapa"), + ("Translate mode", "Modo traduzido"), + ("Use temporary password", "Utilizar senha temporária"), + ("Use permanent password", "Utilizar senha permanente"), + ("Use both passwords", "Utilizar ambas as senhas"), + ("Set permanent password", "Configurar senha permanente"), + ("Set temporary password length", "Configurar extensão da senha temporária"), + ("Enable Remote Restart", "Habilitar reinicialização remota"), + ("Allow remote restart", "Permitir reinicialização remota"), + ("Restart Remote Device", "Reiniciar dispositivo remoto"), + ("Are you sure you want to restart", "Você tem certeza que deseja reiniciar?"), + ("Restarting Remote Device", "Reiniciando dispositivo remoto"), ("remote_restarting_tip", ""), - ("Copied", ""), - ("Exit Fullscreen", ""), - ("Fullscreen", ""), - ("Mobile Actions", ""), - ("Select Monitor", ""), - ("Control Actions", ""), - ("Display Settings", ""), - ("Ratio", ""), - ("Image Quality", ""), - ("Scroll Style", ""), - ("Show Menubar", ""), - ("Hide Menubar", ""), - ("Direct Connection", ""), - ("Relay Connection", ""), - ("Secure Connection", ""), - ("Insecure Connection", ""), - ("Scale original", ""), - ("Scale adaptive", ""), - ("General", ""), - ("Security", ""), - ("Account", ""), - ("Theme", ""), - ("Dark Theme", ""), - ("Dark", ""), - ("Light", ""), - ("Follow System", ""), - ("Enable hardware codec", ""), - ("Unlock Security Settings", ""), - ("Enable Audio", ""), - ("Temporary Password Length", ""), - ("Unlock Network Settings", ""), - ("Server", ""), - ("Direct IP Access", ""), - ("Proxy", ""), - ("Port", ""), - ("Apply", ""), - ("Disconnect all devices?", ""), - ("Clear", ""), - ("Audio Input Device", ""), - ("Deny remote access", ""), - ("Use IP Whitelisting", ""), - ("Network", ""), - ("Enable RDP", ""), - ("Pin menubar", ""), - ("Unpin menubar", ""), - ("Recording", ""), - ("Directory", ""), - ("Automatically record incoming sessions", ""), - ("Change", ""), - ("Start session recording", ""), - ("Stop session recording", ""), - ("Enable Recording Session", ""), - ("Allow recording session", ""), - ("Enable LAN Discovery", ""), - ("Deny LAN Discovery", ""), - ("Write a message", ""), - ("Prompt", ""), - ("elevation_prompt", ""), - ("uac_warning", ""), - ("elevated_foreground_window_warning", ""), - ("Disconnected", ""), - ("Other", ""), - ("Confirm before closing multiple tabs", ""), - ("Keyboard Settings", ""), - ("Custom", ""), - ("Full Access", ""), - ("Screen Share", ""), - ("Wayland requires Ubuntu 21.04 or higher version.", ""), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", ""), - ("JumpLink", "View"), - ("Please Select the screen to be shared(Operate on the peer side).", ""), - ("Show RustDesk", ""), - ("This PC", ""), - ("or", ""), - ("Continue with", ""), + ("Copied", "Copiado"), + ("Exit Fullscreen", "Sair da Tela Cheia"), + ("Fullscreen", "Tela Cheia"), + ("Mobile Actions", "Ações móveis"), + ("Select Monitor", "Selecionar monitor"), + ("Control Actions", "Controlar ações"), + ("Display Settings", "Configurações de exibição"), + ("Ratio", "Proporção"), + ("Image Quality", "Qualidade de imagem"), + ("Scroll Style", "Estilo de rolagem"), + ("Show Menubar", "Exibir barra de menu"), + ("Hide Menubar", "Ocultar barra de menu"), + ("Direct Connection", "Conexão direta"), + ("Relay Connection", "Conexão relay"), + ("Secure Connection", "Conexão segura"), + ("Insecure Connection", "Conexão insegura"), + ("Scale original", "Escala original"), + ("Scale adaptive", "Escala adaptada"), + ("General", "Geral"), + ("Security", "Segurança"), + ("Account", "Conta"), + ("Theme", "Tema"), + ("Dark Theme", "Tema escuro"), + ("Dark", "Escuro"), + ("Light", "Claro"), + ("Follow System", "Seguir sistema"), + ("Enable hardware codec", "Habilitar codec de hardware"), + ("Unlock Security Settings", "Desabilitar configurações de segurança"), + ("Enable Audio", "Habilitar áudio"), + ("Temporary Password Length", "Extensão da senha temporária"), + ("Unlock Network Settings", "Desbloquear configurações de rede"), + ("Server", "Servidor"), + ("Direct IP Access", "Acesso direto por IP"), + ("Proxy", "Proxy"), + ("Port", "Porta"), + ("Apply", "Aplicar"), + ("Disconnect all devices?", "Desconectar todos os dispositivos?"), + ("Clear", "Limpar"), + ("Audio Input Device", "Dispositivo de entrada de áudio"), + ("Deny remote access", "Negar acesso remoto"), + ("Use IP Whitelisting", "Utilizar lista de IPs confiáveis"), + ("Network", "Rede"), + ("Enable RDP", "Habilitar RDP"), + ("Pin menubar", "Fixar barra de menu"), + ("Unpin menubar", "Desafixar barra de menu"), + ("Recording", "Gravando"), + ("Directory", "Diretório"), + ("Automatically record incoming sessions", "Gravar automaticamente sessões de entrada"), + ("Change", "Alterar"), + ("Start session recording", "Iniciar gravação da sessão"), + ("Stop session recording", "Parar gravação da sessão"), + ("Enable Recording Session", "Habilitar gravação da sessão"), + ("Allow recording session", "Permitir gravação da sessão"), + ("Enable LAN Discovery", "Habilitar descoberta da LAN"), + ("Deny LAN Discovery", "Negar descoberta da LAN"), + ("Write a message", "Escrever uma mensagem"), + ("Prompt", "Prompt de comando"), + ("elevation_prompt", "Prompt de comando (Admin)"), + ("uac_warning", "Aviso UAC"), + ("elevated_foreground_window_warning", "Aviso de janela de primeiro plano elevado"), + ("Disconnected", "Desconectado"), + ("Other", "Outro"), + ("Confirm before closing multiple tabs", "Confirmar antes de fechar múltiplas abas"), + ("Keyboard Settings", "Configurações de teclado"), + ("Custom", "Personalizado"), + ("Full Access", "Acesso completo"), + ("Screen Share", "Compartilhamento de tela"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requer Ubuntu 21.04 ou versão superior."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland requer uma versão superior da distribuição linux. Por favor, tente o desktop X11 ou mude seu sistema operacional."), + ("JumpLink", "JumpLink"), + ("Please Select the screen to be shared(Operate on the peer side).", "Por favor, selecione a tela a ser compartilhada (operar no lado do parceiro)."), + ("Show RustDesk", "Exibir RustDesk"), + ("This PC", "Este PC"), + ("or", "ou"), + ("Continue with", "Continuar com"), ].iter().cloned().collect(); } From 49e493aeb4ee4d4bd931259ecc177de4b47b1632 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 8 Nov 2022 10:22:34 +0800 Subject: [PATCH 07/27] flutter assets are missed for login ui Signed-off-by: fufesou --- flutter/assets/Github.svg | 1 + flutter/assets/Google.svg | 1 + flutter/assets/Okta.svg | 30 ++++++++++++++++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 flutter/assets/Github.svg create mode 100644 flutter/assets/Google.svg create mode 100644 flutter/assets/Okta.svg diff --git a/flutter/assets/Github.svg b/flutter/assets/Github.svg new file mode 100644 index 000000000..a5bd1de81 --- /dev/null +++ b/flutter/assets/Github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter/assets/Google.svg b/flutter/assets/Google.svg new file mode 100644 index 000000000..b7bb2f42f --- /dev/null +++ b/flutter/assets/Google.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter/assets/Okta.svg b/flutter/assets/Okta.svg new file mode 100644 index 000000000..0fa45b93d --- /dev/null +++ b/flutter/assets/Okta.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + From 03e041001cb60b4e54fde8cb95b0365d5cd26eaa Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 8 Nov 2022 11:07:20 +0800 Subject: [PATCH 08/27] fix_cm: show window before set size or alignment Signed-off-by: fufesou --- flutter/lib/models/chat_model.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index bc6582617..18a0be279 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:window_manager/window_manager.dart'; +import '../consts.dart'; import '../common.dart'; import '../common/widgets/overlay.dart'; import 'model.dart'; @@ -183,8 +184,11 @@ class ChatModel with ChangeNotifier { if (_isShowCMChatPage) { _isShowCMChatPage = !_isShowCMChatPage; notifyListeners(); - await windowManager.setSizeAlignment(Size(300, 400), Alignment.topRight); + await windowManager.show(); + await windowManager.setSizeAlignment( + kConnectionManagerWindowSize, Alignment.topRight); } else { + await windowManager.show(); await windowManager.setSizeAlignment(Size(600, 400), Alignment.topRight); _isShowCMChatPage = !_isShowCMChatPage; notifyListeners(); From 8984d16c75d661235aa8b568b7c95e2e2cb7ba7d Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 8 Nov 2022 12:01:51 +0800 Subject: [PATCH 09/27] desktop get mouse control by big distance Signed-off-by: fufesou --- flutter/lib/models/input_model.dart | 19 +++++++++++++++++-- flutter/lib/models/model.dart | 4 ++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 280c72e79..ed7e5309a 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -42,6 +42,7 @@ class InputModel { // mouse final isPhysicalMouse = false.obs; int _lastMouseDownButtons = 0; + Offset last_mouse_pos = Offset.zero; get id => parent.target?.id ?? ""; @@ -303,6 +304,22 @@ class InputModel { } void handleMouse(Map evt) { + double x = evt['x']; + double y = max(0.0, evt['y']); + final cursorModel = parent.target!.cursorModel; + + if (!cursorModel.got_mouse_control) { + bool self_get_control = (x - last_mouse_pos.dx).abs() > 12 || + (y - last_mouse_pos.dy).abs() > 12; + if (self_get_control) { + cursorModel.got_mouse_control = true; + } else { + last_mouse_pos = ui.Offset(x, y); + return; + } + } + last_mouse_pos = ui.Offset(x, y); + var type = ''; var isMove = false; switch (evt['type']) { @@ -319,8 +336,6 @@ class InputModel { return; } evt['type'] = type; - double x = evt['x']; - double y = max(0.0, evt['y']); if (isDesktop) { y = y - stateGlobal.tabBarHeight; } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index d8fdca2bc..41b3d056e 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -740,6 +740,7 @@ class CursorModel with ChangeNotifier { double _hoty = 0; double _displayOriginX = 0; double _displayOriginY = 0; + bool got_mouse_control = true; String id = ''; WeakReference parent; @@ -748,13 +749,11 @@ class CursorModel with ChangeNotifier { CursorData? get defaultCache => _getDefaultCache(); double get x => _x - _displayOriginX; - double get y => _y - _displayOriginY; Offset get offset => Offset(_x, _y); double get hotx => _hotx; - double get hoty => _hoty; CursorModel(this.parent); @@ -981,6 +980,7 @@ class CursorModel with ChangeNotifier { /// Update the cursor position. updateCursorPosition(Map evt, String id) async { + got_mouse_control = false; _x = double.parse(evt['x']); _y = double.parse(evt['y']); try { From 8fb664cce9900b674de701c11c0c3f328a0761e2 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 8 Nov 2022 13:37:08 +0800 Subject: [PATCH 10/27] desktop mouse better control Signed-off-by: fufesou --- flutter/lib/consts.dart | 8 +++++++- flutter/lib/models/input_model.dart | 10 ++++++++-- flutter/lib/models/model.dart | 11 +++++++++-- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index ea1142033..8287378e9 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -21,7 +21,7 @@ const String kTabLabelSettingPage = "Settings"; const String kWindowPrefix = "wm_"; -// the executable name of the portable version +// the executable name of the portable version const String kEnvPortableExecutable = "RUSTDESK_APPNAME"; const Color kColorWarn = Color.fromARGB(255, 245, 133, 59); @@ -60,6 +60,12 @@ const kInvalidValueStr = "InvalidValueStr"; const kMobilePageConstraints = BoxConstraints(maxWidth: 600); +/// [kMouseControlDistance] indicates the distance that self-side move to get control of mouse. +const kMouseControlDistance = 12; + +/// [kMouseControlTimeoutMSec] indicates the timeout (in milliseconds) that self-side can get control of mouse. +const kMouseControlTimeoutMSec = 1000; + /// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels /// see [LogicalKeyboardKey.keyLabel] const Map logicalKeyMap = { diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index ed7e5309a..30a01cda7 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -308,9 +308,15 @@ class InputModel { double y = max(0.0, evt['y']); final cursorModel = parent.target!.cursorModel; + if (cursorModel.is_peer_control_protected) { + last_mouse_pos = ui.Offset(x, y); + return; + } + if (!cursorModel.got_mouse_control) { - bool self_get_control = (x - last_mouse_pos.dx).abs() > 12 || - (y - last_mouse_pos.dy).abs() > 12; + bool self_get_control = + (x - last_mouse_pos.dx).abs() > kMouseControlDistance || + (y - last_mouse_pos.dy).abs() > kMouseControlDistance; if (self_get_control) { cursorModel.got_mouse_control = true; } else { diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 41b3d056e..be97b41ff 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -741,6 +741,8 @@ class CursorModel with ChangeNotifier { double _displayOriginX = 0; double _displayOriginY = 0; bool got_mouse_control = true; + DateTime _last_peer_mouse = DateTime.now() + .subtract(Duration(milliseconds: 2 * kMouseControlTimeoutMSec)); String id = ''; WeakReference parent; @@ -756,6 +758,10 @@ class CursorModel with ChangeNotifier { double get hotx => _hotx; double get hoty => _hoty; + bool get is_peer_control_protected => + DateTime.now().difference(_last_peer_mouse).inMilliseconds < + kMouseControlTimeoutMSec; + CursorModel(this.parent); Set get cachedKeys => _cacheKeys; @@ -917,7 +923,7 @@ class CursorModel with ChangeNotifier { if (parent.target?.id != pid) return; _image = image; _images[id] = Tuple3(image, _hotx, _hoty); - await _updateCacheLinux(image, id, width, height); + await _updateCache(image, id, width, height); try { // my throw exception, because the listener maybe already dispose notifyListeners(); @@ -926,7 +932,7 @@ class CursorModel with ChangeNotifier { } } - _updateCacheLinux(ui.Image image, int id, int w, int h) async { + _updateCache(ui.Image image, int id, int w, int h) async { Uint8List? data; img2.Image? image2; if (Platform.isWindows) { @@ -981,6 +987,7 @@ class CursorModel with ChangeNotifier { /// Update the cursor position. updateCursorPosition(Map evt, String id) async { got_mouse_control = false; + _last_peer_mouse = DateTime.now(); _x = double.parse(evt['x']); _y = double.parse(evt['y']); try { From 2cca5afa290af218007b390a4e5487174f5fe9b3 Mon Sep 17 00:00:00 2001 From: mehdi-song Date: Tue, 8 Nov 2022 09:45:15 +0330 Subject: [PATCH 11/27] Create fa.rs Persian Language --- src/lang/fa.rs | 391 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 391 insertions(+) create mode 100644 src/lang/fa.rs diff --git a/src/lang/fa.rs b/src/lang/fa.rs new file mode 100644 index 000000000..9f7408e7b --- /dev/null +++ b/src/lang/fa.rs @@ -0,0 +1,391 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "وضعیت"), + ("Your Desktop", "دسکتاپ شما"), + ("desk_tip", "دسکتاپ شما با این شناسه و رمز عبور قابل دسترسی است"), + ("Password", "رمز عبور"), + ("Ready", "آماده به کار"), + ("Established", "اتصال برقرار شد"), + ("connecting_status", "...در حال برقراری ارتباط با سرور"), + ("Enable Service", "فعالسازی سرویس"), + ("Start Service", "اجرا سرویس"), + ("Service is running", "سرویس در حال اجرا است"), + ("Service is not running", "سرویس اجرا نشده"), + ("not_ready_status", "ارتباط برقرار نشد. لطفا شبکه خود را بررسی کنید"), + ("Control Remote Desktop", "کنترل دسکتاپ میزبان"), + ("Transfer File", "جابه جایی فایل"), + ("Connect", "اتصال"), + ("Recent Sessions", "جلسات اخیر"), + ("Address Book", "دفترچه آدرس"), + ("Confirmation", "تایید"), + ("TCP Tunneling", "TCP تانل"), + ("Remove", "حذف"), + ("Refresh random password", "رمز عبور تصادفی را بروز کنید"), + ("Set your own password", "!رمز عبور دلخواه بگذارید"), + ("Enable Keyboard/Mouse", "Keyboard/Mouse فعالسازی"), + ("Enable Clipboard", "Clipboard فعالسازی"), + ("Enable File Transfer", "انتقال فایل را فعال کنید"), + ("Enable TCP Tunneling", "را فعال کنید TCP تانل"), + ("IP Whitelisting", "های مجاز IP لیست"), + ("ID/Relay Server", "ID/Relay سرور"), + ("Import Server Config", "تنظیم سرور با فایل"), + ("Export Server Config", "ایجاد فایل تظیمات از سرور فعلی"), + ("Import server configuration successfully", "تنظیمات سرور با فایل کانفیگ با موفقیت انجام شد"), + ("Export server configuration successfully", "ایجاد فایل کانفیگ از تنظیمات فعلی با موفقیت انجام شد"), + ("Invalid server configuration", "تنظیمات سرور نامعتبر است"), + ("Clipboard is empty", "خالی است Clipboard"), + ("Stop service", "توقف سرویس"), + ("Change ID", "تعویض شناسه"), + ("Website", "وب سایت"), + ("About", "درباره"), + ("Mute", "بستن صدا"), + ("Audio Input", "ورودی صدا"), + ("Enhancements", "بهبودها"), + ("Hardware Codec", "کدک سخت افزاری"), + ("Adaptive Bitrate", ""), + ("ID Server", "شناسه سرور"), + ("Relay Server", "Relay سرور"), + ("API Server", "API سرور"), + ("invalid_http", "شروع شود http:// یا https:// باید با"), + ("Invalid IP", "نامعتبر است IP آدرس"), + ("id_change_tip", "شناسه باید طبق این شرایط باشد : حروف کوچک و بزرگ انگلیسی و اعداد از 0 تا 9، _ و همچنین حرف اول آن فقط حروف بزرگ یا کوچک انگلیسی و طول آن بین 6 الی 16 کاراکتر باشد"), + ("Invalid format", "فرمت نادرس است"), + ("server_not_support", "هنوز توسط سرور مورد نظر پشتیبانی نمی شود"), + ("Not available", "در دسترسی نیست"), + ("Too frequent", "تعداد زیاد"), + ("Cancel", "لغو"), + ("Skip", "رد کردن"), + ("Close", "بستن"), + ("Retry", "تلاش مجدد"), + ("OK", "قبول"), + ("Password Required", "رمز عبور لازم است"), + ("Please enter your password", "رمز عبور خود را وارد کنید"), + ("Remember password", "رمز عبور را به خاطر بسپار"), + ("Wrong Password", "رمز عبور اشتباه است"), + ("Do you want to enter again?", "آیا میخواهید مجددا وارد شوید؟"), + ("Connection Error", "خطا در اتصال"), + ("Error", "خطا"), + ("Reset by the peer", "توسط میزبان حذف شد"), + ("Connecting...", "...در حال اتصال"), + ("Connection in progress. Please wait.", "در حال اتصال. لطفا متظر بمانید"), + ("Please try 1 minute later", "لطفا بعد از 1 دقیقه مجددا تلاش کنید"), + ("Login Error", "ورود ناموفق بود"), + ("Successful", "ورود با موفقیت انجام شد"), + ("Connected, waiting for image...", "ارتباط وصل شد. برای دریافت تصویر دسکتاپ میزبان منتظر بمانید..."), + ("Name", "نام"), + ("Type", "نوع فایل"), + ("Modified", "تاریخ تغییر"), + ("Size", "سایز"), + ("Show Hidden Files", "نمایش فایل های مخفی"), + ("Receive", "دریافت"), + ("Send", "ارسال"), + ("Refresh File", "به روزرسانی فایل"), + ("Local", "محلی"), + ("Remote", "از راه دور"), + ("Remote Computer", "سیستم میزبان"), + ("Local Computer", "سیستم از راه دور"), + ("Confirm Delete", "حذف را تایید کنید"), + ("Delete", "حذف"), + ("Properties", "Properties"), + ("Multi Select", "انتخاب همزمان"), + ("Select All", "انتخاب همه"), + ("Unselect All", "عدم انتخاب همه"), + ("Empty Directory", "پوشه خالی"), + ("Not an empty directory", "پوشه خالی نیست"), + ("Are you sure you want to delete this file?", "از حذف این فایل مطمئن هستید؟"), + ("Are you sure you want to delete this empty directory?", "از حذف این پوشه خالی مطمئن هستید؟"), + ("Are you sure you want to delete the file of this directory?", "از حذف فایل موجود در این پوشه مطمئن هستید؟"), + ("Do this for all conflicts", "این عمل را برای همه ی تضادها انجام شود"), + ("This is irreversible!", "این برگشت ناپذیر است!"), + ("Deleting", "در حال حذف"), + ("files", "فایل ها"), + ("Waiting", "انتظار"), + ("Finished", "تکمیل شد"), + ("Speed", "سرعت"), + ("Custom Image Quality", "سفارشی سازی کیفیت تصاویر"), + ("Privacy mode", "حالت حریم خصوصی"), + ("Block user input", "ورودی کاربر را مسدود کنید"), + ("Unblock user input", "قفل ورودی کاربر را باز کنید"), + ("Adjust Window", "پنجره را تنظیم کنید"), + ("Original", "اصل"), + ("Shrink", ""), + ("Stretch", ""), + ("Scrollbar", ""), + ("ScrollAuto", ""), + ("Good image quality", "کیفیت خوب تصویر"), + ("Balanced", "متعادل"), + ("Optimize reaction time", "زمان واکنش را بهینه کنید"), + ("Custom", "سفارشی"), + ("Show remote cursor", "نمایش مکان نما موس میزبان"), + ("Show quality monitor", "نمایش کیفیت مانیتور"), + ("Disable clipboard", "Clipboard غیرفعالسازی"), + ("Lock after session end", "قفل کردن حساب کاربری سیستم عامل پس از پایان جلسه"), + ("Insert", "افزودن"), + ("Insert Lock", "افزودن قفل"), + ("Refresh", "تازه سازی"), + ("ID does not exist", "شناسه وجود ندارد"), + ("Failed to connect to rendezvous server", "اتصال به سرور تولید شناسه انجام نشد"), + ("Please try later", "لطفا بعدا تلاش کنید"), + ("Remote desktop is offline", "دسکتاپ از راه دور خاموش است"), + ("Key mismatch", "عدم تطابق کلید"), + ("Timeout", "زمان انتظار به پایان رسید"), + ("Failed to connect to relay server", "سرور وصل نشد Relay به"), + ("Failed to connect via rendezvous server", "اتصال از طریق سرور تولید شناسه انجام نشد"), + ("Failed to connect via relay server", "انجام نشد Relay اتصال از طریق سرور"), + ("Failed to make direct connection to remote desktop", "اتصال مستقیم به دسکتاپ از راه دور با موفقیت انجام نشد"), + ("Set Password", "اختصاص رمزعبور"), + ("OS Password", "رمز عیور سیستم عامل"), + ("install_tip", "لطفا برنامه را نصب کنید UAC و جلوگیری از خطای RustDesk برای راحتی در استفاده از نرم افزار"), + ("Click to upgrade", "برای ارتقا کلیک کنید"), + ("Click to download", "برای دانلود کلیک کنید"), + ("Click to update", "برای به روز رسانی کلیک کنید"), + ("Configure", "تنظیم"), + ("config_acc", "برای کنترل از راه دور دسکتاپ، باید به RustDesk مجوز \"access\" بدهید"), + ("config_screen", "برای دسترسی از راه دور به دسکتاپ خود، باید به RustDesk مجوزهای \"screenshot\" بدهید."), + ("Installing ...", "در حال نصب..."), + ("Install", "نصب"), + ("Installation", "نصب و راه اندازی"), + ("Installation Path", "محل نصب"), + ("Create start menu shortcuts", "Start ایجاد میانبرها در منوی"), + ("Create desktop icon", "ایجاد آیکن در دسکتاپ"), + ("agreement_tip", "با شروع نصب، شرایط توافق نامه مجوز را می پذیرید"), + ("Accept and Install", "قبول و شروع نصب"), + ("End-user license agreement", "قرارداد مجوز کاربر نهایی"), + ("Generating ...", "پدید آوردن..."), + ("Your installation is lower version.", "نسخه قبلی نصب شده است"), + ("not_close_tcp_tip", "هنگام استفاده از تونل این پنجره را نبندید"), + ("Listening ...", "انتظار..."), + ("Remote Host", "دستگاه از راه دور"), + ("Remote Port", "پورت راه دور"), + ("Action", "عملیات"), + ("Add", "افزودن"), + ("Local Port", "پورت محلی"), + ("Local Address", "آدرس محلی"), + ("Change Local Port", "تغییر پورت محلی"), + ("setup_server_tip", "برای اتصال سریعتر، سرور اتصال خود را راه اندازی کنید"), + ("Too short, at least 6 characters.", "بسیار کوتاه حداقل 6 کاراکتر مورد نیاز است"), + ("The confirmation is not identical.", "تأیید ناموفق بود."), + ("Permissions", "دسترسی ها"), + ("Accept", "پذیرفتن"), + ("Dismiss", "رد کردن"), + ("Disconnect", "قطع اتصال"), + ("Allow using keyboard and mouse", "اجازه استفاده از صفحه کلید و ماوس را بدهید"), + ("Allow using clipboard", "را بدهید Clipboard اجازه استفاده از"), + ("Allow hearing sound", "اجازه شنیدن صدا را بدهید"), + ("Allow file copy and paste", "اجازه کپی و چسباندن فایل را بدهید"), + ("Connected", "متصل شده"), + ("Direct and encrypted connection", "اتصال مستقیم و رمزگذاری شده"), + ("Relayed and encrypted connection", "و رمزگذاری شده Relay اتصال از طریق"), + ("Direct and unencrypted connection", "اتصال مستقیم و بدون رمزگذاری"), + ("Relayed and unencrypted connection", "و رمزگذاری نشده Relay اتصال از طریق"), + ("Enter Remote ID", "شناسه از راه دور را وارد کنید"), + ("Enter your password", "زمر عبور خود را وارد کنید"), + ("Logging in...", "در حال ورود..."), + ("Enable RDP session sharing", "اشتراک گذاری جلسه RDP را فعال کنید"), + ("Auto Login", "ورود خودکار"), + ("Enable Direct IP Access", "دسترسی مستقیم IP را فعال کنید"), + ("Rename", "تغییر نام"), + ("Space", "فضا"), + ("Create Desktop Shortcut", "ساخت میانبر روی دسکتاپ"), + ("Change Path", "تغییر مسیر"), + ("Create Folder", "ایجاد پوشه"), + ("Please enter the folder name", "نام پوشه را وارد کنید"), + ("Fix it", "بازسازی"), + ("Warning", "هشدار"), + ("Login screen using Wayland is not supported", "ورود به سیستم با استفاده از Wayland پشتیبانی نمی شود"), + ("Reboot required", "راه اندازی مجدد مورد نیاز است"), + ("Unsupported display server ", "سرور تصویر پشتیبانی نشده است"), + ("x11 expected", ""), + ("Port", "پورت"), + ("Settings", "تنظیمات"), + ("Username", "نام کاربری"), + ("Invalid port", "پورت نامعتبر است"), + ("Closed manually by the peer", "به صورت دستی توسط میزبان بسته شد"), + ("Enable remote configuration modification", "تغییرات پیکربندی از راه دور را مجاز کنید"), + ("Run without install", "بدون نصب اجرا شود"), + ("Always connected via relay", "متصل است Relay همیشه با"), + ("Always connect via relay", "برای اتصال استفاده کنید Relay از"), + ("whitelist_tip", "فقط آدرس های IP مجاز می توانند به این دسکتاپ متصل شوند"), + ("Login", "ورود"), + ("Logout", "خروج"), + ("Tags", "برچسب ها"), + ("Search ID", "جستجوی شناسه"), + ("Current Wayland display server is not supported", "سرور نمای فعلی Wayland پشتیبانی نمی شود"), + ("whitelist_sep", "با کاما، نقطه ویرگول، فاصله یا خط جدید از هم جدا می شوند"), + ("Add ID", "افزودن شناسه"), + ("Add Tag", "افزودن برچسب"), + ("Unselect all tags", "همه برچسب ها را لغو انتخاب کنید"), + ("Network error", "خطای شبکه"), + ("Username missed", "نام کاربری وجود ندارد"), + ("Password missed", "رمزعبور وجود ندارد"), + ("Wrong credentials", "اعتبارنامه نادرست است"), + ("Edit Tag", "برچسب را تغییر دهید"), + ("Unremember Password", "رمز عبور را ذخیره نکنید"), + ("Favorites", "موارد دلخواه"), + ("Add to Favorites", "افزودن به علاقه مندی ها"), + ("Remove from Favorites", "از علاقه مندی ها حذف شود"), + ("Empty", "موردی وجود ندارد"), + ("Invalid folder name", "نام پوشه نامعتبر است"), + ("Socks5 Proxy", "Socks5 Proxy"), + ("Hostname", "Hostname"), + ("Discovered", "پیدا شده"), + ("install_daemon_tip", "برای شروع در هنگام راه اندازی، باید سرویس سیستم را نصب کنید"), + ("Remote ID", "شناسه از راه دور"), + ("Paste", "درج کنید"), + ("Paste here?", "اینجا درج شود؟"), + ("Are you sure to close the connection?", "آیا مطمئن هستید که می خواهید اتصال را پایان دهید؟"), + ("Download new version", "دانلود نسخه جدید"), + ("Touch mode", "حالت لمسی"), + ("Mouse mode", "حالت ماوس"), + ("One-Finger Tap", "با یک انگشت لمس کنید"), + ("Left Mouse", "دکمه سمت چپ ماوس"), + ("One-Long Tap", "لمس طولانی با یک انگشت"), + ("Two-Finger Tap", "با دو انگشت لمس کنید"), + ("Right Mouse", "دکمه سمت راست ماوس"), + ("One-Finger Move", "با یک انگشت حرکت کنید"), + ("Double Tap & Move", "دو ضربه سریع بزنید و حرکت دهید"), + ("Mouse Drag", "کشیدن ماوس"), + ("Three-Finger vertically", "سه انگشت عمودی"), + ("Mouse Wheel", "چرخ ماوس"), + ("Two-Finger Move", "با دو انگشت حرکت کنید"), + ("Canvas Move", ""), + ("Pinch to Zoom", "زوم را کوچک کنید"), + ("Canvas Zoom", ""), + ("Reset canvas", ""), + ("No permission of file transfer", "مجوز انتقال فایل داده نشده"), + ("Note", "یادداشت"), + ("Connection", "ارتباط"), + ("Share Screen", "اشتراک گذاری صفحه"), + ("CLOSE", "بستن"), + ("OPEN", "باز کردن"), + ("Chat", "چت"), + ("Total", "مجموع"), + ("items", "موارد"), + ("Selected", "انتخاب شده"), + ("Screen Capture", "ضبط صفحه"), + ("Input Control", "کنترل ورودی"), + ("Audio Capture", "ضبط صدا"), + ("File Connection", "ارتباط فایل"), + ("Screen Connection", "ارتباط صفحه"), + ("Do you accept?", "شما می پذیرید؟"), + ("Open System Setting", "باز کردن تنظیمات سیستم"), + ("How to get Android input permission?", "چگونه مجوز ورود به سیستم اندروید را دریافت کنیم؟"), + ("android_input_permission_tip1", "برای اینکه یک دستگاه راه دور بتواند دستگاه Android شما را از طریق ماوس یا لمسی کنترل کند، باید به RustDesk اجازه دهید از ویژگی \"Accessibility\" استفاده کند."), + ("android_input_permission_tip2", "به صفحه تنظیمات سیستم زیر بروید، \"Installed Services\" را پیدا کرده و وارد کنید، سرویس \"RustDesk Input\" را فعال کنید"), + ("android_new_connection_tip", "درخواست جدیدی برای مدیریت دستگاه فعلی شما دریافت شده است."), + ("android_service_will_start_tip", "فعال کردن ضبط صفحه به طور خودکار سرویس را راه اندازی می کند و به دستگاه های دیگر امکان می دهد درخواست اتصال به آن دستگاه را داشته باشند."), + ("android_stop_service_tip", "با بستن سرویس، تمام اتصالات برقرار شده به طور خودکار بسته می شود"), + ("android_version_audio_tip", "نسخه فعلی اندروید از ضبط صدا پشتیبانی نمی‌کند، لطفاً به اندروید 10 یا بالاتر به‌روزرسانی کنید"), + ("android_start_service_tip", "برای شروع سرویس اشتراک‌گذاری صفحه، روی مجوز \"شروع مرحله‌بندی سرور\" یا OPEN \"Screen Capture\" کلیک کنید."), + ("Account", "حساب"), + ("Overwrite", "بازنویسی"), + ("This file exists, skip or overwrite this file?", "این فایل وجود دارد، از فایل رد شود یا بازنویسی شود؟"), + ("Quit", "خروج"), + ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), + ("Help", "راهنما"), + ("Failed", "ناموفق"), + ("Succeeded", "موفقیت آمیز"), + ("Someone turns on privacy mode, exit", "اگر شخصی حالت حریم خصوصی را روشن کرد، خارج شوید"), + ("Unsupported", "پشتیبانی نشده"), + ("Peer denied", "توسط میزبان راه دور رد شد"), + ("Please install plugins", "لطفا افزونه ها را نصب کنید"), + ("Peer exit", "میزبان خارج شد"), + ("Failed to turn off", "خاموش کردن با موفقیت انجام نشد"), + ("Turned off", "خاموش شد"), + ("In privacy mode", "در حالت حریم خصوصی"), + ("Out privacy mode", "خارج از حالت حریم خصوصی"), + ("Language", "زبان"), + ("Keep RustDesk background service", "سرویس RustDesk را در پس زمینه نگه دارید"), + ("Ignore Battery Optimizations", "بهینه سازی باتری را نادیده بگیرید"), + ("android_open_battery_optimizations_tip", "به صفحه تنظیمات بعدی بروید"), + ("Connection not allowed", "اتصال مجاز نیست"), + ("Legacy mode", "پشتیبانی موارد قدیمی"), + ("Map mode", "حالت نقشه"), + ("Translate mode", "حالت ترجمه"), + ("Use temporary password", "از رمز عبور موقت استفاده کنید"), + ("Use permanent password", "از رمز عبور دائمی استفاده کنید"), + ("Use both passwords", "از هر دو رمز عبور استفاده کنید"), + ("Set permanent password", "یک رمز عبور دائمی تنظیم کنید"), + ("Set temporary password length", "تنظیم طول رمز عبور موقت"), + ("Enable Remote Restart", "فعال کردن راه‌اندازی مجدد از راه دور"), + ("Allow remote restart", "اجازه راه اندازی مجدد از راه دور"), + ("Restart Remote Device", "راه‌اندازی مجدد دستگاه از راه دور"), + ("Are you sure you want to restart", "ایا مطمئن هستید میخواهید راه اندازی مجدد انجام بدید؟"), + ("Restarting Remote Device", "راه اندازی مجدد یک دستگاه راه دور"), + ("remote_restarting_tip", "دستگاه راه دور دوباره راه اندازی می شود. این پیام را ببندید و پس از مدتی با استفاده از یک رمز عبور دائمی دوباره وصل شوید."), + ("Copied", "کپی شده است"), + ("Exit Fullscreen", "از حالت تمام صفحه خارج شوید"), + ("Fullscreen", "تمام صفحه"), + ("Mobile Actions", "اقدامات موبایل"), + ("Select Monitor", "مانیتور را انتخاب کنید"), + ("Control Actions", "اقدامات مدیریتی"), + ("Display Settings", "تنظیمات نمایشگر"), + ("Ratio", "نسبت"), + ("Image Quality", "کیفیت تصویر"), + ("Scroll Style", "سبک اسکرول"), + ("Show Menubar", "نمایش نوار منو"), + ("Hide Menubar", "پنهان کردن نوار منو"), + ("Direct Connection", "ارتباط مستقیم"), + ("Relay Connection", "Relay ارتباط"), + ("Secure Connection", "ارتباط امن"), + ("Insecure Connection", "ارتباط غیر امن"), + ("Scale original", "مقیاس اصلی"), + ("Scale adaptive", "مقیاس تطبیقی"), + ("General", "عمومی"), + ("Security", "امنیت"), + ("Account", "حساب کاربری"), + ("Theme", "نمایه"), + ("Dark Theme", "نمایه تیره"), + ("Dark", "تیره"), + ("Light", "روشن"), + ("Follow System", "سیستم را دنبال کنید"), + ("Enable hardware codec", "از کدک سخت افزاری استفاده کنید"), + ("Unlock Security Settings", "تنظیمات امنیتی را باز کنید"), + ("Enable Audio", "صدا را روشن کنید"), + ("Temporary Password Length", "طول رمز عبور موقت"), + ("Unlock Network Settings", "باز کردن قفل تنظیمات شبکه"), + ("Server", "سرور"), + ("Direct IP Access", "دسترسی مستقیم به IP"), + ("Proxy", "پروکسی"), + ("Port", "پورت"), + ("Apply", "ثبت"), + ("Disconnect all devices?", "همه دستگاه ها را غیرفعال کنید؟"), + ("Clear", "پاک کردن"), + ("Audio Input Device", "منبع صدا"), + ("Deny remote access", "دسترسی از راه دور را رد کنید"), + ("Use IP Whitelisting", "از لیست سفید IP استفاده کنید"), + ("Network", "شبکه"), + ("Enable RDP", "RDP را فعال کنید"), + ("Pin menubar", "نوار منو ثابت کنید"), + ("Unpin menubar", "پین نوار منو را بردارید"), + ("Recording", "در حال ضبط"), + ("Directory", "مسیر"), + ("Automatically record incoming sessions", "ضبط خودکار جلسات ورودی"), + ("Change", "تغییر"), + ("Start session recording", "شروع ضبط جلسه"), + ("Stop session recording", "توقف ضبط جلسه"), + ("Enable Recording Session", "فعالسازی ضبط جلسه"), + ("Allow recording session", "مجوز ضبط جلسه"), + ("Enable LAN Discovery", "فعالسازی جستجو در شبکه"), + ("Deny LAN Discovery", "غیر فعالسازی جستجو در شبکه"), + ("Write a message", "یک پیام بنویسید"), + ("Prompt", ""), + ("elevation_prompt", "اجرای نرم‌افزار بدون افزایش امتیاز می‌تواند باعث ایجاد مشکلاتی در هنگام کار کردن کاربران راه دور با ویندوزهای خاص شود"), + ("uac_warning", "به دلیل درخواست دسترسی سطح بالا، به طور موقت از دسترسی رد شد. منتظر بمانید تا کاربر راه دور گفتگوی UAC را بپذیرد. برای جلوگیری از این مشکل، توصیه می شود نرم افزار را روی دستگاه از راه دور نصب کنید یا آن را با دسترسی مدیر اجرا کنید."), + ("elevated_foreground_window_warning", "به طور موقت استفاده از ماوس و صفحه کلید امکان پذیر نیست زیرا پنجره دسکتاپ از راه دور فعلی برای کار کردن به دسترسی های بالاتر نیاز دارد، می توانید از کاربر راه دور بخواهید که پنجره فعلی را به حداقل برساند. برای جلوگیری از این مشکل، توصیه می شود نرم افزار را روی یک دستگاه راه دور نصب کنید یا آن را با دسترسی مدیر اجرا کنید"), + ("Disconnected", "قطع ارتباط"), + ("Other", "دیگر"), + ("Confirm before closing multiple tabs", "بستن چندین برگه را تأیید کنید"), + ("Keyboard Settings", "تنظیمات صفحه کلید"), + ("Custom", "سفارشی"), + ("Full Access", "دسترسی کامل"), + ("Screen Share", "اشتراک گذاری صفحه"), + ("Wayland requires Ubuntu 21.04 or higher version.", ""), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", ""), + ("JumpLink", ""), + ("Please Select the screen to be shared(Operate on the peer side).", "لطفاً صفحه‌ای را برای اشتراک‌گذاری انتخاب کنید (در سمت همتا به همتا کار کنید)."), + ("Show RustDesk", "RustDesk را نشان دهید"), + ("This PC", "This PC"), + ].iter().cloned().collect(); +} From c9a00f824733e95b2615d76a31644ce18b91b2ed Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 8 Nov 2022 15:52:41 +0800 Subject: [PATCH 12/27] add fa in lang.rs --- src/lang.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lang.rs b/src/lang.rs index 25e7a3931..db59e9f54 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -23,6 +23,7 @@ mod tw; mod vn; mod kz; mod ua; +mod fa; lazy_static::lazy_static! { pub static ref LANGS: Value = @@ -49,6 +50,7 @@ lazy_static::lazy_static! { ("ko", "한국어"), ("kz", "Қазақ"), ("ua", "Українська"), + ("fa", "فارسی"), ]); } @@ -99,6 +101,7 @@ pub fn translate_locale(name: String, locale: &str) -> String { "ko" => ko::T.deref(), "kz" => kz::T.deref(), "ua" => ua::T.deref(), + "fa" => fa::T.deref(), _ => en::T.deref(), }; if let Some(v) = m.get(&name as &str) { From 253c8efeb59ff2f622980e9c5a9e2091b8edb187 Mon Sep 17 00:00:00 2001 From: "Miguel F. G" <116861809+flusheDData@users.noreply.github.com> Date: Tue, 8 Nov 2022 10:08:39 +0100 Subject: [PATCH 13/27] Update es.rs Two new terms translated --- src/lang/es.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 4064d0fd7..7582926f3 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -387,7 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Seleccione la pantalla que se compartirá (Operar en el lado del par)."), ("Show RustDesk", "Mostrar RustDesk"), ("This PC", "Este PC"), - ("or", ""), - ("Continue with", ""), + ("or", "o"), + ("Continue with", "Continuar con"), ].iter().cloned().collect(); } From bf4a337d7a3f8c913a713fb9c6e605ac869a59a0 Mon Sep 17 00:00:00 2001 From: mehdi-song Date: Tue, 8 Nov 2022 14:13:48 +0330 Subject: [PATCH 14/27] Update fa.rs --- src/lang/fa.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 9f7408e7b..f9a15dc9e 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "لطفاً صفحه‌ای را برای اشتراک‌گذاری انتخاب کنید (در سمت همتا به همتا کار کنید)."), ("Show RustDesk", "RustDesk را نشان دهید"), ("This PC", "This PC"), + ("or", "یا"), + ("Continue with", "ادامه با"), ].iter().cloned().collect(); } From 7e06851f784873188b6f63b8301b1e07efb1c0b4 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 8 Nov 2022 10:42:07 +0800 Subject: [PATCH 15/27] add: rpm flutter --- res/rpm-flutter.spec | 88 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 res/rpm-flutter.spec diff --git a/res/rpm-flutter.spec b/res/rpm-flutter.spec new file mode 100644 index 000000000..b40709845 --- /dev/null +++ b/res/rpm-flutter.spec @@ -0,0 +1,88 @@ +Name: rustdesk +Version: 1.2.0 +Release: 0 +Summary: RPM package +License: GPL-3.0 +Requires: gtk3 libxcb libxdo libXfixes pipewire alsa-lib curl libayatana-appindicator3-1 libvdpau1 libva2 + + +%description +The best open-source remote desktop client software, written in Rust. + +%prep +# we have no source, so nothing here + +%build +# we have no source, so nothing here + +# %global __python %{__python3} + +%install + +mkdir -p "${buildroot}/usr/lib/rustdesk" && cp -r ${HBB}/flutter/build/linux/x64/release/bundle/* -t "${buildroot}/usr/lib/rustdesk" +mkdir -p "${buildroot}/usr/bin" +pushd ${buildroot} && ln -s /usr/lib/rustdesk/rustdesk usr/bin/rustdesk && popd +install -Dm 644 $HBB/res/rustdesk.service -t "${buildroot}/usr/share/rustdesk/files" +install -Dm 644 $HBB/res/rustdesk.desktop -t "${buildroot}/usr/share/rustdesk/files" +install -Dm 644 $HBB/res/rustdesk-link.desktop -t "${buildroot}/usr/share/rustdesk/files" +install -Dm 644 $HBB/res/128x128@2x.png "${buildroot}/usr/share/rustdesk/files/rustdesk.png" + + +%files +/usr/bin/rustdesk +/usr/lib/rustdesk/* +/usr/share/rustdesk/files/rustdesk.service +/usr/share/rustdesk/files/rustdesk.png +/usr/share/rustdesk/files/rustdesk.desktop +/usr/share/rustdesk/files/rustdesk-link.desktop + +%changelog +# let's skip this for now + +# https://www.cnblogs.com/xingmuxin/p/8990255.html +%pre +# can do something for centos7 +case "$1" in + 1) + # for install + ;; + 2) + # for upgrade + systemctl stop rustdesk || true + ;; +esac + +%post +cp /usr/share/rustdesk/files/rustdesk.service /etc/systemd/system/rustdesk.service +cp /usr/share/rustdesk/files/rustdesk.desktop /usr/share/applications/ +cp /usr/share/rustdesk/files/rustdesk-link.desktop /usr/share/applications/ +systemctl daemon-reload +systemctl enable rustdesk +systemctl start rustdesk +update-desktop-database + +%preun +case "$1" in + 0) + # for uninstall + systemctl stop rustdesk || true + systemctl disable rustdesk || true + rm /etc/systemd/system/rustdesk.service || true + ;; + 1) + # for upgrade + ;; +esac + +%postun +case "$1" in + 0) + # for uninstall + rm /usr/share/applications/rustdesk.desktop || true + rm /usr/share/applications/rustdesk-link.desktop || true + update-desktop-database + ;; + 1) + # for upgrade + ;; +esac From 400911f78235469537e966f15f8e0d78c8663f56 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 8 Nov 2022 11:16:48 +0800 Subject: [PATCH 16/27] refactor: change ayaindicator to appindicator --- .github/workflows/flutter-ci.yml | 2 +- .github/workflows/flutter-nightly.yml | 55 ++++++++++++++++++++++++++- .gitignore | 1 + Cargo.toml | 2 +- README.md | 2 +- flatpak/rustdesk.json | 33 ++++++++++++++++ flatpak/rustdesk.yml | 31 --------------- res/PKGBUILD | 2 +- res/rpm-flutter.spec | 2 +- res/rpm.spec | 4 +- 10 files changed, 94 insertions(+), 40 deletions(-) create mode 100644 flatpak/rustdesk.json delete mode 100644 flatpak/rustdesk.yml diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index 8b58db83f..7825286bd 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -38,7 +38,7 @@ jobs: shell: bash run: | case ${{ matrix.job.target }} in - x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libayatana-appindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev;; + x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev;; # arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;; # aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;; esac diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 3062001de..6f6d26047 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -11,6 +11,7 @@ env: FLUTTER_VERSION: "3.0.5" TAG_NAME: "nightly" VCPKG_COMMIT_ID: '6ca56aeb457f033d344a7106cb3f9f1abf8f4e98' + VERSION: "1.2.0" jobs: build-for-windows: @@ -113,7 +114,7 @@ jobs: - name: Install prerequisites run: | case ${{ matrix.job.target }} in - x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libayatana-appindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev;; + x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev;; # arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;; # aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;; esac @@ -220,7 +221,6 @@ jobs: python ttf-arphic-uming libappindicator-gtk3 - libayatana-appindicator scripts: | cd res && HBB=`pwd`/.. FLUTTER=1 makepkg -f @@ -231,3 +231,54 @@ jobs: tag_name: ${{ env.TAG_NAME }} files: | res/rustdesk*.zst + + build-flatpak: + needs: [build-for-linux] + runs-on: ubuntu-18.04 + strategy: + fail-fast: false + matrix: + job: + # - { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } + # - { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true } + # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } + # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } + # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + # - { target: x86_64-apple-darwin , os: macos-10.15 } + - { target: x86_64-unknown-linux-gnu , os: ubuntu-18.04, arch: "x86_64"} + # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Install dependencies + run: | + sudo apt update + sudo apt install -y flatpak flatpak-builder cmake g++ gcc git curl wget nasm yasm libgtk-3-dev + + - name: Download Binary + run: | + wget https://github.com/rustdesk/rustdesk/releases/download/nightly/rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + mv rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb rustdesk-${{ env.VERSION }}.deb + + - name: Install Flatpak deps + run: | + flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/21.08 + flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/21.08 + + - name: Make Flatpak package + run: | + pushd flatpak + git clone https://github.com/flathub/shared-modules.git --depth=1 + flatpak-builder --user --force-clean --repo=repo ./build ./rustdesk.json + flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak org.rustdesk.rustdesk + + - name: Publish flatpak package + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak + diff --git a/.gitignore b/.gitignore index d1e9666bd..1ecea7af8 100644 --- a/.gitignore +++ b/.gitignore @@ -36,5 +36,6 @@ flatpak/.flatpak-builder/build/** flatpak/.flatpak-builder/shared-modules/** flatpak/.flatpak-builder/shared-modules/*.tar.xz flatpak/.flatpak-builder/debian-binary +flatpak/build/** # bridge file lib/generated_bridge.dart diff --git a/Cargo.toml b/Cargo.toml index 1eb92c4e4..a3c8b3d94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -147,7 +147,7 @@ hound = "3.5" name = "RustDesk" identifier = "com.carriez.rustdesk" icon = ["res/32x32.png", "res/128x128.png", "res/128x128@2x.png"] -deb_depends = ["libgtk-3-0", "libxcb-randr0", "libxdo3", "libxfixes3", "libxcb-shape0", "libxcb-xfixes0", "libasound2", "libsystemd0", "pipewire", "curl", "libayatana-appindicator3-1", "libvdpau1", "libva2"] +deb_depends = ["libgtk-3-0", "libxcb-randr0", "libxdo3", "libxfixes3", "libxcb-shape0", "libxcb-xfixes0", "libasound2", "libsystemd0", "pipewire", "curl", "libappindicator3-1", "libvdpau1", "libva2"] osx_minimum_system_version = "10.14" resources = ["res/mac-tray-light.png","res/mac-tray-dark.png"] diff --git a/README.md b/README.md index 59eef0ba0..dfaa389a6 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ Please download sciter dynamic library yourself. ```sh sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \ libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \ - libclang-dev ninja-build libayatana-appindicator3-1 libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libayatana-appindicator3-dev + libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev ``` ### openSUSE Tumbleweed diff --git a/flatpak/rustdesk.json b/flatpak/rustdesk.json new file mode 100644 index 000000000..4cb5bccc4 --- /dev/null +++ b/flatpak/rustdesk.json @@ -0,0 +1,33 @@ +{ + "app-id": "org.rustdesk.rustdesk", + "runtime": "org.freedesktop.Platform", + "runtime-version": "21.08", + "sdk": "org.freedesktop.Sdk", + "command": "rustdesk", + "modules": [ + "shared-modules/libappindicator/libappindicator-gtk3-12.10.json", + { + "name": "rustdesk", + "buildsystem": "simple", + "build-commands": [ + "bsdtar -zxvf rustdesk-1.2.0.deb", + "tar -xvf ./data.tar.xz", + "cp -r ./usr /app/", + "mkdir -p /app/bin && ln -s /app/usr/lib/rustdesk/rustdesk /app/bin/rustdesk" + ], + "sources": [ + { + "type": "file", + "path": "../rustdesk-1.2.0.deb" + } + ] + } + ], + "finish-args": [ + "--share=ipc", + "--socket=x11", + "--socket=wayland", + "--share=network", + "--filesystem=xdg-documents" + ] +} \ No newline at end of file diff --git a/flatpak/rustdesk.yml b/flatpak/rustdesk.yml deleted file mode 100644 index 3d7936635..000000000 --- a/flatpak/rustdesk.yml +++ /dev/null @@ -1,31 +0,0 @@ -app-id: org.rustdesk.rustdesk -runtime: org.freedesktop.Platform -runtime-version: '21.08' -sdk: org.freedesktop.Sdk -command: rustdesk -modules: - # install appindicator - - shared-modules/libappindicator/libappindicator-gtk3-12.10.json - - name: rustdesk - buildsystem: simple - build-commands: - - bsdtar -zxvf rustdesk-1.2.0.deb - - tar -xvf ./data.tar.xz - - cp -r ./usr /app/ - - rm /app/usr/bin/rustdesk - - mkdir -p /app/bin && ln -s /app/usr/lib/rustdesk/flutter_hbb /app/bin/rustdesk - sources: - # Note: replace to deb files with url - - type: file - path: ../rustdesk-1.2.0.deb - -finish-args: - # X11 + XShm access - - --share=ipc - - --socket=x11 - # Wayland access - - --socket=wayland - # Needs to talk to the network: - - --share=network - # Needs to save files locally - - --filesystem=xdg-documents \ No newline at end of file diff --git a/res/PKGBUILD b/res/PKGBUILD index 7c97d419e..bf8d7ba6e 100644 --- a/res/PKGBUILD +++ b/res/PKGBUILD @@ -7,7 +7,7 @@ arch=('x86_64') url="" license=('AGPL-3.0') groups=() -depends=('gtk3' 'xdotool' 'libxcb' 'libxfixes' 'alsa-lib' 'pipewire' 'curl' 'libva' 'libvdpau' 'libayatana-appindicator') +depends=('gtk3' 'xdotool' 'libxcb' 'libxfixes' 'alsa-lib' 'pipewire' 'curl' 'libva' 'libvdpau' 'libappindicator-gtk3') makedepends=() checkdepends=() optdepends=() diff --git a/res/rpm-flutter.spec b/res/rpm-flutter.spec index b40709845..a01926baa 100644 --- a/res/rpm-flutter.spec +++ b/res/rpm-flutter.spec @@ -3,7 +3,7 @@ Version: 1.2.0 Release: 0 Summary: RPM package License: GPL-3.0 -Requires: gtk3 libxcb libxdo libXfixes pipewire alsa-lib curl libayatana-appindicator3-1 libvdpau1 libva2 +Requires: gtk3 libxcb libxdo libXfixes pipewire alsa-lib curl libappindicator libvdpau1 libva2 %description diff --git a/res/rpm.spec b/res/rpm.spec index e88fe1fda..a85f9fcbd 100644 --- a/res/rpm.spec +++ b/res/rpm.spec @@ -1,9 +1,9 @@ Name: rustdesk -Version: 1.1.9 +Version: 1.2.0 Release: 0 Summary: RPM package License: GPL-3.0 -Requires: gtk3 libxcb libxdo libXfixes pipewire alsa-lib curl libayatana-appindicator3-1 libvdpau1 libva2 +Requires: gtk3 libxcb libxdo libXfixes pipewire alsa-lib curl libappindicator libvdpau1 libva2 %description The best open-source remote desktop client software, written in Rust. From d0aedaedce7706a90260af5d23c05f2af4c8ae46 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 8 Nov 2022 14:55:04 +0800 Subject: [PATCH 17/27] opt: transfer file through artifact --- .github/workflows/flutter-nightly.yml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 6f6d26047..c2988c782 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -188,7 +188,13 @@ jobs: prerelease: true tag_name: ${{ env.TAG_NAME }} files: | - rustdesk*.deb + rustdesk-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + + - name: Upload Artifcat + uses: actions/upload-artifact@master + with: + name: rustdesk-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + path: rustdesk-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - name: Build archlinux package uses: vufa/arch-makepkg-action@master @@ -233,8 +239,9 @@ jobs: res/rustdesk*.zst build-flatpak: + name: Build Flatpak needs: [build-for-linux] - runs-on: ubuntu-18.04 + runs-on: ${{ matrix.job.os }} strategy: fail-fast: false matrix: @@ -257,9 +264,14 @@ jobs: sudo apt install -y flatpak flatpak-builder cmake g++ gcc git curl wget nasm yasm libgtk-3-dev - name: Download Binary + uses: actions/download-artifact@master + with: + name: rustdesk-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + path: . + + - name: Rename Binary run: | - wget https://github.com/rustdesk/rustdesk/releases/download/nightly/rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - mv rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb rustdesk-${{ env.VERSION }}.deb + mv rustdesk-${{ matrix.job.target }}-${{ matrix.job.os }}.deb rustdesk-${{ env.VERSION }}.deb - name: Install Flatpak deps run: | From 1109598131c500734a8a68a018caa6c086a1c377 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 8 Nov 2022 16:52:45 +0800 Subject: [PATCH 18/27] fix: nightly ci --- .github/workflows/flutter-nightly.yml | 38 ++++++++++++++++++++------- build.py | 7 +++++ flatpak/rustdesk.json | 7 ++++- flatpak/xdotool.json | 15 +++++++++++ src/ui_interface.rs | 2 ++ 5 files changed, 58 insertions(+), 11 deletions(-) create mode 100644 flatpak/xdotool.json diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index c2988c782..fae447d6b 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -93,7 +93,7 @@ jobs: rustdesk-*.exe build-for-linux: - name: ${{ matrix.job.target }} (${{ matrix.job.os }}) + name: ${{ matrix.job.target }} (${{ matrix.job.os }},${{ matrix.job.extra-build-args }}) runs-on: ${{ matrix.job.os }} strategy: fail-fast: false @@ -105,7 +105,8 @@ jobs: # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } # - { target: x86_64-apple-darwin , os: macos-10.15 } - - { target: x86_64-unknown-linux-gnu , os: ubuntu-18.04} + - { target: x86_64-unknown-linux-gnu , os: ubuntu-18.04, extra-build-args: ""} + - { target: x86_64-unknown-linux-gnu , os: ubuntu-18.04, extra-build-args: "--flatpak"} # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } steps: - name: Checkout source code @@ -160,7 +161,7 @@ jobs: - name: Install cargo bundle tools run: | - cargo install cargo-bundle --force + cargo install cargo-bundle - name: Show version information (Rust, cargo, GCC) shell: bash @@ -173,7 +174,7 @@ jobs: rustc -V - name: Build rustdesk - run: ./build.py --flutter --hwcodec + run: ./build.py --flutter --hwcodec ${{ matrix.job.extra-build-args }} - name: Rename rustdesk shell: bash @@ -188,15 +189,17 @@ jobs: prerelease: true tag_name: ${{ env.TAG_NAME }} files: | - rustdesk-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - name: Upload Artifcat uses: actions/upload-artifact@master + if: ${{ contains(matrix.job.extra-build-args, 'flatpak') }} with: - name: rustdesk-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - path: rustdesk-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + path: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - name: Build archlinux package + if: ${{ matrix.job.extra-build-args == '' }} uses: vufa/arch-makepkg-action@master with: packages: > @@ -231,6 +234,7 @@ jobs: cd res && HBB=`pwd`/.. FLUTTER=1 makepkg -f - name: Publish archlinux package + if: ${{ matrix.job.extra-build-args == '' }} uses: softprops/action-gh-release@v1 with: prerelease: true @@ -238,6 +242,20 @@ jobs: files: | res/rustdesk*.zst + # - name: build RPM package + # id: rpm + # uses: Kingtous/rustdesk-rpmbuild@master + # with: + # spec_file: "res/rpm-flutter.spec" + + # - name: Publish fedora28/centos8 package + # uses: softprops/action-gh-release@v1 + # with: + # prerelease: true + # tag_name: ${{ env.TAG_NAME }} + # files: | + # ${{ steps.rpm.outputs.rpm_dir_path }}/* + build-flatpak: name: Build Flatpak needs: [build-for-linux] @@ -252,7 +270,7 @@ jobs: # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } # - { target: x86_64-apple-darwin , os: macos-10.15 } - - { target: x86_64-unknown-linux-gnu , os: ubuntu-18.04, arch: "x86_64"} + - { target: x86_64-unknown-linux-gnu , os: ubuntu-18.04, arch: x86_64} # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } steps: - name: Checkout source code @@ -266,12 +284,12 @@ jobs: - name: Download Binary uses: actions/download-artifact@master with: - name: rustdesk-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb path: . - name: Rename Binary run: | - mv rustdesk-${{ matrix.job.target }}-${{ matrix.job.os }}.deb rustdesk-${{ env.VERSION }}.deb + mv rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb rustdesk-${{ env.VERSION }}.deb - name: Install Flatpak deps run: | diff --git a/build.py b/build.py index 3e7ce853e..7a02dfbcd 100755 --- a/build.py +++ b/build.py @@ -81,6 +81,11 @@ def make_parser(): action='store_true', help='Build windows portable' ) + parser.add_argument( + '--flatpak', + action='store_true', + help='Build rustdesk libs with the flatpak feature enabled' + ) return parser @@ -188,6 +193,8 @@ def get_features(args): features.append('hwcodec') if args.flutter: features.append('flutter') + if args.flatpak: + features.append('flatpak') print("features:", features) return features diff --git a/flatpak/rustdesk.json b/flatpak/rustdesk.json index 4cb5bccc4..d7f6e316e 100644 --- a/flatpak/rustdesk.json +++ b/flatpak/rustdesk.json @@ -6,6 +6,7 @@ "command": "rustdesk", "modules": [ "shared-modules/libappindicator/libappindicator-gtk3-12.10.json", + "xdotool.json", { "name": "rustdesk", "buildsystem": "simple", @@ -26,8 +27,12 @@ "finish-args": [ "--share=ipc", "--socket=x11", + "--socket=fallback-x11", "--socket=wayland", "--share=network", - "--filesystem=xdg-documents" + "--filesystem=home", + "--device=dri", + "--socket=pulseaudio", + "--talk-name=org.freedesktop.Flatpak" ] } \ No newline at end of file diff --git a/flatpak/xdotool.json b/flatpak/xdotool.json new file mode 100644 index 000000000..d7f41bf0e --- /dev/null +++ b/flatpak/xdotool.json @@ -0,0 +1,15 @@ +{ + "name": "xdotool", + "buildsystem": "simple", + "build-commands": [ + "make -j4 && PREFIX=./build make install", + "cp -r ./build/* /app/" + ], + "sources": [ + { + "type": "archive", + "url": "https://github.com/jordansissel/xdotool/releases/download/v3.20211022.1/xdotool-3.20211022.1.tar.gz", + "sha256": "96f0facfde6d78eacad35b91b0f46fecd0b35e474c03e00e30da3fdd345f9ada" + } + ] +} diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 04ba90cb2..92c63e310 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -807,6 +807,8 @@ pub fn is_root() -> bool { #[inline] pub fn check_super_user_permission() -> bool { + #[cfg(feature = "flatpak")] + return true; #[cfg(any(windows, target_os = "linux"))] return crate::platform::check_super_user_permission().unwrap_or(false); #[cfg(not(any(windows, target_os = "linux")))] From 6a50296fc345af624562a4eb454bd7d7abc41ccf Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 8 Nov 2022 21:49:29 +0800 Subject: [PATCH 19/27] opt: add more deps --- build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.py b/build.py index 7a02dfbcd..c907334ce 100755 --- a/build.py +++ b/build.py @@ -208,7 +208,7 @@ Version: %s Architecture: amd64 Maintainer: open-trade Homepage: https://rustdesk.com -Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, pipewire, curl +Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, pipewire, curl, libappindicator3-1, libva-drm2, libva-x11-2, libvdpau1 Description: A remote control software. """ % version From 26b92932f585f0cd607c4fb497597ea14ac9be35 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 8 Nov 2022 22:15:01 +0800 Subject: [PATCH 20/27] update hwcodec, compile ffmpeg on ubuntu18, glibc 2.27 Signed-off-by: 21pages --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 3fddb5c37..9243933e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2463,7 +2463,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hwcodec" version = "0.1.0" -source = "git+https://github.com/21pages/hwcodec#1f03d203eca24dc976c21a47228f3bc31484c2bc" +source = "git+https://github.com/21pages/hwcodec#bf73e8e650abca3e004e96a245086b3647b9d84a" dependencies = [ "bindgen", "cc", From 60e8dd840f0008190a80f76e4357166b1ee89fa7 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 9 Nov 2022 15:04:24 +0800 Subject: [PATCH 21/27] Remote side has a higher priority on mouse control Signed-off-by: fufesou --- src/hbbs_http.rs | 6 +-- src/server/input_service.rs | 74 +++++++++++++++++++++++++++++-------- src/tray.rs | 7 +++- src/ui_interface.rs | 7 +++- src/ui_session_interface.rs | 3 +- 5 files changed, 72 insertions(+), 25 deletions(-) diff --git a/src/hbbs_http.rs b/src/hbbs_http.rs index 4360b6e8c..ceb3a6081 100644 --- a/src/hbbs_http.rs +++ b/src/hbbs_http.rs @@ -1,12 +1,8 @@ -use hbb_common::{ - anyhow::{self, bail}, - tokio, ResultType, -}; use reqwest::blocking::Response; use serde::de::DeserializeOwned; -use serde_derive::Deserialize; use serde_json::{Map, Value}; +#[cfg(feature = "flutter")] pub mod account; #[derive(Debug)] diff --git a/src/server/input_service.rs b/src/server/input_service.rs index d91e9a799..9b43c5850 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -1,4 +1,5 @@ use super::*; +#[cfg(target_os = "linux")] use crate::common::IS_X11; #[cfg(target_os = "macos")] use dispatch::Queue; @@ -7,6 +8,7 @@ use hbb_common::{config::COMPRESS_LEVEL, get_time, protobuf::EnumOrUnknown}; use rdev::{simulate, EventType, Key as RdevKey}; use std::{ convert::TryFrom, + ops::Sub, sync::atomic::{AtomicBool, Ordering}, time::Instant, }; @@ -100,8 +102,16 @@ pub fn new_pos() -> GenericService { sp } +fn update_last_cursor_pos(x: i32, y: i32) { + let mut lock = LATEST_CURSOR_POS.lock().unwrap(); + if lock.1 .0 != x || lock.1 .1 != y { + (lock.0, lock.1) = (Instant::now(), (x, y)) + } +} + fn run_pos(sp: GenericService, state: &mut StatePos) -> ResultType<()> { if let Some((x, y)) = crate::get_cursor_pos() { + update_last_cursor_pos(x, y); if state.cursor_pos.0 != x || state.cursor_pos.1 != y { state.cursor_pos = (x, y); let mut msg_out = Message::new(); @@ -112,7 +122,7 @@ fn run_pos(sp: GenericService, state: &mut StatePos) -> ResultType<()> { }); let exclude = { let now = get_time(); - let lock = LATEST_INPUT.lock().unwrap(); + let lock = LATEST_INPUT_CURSOR.lock().unwrap(); if now - lock.time < 300 { lock.conn } else { @@ -170,10 +180,15 @@ lazy_static::lazy_static! { Arc::new(Mutex::new(Enigo::new())) }; static ref KEYS_DOWN: Arc>> = Default::default(); - static ref LATEST_INPUT: Arc> = Default::default(); + static ref LATEST_INPUT_CURSOR: Arc> = Default::default(); + static ref LATEST_INPUT_CURSOR_POS: Arc>> = Default::default(); + static ref LATEST_CURSOR_POS: Arc> = Arc::new(Mutex::new((Instant::now().sub(MOUSE_MOVE_PROTECTION_TIMEOUT), (0, 0)))); } static EXITING: AtomicBool = AtomicBool::new(false); +const MOUSE_MOVE_PROTECTION_TIMEOUT: Duration = Duration::from_millis(1_000); +const MOUSE_ACTIVE_DISTANCE: i32 = 5; + // mac key input must be run in main thread, otherwise crash on >= osx 10.15 #[cfg(target_os = "macos")] lazy_static::lazy_static! { @@ -357,17 +372,43 @@ fn fix_modifiers(modifiers: &[EnumOrUnknown], en: &mut Enigo, ck: i3 } } +fn is_mouse_active_by_conn(conn: i32) -> bool { + if LATEST_CURSOR_POS.lock().unwrap().0.elapsed() > MOUSE_MOVE_PROTECTION_TIMEOUT { + return true; + } + + match LATEST_INPUT_CURSOR_POS.lock().unwrap().get(&conn) { + Some((x, y)) => match crate::get_cursor_pos() { + Some((x2, y2)) => { + (x - x2).abs() < MOUSE_ACTIVE_DISTANCE && (y - y2).abs() < MOUSE_ACTIVE_DISTANCE + } + None => true, + }, + None => true, + } +} + fn handle_mouse_(evt: &MouseEvent, conn: i32) { if EXITING.load(Ordering::SeqCst) { return; } + + if !is_mouse_active_by_conn(conn) { + return; + } + #[cfg(windows)] crate::platform::windows::try_change_desktop(); let buttons = evt.mask >> 3; let evt_type = evt.mask & 0x7; if evt_type == 0 { let time = get_time(); - *LATEST_INPUT.lock().unwrap() = Input { time, conn }; + *LATEST_INPUT_CURSOR.lock().unwrap() = Input { time, conn }; + + LATEST_INPUT_CURSOR_POS + .lock() + .unwrap() + .insert(conn, (evt.x, evt.y)); } let mut en = ENIGO.lock().unwrap(); #[cfg(not(target_os = "macos"))] @@ -432,7 +473,10 @@ fn handle_mouse_(evt: &MouseEvent, conn: i32) { // fix shift + scroll(down/up) #[cfg(target_os = "macos")] - if evt.modifiers.contains(&EnumOrUnknown::new(ControlKey::Shift)){ + if evt + .modifiers + .contains(&EnumOrUnknown::new(ControlKey::Shift)) + { x = y; y = 0; } @@ -653,19 +697,19 @@ fn sync_status(evt: &KeyEvent) -> (bool, bool) { let code = evt.chr(); let key = rdev::get_win_key(code, 0); match key { - RdevKey::Home | - RdevKey::UpArrow | - RdevKey::PageUp | - RdevKey::LeftArrow | - RdevKey::RightArrow | - RdevKey::End | - RdevKey::DownArrow | - RdevKey::PageDown | - RdevKey::Insert | - RdevKey::Delete => en.get_key_state(enigo::Key::NumLock), + RdevKey::Home + | RdevKey::UpArrow + | RdevKey::PageUp + | RdevKey::LeftArrow + | RdevKey::RightArrow + | RdevKey::End + | RdevKey::DownArrow + | RdevKey::PageDown + | RdevKey::Insert + | RdevKey::Delete => en.get_key_state(enigo::Key::NumLock), _ => click_numlock, } - }; + }; return (click_capslock, click_numlock); } diff --git a/src/tray.rs b/src/tray.rs index 8e9092fbf..80647fa17 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -1,10 +1,13 @@ -use hbb_common::log::{debug, error, info}; +use hbb_common::log::debug; +#[cfg(target_os = "linux")] +use hbb_common::log::{error, info}; #[cfg(target_os = "linux")] use libappindicator::AppIndicator; +#[cfg(target_os = "linux")] use std::env::temp_dir; use std::{ collections::HashMap, - sync::{Arc, Mutex, RwLock}, + sync::{Arc, Mutex}, }; #[cfg(target_os = "windows")] use trayicon::{MenuBuilder, TrayIconBuilder}; diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 92c63e310..29b09addc 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -20,7 +20,9 @@ use hbb_common::{ tokio::{self, sync::mpsc, time}, }; -use crate::{common::SOFTWARE_UPDATE_URL, hbbs_http::account, ipc, platform}; +use crate::{common::SOFTWARE_UPDATE_URL, ipc, platform}; +#[cfg(feature = "flutter")] +use crate::hbbs_http::account; type Message = RendezvousMessage; @@ -844,14 +846,17 @@ pub(crate) fn check_connect_status(reconnect: bool) -> mpsc::UnboundedSender String { serde_json::to_string(&account::OidcSession::get_result()).unwrap_or_default() } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 2f5543ead..bf03ed2d3 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -22,7 +22,6 @@ use std::collections::{HashMap, HashSet}; use std::ops::{Deref, DerefMut}; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex, RwLock}; -use std::time::Duration; /// IS_IN KEYBOARD_HOOKED sciter only pub static IS_IN: AtomicBool = AtomicBool::new(false); @@ -1323,7 +1322,7 @@ impl Session { #[cfg(any(target_os = "windows", target_os = "macos"))] std::thread::spawn(move || { let func = move |event: Event| match event.event_type { - EventType::KeyPress(key) | EventType::KeyRelease(key) => { + EventType::KeyPress(..) | EventType::KeyRelease(..) => { // grab all keys if !IS_IN.load(Ordering::SeqCst) || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) From d9b6c33de3bd26aedb1b7466b5c0f938f8a10abb Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 9 Nov 2022 15:06:10 +0800 Subject: [PATCH 22/27] remove pipewire for the time being, because some old distro only have pulseaudio, pipewire/pulseaudio seems to be installed already since firefox requires it --- Cargo.toml | 2 +- res/PKGBUILD | 2 +- res/rpm-suse.spec | 2 +- res/rpm.spec | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a3c8b3d94..5dc54d58b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -147,7 +147,7 @@ hound = "3.5" name = "RustDesk" identifier = "com.carriez.rustdesk" icon = ["res/32x32.png", "res/128x128.png", "res/128x128@2x.png"] -deb_depends = ["libgtk-3-0", "libxcb-randr0", "libxdo3", "libxfixes3", "libxcb-shape0", "libxcb-xfixes0", "libasound2", "libsystemd0", "pipewire", "curl", "libappindicator3-1", "libvdpau1", "libva2"] +deb_depends = ["libgtk-3-0", "libxcb-randr0", "libxdo3", "libxfixes3", "libxcb-shape0", "libxcb-xfixes0", "libasound2", "libsystemd0", "curl", "libappindicator3-1", "libvdpau1", "libva2"] osx_minimum_system_version = "10.14" resources = ["res/mac-tray-light.png","res/mac-tray-dark.png"] diff --git a/res/PKGBUILD b/res/PKGBUILD index bf8d7ba6e..cff61516f 100644 --- a/res/PKGBUILD +++ b/res/PKGBUILD @@ -7,7 +7,7 @@ arch=('x86_64') url="" license=('AGPL-3.0') groups=() -depends=('gtk3' 'xdotool' 'libxcb' 'libxfixes' 'alsa-lib' 'pipewire' 'curl' 'libva' 'libvdpau' 'libappindicator-gtk3') +depends=('gtk3' 'xdotool' 'libxcb' 'libxfixes' 'alsa-lib' 'curl' 'libva' 'libvdpau' 'libappindicator-gtk3') makedepends=() checkdepends=() optdepends=() diff --git a/res/rpm-suse.spec b/res/rpm-suse.spec index c328f42aa..5d03d9c8a 100644 --- a/res/rpm-suse.spec +++ b/res/rpm-suse.spec @@ -3,7 +3,7 @@ Version: 1.1.9 Release: 0 Summary: RPM package License: GPL-3.0 -Requires: gtk3 libxcb1 xdotool libXfixes3 pipewire alsa-utils curl libXtst6 libayatana-appindicator3-1 libvdpau1 libva2 +Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils curl libXtst6 libayatana-appindicator3-1 libvdpau1 libva2 %description The best open-source remote desktop client software, written in Rust. diff --git a/res/rpm.spec b/res/rpm.spec index a85f9fcbd..b2a3e27e1 100644 --- a/res/rpm.spec +++ b/res/rpm.spec @@ -3,7 +3,7 @@ Version: 1.2.0 Release: 0 Summary: RPM package License: GPL-3.0 -Requires: gtk3 libxcb libxdo libXfixes pipewire alsa-lib curl libappindicator libvdpau1 libva2 +Requires: gtk3 libxcb libxdo libXfixes alsa-lib curl libappindicator libvdpau1 libva2 %description The best open-source remote desktop client software, written in Rust. From a85f775b3af25dfb149bad141cc6576dceb19b15 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 9 Nov 2022 16:22:31 +0800 Subject: [PATCH 23/27] simplier mouse control Signed-off-by: fufesou --- src/server/input_service.rs | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 9b43c5850..c8b099b6d 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -39,10 +39,12 @@ impl super::service::Reset for StatePos { } } -#[derive(Default)] +#[derive(Default, Clone, Copy)] struct Input { conn: i32, time: i64, + x: i32, + y: i32, } const KEY_CHAR_START: u64 = 9999; @@ -181,7 +183,6 @@ lazy_static::lazy_static! { }; static ref KEYS_DOWN: Arc>> = Default::default(); static ref LATEST_INPUT_CURSOR: Arc> = Default::default(); - static ref LATEST_INPUT_CURSOR_POS: Arc>> = Default::default(); static ref LATEST_CURSOR_POS: Arc> = Arc::new(Mutex::new((Instant::now().sub(MOUSE_MOVE_PROTECTION_TIMEOUT), (0, 0)))); } static EXITING: AtomicBool = AtomicBool::new(false); @@ -373,17 +374,23 @@ fn fix_modifiers(modifiers: &[EnumOrUnknown], en: &mut Enigo, ck: i3 } fn is_mouse_active_by_conn(conn: i32) -> bool { + // out of time protection if LATEST_CURSOR_POS.lock().unwrap().0.elapsed() > MOUSE_MOVE_PROTECTION_TIMEOUT { return true; } - match LATEST_INPUT_CURSOR_POS.lock().unwrap().get(&conn) { - Some((x, y)) => match crate::get_cursor_pos() { - Some((x2, y2)) => { - (x - x2).abs() < MOUSE_ACTIVE_DISTANCE && (y - y2).abs() < MOUSE_ACTIVE_DISTANCE - } - None => true, - }, + let last_input = *LATEST_INPUT_CURSOR.lock().unwrap(); + // last conn input may be protected + if last_input.conn != conn { + return false; + } + + // check if input is in valid range + match crate::get_cursor_pos() { + Some((x, y)) => { + (last_input.x - x).abs() < MOUSE_ACTIVE_DISTANCE + && (last_input.y - y).abs() < MOUSE_ACTIVE_DISTANCE + } None => true, } } @@ -403,12 +410,12 @@ fn handle_mouse_(evt: &MouseEvent, conn: i32) { let evt_type = evt.mask & 0x7; if evt_type == 0 { let time = get_time(); - *LATEST_INPUT_CURSOR.lock().unwrap() = Input { time, conn }; - - LATEST_INPUT_CURSOR_POS - .lock() - .unwrap() - .insert(conn, (evt.x, evt.y)); + *LATEST_INPUT_CURSOR.lock().unwrap() = Input { + time, + conn, + x: evt.x, + y: evt.y, + }; } let mut en = ENIGO.lock().unwrap(); #[cfg(not(target_os = "macos"))] From 5424881d40a6de5e94ac3d50ccd2a978fce91e05 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 9 Nov 2022 16:35:08 +0800 Subject: [PATCH 24/27] better mouse control Signed-off-by: fufesou --- src/server/input_service.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index c8b099b6d..170d672c9 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -379,7 +379,7 @@ fn is_mouse_active_by_conn(conn: i32) -> bool { return true; } - let last_input = *LATEST_INPUT_CURSOR.lock().unwrap(); + let mut last_input = LATEST_INPUT_CURSOR.lock().unwrap(); // last conn input may be protected if last_input.conn != conn { return false; @@ -388,8 +388,13 @@ fn is_mouse_active_by_conn(conn: i32) -> bool { // check if input is in valid range match crate::get_cursor_pos() { Some((x, y)) => { - (last_input.x - x).abs() < MOUSE_ACTIVE_DISTANCE - && (last_input.y - y).abs() < MOUSE_ACTIVE_DISTANCE + let is_same_input = (last_input.x - x).abs() < MOUSE_ACTIVE_DISTANCE + && (last_input.y - y).abs() < MOUSE_ACTIVE_DISTANCE; + if !is_same_input { + last_input.x = -MOUSE_ACTIVE_DISTANCE * 2; + last_input.y = -MOUSE_ACTIVE_DISTANCE * 2; + } + is_same_input } None => true, } From 200d8dc0f5c2431df290a596173751ad9183c1aa Mon Sep 17 00:00:00 2001 From: Kingtous Date: Wed, 9 Nov 2022 15:14:11 +0800 Subject: [PATCH 25/27] fix: last window location calculation --- flutter/lib/common.dart | 24 ++++++++++--------- .../desktop/pages/file_manager_tab_page.dart | 3 +-- .../desktop/pages/port_forward_tab_page.dart | 3 +-- .../lib/desktop/pages/remote_tab_page.dart | 8 ++----- .../lib/desktop/widgets/tabbar_widget.dart | 13 +++++----- flutter/lib/main.dart | 14 +++++++++++ 6 files changed, 38 insertions(+), 27 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index ff69302f2..a652aa915 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'dart:math'; import 'dart:typed_data'; import 'package:back_button_interceptor/back_button_interceptor.dart'; @@ -1036,6 +1037,7 @@ Future saveWindowPosition(WindowType type, {int? windowId}) async { final isMaximized = await wc.isMaximized(); final pos = LastWindowPosition( sz.width, sz.height, position.dx, position.dy, isMaximized); + print("saving frame: ${windowId}: ${pos.width}/${pos.height}, offset:${pos.offsetWidth}/${pos.offsetHeight}"); await Get.find() .setString(kWindowPrefix + type.name, pos.toString()); break; @@ -1081,7 +1083,7 @@ Future _adjustRestoreMainWindowSize(double? width, double? height) async { restoreWidth = maxWidth; } if (restoreHeight > maxHeight) { - restoreWidth = maxHeight; + restoreHeight = maxHeight; } return Size(restoreWidth, restoreHeight); } @@ -1092,11 +1094,11 @@ Future _adjustRestoreMainWindowOffset( if (left == null || top == null) { await windowManager.center(); } else { - double windowLeft = left; - double windowTop = top; + double windowLeft = max(0.0, left); + double windowTop = max(0.0, top); - double frameLeft = 0; - double frameTop = 0; + double frameLeft = double.infinity; + double frameTop = double.infinity; double frameRight = ((isDesktop || isWebDesktop) ? kDesktopMaxDisplayWidth : kMobileMaxDisplayWidth) @@ -1107,12 +1109,11 @@ Future _adjustRestoreMainWindowOffset( .toDouble(); if (isDesktop || isWebDesktop) { - final screen = (await window_size.getWindowInfo()).screen; - if (screen != null) { - frameLeft = screen.visibleFrame.left; - frameTop = screen.visibleFrame.top; - frameRight = screen.visibleFrame.right; - frameBottom = screen.visibleFrame.bottom; + for(final screen in await window_size.getScreenList()) { + frameLeft = min(screen.visibleFrame.left, frameLeft); + frameTop = min(screen.visibleFrame.top, frameTop); + frameRight = max(screen.visibleFrame.right, frameRight); + frameBottom = max(screen.visibleFrame.bottom, frameBottom); } } @@ -1174,6 +1175,7 @@ Future restoreWindowPosition(WindowType type, {int? windowId}) async { await _adjustRestoreMainWindowSize(lpos.width, lpos.height); final offset = await _adjustRestoreMainWindowOffset( lpos.offsetWidth, lpos.offsetHeight); + print("restore lpos: ${size.width}/${size.height}, offset:${offset?.dx}/${offset?.dy}"); if (offset == null) { await wc.center(); } else { diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 44440d4b1..d0f6c800e 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -96,8 +96,7 @@ class _FileManagerTabPageState extends State { void onRemoveId(String id) { if (tabController.state.value.tabs.isEmpty) { - WindowController.fromWindowId(windowId()).hide(); - rustDeskWinManager.call(WindowType.Main, kWindowEventHide, {"id": windowId()}); + WindowController.fromWindowId(windowId()).close(); } } diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index 94f652a76..403afe343 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -107,8 +107,7 @@ class _PortForwardTabPageState extends State { void onRemoveId(String id) { if (tabController.state.value.tabs.isEmpty) { - WindowController.fromWindowId(windowId()).hide(); - rustDeskWinManager.call(WindowType.Main, kWindowEventHide, {"id": windowId()}); + WindowController.fromWindowId(windowId()).close(); } } diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 94c54c7b5..3068f2db7 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -97,9 +97,6 @@ class _ConnectionTabPageState extends State { } _update_remote_count(); }); - Future.delayed(Duration.zero, () { - restoreWindowPosition(WindowType.RemoteDesktop, windowId: windowId()); - }); } @override @@ -321,10 +318,9 @@ class _ConnectionTabPageState extends State { ); } - void onRemoveId(String id) { + void onRemoveId(String id) async { if (tabController.state.value.tabs.isEmpty) { - WindowController.fromWindowId(windowId()).hide(); - rustDeskWinManager.call(WindowType.Main, kWindowEventHide, {"id": windowId()}); + await WindowController.fromWindowId(windowId()).close(); } ConnectionTypeState.delete(id); _update_remote_count(); diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 3fcc087ee..d1ccd8ab1 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -500,15 +500,19 @@ class WindowActionPanelState extends State @override void onWindowClose() async { + print("onWindowClose"); // hide window on close if (widget.isMainWindow) { await windowManager.hide(); rustDeskWinManager.unregisterActiveWindow(0); } else { widget.onClose?.call(); - WindowController.fromWindowId(windowId!).hide(); + final frame = await WindowController.fromWindowId(windowId!).getFrame(); + await WindowController.fromWindowId(windowId!).hide(); rustDeskWinManager .call(WindowType.Main, kWindowEventHide, {"id": windowId!}); + final frame2 = await WindowController.fromWindowId(windowId!).getFrame(); + print("${frame} ---hide--> ${frame2}"); } super.onWindowClose(); } @@ -555,12 +559,9 @@ class WindowActionPanelState extends State // note: the main window can be restored by tray icon Future.delayed(Duration.zero, () async { if (widget.isMainWindow) { - await windowManager.hide(); - rustDeskWinManager.unregisterActiveWindow(0); + await windowManager.close(); } else { - await WindowController.fromWindowId(windowId!).hide(); - rustDeskWinManager.call( - WindowType.Main, kWindowEventHide, {"id": windowId!}); + await WindowController.fromWindowId(windowId!).close(); } }); } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 86bd04de1..ae15b5cf6 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -168,6 +168,20 @@ void runMultiWindow( widget, MyTheme.currentThemeMode(), ); + switch (appType) { + case kAppTypeDesktopRemote: + await restoreWindowPosition(WindowType.RemoteDesktop, windowId: windowId!); + break; + case kAppTypeDesktopFileTransfer: + await restoreWindowPosition(WindowType.FileTransfer, windowId: windowId!); + break; + case kAppTypeDesktopPortForward: + await restoreWindowPosition(WindowType.PortForward, windowId: windowId!); + break; + default: + // no such appType + exit(0); + } } void runConnectionManagerScreen() async { From 7fe42e312fc115ef98370d1e22b9e7b87421ee55 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Wed, 9 Nov 2022 17:23:18 +0800 Subject: [PATCH 26/27] opt: add debug output --- flutter/lib/common.dart | 4 ++-- flutter/lib/desktop/widgets/tabbar_widget.dart | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index a652aa915..ec83e1123 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1037,7 +1037,7 @@ Future saveWindowPosition(WindowType type, {int? windowId}) async { final isMaximized = await wc.isMaximized(); final pos = LastWindowPosition( sz.width, sz.height, position.dx, position.dy, isMaximized); - print("saving frame: ${windowId}: ${pos.width}/${pos.height}, offset:${pos.offsetWidth}/${pos.offsetHeight}"); + debugPrint("saving frame: ${windowId}: ${pos.width}/${pos.height}, offset:${pos.offsetWidth}/${pos.offsetHeight}"); await Get.find() .setString(kWindowPrefix + type.name, pos.toString()); break; @@ -1175,7 +1175,7 @@ Future restoreWindowPosition(WindowType type, {int? windowId}) async { await _adjustRestoreMainWindowSize(lpos.width, lpos.height); final offset = await _adjustRestoreMainWindowOffset( lpos.offsetWidth, lpos.offsetHeight); - print("restore lpos: ${size.width}/${size.height}, offset:${offset?.dx}/${offset?.dy}"); + debugPrint("restore lpos: ${size.width}/${size.height}, offset:${offset?.dx}/${offset?.dy}"); if (offset == null) { await wc.center(); } else { diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index d1ccd8ab1..b9e126c61 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -500,19 +500,15 @@ class WindowActionPanelState extends State @override void onWindowClose() async { - print("onWindowClose"); // hide window on close if (widget.isMainWindow) { await windowManager.hide(); rustDeskWinManager.unregisterActiveWindow(0); } else { widget.onClose?.call(); - final frame = await WindowController.fromWindowId(windowId!).getFrame(); await WindowController.fromWindowId(windowId!).hide(); rustDeskWinManager .call(WindowType.Main, kWindowEventHide, {"id": windowId!}); - final frame2 = await WindowController.fromWindowId(windowId!).getFrame(); - print("${frame} ---hide--> ${frame2}"); } super.onWindowClose(); } From dd04f76ec0b05def2636d1a621950ed016019b04 Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 9 Nov 2022 17:28:47 +0800 Subject: [PATCH 27/27] close all connections when stop service Signed-off-by: 21pages --- .../lib/desktop/pages/connection_page.dart | 2 +- libs/hbb_common/protos/message.proto | 1 + src/rendezvous_mediator.rs | 2 ++ src/server.rs | 11 +++++++ src/server/connection.rs | 33 ++++++++++++++----- 5 files changed, 40 insertions(+), 9 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 506a03b7a..fc5b8e574 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -288,7 +288,7 @@ class _ConnectionPageState extends State offstage: !svcStopped.value, child: GestureDetector( onTap: () async { - bool checked = + bool checked = !bind.mainIsInstalled() || await bind.mainCheckSuperUserPermission(); if (checked) { bind.mainSetOption(key: "stop-service", value: ""); diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index aaa02c327..34983599a 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -564,6 +564,7 @@ message Misc { bool restart_remote_device = 14; bool uac = 15; bool foreground_window_elevated = 16; + bool stop_service = 17; } } diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 9fc59816f..9350085c4 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -90,6 +90,8 @@ impl RendezvousMediator { })); } join_all(futs).await; + } else { + server.write().unwrap().close_connections(); } sleep(1.).await; } diff --git a/src/server.rs b/src/server.rs index 04814db42..08b8c5b5b 100644 --- a/src/server.rs +++ b/src/server.rs @@ -264,6 +264,17 @@ impl Server { self.connections.remove(&conn.id()); } + pub fn close_connections(&mut self) { + let conn_inners: Vec<_> = self.connections.values_mut().collect(); + for c in conn_inners { + let mut misc = Misc::new(); + misc.set_stop_service(true); + let mut msg = Message::new(); + msg.set_misc(misc); + c.send(Arc::new(msg)); + } + } + fn add_service(&mut self, service: Box) { let name = service.name(); self.services.insert(name, service); diff --git a/src/server/connection.rs b/src/server/connection.rs index c4dc615be..96d202199 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -250,14 +250,7 @@ impl Connection { } } ipc::Data::Close => { - conn.close_manually = true; - let mut misc = Misc::new(); - misc.set_close_reason("Closed manually by the peer".into()); - let mut msg_out = Message::new(); - msg_out.set_misc(misc); - conn.send(msg_out).await; - conn.on_close("Close requested from connection manager", false).await; - SESSIONS.lock().unwrap().remove(&conn.lr.my_id); + conn.on_close_manually("connection manager").await; break; } ipc::Data::ChatMessage{text} => { @@ -404,6 +397,18 @@ impl Connection { _ => {} } } + match &msg.union { + Some(message::Union::Misc(m)) => { + match &m.union { + Some(misc::Union::StopService(_)) => { + conn.on_close_manually("stop service").await; + break; + } + _ => {}, + } + } + _ => {} + } if let Err(err) = conn.stream.send(msg).await { conn.on_close(&err.to_string(), false).await; break; @@ -1490,6 +1495,18 @@ impl Connection { self.port_forward_socket.take(); } + async fn on_close_manually(&mut self, close_from: &str) { + self.close_manually = true; + let mut misc = Misc::new(); + misc.set_close_reason("Closed manually by the peer".into()); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + self.send(msg_out).await; + self.on_close(&format!("Close requested from {}", close_from), false) + .await; + SESSIONS.lock().unwrap().remove(&self.lr.my_id); + } + fn read_dir(&mut self, dir: &str, include_hidden: bool) { let dir = dir.to_string(); self.send_fs(ipc::FS::ReadDir {