From 1e9625045b222fd9d63013b37ceb88944ac5b6d9 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 2 Feb 2023 20:05:57 +0900 Subject: [PATCH 001/199] fix chat text selectable --- flutter/lib/common/widgets/chat_page.dart | 25 +++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/flutter/lib/common/widgets/chat_page.dart b/flutter/lib/common/widgets/chat_page.dart index d1d96199a..62f81b797 100644 --- a/flutter/lib/common/widgets/chat_page.dart +++ b/flutter/lib/common/widgets/chat_page.dart @@ -95,10 +95,31 @@ class ChatPage extends StatelessWidget implements PageShape { color: Theme.of(context).colorScheme.primary)), messageOptions: MessageOptions( showOtherUsersAvatar: false, - showTime: true, - currentUserTextColor: Colors.white, textColor: Colors.white, maxWidth: constraints.maxWidth * 0.7, + messageTextBuilder: (message, _, __) { + final isOwnMessage = + message.user.id == currentUser.id; + return Column( + crossAxisAlignment: isOwnMessage + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + Text(message.text, + style: TextStyle(color: Colors.white)), + Padding( + padding: const EdgeInsets.only(top: 5), + child: Text( + "${message.createdAt.hour}:${message.createdAt.minute}", + style: TextStyle( + color: Colors.white, + fontSize: 10, + ), + ), + ), + ], + ); + }, messageDecorationBuilder: (_, __, ___) => defaultMessageDecoration( color: MyTheme.accent80, From c6269b54af37e60fb4a03e6f06623065ea998a0e Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 2 Feb 2023 21:39:25 +0900 Subject: [PATCH 002/199] add requestChatInputFocus() --- flutter/lib/models/chat_model.dart | 12 ++++++++++++ flutter/test/cm_test.dart | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index 18a0be279..bab88a9dd 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:dash_chat_2/dash_chat_2.dart'; import 'package:draggable_float_widget/draggable_float_widget.dart'; import 'package:flutter/material.dart'; @@ -139,6 +141,7 @@ class ChatModel with ChangeNotifier { }); overlayState.insert(overlay); chatWindowOverlayEntry = overlay; + requestChatInputFocus(); } hideChatWindowOverlay() { @@ -188,6 +191,7 @@ class ChatModel with ChangeNotifier { await windowManager.setSizeAlignment( kConnectionManagerWindowSize, Alignment.topRight); } else { + requestChatInputFocus(); await windowManager.show(); await windowManager.setSizeAlignment(Size(600, 400), Alignment.topRight); _isShowCMChatPage = !_isShowCMChatPage; @@ -292,4 +296,12 @@ class ChatModel with ChangeNotifier { resetClientMode() { _messages[clientModeID]?.clear(); } + + void requestChatInputFocus() { + Timer(Duration(milliseconds: 100), () { + if (inputNode.hasListeners && inputNode.canRequestFocus) { + inputNode.requestFocus(); + } + }); + } } diff --git a/flutter/test/cm_test.dart b/flutter/test/cm_test.dart index 592a28fcf..2c037c7b0 100644 --- a/flutter/test/cm_test.dart +++ b/flutter/test/cm_test.dart @@ -16,7 +16,7 @@ final testClients = [ Client(3, false, false, "UserDDDDDDDDDDDd", "441123123", true, false, false) ]; -/// -t lib/cm_main.dart to test cm +/// flutter run -d {platform} -t lib/cm_test.dart to test cm void main(List args) async { isTest = true; WidgetsFlutterBinding.ensureInitialized(); From c306ec3ba76217f24d66384f31c1d8fecb388291 Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 6 Feb 2023 09:54:21 +0900 Subject: [PATCH 003/199] opt chat window on its overlay, make window focusable as a desktop app --- flutter/lib/common/widgets/overlay.dart | 64 ++++++++++++++----------- flutter/lib/models/chat_model.dart | 31 ++++++++++-- 2 files changed, 61 insertions(+), 34 deletions(-) diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart index 4b4172ffd..d84789d9c 100644 --- a/flutter/lib/common/widgets/overlay.dart +++ b/flutter/lib/common/widgets/overlay.dart @@ -1,6 +1,7 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; +import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart'; import 'package:provider/provider.dart'; import '../../consts.dart'; @@ -91,28 +92,31 @@ class DraggableChatWindow extends StatelessWidget { bottom: BorderSide( color: Theme.of(context).hintColor.withOpacity(0.4)))), height: 38, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8), - child: Row(children: [ - Icon(Icons.chat_bubble_outline, - size: 20, color: Theme.of(context).colorScheme.primary), - SizedBox(width: 6), - Text(translate("Chat")) - ])), - Padding( - padding: EdgeInsets.all(2), - child: ActionIcon( - message: 'Close', - icon: IconFont.close, - onTap: chatModel.hideChatWindowOverlay, - isClose: true, - boxSize: 32, - )) - ], - ), + child: Obx(() => Opacity( + opacity: chatModel.isWindowFocus.value ? 1.0 : 0.4, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 15, vertical: 8), + child: Row(children: [ + Icon(Icons.chat_bubble_outline, + size: 20, color: Theme.of(context).colorScheme.primary), + SizedBox(width: 6), + Text(translate("Chat")) + ])), + Padding( + padding: EdgeInsets.all(2), + child: ActionIcon( + message: 'Close', + icon: IconFont.close, + onTap: chatModel.hideChatWindowOverlay, + isClose: true, + boxSize: 32, + )) + ], + ))), ); } } @@ -304,15 +308,17 @@ class _DraggableState extends State { if (widget.checkKeyboard) { checkKeyboard(); } - if (widget.checkKeyboard) { + if (widget.checkScreenSize) { checkScreenSize(); } - return Positioned( - top: _position.dy, - left: _position.dx, - width: widget.width, - height: widget.height, - child: widget.builder(context, onPanUpdate)); + return Stack(children: [ + Positioned( + top: _position.dy, + left: _position.dx, + width: widget.width, + height: widget.height, + child: widget.builder(context, onPanUpdate)) + ]); } } diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index bab88a9dd..dd35bd22f 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -4,6 +4,8 @@ import 'package:dash_chat_2/dash_chat_2.dart'; import 'package:draggable_float_widget/draggable_float_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get_rx/src/rx_types/rx_types.dart'; +import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart'; import 'package:window_manager/window_manager.dart'; import '../consts.dart'; @@ -37,6 +39,8 @@ class ChatModel with ChangeNotifier { OverlayEntry? chatWindowOverlayEntry; bool isConnManager = false; + RxBool isWindowFocus = true.obs; + final ChatUser me = ChatUser( id: "", firstName: "Me", @@ -133,11 +137,28 @@ class ChatModel with ChangeNotifier { final overlayState = _getOverlayState(); if (overlayState == null) return; final overlay = OverlayEntry(builder: (context) { - return DraggableChatWindow( - position: const Offset(20, 80), - width: 250, - height: 350, - chatModel: this); + bool innerClicked = false; + return Listener( + onPointerDown: (_) { + if (!innerClicked) { + isWindowFocus.value = false; + } + innerClicked = false; + }, + child: Obx(() => Container( + color: isWindowFocus.value ? Colors.red.withOpacity(0.3) : null, + child: Listener( + onPointerDown: (_) { + innerClicked = true; + if (!isWindowFocus.value) { + isWindowFocus.value = true; + } + }, + child: DraggableChatWindow( + position: const Offset(20, 80), + width: 250, + height: 350, + chatModel: this))))); }); overlayState.insert(overlay); chatWindowOverlayEntry = overlay; From 893f18cdec1b4fedf72af67bbfb7fe03e047db11 Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 7 Feb 2023 00:11:48 +0900 Subject: [PATCH 004/199] add PenetrableOverlayState, opt chat page over remote_page --- flutter/lib/common/widgets/overlay.dart | 106 ++++++++++++++---- flutter/lib/desktop/pages/remote_page.dart | 89 ++++++++------- .../lib/desktop/widgets/remote_menubar.dart | 13 ++- flutter/lib/models/chat_model.dart | 75 +++++-------- 4 files changed, 177 insertions(+), 106 deletions(-) diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart index d84789d9c..3e248700f 100644 --- a/flutter/lib/common/widgets/overlay.dart +++ b/flutter/lib/common/widgets/overlay.dart @@ -1,7 +1,7 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; -import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart'; +import 'package:get/get.dart'; import 'package:provider/provider.dart'; import '../../consts.dart'; @@ -92,31 +92,30 @@ class DraggableChatWindow extends StatelessWidget { bottom: BorderSide( color: Theme.of(context).hintColor.withOpacity(0.4)))), height: 38, - child: Obx(() => Opacity( - opacity: chatModel.isWindowFocus.value ? 1.0 : 0.4, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 15, vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8), + child: Obx(() => Opacity( + opacity: chatModel.isWindowFocus.value ? 1.0 : 0.4, child: Row(children: [ Icon(Icons.chat_bubble_outline, size: 20, color: Theme.of(context).colorScheme.primary), SizedBox(width: 6), Text(translate("Chat")) - ])), - Padding( - padding: EdgeInsets.all(2), - child: ActionIcon( - message: 'Close', - icon: IconFont.close, - onTap: chatModel.hideChatWindowOverlay, - isClose: true, - boxSize: 32, - )) - ], - ))), + ])))), + Padding( + padding: EdgeInsets.all(2), + child: ActionIcon( + message: 'Close', + icon: IconFont.close, + onTap: chatModel.hideChatWindowOverlay, + isClose: true, + boxSize: 32, + )) + ], + ), ); } } @@ -372,3 +371,68 @@ class QualityMonitor extends StatelessWidget { ) : const SizedBox.shrink())); } + +class PenetrableOverlayState { + final _middleBlocked = false.obs; + final _overlayKey = GlobalKey(); + + VoidCallback? onMiddleBlockedClick; // to-do use listener + + RxBool get middleBlocked => _middleBlocked; + GlobalKey get overlayKey => _overlayKey; + OverlayState? get overlayState => _overlayKey.currentState; + + OverlayState? getOverlayStateOrGlobal() { + if (overlayState == null) { + if (globalKey.currentState == null || + globalKey.currentState!.overlay == null) return null; + return globalKey.currentState!.overlay; + } else { + return overlayState; + } + } + + void addMiddleBlockedListener(void Function(bool) cb) { + _middleBlocked.listen(cb); + } + + void setMiddleBlocked(bool blocked) { + if (blocked != _middleBlocked.value) { + _middleBlocked.value = blocked; + } + } +} + +class PenetrableOverlay extends StatelessWidget { + final Widget underlying; + final List? upperLayer; + + final PenetrableOverlayState state; + + PenetrableOverlay( + {required this.underlying, required this.state, this.upperLayer}); + + @override + Widget build(BuildContext context) { + final initialEntries = [ + OverlayEntry(builder: (_) => underlying), + + /// middle layer + OverlayEntry( + builder: (context) => Obx(() => Listener( + onPointerDown: (_) { + state.onMiddleBlockedClick?.call(); + }, + child: Container( + color: state.middleBlocked.value + ? Colors.red.withOpacity(0.3) + : null)))), + ]; + + if (upperLayer != null) { + initialEntries.addAll(upperLayer!); + } + + return Overlay(key: state.overlayKey, initialEntries: initialEntries); + } +} diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 2e4668159..4bda68c2d 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -62,6 +62,8 @@ class _RemotePageState extends State late RxBool _remoteCursorMoved; late RxBool _keyboardEnabled; + final overlayState = PenetrableOverlayState(); + final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode"); Function(bool)? _onEnterOrLeaveImage4Menubar; @@ -133,6 +135,12 @@ class _RemotePageState extends State // }); // _isCustomCursorInited = true; // } + + _ffi.chatModel.setPenetrableOverlayState(overlayState); + // make remote page penetrable automatically, effective for chat over remote + overlayState.onMiddleBlockedClick = () { + overlayState.setMiddleBlocked(false); + }; } @override @@ -192,39 +200,47 @@ class _RemotePageState extends State Widget buildBody(BuildContext context) { return Scaffold( - backgroundColor: Theme.of(context).backgroundColor, - body: Overlay( - initialEntries: [ - OverlayEntry(builder: (context) { - _ffi.chatModel.setOverlayState(Overlay.of(context)); - _ffi.dialogManager.setOverlayState(Overlay.of(context)); - return Container( - color: Colors.black, - child: RawKeyFocusScope( - focusNode: _rawKeyFocusNode, - onFocusChange: (bool imageFocused) { - debugPrint( - "onFocusChange(window active:${!_isWindowBlur}) $imageFocused"); - // See [onWindowBlur]. - if (Platform.isWindows) { - if (_isWindowBlur) { - imageFocused = false; - Future.delayed(Duration.zero, () { - _rawKeyFocusNode.unfocus(); - }); - } - if (imageFocused) { - _ffi.inputModel.enterOrLeave(true); - } else { - _ffi.inputModel.enterOrLeave(false); - } - } - }, - inputModel: _ffi.inputModel, - child: getBodyForDesktop(context))); - }) - ], - )); + backgroundColor: Theme.of(context).backgroundColor, + body: PenetrableOverlay( + state: overlayState, + underlying: Container( + color: Colors.black, + child: RawKeyFocusScope( + focusNode: _rawKeyFocusNode, + onFocusChange: (bool imageFocused) { + debugPrint( + "onFocusChange(window active:${!_isWindowBlur}) $imageFocused"); + // See [onWindowBlur]. + if (Platform.isWindows) { + if (_isWindowBlur) { + imageFocused = false; + Future.delayed(Duration.zero, () { + _rawKeyFocusNode.unfocus(); + }); + } + if (imageFocused) { + _ffi.inputModel.enterOrLeave(true); + } else { + _ffi.inputModel.enterOrLeave(false); + } + } + }, + inputModel: _ffi.inputModel, + child: getBodyForDesktop(context))), + upperLayer: [ + OverlayEntry( + builder: (context) => RemoteMenubar( + id: widget.id, + ffi: _ffi, + state: widget.menubarState, + onEnterOrLeaveImageSetter: (func) => + _onEnterOrLeaveImage4Menubar = func, + onEnterOrLeaveImageCleaner: () => + _onEnterOrLeaveImage4Menubar = null, + )) + ], + ), + ); } @override @@ -345,13 +361,6 @@ class _RemotePageState extends State QualityMonitor(_ffi.qualityMonitorModel), null, null), ), ); - paints.add(RemoteMenubar( - id: widget.id, - ffi: _ffi, - state: widget.menubarState, - onEnterOrLeaveImageSetter: (func) => _onEnterOrLeaveImage4Menubar = func, - onEnterOrLeaveImageCleaner: () => _onEnterOrLeaveImage4Menubar = null, - )); return Stack( children: paints, ); diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 64d289fcc..6ad030464 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -297,12 +297,23 @@ class _RemoteMenubarState extends State { ); } + final _chatButtonKey = GlobalKey(); Widget _buildChat(BuildContext context) { return IconButton( + key: _chatButtonKey, tooltip: translate('Chat'), onPressed: () { + RenderBox? renderBox = + _chatButtonKey.currentContext?.findRenderObject() as RenderBox?; + + Offset? initPos; + if (renderBox != null) { + final pos = renderBox.localToGlobal(Offset.zero); + initPos = Offset(pos.dx, pos.dy + _MenubarTheme.dividerHeight); + } + widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID); - widget.ffi.chatModel.toggleChatOverlay(); + widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos); }, icon: const Icon( Icons.message, diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index dd35bd22f..b61ce79a7 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -5,7 +5,6 @@ import 'package:draggable_float_widget/draggable_float_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:get/get_rx/src/rx_types/rx_types.dart'; -import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart'; import 'package:window_manager/window_manager.dart'; import '../consts.dart'; @@ -30,16 +29,12 @@ class MessageBody { class ChatModel with ChangeNotifier { static final clientModeID = -1; - /// _overlayState: - /// Desktop: store session overlay by using [setOverlayState]. - /// Mobile: always null, use global overlay. - /// see [_getOverlayState] in [showChatIconOverlay] or [showChatWindowOverlay] - OverlayState? _overlayState; OverlayEntry? chatIconOverlayEntry; OverlayEntry? chatWindowOverlayEntry; bool isConnManager = false; RxBool isWindowFocus = true.obs; + PenetrableOverlayState? pOverlayState; final ChatUser me = ChatUser( id: "", @@ -58,6 +53,19 @@ class ChatModel with ChangeNotifier { bool get isShowCMChatPage => _isShowCMChatPage; + void setPenetrableOverlayState(PenetrableOverlayState state) { + pOverlayState = state; + + pOverlayState!.addMiddleBlockedListener((v) { + if (!v) { + isWindowFocus.value = false; + if (isWindowFocus.value) { + isWindowFocus.toggle(); + } + } + }); + } + final WeakReference parent; ChatModel(this.parent); @@ -74,20 +82,6 @@ class ChatModel with ChangeNotifier { } } - setOverlayState(OverlayState? os) { - _overlayState = os; - } - - OverlayState? _getOverlayState() { - if (_overlayState == null) { - if (globalKey.currentState == null || - globalKey.currentState!.overlay == null) return null; - return globalKey.currentState!.overlay; - } else { - return _overlayState; - } - } - showChatIconOverlay({Offset offset = const Offset(200, 50)}) { if (chatIconOverlayEntry != null) { chatIconOverlayEntry!.remove(); @@ -100,7 +94,7 @@ class ChatModel with ChangeNotifier { } } - final overlayState = _getOverlayState(); + final overlayState = pOverlayState?.getOverlayStateOrGlobal(); if (overlayState == null) return; final overlay = OverlayEntry(builder: (context) { @@ -132,33 +126,26 @@ class ChatModel with ChangeNotifier { } } - showChatWindowOverlay() { + showChatWindowOverlay({Offset? chatInitPos}) { if (chatWindowOverlayEntry != null) return; - final overlayState = _getOverlayState(); + isWindowFocus.value = true; + pOverlayState?.setMiddleBlocked(true); + + final overlayState = pOverlayState?.getOverlayStateOrGlobal(); if (overlayState == null) return; final overlay = OverlayEntry(builder: (context) { - bool innerClicked = false; return Listener( onPointerDown: (_) { - if (!innerClicked) { - isWindowFocus.value = false; + if (!isWindowFocus.value) { + isWindowFocus.value = true; + pOverlayState?.setMiddleBlocked(true); } - innerClicked = false; }, - child: Obx(() => Container( - color: isWindowFocus.value ? Colors.red.withOpacity(0.3) : null, - child: Listener( - onPointerDown: (_) { - innerClicked = true; - if (!isWindowFocus.value) { - isWindowFocus.value = true; - } - }, - child: DraggableChatWindow( - position: const Offset(20, 80), - width: 250, - height: 350, - chatModel: this))))); + child: DraggableChatWindow( + position: chatInitPos ?? Offset(20, 80), + width: 250, + height: 350, + chatModel: this)); }); overlayState.insert(overlay); chatWindowOverlayEntry = overlay; @@ -167,6 +154,7 @@ class ChatModel with ChangeNotifier { hideChatWindowOverlay() { if (chatWindowOverlayEntry != null) { + pOverlayState?.setMiddleBlocked(false); chatWindowOverlayEntry!.remove(); chatWindowOverlayEntry = null; return; @@ -176,13 +164,13 @@ class ChatModel with ChangeNotifier { _isChatOverlayHide() => ((!isDesktop && chatIconOverlayEntry == null) || chatWindowOverlayEntry == null); - toggleChatOverlay() { + toggleChatOverlay({Offset? chatInitPos}) { if (_isChatOverlayHide()) { gFFI.invokeMethod("enable_soft_keyboard", true); if (!isDesktop) { showChatIconOverlay(); } - showChatWindowOverlay(); + showChatWindowOverlay(chatInitPos: chatInitPos); } else { hideChatIconOverlay(); hideChatWindowOverlay(); @@ -310,7 +298,6 @@ class ChatModel with ChangeNotifier { close() { hideChatIconOverlay(); hideChatWindowOverlay(); - _overlayState = null; notifyListeners(); } From 28ad271693c4ba550d41b82a68a7a90d392118dc Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 3 Nov 2022 21:09:37 +0800 Subject: [PATCH 005/199] wip: dual audio transmission server --- src/client/io_loop.rs | 54 +++++++++++++++++++++++++++++++++++++++- src/server.rs | 21 ++++++++++++++++ src/server/connection.rs | 6 +++++ 3 files changed, 80 insertions(+), 1 deletion(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 0178fe9e8..bcbea994b 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -2,13 +2,16 @@ use crate::client::{ Client, CodecFormat, MediaData, MediaSender, QualityStatus, MILLI1, SEC30, SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED, }; -use crate::common; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::common::{check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}; +use crate::{audio_service, common, ConnInner, CLIENT_SERVER}; #[cfg(windows)] use clipboard::{cliprdr::CliprdrClientContext, ContextSend}; +use hbb_common::futures::channel::mpsc::unbounded; +use hbb_common::tokio::sync::mpsc::error::TryRecvError; +use crate::server::Service; use crate::ui_session_interface::{InvokeUiSession, Session}; use crate::{client::Data, client::Interface}; @@ -253,6 +256,55 @@ impl Remote { } } + // Start a local audio recorder, records audio and send to remote + fn start_client_audio( + &mut self, + audio_sender: MediaSender, + ) -> Option> { + if self.handler.is_file_transfer() || self.handler.is_port_forward() { + return None; + } + // Create a channel to receive error or closed message + let (tx, rx) = std::sync::mpsc::channel(); + let (tx_audio_data, mut rx_audio_data) = hbb_common::tokio::sync::mpsc::unbounded_channel(); + // Create a stand-alone inner, add subscribe to audio service + let client_conn_inner = ConnInner::new( + CLIENT_SERVER.write().unwrap().get_new_id(), + Some(tx_audio_data), + None, + ); + CLIENT_SERVER + .write() + .unwrap() + .subscribe(audio_service::NAME, client_conn_inner, true); + std::thread::spawn(move || { + loop { + // check if client is closed + match rx.try_recv() { + Ok(_) | Err(std::sync::mpsc::TryRecvError::Disconnected) => { + log::debug!("Exit local audio service of client"); + break; + } + _ => {} + } + match rx_audio_data.try_recv() { + Ok((instant, msg)) => match msg.union { + Some(_) => todo!(), + None => todo!(), + }, + Err(err) => { + if err == TryRecvError::Empty { + // ignore + } else { + log::debug!("Failed to record local audio channel: {}", err); + } + } + } + } + }); + Some(tx) + } + fn start_clipboard(&mut self) -> Option> { if self.handler.is_file_transfer() || self.handler.is_port_forward() { return None; diff --git a/src/server.rs b/src/server.rs index 109fc1e9a..bef49f132 100644 --- a/src/server.rs +++ b/src/server.rs @@ -29,6 +29,13 @@ use service::{GenericService, Service, Subscriber}; use service::ServiceTmpl; use crate::ipc::{connect, Data}; +pub use service::{GenericService, Service, ServiceTmpl, Subscriber}; +use std::{ + collections::HashMap, + net::SocketAddr, + sync::{Arc, Mutex, RwLock, Weak}, + time::Duration, +}; pub mod audio_service; cfg_if::cfg_if! { @@ -65,6 +72,13 @@ type ConnMap = HashMap; lazy_static::lazy_static! { pub static ref CHILD_PROCESS: Childs = Default::default(); pub static ref CONN_COUNT: Arc> = Default::default(); + // A client server used to provide local services(audio, video, clipboard, etc.) + // for all initiative connections. + // + // [Note] + // Now we use this [`CLIENT_SERVER`] to do following operations: + // - record local audio, and send to remote + pub static ref CLIENT_SERVER: ServerPtr = new(); } pub struct Server { @@ -316,6 +330,13 @@ impl Server { } } } + + // get a new unique id + pub fn get_new_id(&mut self) -> i32 { + let new_id = self.id_count; + self.id_count += 1; + new_id + } } impl Drop for Server { diff --git a/src/server/connection.rs b/src/server/connection.rs index e4b667d54..d340021ad 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -108,6 +108,12 @@ pub struct Connection { from_switch: bool, } +impl ConnInner { + pub fn new(id: i32, tx: Option, tx_video: Option) -> Self { + Self { id, tx, tx_video } + } +} + impl Subscriber for ConnInner { #[inline] fn id(&self) -> i32 { From 1f40963b5d23fd4cc6c7be75aa55077b977ed5f0 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 4 Nov 2022 12:02:17 +0800 Subject: [PATCH 006/199] wip: connection --- src/client.rs | 16 ++++++++++--- src/client/io_loop.rs | 51 ++++++++++++++++++++++++++++------------ src/flutter_ffi.rs | 3 +++ src/server/connection.rs | 8 +++++++ 4 files changed, 60 insertions(+), 18 deletions(-) diff --git a/src/client.rs b/src/client.rs index e0ac68c5d..08a8de747 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1543,7 +1543,6 @@ where F: 'static + FnMut(&[u8]) + Send, { let (video_sender, video_receiver) = mpsc::channel::(); - let (audio_sender, audio_receiver) = mpsc::channel::(); let mut video_callback = video_callback; let latency_controller = LatencyController::new(); @@ -1573,8 +1572,19 @@ where } log::info!("Video decoder loop exits"); }); + let audio_sender = start_audio_thread(Some(latency_controller_cl)); + return (video_sender, audio_sender); +} + +/// Start an audio thread +/// Return a audio [`MediaSender`] +pub fn start_audio_thread( + latency_controller: Option>>, +) -> MediaSender { + let latency_controller = latency_controller.unwrap_or(LatencyController::new()); + let (audio_sender, audio_receiver) = mpsc::channel::(); std::thread::spawn(move || { - let mut audio_handler = AudioHandler::new(latency_controller_cl); + let mut audio_handler = AudioHandler::new(latency_controller); loop { if let Ok(data) = audio_receiver.recv() { match data { @@ -1592,7 +1602,7 @@ where } log::info!("Audio decoder loop exits"); }); - return (video_sender, audio_sender); + audio_sender } /// Handle latency test. diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index bcbea994b..857f94891 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -32,6 +32,7 @@ use hbb_common::tokio::{ }; use hbb_common::{allow_err, message_proto::*, sleep}; use hbb_common::{fs, log, Stream}; +use std::borrow::Borrow; use std::collections::HashMap; use std::sync::atomic::{AtomicUsize, Ordering}; @@ -89,6 +90,7 @@ impl Remote { pub async fn io_loop(&mut self, key: &str, token: &str) { let stop_clipboard = self.start_clipboard(); + let stop_client_audio = self.start_client_audio(); let mut last_recv_time = Instant::now(); let mut received = false; let conn_type = if self.handler.is_file_transfer() { @@ -96,6 +98,7 @@ impl Remote { } else { ConnType::default() }; + match Client::start( &self.handler.id, key, @@ -224,6 +227,9 @@ impl Remote { if let Some(stop) = stop_clipboard { stop.send(()).ok(); } + if let Some(stop) = stop_client_audio { + stop.send(()).ok(); + } SERVER_KEYBOARD_ENABLED.store(false, Ordering::SeqCst); SERVER_CLIPBOARD_ENABLED.store(false, Ordering::SeqCst); SERVER_FILE_TRANSFER_ENABLED.store(false, Ordering::SeqCst); @@ -257,10 +263,7 @@ impl Remote { } // Start a local audio recorder, records audio and send to remote - fn start_client_audio( - &mut self, - audio_sender: MediaSender, - ) -> Option> { + fn start_client_audio(&mut self) -> Option> { if self.handler.is_file_transfer() || self.handler.is_port_forward() { return None; } @@ -268,29 +271,47 @@ impl Remote { let (tx, rx) = std::sync::mpsc::channel(); let (tx_audio_data, mut rx_audio_data) = hbb_common::tokio::sync::mpsc::unbounded_channel(); // Create a stand-alone inner, add subscribe to audio service - let client_conn_inner = ConnInner::new( - CLIENT_SERVER.write().unwrap().get_new_id(), - Some(tx_audio_data), - None, + let conn_id = CLIENT_SERVER.write().unwrap().get_new_id(); + let client_conn_inner = ConnInner::new(conn_id.clone(), Some(tx_audio_data), None); + // now we subscribe + CLIENT_SERVER.write().unwrap().subscribe( + audio_service::NAME, + client_conn_inner.clone(), + true, ); - CLIENT_SERVER - .write() - .unwrap() - .subscribe(audio_service::NAME, client_conn_inner, true); + let tx_audio = self.sender.clone(); std::thread::spawn(move || { loop { // check if client is closed match rx.try_recv() { Ok(_) | Err(std::sync::mpsc::TryRecvError::Disconnected) => { log::debug!("Exit local audio service of client"); + // unsubscribe + CLIENT_SERVER.write().unwrap().subscribe( + audio_service::NAME, + client_conn_inner, + false, + ); break; } _ => {} } match rx_audio_data.try_recv() { - Ok((instant, msg)) => match msg.union { - Some(_) => todo!(), - None => todo!(), + Ok((instant, msg)) => match &msg.union { + Some(message::Union::AudioFrame(frame)) => { + let mut msg = Message::new(); + msg.set_audio_frame(frame.clone()); + tx_audio.send(Data::Message(msg)).ok(); + log::debug!("send audio frame {}", frame.timestamp); + } + Some(message::Union::Misc(misc)) => { + let mut msg = Message::new(); + msg.set_misc(misc.clone()); + tx_audio.send(Data::Message(msg)).ok(); + log::debug!("send audio misc {:?}", misc.audio_format()); + } + _ => {} + None => {} }, Err(err) => { if err == TryRecvError::Empty { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index ca9314c43..4b671ff1b 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1244,6 +1244,9 @@ pub fn main_current_is_wayland() -> SyncReturn { pub fn main_is_login_wayland() -> SyncReturn { SyncReturn(is_login_wayland()) +pub fn main_start_pa() { + #[cfg(target_os = "linux")] + std::thread::spawn(crate::ipc::start_pa); } pub fn main_hide_docker() -> SyncReturn { diff --git a/src/server/connection.rs b/src/server/connection.rs index d340021ad..34adeb59b 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1533,6 +1533,10 @@ impl Connection { } _ => {} }, + Some(misc::Union::AudioFormat(format)) => { + // TODO: implement audio format handler + println!("recv audio format"); + } #[cfg(feature = "flutter")] Some(misc::Union::SwitchSidesRequest(s)) => { if let Ok(uuid) = uuid::Uuid::from_slice(&s.uuid.to_vec()[..]) { @@ -1550,6 +1554,10 @@ impl Connection { } _ => {} }, + Some(message::Union::AudioFrame(audio_frame)) => { + // TODO: implement audio frame handler + println!("recv audio frame"); + } _ => {} } } From 65ab43aa4a9ebc7563a1eceb822c57f309d24eb3 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 17 Dec 2022 10:39:07 +0800 Subject: [PATCH 007/199] opt: compile --- src/flutter_ffi.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 4b671ff1b..d9f67e566 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1244,6 +1244,8 @@ pub fn main_current_is_wayland() -> SyncReturn { pub fn main_is_login_wayland() -> SyncReturn { SyncReturn(is_login_wayland()) +} + pub fn main_start_pa() { #[cfg(target_os = "linux")] std::thread::spawn(crate::ipc::start_pa); From 8e2d6945d0e0b3fd16de4d7b8883867c3236a4c8 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 29 Jan 2023 11:55:37 +0800 Subject: [PATCH 008/199] feat: add audio thread in server being controlled --- src/client/io_loop.rs | 3 +-- src/server/connection.rs | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 857f94891..bac1e5d23 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -297,7 +297,7 @@ impl Remote { _ => {} } match rx_audio_data.try_recv() { - Ok((instant, msg)) => match &msg.union { + Ok((_instant, msg)) => match &msg.union { Some(message::Union::AudioFrame(frame)) => { let mut msg = Message::new(); msg.set_audio_frame(frame.clone()); @@ -311,7 +311,6 @@ impl Remote { log::debug!("send audio misc {:?}", misc.audio_format()); } _ => {} - None => {} }, Err(err) => { if err == TryRecvError::Empty { diff --git a/src/server/connection.rs b/src/server/connection.rs index 34adeb59b..2ee3bc8ed 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -5,7 +5,7 @@ use crate::clipboard_file::*; use crate::common::update_clipboard; #[cfg(windows)] use crate::portable_service::client as portable_client; -use crate::video_service; +use crate::{video_service, client::{MediaSender, start_audio_thread, LatencyController, MediaData}}; #[cfg(any(target_os = "android", target_os = "ios"))] use crate::{common::DEVICE_NAME, flutter::connection_manager::start_channel}; use crate::{ipc, VERSION}; @@ -95,6 +95,7 @@ pub struct Connection { disable_clipboard: bool, // by peer disable_audio: bool, // by peer enable_file_transfer: bool, // by peer + audio_sender: MediaSender, // audio by the remote peer/client tx_input: std_mpsc::Sender, // handle input messages video_ack_required: bool, peer_info: (String, String), @@ -168,6 +169,9 @@ impl Connection { let mut hbbs_rx = crate::hbbs_http::sync::signal_receiver(); let tx_cloned = tx.clone(); + // Start a audio thread to play the audio sent by peer. + let latency_controller = LatencyController::new(); + let audio_sender = start_audio_thread(Some(latency_controller)); let mut conn = Self { inner: ConnInner { id, @@ -209,6 +213,7 @@ impl Connection { #[cfg(windows)] portable: Default::default(), from_switch: false, + audio_sender, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] tokio::spawn(async move { @@ -1534,8 +1539,9 @@ impl Connection { _ => {} }, Some(misc::Union::AudioFormat(format)) => { - // TODO: implement audio format handler - println!("recv audio format"); + if !self.disable_audio { + allow_err!(self.audio_sender.send(MediaData::AudioFormat(format))); + } } #[cfg(feature = "flutter")] Some(misc::Union::SwitchSidesRequest(s)) => { @@ -1554,9 +1560,10 @@ impl Connection { } _ => {} }, - Some(message::Union::AudioFrame(audio_frame)) => { - // TODO: implement audio frame handler - println!("recv audio frame"); + Some(message::Union::AudioFrame(frame)) => { + if !self.disable_audio { + allow_err!(self.audio_sender.send(MediaData::AudioFrame(frame))); + } } _ => {} } From 45a6fc361883a6fd1ff76a4fe3a7ca9bd54b09da Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 29 Jan 2023 14:10:06 +0800 Subject: [PATCH 009/199] opt: remove latency detector on single audio --- src/client.rs | 5 +++++ src/client/helper.rs | 11 +++++++++++ src/server/connection.rs | 4 +++- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 08a8de747..b2cd0f2f7 100644 --- a/src/client.rs +++ b/src/client.rs @@ -714,6 +714,7 @@ impl AudioHandler { .check_audio(frame.timestamp) .not() { + log::debug!("audio frame {} is ignored", frame.timestamp); return; } } @@ -724,6 +725,7 @@ impl AudioHandler { } #[cfg(target_os = "linux")] if self.simple.is_none() { + log::debug!("PulseAudio simple binding does not exists"); return; } #[cfg(target_os = "android")] @@ -768,6 +770,7 @@ impl AudioHandler { unsafe { std::slice::from_raw_parts::(buffer.as_ptr() as _, n * 4) }; self.simple.as_mut().map(|x| x.write(data_u8)); } + log::debug!("write Audio frame {} to system.", frame.timestamp); } }); } @@ -1589,9 +1592,11 @@ pub fn start_audio_thread( if let Ok(data) = audio_receiver.recv() { match data { MediaData::AudioFrame(af) => { + log::debug!("recved audio frame={}", af.timestamp); audio_handler.handle_frame(af); } MediaData::AudioFormat(f) => { + log::debug!("recved audio format, sample rate={}", f.sample_rate); audio_handler.handle_format(f); } _ => {} diff --git a/src/client/helper.rs b/src/client/helper.rs index e4736c0e8..005b2df72 100644 --- a/src/client/helper.rs +++ b/src/client/helper.rs @@ -18,6 +18,7 @@ pub struct LatencyController { last_video_remote_ts: i64, // generated on remote device update_time: Instant, allow_audio: bool, + enabled: bool } impl Default for LatencyController { @@ -26,6 +27,7 @@ impl Default for LatencyController { last_video_remote_ts: Default::default(), update_time: Instant::now(), allow_audio: Default::default(), + enabled: true } } } @@ -36,6 +38,11 @@ impl LatencyController { Arc::new(Mutex::new(LatencyController::default())) } + /// Set whether this [LatencyController] should be enabled. + pub fn set_enabled(&mut self, enable: bool) { + self.enabled = enable; + } + /// Update the latency controller with the latest video timestamp. pub fn update_video(&mut self, timestamp: i64) { self.last_video_remote_ts = timestamp; @@ -44,6 +51,10 @@ impl LatencyController { /// Check if the audio should be played based on the current latency. pub fn check_audio(&mut self, timestamp: i64) -> bool { + if !self.enabled { + self.allow_audio = true; + return self.allow_audio; + } // Compute audio latency. let expected = self.update_time.elapsed().as_millis() as i64 + self.last_video_remote_ts; let latency = expected - timestamp; diff --git a/src/server/connection.rs b/src/server/connection.rs index 2ee3bc8ed..1924cfca0 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -171,6 +171,8 @@ impl Connection { let tx_cloned = tx.clone(); // Start a audio thread to play the audio sent by peer. let latency_controller = LatencyController::new(); + // No video frame will be sent here, so we need to disable latency controller, or audio check may fail. + latency_controller.lock().unwrap().set_enabled(false); let audio_sender = start_audio_thread(Some(latency_controller)); let mut conn = Self { inner: ConnInner { @@ -1561,7 +1563,7 @@ impl Connection { _ => {} }, Some(message::Union::AudioFrame(frame)) => { - if !self.disable_audio { + if !self.disable_audio { allow_err!(self.audio_sender.send(MediaData::AudioFrame(frame))); } } From e7e8e1a18b6e3bcdf190931869527489704296a4 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 29 Jan 2023 22:23:18 +0800 Subject: [PATCH 010/199] opt: send audio frame when connected --- src/client/io_loop.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index bac1e5d23..f16c9af72 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -90,7 +90,6 @@ impl Remote { pub async fn io_loop(&mut self, key: &str, token: &str) { let stop_clipboard = self.start_clipboard(); - let stop_client_audio = self.start_client_audio(); let mut last_recv_time = Instant::now(); let mut received = false; let conn_type = if self.handler.is_file_transfer() { @@ -114,6 +113,8 @@ impl Remote { SERVER_FILE_TRANSFER_ENABLED.store(true, Ordering::SeqCst); self.handler.set_connection_type(peer.is_secured(), direct); // flutter -> connection_ready self.handler.set_connection_info(direct, false); + // Start client audio when connection is established. + let stop_client_audio = self.start_client_audio(); // just build for now #[cfg(not(windows))] @@ -218,6 +219,10 @@ impl Remote { } } log::debug!("Exit io_loop of id={}", self.handler.id); + // Stop client audio server. + if let Some(stop) = stop_client_audio { + stop.send(()).ok(); + } } Err(err) => { self.handler @@ -227,9 +232,6 @@ impl Remote { if let Some(stop) = stop_clipboard { stop.send(()).ok(); } - if let Some(stop) = stop_client_audio { - stop.send(()).ok(); - } SERVER_KEYBOARD_ENABLED.store(false, Ordering::SeqCst); SERVER_CLIPBOARD_ENABLED.store(false, Ordering::SeqCst); SERVER_FILE_TRANSFER_ENABLED.store(false, Ordering::SeqCst); From 4f3c5b42ae158a52a1d276964b38034241e0b187 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 30 Jan 2023 01:39:42 +0800 Subject: [PATCH 011/199] opt: send audio format and data after login successfully. --- src/client/io_loop.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index f16c9af72..d568feb4e 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -44,6 +44,8 @@ pub struct Remote { audio_sender: MediaSender, receiver: mpsc::UnboundedReceiver, sender: mpsc::UnboundedSender, + // Stop sending local audio to remote client. + stop_local_audio_sender: Option>, old_clipboard: Arc>, read_jobs: Vec, write_jobs: Vec, @@ -85,6 +87,7 @@ impl Remote { data_count: Arc::new(AtomicUsize::new(0)), frame_count, video_format: CodecFormat::Unknown, + stop_local_audio_sender: None, } } @@ -113,8 +116,6 @@ impl Remote { SERVER_FILE_TRANSFER_ENABLED.store(true, Ordering::SeqCst); self.handler.set_connection_type(peer.is_secured(), direct); // flutter -> connection_ready self.handler.set_connection_info(direct, false); - // Start client audio when connection is established. - let stop_client_audio = self.start_client_audio(); // just build for now #[cfg(not(windows))] @@ -220,8 +221,8 @@ impl Remote { } log::debug!("Exit io_loop of id={}", self.handler.id); // Stop client audio server. - if let Some(stop) = stop_client_audio { - stop.send(()).ok(); + if let Some(s) = self.stop_local_audio_sender.take() { + s.send(()).ok(); } } Err(err) => { @@ -865,6 +866,15 @@ impl Remote { }); } } + // Start audio thread for playback + if !self.handler.is_file_transfer() && !self.handler.is_port_forward() { + // Cancel previous local audio session. + if let Some(sender) = self.stop_local_audio_sender.take() { + allow_err!(sender.send(())); + } + // Start client audio when connection is established. + self.stop_local_audio_sender = self.start_client_audio(); + } if self.handler.is_file_transfer() { self.handler.load_last_jobs(); From 3b34e2ea453fe6a7667e980fcd05b5d009cc065c Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 30 Jan 2023 11:15:47 +0800 Subject: [PATCH 012/199] feat: run local audio server at start --- flutter/lib/models/native_model.dart | 8 ++++++-- src/ui.rs | 3 +++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 628bf502d..34a673953 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -118,8 +118,12 @@ class PlatformFFI { // Start a dbus service, no need to await _ffiBind.mainStartDbusServer(); } else if (Platform.isMacOS && isMain) { - // Start an ipc server for handling url schemes. - _ffiBind.mainStartIpcUrlServer(); + Future.wait([ + // Start dbus service. + _ffiBind.mainStartDbusServer(), + // Start local audio pulseaudio server. + _ffiBind.mainStartPa() + ]); } _startListenEvent(_ffiBind); // global event try { diff --git a/src/ui.rs b/src/ui.rs index 8763194fe..7973a0ba4 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -95,6 +95,9 @@ pub fn start(args: &mut [String]) { frame.event_handler(UI {}); frame.sciter_handler(UIHostHandler {}); page = "index.html"; + // Start pulse audio local server. + #[cfg(target_os = "linux")] + std::thread::spawn(crate::ipc::start_pa); } else if args[0] == "--install" { frame.event_handler(UI {}); frame.sciter_handler(UIHostHandler {}); From 9134c2826e0205aaff80cffe299c0f5c6a71ecc0 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 30 Jan 2023 11:32:46 +0800 Subject: [PATCH 013/199] feat: set audio only mode --- src/client/helper.rs | 19 ++++++++++--------- src/server/connection.rs | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/client/helper.rs b/src/client/helper.rs index 005b2df72..248cf5928 100644 --- a/src/client/helper.rs +++ b/src/client/helper.rs @@ -18,7 +18,7 @@ pub struct LatencyController { last_video_remote_ts: i64, // generated on remote device update_time: Instant, allow_audio: bool, - enabled: bool + audio_only: bool } impl Default for LatencyController { @@ -27,7 +27,7 @@ impl Default for LatencyController { last_video_remote_ts: Default::default(), update_time: Instant::now(), allow_audio: Default::default(), - enabled: true + audio_only: true } } } @@ -38,9 +38,9 @@ impl LatencyController { Arc::new(Mutex::new(LatencyController::default())) } - /// Set whether this [LatencyController] should be enabled. - pub fn set_enabled(&mut self, enable: bool) { - self.enabled = enable; + /// Set whether this [LatencyController] should be working in audio only mode. + pub fn set_audio_only(&mut self, only: bool) { + self.audio_only = only; } /// Update the latency controller with the latest video timestamp. @@ -51,10 +51,6 @@ impl LatencyController { /// Check if the audio should be played based on the current latency. pub fn check_audio(&mut self, timestamp: i64) -> bool { - if !self.enabled { - self.allow_audio = true; - return self.allow_audio; - } // Compute audio latency. let expected = self.update_time.elapsed().as_millis() as i64 + self.last_video_remote_ts; let latency = expected - timestamp; @@ -70,6 +66,11 @@ impl LatencyController { self.allow_audio = true; } } + // No video frame here, which means the update time is not triggered. + // We manually update the time here. + if self.audio_only { + self.update_time = Instant::now(); + } self.allow_audio } } diff --git a/src/server/connection.rs b/src/server/connection.rs index 1924cfca0..d5c2103b1 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -172,7 +172,7 @@ impl Connection { // Start a audio thread to play the audio sent by peer. let latency_controller = LatencyController::new(); // No video frame will be sent here, so we need to disable latency controller, or audio check may fail. - latency_controller.lock().unwrap().set_enabled(false); + latency_controller.lock().unwrap().set_audio_only(true); let audio_sender = start_audio_thread(Some(latency_controller)); let mut conn = Self { inner: ConnInner { From 95d06e160b21df29a62926fefd46c9f318c2cae1 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 30 Jan 2023 11:51:03 +0800 Subject: [PATCH 014/199] fix: latency --- src/client/helper.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/client/helper.rs b/src/client/helper.rs index 248cf5928..e3acf3a44 100644 --- a/src/client/helper.rs +++ b/src/client/helper.rs @@ -27,7 +27,7 @@ impl Default for LatencyController { last_video_remote_ts: Default::default(), update_time: Instant::now(), allow_audio: Default::default(), - audio_only: true + audio_only: false } } } @@ -53,7 +53,11 @@ impl LatencyController { pub fn check_audio(&mut self, timestamp: i64) -> bool { // Compute audio latency. let expected = self.update_time.elapsed().as_millis() as i64 + self.last_video_remote_ts; - let latency = expected - timestamp; + let latency = if self.audio_only { + expected + } else { + expected - timestamp + }; // Set MAX and MIN, avoid fixing too frequently. if self.allow_audio { if latency.abs() > MAX_LATENCY { @@ -66,11 +70,9 @@ impl LatencyController { self.allow_audio = true; } } - // No video frame here, which means the update time is not triggered. + // No video frame here, which means the update time is not up to date. // We manually update the time here. - if self.audio_only { - self.update_time = Instant::now(); - } + self.update_time = Instant::now(); self.allow_audio } } From cb228bef2b7093115909686a31d304d47eaa6e1e Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 30 Jan 2023 20:30:35 +0800 Subject: [PATCH 015/199] feat: add audio switch ui --- flutter/lib/consts.dart | 6 +++ .../lib/desktop/widgets/remote_menubar.dart | 26 +++++++++++++ libs/hbb_common/protos/message.proto | 6 +++ libs/hbb_common/src/config.rs | 10 +++++ src/client.rs | 39 +++++++++++++++++++ src/flutter_ffi.rs | 14 +++++++ src/lang/ca.rs | 3 ++ src/lang/cn.rs | 3 ++ src/lang/cs.rs | 3 ++ src/lang/da.rs | 3 ++ src/lang/de.rs | 3 ++ src/lang/eo.rs | 3 ++ src/lang/es.rs | 4 ++ src/lang/fa.rs | 3 ++ src/lang/fr.rs | 3 ++ src/lang/gr.rs | 3 ++ src/lang/hu.rs | 3 ++ src/lang/id.rs | 3 ++ src/lang/it.rs | 3 ++ src/lang/ja.rs | 3 ++ src/lang/ko.rs | 3 ++ src/lang/kz.rs | 3 ++ src/lang/pl.rs | 3 ++ src/lang/pt_PT.rs | 3 ++ src/lang/ptbr.rs | 3 ++ src/lang/ro.rs | 3 ++ src/lang/ru.rs | 5 +++ src/lang/sk.rs | 3 ++ src/lang/sl.rs | 3 ++ src/lang/sq.rs | 3 ++ src/lang/sr.rs | 3 ++ src/lang/sv.rs | 3 ++ src/lang/template.rs | 3 ++ src/lang/th.rs | 3 ++ src/lang/tr.rs | 3 ++ src/lang/tw.rs | 3 ++ src/lang/ua.rs | 3 ++ src/lang/vn.rs | 3 ++ src/ui_session_interface.rs | 19 +++++++++ 39 files changed, 219 insertions(+) diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index c95c62fcc..99130f892 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -106,6 +106,12 @@ const kRemoteImageQualityLow = 'low'; /// [kRemoteImageQualityCustom] Custom image quality. const kRemoteImageQualityCustom = 'custom'; +/// [kRemoteAudioGuestToHost] Guest to host audio mode(default). +const kRemoteAudioGuestToHost = 'guest-to-host'; + +/// [kRemoteAudioTwoWay] two-way audio mode(default). +const kRemoteAudioTwoWay = 'two-way'; + const kIgnoreDpi = true; /// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 36b9504c0..1e5723b61 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1106,6 +1106,30 @@ class _RemoteMenubarState extends State { padding: padding, ), MenuEntryDivider(), + MenuEntryRadios( + text: translate('Audio Transmission Mode'), + optionsGetter: () => [ + MenuEntryRadioOption( + text: translate('Guest to Host'), + value: kRemoteAudioGuestToHost, + dismissOnClicked: true, + ), + MenuEntryRadioOption( + text: translate('Two way'), + value: kRemoteAudioTwoWay, + dismissOnClicked: true, + ), + ], + curOptionGetter: () async => + // null means peer id is not found, which there's no need to care about + await bind.sessionGetAudioMode(id: widget.id) ?? '', + optionSetter: (String oldValue, String newValue) async { + if (oldValue != newValue) { + await bind.sessionSetAudioMode(id: widget.id, value: newValue); + } + }, + padding: padding, + ), ]; if (widget.state.viewStyle.value == kRemoteViewStyleOriginal) { @@ -1337,6 +1361,8 @@ class _RemoteMenubarState extends State { if (perms['audio'] != false) { displayMenu .add(_createSwitchMenuEntry('Mute', 'disable-audio', padding, true)); + displayMenu + .add(_createSwitchMenuEntry('Mute', 'disable-audio', padding, true)); } if (Platform.isWindows && diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index b7965f237..da4865069 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -444,6 +444,11 @@ enum ImageQuality { Best = 4; } +enum AudioMode { + GuestToHost = 0; + TwoWay = 1; +} + message VideoCodecState { enum PreferCodec { Auto = 0; @@ -475,6 +480,7 @@ message OptionMessage { BoolOption enable_file_transfer = 9; VideoCodecState video_codec_state = 10; int32 custom_fps = 11; + AudioMode audio_mode = 12; } message TestDelay { diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 71dd9a5c6..6032ae9c7 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -212,6 +212,11 @@ pub struct PeerConfig { deserialize_with = "PeerConfig::deserialize_image_quality" )] pub image_quality: String, + #[serde( + default = "PeerConfig::default_audio_mode", + deserialize_with = "PeerConfig::deserialize_audio_mode" + )] + pub audio_mode: String, #[serde( default = "PeerConfig::default_custom_image_quality", deserialize_with = "PeerConfig::deserialize_custom_image_quality" @@ -996,6 +1001,11 @@ impl PeerConfig { deserialize_image_quality, UserDefaultConfig::load().get("image_quality") ); + serde_field_string!( + default_audio_mode, + deserialize_audio_mode, + "guest-to-host".to_owned() + ); fn default_custom_image_quality() -> Vec { let f: f64 = UserDefaultConfig::load() diff --git a/src/client.rs b/src/client.rs index b2cd0f2f7..54796a935 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1252,6 +1252,27 @@ impl LoginConfigHandler { } } + /// Parse the audio mode option. + /// Return [`AudioMode`] if the option is valid, otherwise return `None`. + /// + /// # Arguments + /// + /// * `q` - The audio mode option. + /// * `ignore_default` - Ignore the default value. + fn get_audio_mode_enum(&self, q: &str, ignore_default: bool) -> Option { + if q == "guest-to-host" { + Some(AudioMode::GuestToHost) + } else if q == "two-way" { + Some(AudioMode::TwoWay) + } else { + if ignore_default { + None + } else { + Some(AudioMode::GuestToHost) + } + } + } + /// Get the status of a toggle option. /// /// # Arguments @@ -1338,6 +1359,24 @@ impl LoginConfigHandler { res } + pub fn save_audio_mode(&mut self, value: String) -> Option { + let mut res = None; + if let Some(q) = self.get_audio_mode_enum(&value, false) { + let mut misc = Misc::new(); + misc.set_option(OptionMessage { + audio_mode: q.into(), + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + res = Some(msg_out); + } + let mut config = self.load_config(); + config.audio_mode = value; + self.save_config(config); + res + } + /// Create a [`Message`] for saving custom fps. /// /// # Arguments diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index d9f67e566..10fd67fdd 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -233,6 +233,20 @@ pub fn session_set_image_quality(id: String, value: String) { } } +pub fn session_get_audio_mode(id: String) -> Option { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + Some(session.get_audio_mode()) + } else { + None + } +} + +pub fn session_set_audio_mode(id: String, value: String) { + if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + session.save_audio_mode(value); + } +} + pub fn session_get_keyboard_mode(id: String) -> Option { if let Some(session) = SESSIONS.read().unwrap().get(&id) { Some(session.get_keyboard_mode()) diff --git a/src/lang/ca.rs b/src/lang/ca.rs index f2210f971..197435158 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 00d62946f..c74f352ce 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "帧率"), ("Auto", "自动"), ("Other Default Options", "其它默认选项"), + ("Guest to Host", "被控到主机"), + ("Two way", "双向"), + ("Audio Transmission Mode", "音频传输模式"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 453ecefb3..d956ddf53 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index dcaeb3eaa..9e771567a 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -436,6 +436,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), ("Closed as expected", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/de.rs b/src/lang/de.rs index 2d6d3d069..a112385a6 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "fps"), ("Auto", "Automatisch"), ("Other Default Options", "Weitere Standardoptionen"), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 0c7f13d7e..342eac51c 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 5fdb7ee2c..74acd8c69 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -445,5 +445,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", "Otras opciones predeterminadas"), + ("Closed as expected", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index dd1c75bac..50e883227 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "خودکار"), ("Other Default Options", "سایر گزینه های پیش فرض"), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 3b7f23ab9..9bfdb6b1e 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "Auto"), ("Other Default Options", "Autres options par défaut"), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index bc25ab6c6..a569b750f 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 49ce8f140..e28294de0 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 0fa6e0293..ece6c9233 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index d84b56a8a..e252219c1 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "Auto"), ("Other Default Options", "Altre Opzioni Predefinite"), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 35e20d7fd..036bc8ec7 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index d03b07992..6da983849 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 2006c67d1..459139f58 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index b7ccbdbb1..483879d49 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "Auto"), ("Other Default Options", "Inne opcje domyślne"), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 64e5e9315..cff00333a 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 0f64ae67f..9fe5eab8c 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 7e209dff8..36e2a99de 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 54b064c18..31f24a5e8 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -445,5 +445,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "Авто"), ("Other Default Options", "Другие параметры по умолчанию"), + ("Please confirm if you want to share your desktop?", "Подтвердите, что хотите поделиться своим рабочим столом?"), + ("Closed as expected", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index a703c0799..8cf858df0 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 16c948ceb..0e2208c3c 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 285a51732..44159fb4a 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index dd943e0e6..892b3664e 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 3050ff635..619a68508 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 7572da9de..f0458b115 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 535e4e772..f61ba325a 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 80b384c6c..cade148a7 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index f5d9539d8..46cc90c1e 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "幀率"), ("Auto", "自動"), ("Other Default Options", "其它默認選項"), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 37a7d6bcd..7c355edd5 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index d78f5aa7b..f7640ae50 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 4fc5db743..234c9a4d7 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -89,6 +89,18 @@ impl Session { self.lc.write().unwrap().save_keyboard_mode(value); } + pub fn get_audio_mode(&self) -> String { + self.lc.read().unwrap().audio_mode.clone() + } + + pub fn save_audio_mode(&self, value: String) { + let msg = self.lc.write().unwrap().save_audio_mode(value); + // Notify remote guest that the audio mode has been changed. + if let Some(msg) = msg { + self.send(Data::Message(msg)); + } + } + pub fn save_view_style(&mut self, value: String) { self.lc.write().unwrap().save_view_style(value); } @@ -653,6 +665,13 @@ impl Session { } } } + + fn get_audio_transmission_mode(&self, id: &str) { + + } + fn set_audio_transmission_mode(&self, id: &str, mode: String) { + + } } pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { From 393e0e9afbc69df74f95ee98306fc322c5ef456f Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 30 Jan 2023 21:53:26 +0800 Subject: [PATCH 016/199] add: divider --- flutter/lib/desktop/widgets/remote_menubar.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 1e5723b61..bb2079930 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1130,6 +1130,7 @@ class _RemoteMenubarState extends State { }, padding: padding, ), + MenuEntryDivider(), ]; if (widget.state.viewStyle.value == kRemoteViewStyleOriginal) { From 8ab49d11d149de458d6ea95d1543b9c384568632 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 30 Jan 2023 22:06:52 +0800 Subject: [PATCH 017/199] feat: add audio mode config --- src/client.rs | 5 ++-- src/client/io_loop.rs | 46 +++++++++++++++++++++++++++++-------- src/ui_session_interface.rs | 4 ++++ 3 files changed, 43 insertions(+), 12 deletions(-) diff --git a/src/client.rs b/src/client.rs index 54796a935..d76f930c8 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1259,7 +1259,7 @@ impl LoginConfigHandler { /// /// * `q` - The audio mode option. /// * `ignore_default` - Ignore the default value. - fn get_audio_mode_enum(&self, q: &str, ignore_default: bool) -> Option { + pub fn get_audio_mode_enum(q: &str, ignore_default: bool) -> Option { if q == "guest-to-host" { Some(AudioMode::GuestToHost) } else if q == "two-way" { @@ -1361,7 +1361,7 @@ impl LoginConfigHandler { pub fn save_audio_mode(&mut self, value: String) -> Option { let mut res = None; - if let Some(q) = self.get_audio_mode_enum(&value, false) { + if let Some(q) = LoginConfigHandler::get_audio_mode_enum(&value, false) { let mut misc = Misc::new(); misc.set_option(OptionMessage { audio_mode: q.into(), @@ -1981,6 +1981,7 @@ pub enum Data { RemovePortForward(i32), AddPortForward((i32, String, i32)), ToggleClipboardFile, + ChangeAudioMode(AudioMode), NewRDP, SetConfirmOverrideFile((i32, i32, bool, bool, bool)), AddJob((i32, String, String, i32, bool, bool)), diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index d568feb4e..af8c1048b 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1,5 +1,5 @@ use crate::client::{ - Client, CodecFormat, MediaData, MediaSender, QualityStatus, MILLI1, SEC30, + Client, CodecFormat, LoginConfigHandler, MediaData, MediaSender, QualityStatus, MILLI1, SEC30, SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -386,6 +386,24 @@ impl Remote { Data::ToggleClipboardFile => { self.check_clipboard_file_context(); } + Data::ChangeAudioMode(audio_mode) => { + match audio_mode { + AudioMode::GuestToHost => { + if let Some(sender) = self.stop_local_audio_sender.take() { + allow_err!(sender.send(())); + } + } + AudioMode::TwoWay => { + // Start audio thread for playback. + // Cancel previous local audio session. + if let Some(sender) = self.stop_local_audio_sender.take() { + allow_err!(sender.send(())); + } + // Start client audio when connection is established. + self.stop_local_audio_sender = self.start_client_audio(); + } + } + } Data::Message(msg) => { allow_err!(peer.send(&msg).await); } @@ -866,19 +884,27 @@ impl Remote { }); } } - // Start audio thread for playback - if !self.handler.is_file_transfer() && !self.handler.is_port_forward() { - // Cancel previous local audio session. - if let Some(sender) = self.stop_local_audio_sender.take() { - allow_err!(sender.send(())); - } - // Start client audio when connection is established. - self.stop_local_audio_sender = self.start_client_audio(); - } if self.handler.is_file_transfer() { self.handler.load_last_jobs(); } + + // Start audio thread for playback if current audio mode is two-way transmission. + if !self.handler.is_file_transfer() && !self.handler.is_port_forward() { + let audio_mode = LoginConfigHandler::get_audio_mode_enum( + self.handler.load_config().audio_mode.as_str(), + false, + ) + .unwrap_or(AudioMode::GuestToHost); + if audio_mode == AudioMode::TwoWay { + // Cancel previous local audio session. + if let Some(sender) = self.stop_local_audio_sender.take() { + allow_err!(sender.send(())); + } + // Start client audio when connection is established. + self.stop_local_audio_sender = self.start_client_audio(); + } + } } _ => {} }, diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 234c9a4d7..73414e405 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -94,6 +94,10 @@ impl Session { } pub fn save_audio_mode(&self, value: String) { + let mode = LoginConfigHandler::get_audio_mode_enum(value.as_str(), false); + if let Some(mode)= mode { + self.send(Data::ChangeAudioMode(mode)); + } let msg = self.lc.write().unwrap().save_audio_mode(value); // Notify remote guest that the audio mode has been changed. if let Some(msg) = msg { From 05822991bfaa48fbf86ff31b734d77a431a75cde Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 30 Jan 2023 22:57:20 +0800 Subject: [PATCH 018/199] opt: rename to dual-way --- flutter/lib/consts.dart | 4 ++-- flutter/lib/desktop/widgets/remote_menubar.dart | 4 ++-- libs/hbb_common/protos/message.proto | 2 +- src/client.rs | 4 ++-- src/client/io_loop.rs | 7 ++++--- src/lang/ca.rs | 2 +- src/lang/cn.rs | 2 +- src/lang/cs.rs | 2 +- src/lang/da.rs | 2 +- src/lang/de.rs | 2 +- src/lang/eo.rs | 2 +- src/lang/es.rs | 2 +- src/lang/fa.rs | 2 +- src/lang/fr.rs | 2 +- src/lang/gr.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/ro.rs | 2 +- src/lang/ru.rs | 2 +- src/lang/sk.rs | 2 +- src/lang/sl.rs | 2 +- src/lang/sq.rs | 2 +- src/lang/sr.rs | 2 +- src/lang/sv.rs | 2 +- src/lang/template.rs | 2 +- src/lang/th.rs | 2 +- src/lang/tr.rs | 2 +- src/lang/tw.rs | 2 +- src/lang/ua.rs | 2 +- src/lang/vn.rs | 2 +- src/ui_session_interface.rs | 7 ------- 38 files changed, 43 insertions(+), 49 deletions(-) diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 99130f892..26e25a209 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -109,8 +109,8 @@ const kRemoteImageQualityCustom = 'custom'; /// [kRemoteAudioGuestToHost] Guest to host audio mode(default). const kRemoteAudioGuestToHost = 'guest-to-host'; -/// [kRemoteAudioTwoWay] two-way audio mode(default). -const kRemoteAudioTwoWay = 'two-way'; +/// [kRemoteAudioDualWay] dual-way audio mode(default). +const kRemoteAudioDualWay = 'dual-way'; const kIgnoreDpi = true; diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index bb2079930..9864947c6 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1115,8 +1115,8 @@ class _RemoteMenubarState extends State { dismissOnClicked: true, ), MenuEntryRadioOption( - text: translate('Two way'), - value: kRemoteAudioTwoWay, + text: translate('Dual way'), + value: kRemoteAudioDualWay, dismissOnClicked: true, ), ], diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index da4865069..48b999438 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -446,7 +446,7 @@ enum ImageQuality { enum AudioMode { GuestToHost = 0; - TwoWay = 1; + DualWay = 1; } message VideoCodecState { diff --git a/src/client.rs b/src/client.rs index d76f930c8..649b180bb 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1262,8 +1262,8 @@ impl LoginConfigHandler { pub fn get_audio_mode_enum(q: &str, ignore_default: bool) -> Option { if q == "guest-to-host" { Some(AudioMode::GuestToHost) - } else if q == "two-way" { - Some(AudioMode::TwoWay) + } else if q == "dual-way" { + Some(AudioMode::DualWay) } else { if ignore_default { None diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index af8c1048b..a284fdade 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -393,7 +393,7 @@ impl Remote { allow_err!(sender.send(())); } } - AudioMode::TwoWay => { + AudioMode::DualWay => { // Start audio thread for playback. // Cancel previous local audio session. if let Some(sender) = self.stop_local_audio_sender.take() { @@ -889,14 +889,15 @@ impl Remote { self.handler.load_last_jobs(); } - // Start audio thread for playback if current audio mode is two-way transmission. + // Start audio thread for playback if current audio mode is dual-way transmission. if !self.handler.is_file_transfer() && !self.handler.is_port_forward() { let audio_mode = LoginConfigHandler::get_audio_mode_enum( self.handler.load_config().audio_mode.as_str(), false, ) .unwrap_or(AudioMode::GuestToHost); - if audio_mode == AudioMode::TwoWay { + log::debug!("current audio mode: {:?}", audio_mode); + if audio_mode == AudioMode::DualWay { // Cancel previous local audio session. if let Some(sender) = self.stop_local_audio_sender.take() { allow_err!(sender.send(())); diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 197435158..e45dc5fb5 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index c74f352ce..84bfcb384 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", "自动"), ("Other Default Options", "其它默认选项"), ("Guest to Host", "被控到主机"), - ("Two way", "双向"), + ("Dual way", "双向"), ("Audio Transmission Mode", "音频传输模式"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index d956ddf53..ef9cd7bf8 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 9e771567a..32aa1f0af 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -437,7 +437,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please confirm if you want to share your desktop?", ""), ("Closed as expected", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ("Display", ""), ("Default View Style", ""), diff --git a/src/lang/de.rs b/src/lang/de.rs index a112385a6..f8fac0737 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", "Automatisch"), ("Other Default Options", "Weitere Standardoptionen"), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 342eac51c..4aa2be8db 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 74acd8c69..932936da3 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -447,7 +447,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", "Otras opciones predeterminadas"), ("Closed as expected", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 50e883227..b8c45fbee 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", "خودکار"), ("Other Default Options", "سایر گزینه های پیش فرض"), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 9bfdb6b1e..64a8b4e40 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", "Auto"), ("Other Default Options", "Autres options par défaut"), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index a569b750f..3918db55b 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index e28294de0..edad7ecd0 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index ece6c9233..1b2dc4ad5 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index e252219c1..27432303d 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", "Auto"), ("Other Default Options", "Altre Opzioni Predefinite"), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 036bc8ec7..ae375b8ee 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 6da983849..417f88fe6 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 459139f58..e852278dd 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 483879d49..4cce52e08 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", "Auto"), ("Other Default Options", "Inne opcje domyślne"), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index cff00333a..29252926f 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 9fe5eab8c..8ec40cf13 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 36e2a99de..c4f798abf 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 31f24a5e8..949eba641 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -448,7 +448,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please confirm if you want to share your desktop?", "Подтвердите, что хотите поделиться своим рабочим столом?"), ("Closed as expected", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 8cf858df0..7de4d10ce 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 0e2208c3c..bf30f96d8 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 44159fb4a..db560166a 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 892b3664e..599cd651b 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 619a68508..c0616300c 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index f0458b115..282b564d7 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index f61ba325a..b2bee959a 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index cade148a7..b6efeaf0a 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 46cc90c1e..eea71e6bf 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", "自動"), ("Other Default Options", "其它默認選項"), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 7c355edd5..f0d85a551 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index f7640ae50..5e4009570 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 73414e405..1e7848505 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -669,13 +669,6 @@ impl Session { } } } - - fn get_audio_transmission_mode(&self, id: &str) { - - } - fn set_audio_transmission_mode(&self, id: &str, mode: String) { - - } } pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { From cab1fc719aed30a7f0afac289d55e1b03375ac91 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 30 Jan 2023 23:42:38 +0800 Subject: [PATCH 019/199] feat: add audio mode in sciter --- src/ui/header.tis | 10 +++++++++- src/ui/remote.rs | 2 ++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/ui/header.tis b/src/ui/header.tis index dd0b35541..e3f0c70a1 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -183,6 +183,9 @@ class Header: Reactor.Component {
  • {svg_checkmark}{translate('Balanced')}
  • {svg_checkmark}{translate('Optimize reaction time')}
  • {svg_checkmark}{translate('Custom')}
  • +
    +
  • {svg_checkmark}{translate('Guest to Host')}
  • +
  • {svg_checkmark}{translate('Dual way')}
  • {show_codec ?
  • {svg_checkmark}Auto
  • @@ -378,7 +381,7 @@ class Header: Reactor.Component { togglePrivacyMode(me.id); } else if (me.id == "show-quality-monitor") { toggleQualityMonitor(me.id); - }else if (me.attributes.hasClass("toggle-option")) { + } else if (me.attributes.hasClass("toggle-option")) { handler.toggle_option(me.id); toggleMenuState(); } else if (!me.attributes.hasClass("selected")) { @@ -391,6 +394,8 @@ class Header: Reactor.Component { } else if (type == "codec-preference") { handler.set_option("codec-preference", me.id); handler.change_prefer_codec(); + } else if (type == "audio-mode") { + handler.save_audio_mode(me.id); } toggleMenuState(); } @@ -434,6 +439,9 @@ function toggleMenuState() { var c = handler.get_option("codec-preference"); if (!c) c = "auto"; values.push(c); + var a = handler.get_audio_mode(); + if (!a) a = "guest-to-host"; + values.push(a); for (var el in $$(menu#display-options li)) { el.attributes.toggleClass("selected", values.indexOf(el.id) >= 0); } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 21504d20d..541d3a141 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -420,6 +420,8 @@ impl sciter::EventHandler for SciterSession { fn supported_hwcodec(); fn change_prefer_codec(); fn restart_remote_device(); + fn save_audio_mode(String); + fn get_audio_mode(); } } From 7e5c5b50e5a6dd6b9c1f265cfb1520db4319e739 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 31 Jan 2023 10:01:31 +0800 Subject: [PATCH 020/199] feat: set to default input device when in dual-way --- src/client/io_loop.rs | 10 +++++++-- src/common.rs | 44 +++++++++++++++++++++++++++++++++++++++- src/flutter_ffi.rs | 10 +++++++++ src/platform/linux.rs | 18 ++++++++++++++++ src/server/connection.rs | 7 ++++++- 5 files changed, 85 insertions(+), 4 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index a284fdade..9117c8c5f 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -2,13 +2,14 @@ use crate::client::{ Client, CodecFormat, LoginConfigHandler, MediaData, MediaSender, QualityStatus, MILLI1, SEC30, SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED, }; +use crate::common::{get_default_sound_input, set_sound_input}; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::common::{check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}; use crate::{audio_service, common, ConnInner, CLIENT_SERVER}; #[cfg(windows)] use clipboard::{cliprdr::CliprdrClientContext, ContextSend}; -use hbb_common::futures::channel::mpsc::unbounded; + use hbb_common::tokio::sync::mpsc::error::TryRecvError; use crate::server::Service; @@ -32,7 +33,7 @@ use hbb_common::tokio::{ }; use hbb_common::{allow_err, message_proto::*, sleep}; use hbb_common::{fs, log, Stream}; -use std::borrow::Borrow; + use std::collections::HashMap; use std::sync::atomic::{AtomicUsize, Ordering}; @@ -270,6 +271,11 @@ impl Remote { if self.handler.is_file_transfer() || self.handler.is_port_forward() { return None; } + // Switch to default input device + let default_sound_device = get_default_sound_input(); + if let Some(device) = default_sound_device { + set_sound_input(device); + } // Create a channel to receive error or closed message let (tx, rx) = std::sync::mpsc::channel(); let (tx_audio_data, mut rx_audio_data) = hbb_common::tokio::sync::mpsc::unbounded_channel(); diff --git a/src/common.rs b/src/common.rs index c2d5a81f0..9cbc9b150 100644 --- a/src/common.rs +++ b/src/common.rs @@ -30,6 +30,8 @@ use hbb_common::{ // #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] use hbb_common::{config::RENDEZVOUS_PORT, futures::future::join_all}; +use crate::ui_interface::{set_option, get_option}; + pub type NotifyMessageBox = fn(String, String, String, String) -> dyn Future; pub const CLIPBOARD_NAME: &'static str = "clipboard"; @@ -105,6 +107,46 @@ pub fn check_clipboard( None } +/// Set sound input device. +pub fn set_sound_input(device: String) { + let prior_device = get_option("audio-input".to_owned()); + if prior_device != device { + log::info!("switch to audio input device {}", device); + set_option("audio-input".to_owned(), device); + } else { + log::info!("audio input is already set to {}", device); + } +} + +/// Get system's default sound input device name. +#[inline] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn get_default_sound_input() -> Option { + #[cfg(not(target_os = "linux"))] + { + use cpal::traits::{DeviceTrait, HostTrait}; + let host = cpal::default_host(); + let dev = host.default_input_device(); + return if let Some(dev) = dev { + match dev.name() { + Ok(name) => Some(name), + Err(_) => None, + } + } else { + None + }; + } + #[cfg(target_os = "linux")] + { + let input = crate::platform::linux::get_default_pa_source(); + return if let Some(input) = input { + Some(input.1) + } else { + None + }; + } +} + #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn update_clipboard(clipboard: Clipboard, old: Option<&Arc>>) { let content = if clipboard.compress { @@ -715,5 +757,5 @@ pub fn make_fd_to_json(id: i32, path: String, entries: &Vec) -> Strin #[cfg(test)] mod test_common { - use super::*; + } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 10fd67fdd..31cb07c07 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -4,6 +4,9 @@ use std::str::FromStr; use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; use serde_json::json; +use crate::common::{is_keyboard_mode_supported, get_default_sound_input}; +use hbb_common::message_proto::KeyboardMode; +use hbb_common::ResultType; use hbb_common::{ config::{self, LocalConfig, ONLINE, PeerConfig}, fs, log, @@ -534,6 +537,13 @@ pub fn main_get_sound_inputs() -> Vec { vec![String::from("")] } +pub fn main_get_default_sound_input() -> Option { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return get_default_sound_input(); + #[cfg(any(target_os = "android", target_os = "ios"))] + String::from("") +} + pub fn main_get_hostname() -> SyncReturn { SyncReturn(crate::common::hostname()) } diff --git a/src/platform/linux.rs b/src/platform/linux.rs index ac3b32a49..8fa95ac90 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -534,6 +534,24 @@ pub fn get_pa_sources() -> Vec<(String, String)> { out } +pub fn get_default_pa_source() -> Option<(String, String)> { + use pulsectl::controllers::*; + match SourceController::create() { + Ok(mut handler) => { + if let Ok(dev) = handler.get_default_device() { + return Some(( + dev.name.unwrap_or("".to_owned()), + dev.description.unwrap_or("".to_owned()), + )); + } + } + Err(err) => { + log::error!("Failed to get_pa_source: {:?}", err); + } + } + None +} + pub fn lock_screen() { std::process::Command::new("xdg-screensaver") .arg("lock") diff --git a/src/server/connection.rs b/src/server/connection.rs index d5c2103b1..20cbe0f86 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -5,7 +5,7 @@ use crate::clipboard_file::*; use crate::common::update_clipboard; #[cfg(windows)] use crate::portable_service::client as portable_client; -use crate::{video_service, client::{MediaSender, start_audio_thread, LatencyController, MediaData}}; +use crate::{video_service, client::{MediaSender, start_audio_thread, LatencyController, MediaData}, common::{get_default_sound_input, set_sound_input}}; #[cfg(any(target_os = "android", target_os = "ios"))] use crate::{common::DEVICE_NAME, flutter::connection_manager::start_channel}; use crate::{ipc, VERSION}; @@ -1542,6 +1542,11 @@ impl Connection { }, Some(misc::Union::AudioFormat(format)) => { if !self.disable_audio { + // Switch to default input device + let default_sound_device = get_default_sound_input(); + if let Some(device) = default_sound_device { + set_sound_input(device); + } allow_err!(self.audio_sender.send(MediaData::AudioFormat(format))); } } From 60925057f0c66ded4d9dc66b52d85b86059ffc1c Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 31 Jan 2023 10:23:58 +0800 Subject: [PATCH 021/199] fix: poison error on setting sound input --- src/common.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/common.rs b/src/common.rs index 9cbc9b150..70d50619e 100644 --- a/src/common.rs +++ b/src/common.rs @@ -112,7 +112,9 @@ pub fn set_sound_input(device: String) { let prior_device = get_option("audio-input".to_owned()); if prior_device != device { log::info!("switch to audio input device {}", device); - set_option("audio-input".to_owned(), device); + std::thread::spawn(move || { + set_option("audio-input".to_owned(), device); + }); } else { log::info!("audio input is already set to {}", device); } From 038d660e6063c6a8222cd7f8c2753ee07492a6e8 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 31 Jan 2023 11:10:14 +0800 Subject: [PATCH 022/199] fix: android build --- src/common.rs | 6 ++++++ src/flutter_ffi.rs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/common.rs b/src/common.rs index 70d50619e..3e6409c53 100644 --- a/src/common.rs +++ b/src/common.rs @@ -149,6 +149,12 @@ pub fn get_default_sound_input() -> Option { } } +#[inline] +#[cfg(any(target_os = "android", target_os = "ios"))] +pub fn get_default_sound_input() -> Option { + None +} + #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn update_clipboard(clipboard: Clipboard, old: Option<&Arc>>) { let content = if clipboard.compress { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 31cb07c07..0fe6818de 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -541,7 +541,7 @@ pub fn main_get_default_sound_input() -> Option { #[cfg(not(any(target_os = "android", target_os = "ios")))] return get_default_sound_input(); #[cfg(any(target_os = "android", target_os = "ios"))] - String::from("") + None } pub fn main_get_hostname() -> SyncReturn { From ebec8811c2ec49da7bd3f59db98d38ad0ead84a6 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 31 Jan 2023 13:32:10 +0800 Subject: [PATCH 023/199] opt: add microphone permission tip --- flutter/lib/common.dart | 27 ++++++++++++++ .../lib/desktop/pages/desktop_home_page.dart | 35 +++++++++++++++++-- flutter/macos/Runner/Info.plist | 2 ++ flutter/macos/Runner/MainFlutterWindow.swift | 18 ++++++++++ src/lang/ca.rs | 1 + src/lang/cn.rs | 1 + src/lang/cs.rs | 1 + src/lang/da.rs | 1 + src/lang/de.rs | 1 + src/lang/en.rs | 3 +- src/lang/eo.rs | 1 + src/lang/es.rs | 1 + src/lang/fa.rs | 1 + src/lang/fr.rs | 1 + src/lang/gr.rs | 1 + src/lang/hu.rs | 1 + src/lang/id.rs | 1 + src/lang/it.rs | 1 + src/lang/ja.rs | 1 + src/lang/ko.rs | 1 + src/lang/kz.rs | 1 + src/lang/pl.rs | 1 + src/lang/pt_PT.rs | 1 + src/lang/ptbr.rs | 1 + src/lang/ro.rs | 1 + src/lang/ru.rs | 1 + src/lang/sk.rs | 1 + src/lang/sl.rs | 1 + src/lang/sq.rs | 1 + src/lang/sr.rs | 1 + src/lang/sv.rs | 1 + src/lang/template.rs | 1 + src/lang/th.rs | 1 + src/lang/tr.rs | 1 + src/lang/tw.rs | 1 + src/lang/ua.rs | 1 + src/lang/vn.rs | 1 + 37 files changed, 114 insertions(+), 3 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 30d38b8db..df2a75f56 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1723,3 +1723,30 @@ Future updateSystemWindowTheme() async { } } } +/// macOS only +/// +/// Note: not found a general solution for rust based AVFoundation bingding. +/// [AVFoundation] crate has compile error. +const kMacOSPermChannel = MethodChannel("org.rustdesk.rustdesk/macos"); + +enum PermissionAuthorizeType { + undetermined, + authorized, + denied, // and restricted +} + +Future osxCanRecordAudio() async { + int res = await kMacOSPermChannel.invokeMethod("canRecordAudio"); + print(res); + if (res > 0) { + return PermissionAuthorizeType.authorized; + } else if (res == 0) { + return PermissionAuthorizeType.undetermined; + } else { + return PermissionAuthorizeType.denied; + } +} + +Future osxRequestAudio() async { + return await kMacOSPermChannel.invokeMethod("requestRecordAudio"); +} diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 0501c298a..71dd2c96e 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -44,6 +44,7 @@ class _DesktopHomePageState extends State var watchIsCanScreenRecording = false; var watchIsProcessTrust = false; var watchIsInputMonitoring = false; + var watchIsCanRecordAudio = false; Timer? _updateTimer; @override @@ -79,7 +80,16 @@ class _DesktopHomePageState extends State buildTip(context), buildIDBoard(context), buildPasswordBoard(context), - buildHelpCards(), + FutureBuilder( + future: buildHelpCards(), + builder: (_, data) { + if (data.hasData) { + return data.data!; + } else { + return const Offstage(); + } + }, + ), ], ), ), @@ -302,7 +312,7 @@ class _DesktopHomePageState extends State ); } - Widget buildHelpCards() { + Future buildHelpCards() async { if (updateUrl.isNotEmpty) { return buildInstallCard( "Status", @@ -348,6 +358,13 @@ class _DesktopHomePageState extends State return buildInstallCard("", "install_daemon_tip", "Install", () async { bind.mainIsInstalledDaemon(prompt: true); }); + } else if ((await osxCanRecordAudio() != + PermissionAuthorizeType.authorized)) { + return buildInstallCard("Permissions", "config_microphone", "Configure", + () async { + osxRequestAudio(); + watchIsCanRecordAudio = true; + }); } } else if (Platform.isLinux) { if (bind.mainCurrentIsWayland()) { @@ -481,6 +498,20 @@ class _DesktopHomePageState extends State setState(() {}); } } + if (watchIsCanRecordAudio) { + if (Platform.isMacOS) { + Future.microtask(() async { + if ((await osxCanRecordAudio() == + PermissionAuthorizeType.authorized)) { + watchIsCanRecordAudio = false; + setState(() {}); + } + }); + } else { + watchIsCanRecordAudio = false; + setState(() {}); + } + } }); Get.put(svcStopped, tag: 'stop-service'); rustDeskWinManager.registerActiveWindowListener(onActiveWindowChanged); diff --git a/flutter/macos/Runner/Info.plist b/flutter/macos/Runner/Info.plist index c926019ab..96616e8c4 100644 --- a/flutter/macos/Runner/Info.plist +++ b/flutter/macos/Runner/Info.plist @@ -43,6 +43,8 @@ $(PRODUCT_COPYRIGHT) NSMainNibFile MainMenu + NSMicrophoneUsageDescription + Record the sound from microphone for the purpose of the remote desktop. NSPrincipalClass NSApplication diff --git a/flutter/macos/Runner/MainFlutterWindow.swift b/flutter/macos/Runner/MainFlutterWindow.swift index 97b46bb84..21e870320 100644 --- a/flutter/macos/Runner/MainFlutterWindow.swift +++ b/flutter/macos/Runner/MainFlutterWindow.swift @@ -1,4 +1,5 @@ import Cocoa +import AVFoundation import FlutterMacOS import desktop_multi_window // import bitsdojo_window_macos @@ -81,6 +82,23 @@ class MainFlutterWindow: NSWindow { case "terminate": NSApplication.shared.terminate(self) result(nil) + case "canRecordAudio": + switch AVCaptureDevice.authorizationStatus(for: .audio) { + case .authorized: + result(1) + break + case .notDetermined: + result(0) + break + default: + result(-1) + break + } + case "requestRecordAudio": + AVCaptureDevice.requestAccess(for: .audio, completionHandler: { granted in + result(granted) + }) + break default: result(FlutterMethodNotImplemented) } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index e45dc5fb5..e65927876 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 84bfcb384..bcb2c3daf 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "如果你使用英伟达显卡, 并且远程窗口在会话建立后会立刻关闭, 那么安装 nouveau 驱动并且选择使用软件渲染可能会有帮助。重启软件后生效。"), ("Always use software rendering", "使用软件渲染"), ("config_input", "为了能够通过键盘控制远程桌面, 请给予 RustDesk \"输入监控\" 权限。"), + ("config_microphone", "为了支持通过麦克风进行音频传输,请给予 RustDesk \"录音\"权限。"), ("request_elevation_tip", "如果对面有人, 也可以请求提升权限。"), ("Wait", "等待"), ("Elevation Error", "提权失败"), diff --git a/src/lang/cs.rs b/src/lang/cs.rs index ef9cd7bf8..d16e3abef 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/da.rs b/src/lang/da.rs index 32aa1f0af..23884b995 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/de.rs b/src/lang/de.rs index f8fac0737..1839edb87 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Wenn Sie eine Nvidia-Grafikkarte haben und sich das entfernte Fenster sofort nach dem Herstellen der Verbindung schließt, kann es helfen, den Nouveau-Treiber zu installieren und Software-Rendering zu verwenden. Ein Neustart der Software ist erforderlich."), ("Always use software rendering", "Software-Rendering immer verwenden"), ("config_input", "Um den entfernten Desktop mit der Tastatur steuern zu können, müssen Sie RustDesk \"Input Monitoring\"-Rechte erteilen."), + ("config_microphone", ""), ("request_elevation_tip", "Sie können auch erhöhte Rechte anfordern, wenn sich jemand auf der Gegenseite befindet."), ("Wait", "Warten"), ("Elevation Error", "Berechtigungsfehler"), diff --git a/src/lang/en.rs b/src/lang/en.rs index 6eed43a77..37c08a974 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -41,6 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("config_input", "In order to control remote desktop with keyboard, you need to grant RustDesk \"Input Monitoring\" permissions."), ("request_elevation_tip","You can also request elevation if there is someone on the remote side."), ("wait_accept_uac_tip","Please wait for the remote user to accept the UAC dialog."), - ("still_click_uac_tip", "Still requires the remote user to click OK on the UAC window of running RustDesk.") + ("still_click_uac_tip", "Still requires the remote user to click OK on the UAC window of running RustDesk."), + ("config_microphone", "In order to speak remotely, you need to grant RustDesk \"Record Audio\" permissions.") ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 4aa2be8db..aa8829874 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/es.rs b/src/lang/es.rs index 932936da3..da13843f0 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Si tienes una gráfica Nvidia y la ventana remota se cierra inmediatamente, instalar el driver nouveau y elegir renderizado por software podría ayudar. Se requiere reiniciar la aplicación."), ("Always use software rendering", "Usar siempre renderizado por software"), ("config_input", "Para controlar el escritorio remoto con el teclado necesitas dar a RustDesk permisos de \"Monitorización de entrada\"."), + ("config_microphone", ""), ("request_elevation_tip", "También puedes solicitar elevación si hay alguien en el lado remoto."), ("Wait", "Esperar"), ("Elevation Error", "Error de elevación"), diff --git a/src/lang/fa.rs b/src/lang/fa.rs index b8c45fbee..7664af99e 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "اگر کارت گرافیک Nvidia دارید و پنجره راه دور بلافاصله پس از اتصال بسته می شود، درایور nouveau را نصب نمایید و انتخاب گزینه استفاده از رندر نرم افزار می تواند کمک کننده باشد. راه اندازی مجدد نرم افزار مورد نیاز است."), ("Always use software rendering", "همیشه از رندر نرم افزاری استفاده کنید"), ("config_input", "برای کنترل دسکتاپ از راه دور با صفحه کلید، باید مجوز RustDesk \"Input Monitoring\" را بدهید."), + ("config_microphone", ""), ("request_elevation_tip", "همچنین می توانید در صورت وجود شخصی در سمت راه دور درخواست ارتفاع دهید."), ("Wait", "صبر کنید"), ("Elevation Error", "خطای ارتفاع"), diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 64a8b4e40..db49b5a78 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Si vous avez une carte graphique NVIDIA et que la fenêtre distante se ferme immédiatement après la connexion, l'installation du pilote Nouveau et le choix d'utiliser le rendu du logiciel peuvent aider. Un redémarrage du logiciel est requis."), ("Always use software rendering", "Utiliser toujours le rendu logiciel"), ("config_input", "Afin de contrôler le bureau à distance avec le clavier, vous devez accorder à RustDesk l'autorisation \"Surveillance de l’entrée\"."), + ("config_microphone", ""), ("request_elevation_tip", "Vous pouvez également demander une augmentation des privilèges s'il y a quelqu'un du côté distant."), ("Wait", "En cours"), ("Elevation Error", "Erreur d'augmentation des privilèges"), diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 3918db55b..5312e6381 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Εάν έχετε κάρτα γραφικών Nvidia και το παράθυρο σύνδεσης κλείνει αμέσως μετά τη σύνδεση, η εγκατάσταση του προγράμματος οδήγησης nouveau και η επιλογή χρήσης της επιτάχυνσης γραφικών μέσω λογισμικού μπορεί να βοηθήσει. Απαιτείται επανεκκίνηση."), ("Always use software rendering", "Επιτάχυνση γραφικών μέσω λογισμικού"), ("config_input", "Για να ελέγξετε την απομακρυσμένη επιφάνεια εργασίας με πληκτρολόγιο, πρέπει να εκχωρήσετε δικαιώματα στο RustDesk"), + ("config_microphone", ""), ("request_elevation_tip", "αίτημα ανύψωσης δικαιωμάτων χρήστη"), ("Wait", "Περιμένετε"), ("Elevation Error", "Σφάλμα ανύψωσης δικαιωμάτων χρήστη"), diff --git a/src/lang/hu.rs b/src/lang/hu.rs index edad7ecd0..2f6c490ad 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/id.rs b/src/lang/id.rs index 1b2dc4ad5..7b9325076 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/it.rs b/src/lang/it.rs index 27432303d..31864b220 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Se si dispone di una scheda grafica Nvidia e la finestra remota si chiude immediatamente dopo la connessione, l'installazione del driver nouveau e la scelta di utilizzare il rendering software possono aiutare. È necessario un riavvio del software."), ("Always use software rendering", "Usa sempre il render Software"), ("config_input", "Per controllare il desktop remoto con la tastiera, è necessario concedere le autorizzazioni a RustDesk \"Monitoraggio dell'input\"."), + ("config_microphone", ""), ("request_elevation_tip", "È possibile richiedere l'elevazione se c'è qualcuno sul lato remoto."), ("Wait", "Attendi"), ("Elevation Error", "Errore durante l'elevazione dei diritti"), diff --git a/src/lang/ja.rs b/src/lang/ja.rs index ae375b8ee..5f2b68c46 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 417f88fe6..59cc9fdff 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/kz.rs b/src/lang/kz.rs index e852278dd..8a939764b 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 4cce52e08..788aa8b62 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Jeżeli posiadasz kartę graficzną Nvidia i okno zamyka się natychmiast po nawiązaniu połączenia, instalacja sterownika nouveau i wybór renderowania programowego mogą pomóc. Restart aplikacji jest wymagany."), ("Always use software rendering", "Zawsze używaj renderowania programowego"), ("config_input", "By kontrolować zdalne urządzenie przy pomocy klawiatury, musisz udzielić aplikacji RustDesk uprawnień do \"Urządzeń Wejściowych\"."), + ("config_microphone", ""), ("request_elevation_tip", "Możesz poprosić o podniesienie uprawnień jeżeli ktoś posiada dostęp do zdalnego urządzenia."), ("Wait", "Czekaj"), ("Elevation Error", "Błąd przy podnoszeniu uprawnień"), diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 29252926f..c6899ee54 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 8ec40cf13..cdac5f686 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/ro.rs b/src/lang/ro.rs index c4f798abf..5865d0206 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 949eba641..fe1de2e91 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Если у вас видеокарта Nvidia и удалённое окно закрывается сразу после подключения, может помочь установка драйвера Nouveau и выбор использования программной визуализации. Потребуется перезапуск."), ("Always use software rendering", "Использовать программную визуализацию"), ("config_input", "Чтобы управлять удалённым рабочим столом с помощью клавиатуры, необходимо предоставить RustDesk разрешения \"Мониторинг ввода\"."), + ("config_microphone", ""), ("request_elevation_tip", "Также можно запросить повышение прав, если кто-то есть на удалённой стороне."), ("Wait", "Ждите"), ("Elevation Error", "Ошибка повышения прав"), diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 7de4d10ce..88f09313f 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/sl.rs b/src/lang/sl.rs index bf30f96d8..f78a6e9e3 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/sq.rs b/src/lang/sq.rs index db560166a..63e834c25 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 599cd651b..33355fd38 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/sv.rs b/src/lang/sv.rs index c0616300c..8af2ccb8a 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/template.rs b/src/lang/template.rs index 282b564d7..1abc20b36 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/th.rs b/src/lang/th.rs index b2bee959a..173143821 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/tr.rs b/src/lang/tr.rs index b6efeaf0a..072275334 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/tw.rs b/src/lang/tw.rs index eea71e6bf..8c0968901 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "如果你使用英偉達顯卡, 並且遠程窗口在會話建立後會立刻關閉, 那麼安裝nouveau驅動並且選擇使用軟件渲染可能會有幫助。重啟軟件後生效。"), ("Always use software rendering", "使用軟件渲染"), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", "如果對面有人, 也可以請求提升權限。"), ("Wait", "等待"), ("Elevation Error", "提權失敗"), diff --git a/src/lang/ua.rs b/src/lang/ua.rs index f0d85a551..1934a8eb4 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 5e4009570..24c0d9009 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), From 2452a58eaa5e0d14fd5a16e135a40a9acaf547ea Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 31 Jan 2023 14:07:14 +0800 Subject: [PATCH 024/199] opt: rename and move audio transmission mode --- .../lib/desktop/widgets/remote_menubar.dart | 53 ++++++++++--------- src/lang/ca.rs | 2 + src/lang/cn.rs | 2 + src/lang/cs.rs | 2 + src/lang/da.rs | 4 +- src/lang/de.rs | 2 + src/lang/eo.rs | 2 + src/lang/es.rs | 4 +- src/lang/fa.rs | 2 + src/lang/fr.rs | 2 + src/lang/gr.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/ro.rs | 2 + src/lang/ru.rs | 4 +- src/lang/sk.rs | 2 + src/lang/sl.rs | 4 +- src/lang/sq.rs | 2 + src/lang/sr.rs | 2 + src/lang/sv.rs | 2 + src/lang/template.rs | 2 + src/lang/th.rs | 2 + src/lang/tr.rs | 2 + src/lang/tw.rs | 2 + src/lang/ua.rs | 2 + src/lang/vn.rs | 2 + 33 files changed, 91 insertions(+), 34 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 9864947c6..0df962cba 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -884,7 +884,33 @@ class _RemoteMenubarState extends State { // )); // } } - + displayMenu.addAll([ + MenuEntryDivider(), + MenuEntryRadios( + text: translate('Audio Transmission Mode'), + optionsGetter: () => [ + MenuEntryRadioOption( + text: translate('Guest to host audio transmission'), + value: kRemoteAudioGuestToHost, + dismissOnClicked: true, + ), + MenuEntryRadioOption( + text: translate('Dual-way audio transmission'), + value: kRemoteAudioDualWay, + dismissOnClicked: true, + ), + ], + curOptionGetter: () async => + // null means peer id is not found, which there's no need to care about + await bind.sessionGetAudioMode(id: widget.id) ?? '', + optionSetter: (String oldValue, String newValue) async { + if (oldValue != newValue) { + await bind.sessionSetAudioMode(id: widget.id, value: newValue); + } + }, + padding: padding, + ), + ]); return displayMenu; } @@ -1106,31 +1132,6 @@ class _RemoteMenubarState extends State { padding: padding, ), MenuEntryDivider(), - MenuEntryRadios( - text: translate('Audio Transmission Mode'), - optionsGetter: () => [ - MenuEntryRadioOption( - text: translate('Guest to Host'), - value: kRemoteAudioGuestToHost, - dismissOnClicked: true, - ), - MenuEntryRadioOption( - text: translate('Dual way'), - value: kRemoteAudioDualWay, - dismissOnClicked: true, - ), - ], - curOptionGetter: () async => - // null means peer id is not found, which there's no need to care about - await bind.sessionGetAudioMode(id: widget.id) ?? '', - optionSetter: (String oldValue, String newValue) async { - if (oldValue != newValue) { - await bind.sessionSetAudioMode(id: widget.id, value: newValue); - } - }, - padding: padding, - ), - MenuEntryDivider(), ]; if (widget.state.viewStyle.value == kRemoteViewStyleOriginal) { diff --git a/src/lang/ca.rs b/src/lang/ca.rs index e65927876..4404e178d 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index bcb2c3daf..08f6824c7 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", "其它默认选项"), ("Guest to Host", "被控到主机"), ("Dual way", "双向"), + ("Guest to host audio transmission", "被控到主机音频传输"), + ("Dual-way audio transmission", "双向音频传输"), ("Audio Transmission Mode", "音频传输模式"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index d16e3abef..a2a19a37a 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 23884b995..905f4814e 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -437,8 +437,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), ("Closed as expected", ""), - ("Guest to Host", ""), - ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ("Display", ""), ("Default View Style", ""), diff --git a/src/lang/de.rs b/src/lang/de.rs index 1839edb87..4028e3337 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", "Weitere Standardoptionen"), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index aa8829874..fe3830b99 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index da13843f0..b9b31f109 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -447,8 +447,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", "Otras opciones predeterminadas"), ("Closed as expected", ""), - ("Guest to Host", ""), - ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 7664af99e..0b92c6658 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", "سایر گزینه های پیش فرض"), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index db49b5a78..4965f6dab 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", "Autres options par défaut"), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 5312e6381..e40151ccf 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 2f6c490ad..0e1887e48 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 7b9325076..689ae98cf 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 31864b220..65f91ecec 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", "Altre Opzioni Predefinite"), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 5f2b68c46..33fb2da05 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 59cc9fdff..c874dd695 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 8a939764b..01014bab0 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 788aa8b62..9dd005bdd 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", "Inne opcje domyślne"), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index c6899ee54..716d3df82 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index cdac5f686..c7d0cd6ec 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 5865d0206..2d48b91b4 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index fe1de2e91..8224cd5eb 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -448,8 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", "Другие параметры по умолчанию"), ("Please confirm if you want to share your desktop?", "Подтвердите, что хотите поделиться своим рабочим столом?"), ("Closed as expected", ""), - ("Guest to Host", ""), - ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 88f09313f..5e0330954 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index f78a6e9e3..a75da46bd 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -446,8 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 63e834c25..d3964a2e9 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 33355fd38..78059645d 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 8af2ccb8a..ca2257756 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 1abc20b36..4355d643a 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 173143821..57dfe6e43 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 072275334..49a42af4a 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 8c0968901..50e684258 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", "其它默認選項"), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 1934a8eb4..f37ed341e 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 24c0d9009..5788a7f3d 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } From efa4530c97f6eee9c8c8dcd36188218ada8e52f1 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 31 Jan 2023 22:49:17 +0800 Subject: [PATCH 025/199] feat: add chat svg --- flutter/assets/chat.svg | 1 + .../lib/desktop/widgets/remote_menubar.dart | 96 +++++++++++-------- src/lang/cn.rs | 2 + 3 files changed, 58 insertions(+), 41 deletions(-) create mode 100644 flutter/assets/chat.svg diff --git a/flutter/assets/chat.svg b/flutter/assets/chat.svg new file mode 100644 index 000000000..03491be6e --- /dev/null +++ b/flutter/assets/chat.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 0df962cba..0004c65fe 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -9,6 +9,7 @@ import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:debounce_throttle/debounce_throttle.dart'; @@ -478,20 +479,6 @@ class _RemoteMenubarState extends State { ); } - Widget _buildChat(BuildContext context) { - return IconButton( - tooltip: translate('Chat'), - onPressed: () { - widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID); - widget.ffi.chatModel.toggleChatOverlay(); - }, - icon: const Icon( - Icons.message, - color: _MenubarTheme.commonColor, - ), - ); - } - Widget _buildMonitor(BuildContext context) { final pi = widget.ffi.ffiModel.pi; return mod_menu.PopupMenuButton( @@ -695,6 +682,60 @@ class _RemoteMenubarState extends State { ); } + Widget _buildChat(BuildContext context) { + FfiModel ffiModel = Provider.of(context); + return mod_menu.PopupMenuButton( + padding: EdgeInsets.zero, + icon: SvgPicture.asset( + "assets/chat.svg", + color: _MenubarTheme.commonColor, + width: Theme.of(context).iconTheme.size ?? 24.0, + height: Theme.of(context).iconTheme.size ?? 24.0, + ), + tooltip: translate('Chat'), + position: mod_menu.PopupMenuPosition.under, + itemBuilder: (BuildContext context) => _getChatMenu(context) + .map((entry) => entry.build( + context, + const MenuConfig( + commonColor: _MenubarTheme.commonColor, + height: _MenubarTheme.height, + dividerHeight: _MenubarTheme.dividerHeight, + ))) + .expand((i) => i) + .toList(), + ); + } + + List> _getChatMenu(BuildContext context) { + final List> chatMenu = []; + const EdgeInsets padding = EdgeInsets.only(left: 14.0, right: 5.0); + chatMenu.addAll([ + MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Text chat'), + style: style, + ), + proc: () { + widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID); + widget.ffi.chatModel.toggleChatOverlay(); + }, + padding: padding, + dismissOnClicked: true, + ), + MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Voice call'), + style: style, + ), + proc: () {}, + padding: padding, + dismissOnClicked: true, + ), + ]); + return chatMenu; + } + List> _getControlMenu(BuildContext context) { final pi = widget.ffi.ffiModel.pi; final perms = widget.ffi.ffiModel.permissions; @@ -884,33 +925,6 @@ class _RemoteMenubarState extends State { // )); // } } - displayMenu.addAll([ - MenuEntryDivider(), - MenuEntryRadios( - text: translate('Audio Transmission Mode'), - optionsGetter: () => [ - MenuEntryRadioOption( - text: translate('Guest to host audio transmission'), - value: kRemoteAudioGuestToHost, - dismissOnClicked: true, - ), - MenuEntryRadioOption( - text: translate('Dual-way audio transmission'), - value: kRemoteAudioDualWay, - dismissOnClicked: true, - ), - ], - curOptionGetter: () async => - // null means peer id is not found, which there's no need to care about - await bind.sessionGetAudioMode(id: widget.id) ?? '', - optionSetter: (String oldValue, String newValue) async { - if (oldValue != newValue) { - await bind.sessionSetAudioMode(id: widget.id, value: newValue); - } - }, - padding: padding, - ), - ]); return displayMenu; } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 08f6824c7..65039f0fe 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -450,6 +450,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Dual way", "双向"), ("Guest to host audio transmission", "被控到主机音频传输"), ("Dual-way audio transmission", "双向音频传输"), + ("Voice call", "语音通话"), + ("Text chat", "文字聊天"), ("Audio Transmission Mode", "音频传输模式"), ].iter().cloned().collect(); } From b335d2c82840dd3ef09efc9ebdd7d417ca3a9a25 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 5 Feb 2023 15:36:31 +0800 Subject: [PATCH 026/199] fix: import --- src/client/io_loop.rs | 1 - src/flutter_ffi.rs | 3 --- src/server.rs | 7 ------- 3 files changed, 11 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 9117c8c5f..dcfa7b74e 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -12,7 +12,6 @@ use clipboard::{cliprdr::CliprdrClientContext, ContextSend}; use hbb_common::tokio::sync::mpsc::error::TryRecvError; -use crate::server::Service; use crate::ui_session_interface::{InvokeUiSession, Session}; use crate::{client::Data, client::Interface}; diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 0fe6818de..1ecbb0646 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -11,15 +11,12 @@ use hbb_common::{ config::{self, LocalConfig, ONLINE, PeerConfig}, fs, log, }; -use hbb_common::message_proto::KeyboardMode; -use hbb_common::ResultType; use crate::{ client::file_trait::FileManager, common::make_fd_to_json, flutter::{session_add, session_start_}, }; -use crate::common::is_keyboard_mode_supported; use crate::flutter::{self, SESSIONS}; use crate::ui_interface::{self, *}; diff --git a/src/server.rs b/src/server.rs index bef49f132..616d92375 100644 --- a/src/server.rs +++ b/src/server.rs @@ -29,13 +29,6 @@ use service::{GenericService, Service, Subscriber}; use service::ServiceTmpl; use crate::ipc::{connect, Data}; -pub use service::{GenericService, Service, ServiceTmpl, Subscriber}; -use std::{ - collections::HashMap, - net::SocketAddr, - sync::{Arc, Mutex, RwLock, Weak}, - time::Duration, -}; pub mod audio_service; cfg_if::cfg_if! { From 45b93100d6d0837d53d12dca30605cc0b10b1ea4 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 5 Feb 2023 23:47:06 +0800 Subject: [PATCH 027/199] feat: add voice call proto --- libs/hbb_common/protos/message.proto | 14 ++++++ src/client.rs | 49 +++++++++++---------- src/client/io_loop.rs | 64 ++++++++++++++++++---------- src/flutter_ffi.rs | 18 ++++++-- src/server/connection.rs | 6 +++ src/ui/remote.rs | 8 ++-- src/ui_session_interface.rs | 48 +++++++++++++-------- 7 files changed, 138 insertions(+), 69 deletions(-) diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 48b999438..323b464fa 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -604,6 +604,18 @@ message Misc { } } +message VoiceCallRequest { + int64 req_timestamp = 1; + // Indicates whether the request is a connect action or a disconnect action. + bool is_connect = 2; +} + +message VoiceCallResponse { + bool accepted = 1; + int64 req_timestamp = 2; // Should copy from [VoiceCallRequest::req_timestamp]. + int64 ack_timestamp = 3; +} + message Message { oneof union { SignedId signed_id = 3; @@ -626,5 +638,7 @@ message Message { Cliprdr cliprdr = 20; MessageBox message_box = 21; SwitchSidesResponse switch_sides_response = 22; + VoiceCallRequest voice_call_request = 23; + VoiceCallResponse voice_call_response = 24; } } diff --git a/src/client.rs b/src/client.rs index 649b180bb..5911c40ed 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,58 +1,61 @@ -pub use async_trait::async_trait; -use bytes::Bytes; -#[cfg(not(any(target_os = "android", target_os = "linux")))] -use cpal::{ - traits::{DeviceTrait, HostTrait, StreamTrait}, - Device, Host, StreamConfig, -}; -use magnum_opus::{Channels::*, Decoder as AudioDecoder}; -use sha2::{Digest, Sha256}; use std::{ collections::HashMap, net::SocketAddr, ops::{Deref, Not}, str::FromStr, - sync::{atomic::AtomicBool, mpsc, Arc, Mutex, RwLock}, + sync::{Arc, atomic::AtomicBool, mpsc, Mutex, RwLock}, }; + +pub use async_trait::async_trait; +use bytes::Bytes; +#[cfg(not(any(target_os = "android", target_os = "linux")))] +use cpal::{ + Device, + Host, StreamConfig, traits::{DeviceTrait, HostTrait, StreamTrait}, +}; +use magnum_opus::{Channels::*, Decoder as AudioDecoder}; +use sha2::{Digest, Sha256}; use uuid::Uuid; pub use file_trait::FileManager; use hbb_common::{ + AddrMangle, allow_err, anyhow::{anyhow, Context}, bail, config::{ - Config, PeerConfig, PeerInfoSerde, CONNECT_TIMEOUT, READ_TIMEOUT, RELAY_PORT, + Config, CONNECT_TIMEOUT, PeerConfig, PeerInfoSerde, READ_TIMEOUT, RELAY_PORT, RENDEZVOUS_TIMEOUT, - }, - get_version_number, log, - message_proto::{option_message::BoolOption, *}, + }, get_version_number, + log, + message_proto::{*, option_message::BoolOption}, protobuf::Message as _, rand, rendezvous_proto::*, + ResultType, socket_client, sodiumoxide::crypto::{box_, secretbox, sign}, - timeout, - tokio::time::Duration, - AddrMangle, ResultType, Stream, + Stream, timeout, tokio::time::Duration, }; -pub use helper::LatencyController; pub use helper::*; +pub use helper::LatencyController; use scrap::{ codec::{Decoder, DecoderCfg}, record::{Recorder, RecorderContext}, VpxDecoderConfig, VpxVideoCodecId, }; +use crate::{ + common::{self, is_keyboard_mode_supported}, + server::video_service::{SCRAP_X11_REF_URL, SCRAP_X11_REQUIRED}, +}; + pub use super::lang::*; pub mod file_trait; pub mod helper; pub mod io_loop; -use crate::{ - common::{self, is_keyboard_mode_supported}, - server::video_service::{SCRAP_X11_REF_URL, SCRAP_X11_REQUIRED}, -}; + pub static SERVER_KEYBOARD_ENABLED: AtomicBool = AtomicBool::new(true); pub static SERVER_FILE_TRANSFER_ENABLED: AtomicBool = AtomicBool::new(true); pub static SERVER_CLIPBOARD_ENABLED: AtomicBool = AtomicBool::new(true); @@ -1989,6 +1992,8 @@ pub enum Data { RecordScreen(bool, i32, i32, String), ElevateDirect, ElevateWithLogon(String, String), + NewVoiceCall, + CloseVoiceCall, } /// Keycode for key events. diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index dcfa7b74e..67946f546 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1,42 +1,38 @@ -use crate::client::{ - Client, CodecFormat, LoginConfigHandler, MediaData, MediaSender, QualityStatus, MILLI1, SEC30, - SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED, -}; -use crate::common::{get_default_sound_input, set_sound_input}; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::common::{check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}; -use crate::{audio_service, common, ConnInner, CLIENT_SERVER}; +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use std::sync::atomic::{AtomicUsize, Ordering}; #[cfg(windows)] use clipboard::{cliprdr::CliprdrClientContext, ContextSend}; - -use hbb_common::tokio::sync::mpsc::error::TryRecvError; - -use crate::ui_session_interface::{InvokeUiSession, Session}; -use crate::{client::Data, client::Interface}; - +use hbb_common::{allow_err, message_proto::*, sleep, get_time}; +use hbb_common::{fs, log, Stream}; use hbb_common::config::{PeerConfig, TransferSerde}; use hbb_common::fs::{ - can_enable_overwrite_detection, get_job, get_string, new_send_confirm, DigestCheckResult, + can_enable_overwrite_detection, DigestCheckResult, get_job, get_string, new_send_confirm, RemoveJobMeta, }; use hbb_common::message_proto::permission_info::Permission; use hbb_common::protobuf::Message as _; use hbb_common::rendezvous_proto::ConnType; -#[cfg(windows)] -use hbb_common::tokio::sync::Mutex as TokioMutex; use hbb_common::tokio::{ self, sync::mpsc, time::{self, Duration, Instant, Interval}, }; -use hbb_common::{allow_err, message_proto::*, sleep}; -use hbb_common::{fs, log, Stream}; +use hbb_common::tokio::sync::mpsc::error::TryRecvError; +#[cfg(windows)] +use hbb_common::tokio::sync::Mutex as TokioMutex; -use std::collections::HashMap; - -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::{Arc, Mutex}; +use crate::{audio_service, CLIENT_SERVER, common, ConnInner}; +use crate::{client::Data, client::Interface}; +use crate::client::{ + Client, CodecFormat, LoginConfigHandler, MediaData, MediaSender, MILLI1, QualityStatus, SEC30, + SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED, +}; +use crate::common::{get_default_sound_input, set_sound_input}; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::common::{check_clipboard, CLIPBOARD_INTERVAL, ClipboardContext, update_clipboard}; +use crate::ui_session_interface::{InvokeUiSession, Session}; pub struct Remote { handler: Session, @@ -752,6 +748,22 @@ impl Remote { msg.set_misc(misc); allow_err!(peer.send(&msg).await); } + Data::NewVoiceCall => { + let mut request = VoiceCallRequest::new(); + request.is_connect = true; + request.req_timestamp = get_time(); + let mut msg = Message::new(); + msg.set_voice_call_request(request); + allow_err!(peer.send(&msg).await); + } + Data::CloseVoiceCall => { + let mut request = VoiceCallRequest::new(); + request.is_connect = false; + request.req_timestamp = get_time(); + let mut msg = Message::new(); + msg.set_voice_call_request(request); + allow_err!(peer.send(&msg).await); + } _ => {} } true @@ -1262,6 +1274,12 @@ impl Remote { self.handler .msgbox(&msgbox.msgtype, &msgbox.title, &msgbox.text, &link); } + Some(message::Union::VoiceCallRequest(request)) => { + // TODO + } + Some(message::Union::VoiceCallResponse(response)) => { + // TODO + } _ => {} } } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 1ecbb0646..15bfe90d4 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -4,19 +4,19 @@ use std::str::FromStr; use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; use serde_json::json; -use crate::common::{is_keyboard_mode_supported, get_default_sound_input}; -use hbb_common::message_proto::KeyboardMode; -use hbb_common::ResultType; use hbb_common::{ config::{self, LocalConfig, ONLINE, PeerConfig}, fs, log, }; +use hbb_common::message_proto::KeyboardMode; +use hbb_common::ResultType; use crate::{ client::file_trait::FileManager, common::make_fd_to_json, flutter::{session_add, session_start_}, }; +use crate::common::{get_default_sound_input, is_keyboard_mode_supported}; use crate::flutter::{self, SESSIONS}; use crate::ui_interface::{self, *}; @@ -840,6 +840,18 @@ pub fn session_new_rdp(id: String) { } } +pub fn session_request_voice_call(id: String) { + if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + session.request_voice_call(); + } +} + +pub fn session_close_voice_call(id: String) { + if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + session.close_voice_call(); + } +} + pub fn main_get_last_remote_id() -> String { LocalConfig::get_remote_id() } diff --git a/src/server/connection.rs b/src/server/connection.rs index 20cbe0f86..c3acae9cc 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1572,6 +1572,12 @@ impl Connection { allow_err!(self.audio_sender.send(MediaData::AudioFrame(frame))); } } + Some(message::Union::VoiceCallRequest(request)) => { + // TODO + } + Some(message::Union::VoiceCallResponse(response)) => { + // TODO + } _ => {} } } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 541d3a141..1b0d172b9 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -6,12 +6,12 @@ use std::{ use sciter::{ dom::{ - event::{EventReason, BEHAVIOR_EVENTS, EVENT_GROUPS, PHASE_MASK}, - Element, HELEMENT, + Element, + event::{BEHAVIOR_EVENTS, EVENT_GROUPS, EventReason, PHASE_MASK}, HELEMENT, }, make_args, - video::{video_destination, AssetPtr, COLOR_SPACE}, Value, + video::{AssetPtr, COLOR_SPACE, video_destination}, }; use hbb_common::{ @@ -422,6 +422,8 @@ impl sciter::EventHandler for SciterSession { fn restart_remote_device(); fn save_audio_mode(String); fn get_audio_mode(); + fn request_voice_call(); + fn close_voice_call(); } } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 1e7848505..147cd9149 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1,26 +1,30 @@ -use crate::client::io_loop::Remote; -use crate::client::{ - check_if_retry, handle_hash, handle_login_error, handle_login_from_ui, handle_test_delay, - input_os_password, load_config, send_mouse, start_video_audio_threads, FileManager, Key, - LoginConfigHandler, QualityStatus, KEY_MAP, -}; -use crate::common::{self, GrabState}; -use crate::keyboard; -use crate::{client::Data, client::Interface}; -use async_trait::async_trait; -use bytes::Bytes; -use hbb_common::config::{Config, LocalConfig, PeerConfig, RS_PUB_KEY}; -use hbb_common::rendezvous_proto::ConnType; -use hbb_common::tokio::{self, sync::mpsc}; -use hbb_common::{allow_err, message_proto::*}; -use hbb_common::{fs, get_version_number, log, Stream}; -use rdev::{Event, EventType::*}; use std::collections::HashMap; use std::ops::{Deref, DerefMut}; use std::str::FromStr; -use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex, RwLock}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + +use async_trait::async_trait; +use bytes::Bytes; +use rdev::{Event, EventType::*}; use uuid::Uuid; + +use hbb_common::{allow_err, message_proto::*}; +use hbb_common::{fs, get_version_number, log, Stream}; +use hbb_common::config::{Config, LocalConfig, PeerConfig, RS_PUB_KEY}; +use hbb_common::rendezvous_proto::ConnType; +use hbb_common::tokio::{self, sync::mpsc}; + +use crate::{client::Data, client::Interface}; +use crate::client::{ + check_if_retry, FileManager, handle_hash, handle_login_error, handle_login_from_ui, + handle_test_delay, input_os_password, Key, KEY_MAP, load_config, LoginConfigHandler, + QualityStatus, send_mouse, start_video_audio_threads, +}; +use crate::client::io_loop::Remote; +use crate::common::{self, GrabState}; +use crate::keyboard; + pub static IS_IN: AtomicBool = AtomicBool::new(false); #[derive(Clone, Default)] @@ -669,6 +673,14 @@ impl Session { } } } + + pub fn request_voice_call(&self) { + self.send(Data::NewVoiceCall); + } + + pub fn close_voice_call(&self) { + self.send(Data::CloseVoiceCall); + } } pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { From a04980fa1325a2da1a2625983b1aa016a3153187 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 6 Feb 2023 09:37:52 +0800 Subject: [PATCH 028/199] refactor: remove audio mode --- libs/hbb_common/protos/message.proto | 6 ----- src/client.rs | 40 ---------------------------- src/client/io_loop.rs | 36 ------------------------- src/flutter_ffi.rs | 14 ---------- src/ui/header.tis | 5 ---- src/ui/remote.rs | 2 -- src/ui_session_interface.rs | 16 ----------- 7 files changed, 119 deletions(-) diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 323b464fa..ed2706382 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -444,11 +444,6 @@ enum ImageQuality { Best = 4; } -enum AudioMode { - GuestToHost = 0; - DualWay = 1; -} - message VideoCodecState { enum PreferCodec { Auto = 0; @@ -480,7 +475,6 @@ message OptionMessage { BoolOption enable_file_transfer = 9; VideoCodecState video_codec_state = 10; int32 custom_fps = 11; - AudioMode audio_mode = 12; } message TestDelay { diff --git a/src/client.rs b/src/client.rs index 5911c40ed..2ea33b655 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1255,27 +1255,6 @@ impl LoginConfigHandler { } } - /// Parse the audio mode option. - /// Return [`AudioMode`] if the option is valid, otherwise return `None`. - /// - /// # Arguments - /// - /// * `q` - The audio mode option. - /// * `ignore_default` - Ignore the default value. - pub fn get_audio_mode_enum(q: &str, ignore_default: bool) -> Option { - if q == "guest-to-host" { - Some(AudioMode::GuestToHost) - } else if q == "dual-way" { - Some(AudioMode::DualWay) - } else { - if ignore_default { - None - } else { - Some(AudioMode::GuestToHost) - } - } - } - /// Get the status of a toggle option. /// /// # Arguments @@ -1362,24 +1341,6 @@ impl LoginConfigHandler { res } - pub fn save_audio_mode(&mut self, value: String) -> Option { - let mut res = None; - if let Some(q) = LoginConfigHandler::get_audio_mode_enum(&value, false) { - let mut misc = Misc::new(); - misc.set_option(OptionMessage { - audio_mode: q.into(), - ..Default::default() - }); - let mut msg_out = Message::new(); - msg_out.set_misc(misc); - res = Some(msg_out); - } - let mut config = self.load_config(); - config.audio_mode = value; - self.save_config(config); - res - } - /// Create a [`Message`] for saving custom fps. /// /// # Arguments @@ -1984,7 +1945,6 @@ pub enum Data { RemovePortForward(i32), AddPortForward((i32, String, i32)), ToggleClipboardFile, - ChangeAudioMode(AudioMode), NewRDP, SetConfirmOverrideFile((i32, i32, bool, bool, bool)), AddJob((i32, String, String, i32, bool, bool)), diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 67946f546..d0e72a7e6 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -387,24 +387,6 @@ impl Remote { Data::ToggleClipboardFile => { self.check_clipboard_file_context(); } - Data::ChangeAudioMode(audio_mode) => { - match audio_mode { - AudioMode::GuestToHost => { - if let Some(sender) = self.stop_local_audio_sender.take() { - allow_err!(sender.send(())); - } - } - AudioMode::DualWay => { - // Start audio thread for playback. - // Cancel previous local audio session. - if let Some(sender) = self.stop_local_audio_sender.take() { - allow_err!(sender.send(())); - } - // Start client audio when connection is established. - self.stop_local_audio_sender = self.start_client_audio(); - } - } - } Data::Message(msg) => { allow_err!(peer.send(&msg).await); } @@ -905,24 +887,6 @@ impl Remote { if self.handler.is_file_transfer() { self.handler.load_last_jobs(); } - - // Start audio thread for playback if current audio mode is dual-way transmission. - if !self.handler.is_file_transfer() && !self.handler.is_port_forward() { - let audio_mode = LoginConfigHandler::get_audio_mode_enum( - self.handler.load_config().audio_mode.as_str(), - false, - ) - .unwrap_or(AudioMode::GuestToHost); - log::debug!("current audio mode: {:?}", audio_mode); - if audio_mode == AudioMode::DualWay { - // Cancel previous local audio session. - if let Some(sender) = self.stop_local_audio_sender.take() { - allow_err!(sender.send(())); - } - // Start client audio when connection is established. - self.stop_local_audio_sender = self.start_client_audio(); - } - } } _ => {} }, diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 15bfe90d4..e28332943 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -233,20 +233,6 @@ pub fn session_set_image_quality(id: String, value: String) { } } -pub fn session_get_audio_mode(id: String) -> Option { - if let Some(session) = SESSIONS.read().unwrap().get(&id) { - Some(session.get_audio_mode()) - } else { - None - } -} - -pub fn session_set_audio_mode(id: String, value: String) { - if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { - session.save_audio_mode(value); - } -} - pub fn session_get_keyboard_mode(id: String) -> Option { if let Some(session) = SESSIONS.read().unwrap().get(&id) { Some(session.get_keyboard_mode()) diff --git a/src/ui/header.tis b/src/ui/header.tis index e3f0c70a1..009995f4f 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -183,9 +183,6 @@ class Header: Reactor.Component {
  • {svg_checkmark}{translate('Balanced')}
  • {svg_checkmark}{translate('Optimize reaction time')}
  • {svg_checkmark}{translate('Custom')}
  • -
    -
  • {svg_checkmark}{translate('Guest to Host')}
  • -
  • {svg_checkmark}{translate('Dual way')}
  • {show_codec ?
  • {svg_checkmark}Auto
  • @@ -394,8 +391,6 @@ class Header: Reactor.Component { } else if (type == "codec-preference") { handler.set_option("codec-preference", me.id); handler.change_prefer_codec(); - } else if (type == "audio-mode") { - handler.save_audio_mode(me.id); } toggleMenuState(); } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 1b0d172b9..5d6692c3b 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -420,8 +420,6 @@ impl sciter::EventHandler for SciterSession { fn supported_hwcodec(); fn change_prefer_codec(); fn restart_remote_device(); - fn save_audio_mode(String); - fn get_audio_mode(); fn request_voice_call(); fn close_voice_call(); } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 147cd9149..2f6827523 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -93,22 +93,6 @@ impl Session { self.lc.write().unwrap().save_keyboard_mode(value); } - pub fn get_audio_mode(&self) -> String { - self.lc.read().unwrap().audio_mode.clone() - } - - pub fn save_audio_mode(&self, value: String) { - let mode = LoginConfigHandler::get_audio_mode_enum(value.as_str(), false); - if let Some(mode)= mode { - self.send(Data::ChangeAudioMode(mode)); - } - let msg = self.lc.write().unwrap().save_audio_mode(value); - // Notify remote guest that the audio mode has been changed. - if let Some(msg) = msg { - self.send(Data::Message(msg)); - } - } - pub fn save_view_style(&mut self, value: String) { self.lc.write().unwrap().save_view_style(value); } From b412a7122b837dd3d9d31c29f04ffc237356d97c Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 6 Feb 2023 11:42:25 +0800 Subject: [PATCH 029/199] feat: rust connection implementation --- src/client/helper.rs | 23 +++++- src/client/io_loop.rs | 85 ++++++++++++-------- src/flutter.rs | 16 ++++ src/ipc.rs | 5 +- src/lang/cn.rs | 1 + src/server/connection.rs | 151 ++++++++++++++++++++++++------------ src/ui/remote.rs | 16 ++++ src/ui_session_interface.rs | 4 + 8 files changed, 220 insertions(+), 81 deletions(-) diff --git a/src/client/helper.rs b/src/client/helper.rs index e3acf3a44..20acd811a 100644 --- a/src/client/helper.rs +++ b/src/client/helper.rs @@ -5,7 +5,7 @@ use std::{ use hbb_common::{ log, - message_proto::{video_frame, VideoFrame}, + message_proto::{video_frame, VideoFrame, Message, VoiceCallRequest, VoiceCallResponse}, get_time, }; const MAX_LATENCY: i64 = 500; @@ -115,3 +115,24 @@ pub struct QualityStatus { pub target_bitrate: Option, pub codec_format: Option, } + +#[inline] +pub fn new_voice_call_request(is_connect: bool) -> Message { + let mut req = VoiceCallRequest::new(); + req.is_connect = is_connect; + req.req_timestamp = get_time(); + let mut msg = Message::new(); + msg.set_voice_call_request(req); + msg +} + +#[inline] +pub fn new_voice_call_response(request_timestamp: i64, accepted: bool) -> Message { + let mut resp = VoiceCallResponse::new(); + resp.accepted = accepted; + resp.req_timestamp = request_timestamp; + resp.ack_timestamp = get_time(); + let mut msg = Message::new(); + msg.set_voice_call_response(resp); + msg +} \ No newline at end of file diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index d0e72a7e6..8f2b45321 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1,38 +1,40 @@ use std::collections::HashMap; -use std::sync::{Arc, Mutex}; +use std::num::NonZeroI64; use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; #[cfg(windows)] use clipboard::{cliprdr::CliprdrClientContext, ContextSend}; -use hbb_common::{allow_err, message_proto::*, sleep, get_time}; -use hbb_common::{fs, log, Stream}; use hbb_common::config::{PeerConfig, TransferSerde}; use hbb_common::fs::{ - can_enable_overwrite_detection, DigestCheckResult, get_job, get_string, new_send_confirm, + can_enable_overwrite_detection, get_job, get_string, new_send_confirm, DigestCheckResult, RemoveJobMeta, }; use hbb_common::message_proto::permission_info::Permission; use hbb_common::protobuf::Message as _; use hbb_common::rendezvous_proto::ConnType; +use hbb_common::tokio::sync::mpsc::error::TryRecvError; +#[cfg(windows)] +use hbb_common::tokio::sync::Mutex as TokioMutex; use hbb_common::tokio::{ self, sync::mpsc, time::{self, Duration, Instant, Interval}, }; -use hbb_common::tokio::sync::mpsc::error::TryRecvError; -#[cfg(windows)] -use hbb_common::tokio::sync::Mutex as TokioMutex; +use hbb_common::{allow_err, get_time, message_proto::*, sleep}; +use hbb_common::{fs, log, Stream}; -use crate::{audio_service, CLIENT_SERVER, common, ConnInner}; -use crate::{client::Data, client::Interface}; use crate::client::{ - Client, CodecFormat, LoginConfigHandler, MediaData, MediaSender, MILLI1, QualityStatus, SEC30, - SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED, + new_voice_call_request, Client, CodecFormat, LoginConfigHandler, MediaData, MediaSender, + QualityStatus, MILLI1, SEC30, SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, + SERVER_KEYBOARD_ENABLED, }; -use crate::common::{get_default_sound_input, set_sound_input}; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::common::{check_clipboard, CLIPBOARD_INTERVAL, ClipboardContext, update_clipboard}; +use crate::common::{check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}; +use crate::common::{get_default_sound_input, set_sound_input}; use crate::ui_session_interface::{InvokeUiSession, Session}; +use crate::{audio_service, common, ConnInner, CLIENT_SERVER}; +use crate::{client::Data, client::Interface}; pub struct Remote { handler: Session, @@ -41,7 +43,8 @@ pub struct Remote { receiver: mpsc::UnboundedReceiver, sender: mpsc::UnboundedSender, // Stop sending local audio to remote client. - stop_local_audio_sender: Option>, + stop_voice_call_sender: Option>, + voice_call_request_timestamp: Option, old_clipboard: Arc>, read_jobs: Vec, write_jobs: Vec, @@ -83,7 +86,8 @@ impl Remote { data_count: Arc::new(AtomicUsize::new(0)), frame_count, video_format: CodecFormat::Unknown, - stop_local_audio_sender: None, + stop_voice_call_sender: None, + voice_call_request_timestamp: None, } } @@ -217,7 +221,7 @@ impl Remote { } log::debug!("Exit io_loop of id={}", self.handler.id); // Stop client audio server. - if let Some(s) = self.stop_local_audio_sender.take() { + if let Some(s) = self.stop_voice_call_sender.take() { s.send(()).ok(); } } @@ -261,8 +265,15 @@ impl Remote { } } - // Start a local audio recorder, records audio and send to remote - fn start_client_audio(&mut self) -> Option> { + fn stop_voice_call(&mut self) { + let voice_call_sender = std::mem::replace(&mut self.stop_voice_call_sender, None); + if let Some(stopper) = voice_call_sender { + let _ = stopper.send(()); + } + } + + // Start a voice call recorder, records audio and send to remote + fn start_voice_call(&mut self) -> Option> { if self.handler.is_file_transfer() || self.handler.is_port_forward() { return None; } @@ -731,19 +742,17 @@ impl Remote { allow_err!(peer.send(&msg).await); } Data::NewVoiceCall => { - let mut request = VoiceCallRequest::new(); - request.is_connect = true; - request.req_timestamp = get_time(); - let mut msg = Message::new(); - msg.set_voice_call_request(request); + let msg = new_voice_call_request(true); + // Save the voice call request timestamp for the further validation. + self.voice_call_request_timestamp = Some( + NonZeroI64::new(msg.voice_call_request().req_timestamp) + .unwrap_or(NonZeroI64::new(get_time()).unwrap()), + ); allow_err!(peer.send(&msg).await); } Data::CloseVoiceCall => { - let mut request = VoiceCallRequest::new(); - request.is_connect = false; - request.req_timestamp = get_time(); - let mut msg = Message::new(); - msg.set_voice_call_request(request); + self.stop_voice_call(); + let msg = new_voice_call_request(false); allow_err!(peer.send(&msg).await); } _ => {} @@ -1238,11 +1247,25 @@ impl Remote { self.handler .msgbox(&msgbox.msgtype, &msgbox.title, &msgbox.text, &link); } - Some(message::Union::VoiceCallRequest(request)) => { - // TODO + Some(message::Union::VoiceCallRequest(_request)) => { + // TODO: maybe we will do voice call from the peer. } Some(message::Union::VoiceCallResponse(response)) => { - // TODO + let ts = std::mem::replace(&mut self.voice_call_request_timestamp, None); + if let Some(ts) = ts { + if response.req_timestamp != ts.get() { + log::debug!("Possible encountering a voice call attack."); + } else { + if response.accepted { + // The peer accepts the voice call. + self.handler.on_voice_call_start(); + self.stop_voice_call_sender = self.start_voice_call(); + } else { + // The peer refused the voice call. + self.handler.on_voice_call_stop("Refused"); + } + } + } } _ => {} } diff --git a/src/flutter.rs b/src/flutter.rs index b4f1f6bc6..7062d85df 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -394,6 +394,22 @@ impl InvokeUiSession for FlutterHandler { fn switch_back(&self, peer_id: &str) { self.push_event("switch_back", [("peer_id", peer_id)].into()); } + + fn on_voice_call_start(&self) { + self.push_event("on_voice_call_start", [].into()); + } + + fn on_voice_call_stop(&self, reason: &str) { + self.push_event("on_voice_call_stop", [("reason", reason)].into()) + } + + fn on_voice_call_waiting(&self) { + self.push_event("on_voice_call_waiting", [].into()); + } + + fn on_voice_call_incoming(&self) { + self.push_event("on_voice_call_incoming", [].into()); + } } /// Create a new remote session with the given id. diff --git a/src/ipc.rs b/src/ipc.rs index d610fb84d..18f618847 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -210,7 +210,10 @@ pub enum Data { DataPortableService(DataPortableService), SwitchSidesRequest(String), SwitchSidesBack, - UrlLink(String) + UrlLink(String), + VoiceCallIncoming, + VoiceCallResponse(bool), + CloseVoiceCall(String), } #[tokio::main(flavor = "current_thread")] diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 65039f0fe..5a9abba9c 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -453,5 +453,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "语音通话"), ("Text chat", "文字聊天"), ("Audio Transmission Mode", "音频传输模式"), + ("Refused", "已拒绝") ].iter().cloned().collect(); } diff --git a/src/server/connection.rs b/src/server/connection.rs index c3acae9cc..1007c71ca 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -5,7 +5,11 @@ use crate::clipboard_file::*; use crate::common::update_clipboard; #[cfg(windows)] use crate::portable_service::client as portable_client; -use crate::{video_service, client::{MediaSender, start_audio_thread, LatencyController, MediaData}, common::{get_default_sound_input, set_sound_input}}; +use crate::{ + client::{start_audio_thread, LatencyController, MediaData, MediaSender, new_voice_call_request, new_voice_call_response}, + common::{get_default_sound_input, set_sound_input}, + video_service, +}; #[cfg(any(target_os = "android", target_os = "ios"))] use crate::{common::DEVICE_NAME, flutter::connection_manager::start_channel}; use crate::{ipc, VERSION}; @@ -32,7 +36,10 @@ use serde_json::{json, value::Value}; use sha2::{Digest, Sha256}; #[cfg(not(any(target_os = "android", target_os = "ios")))] use std::sync::atomic::Ordering; -use std::sync::{atomic::AtomicI64, mpsc as std_mpsc}; +use std::{ + num::NonZeroI64, + sync::{atomic::AtomicI64, mpsc as std_mpsc}, +}; #[cfg(not(any(target_os = "android", target_os = "ios")))] use system_shutdown; @@ -90,13 +97,19 @@ pub struct Connection { recording: bool, last_test_delay: i64, lock_after_session_end: bool, - show_remote_cursor: bool, // by peer + show_remote_cursor: bool, + // by peer ip: String, - disable_clipboard: bool, // by peer - disable_audio: bool, // by peer - enable_file_transfer: bool, // by peer - audio_sender: MediaSender, // audio by the remote peer/client - tx_input: std_mpsc::Sender, // handle input messages + disable_clipboard: bool, + // by peer + disable_audio: bool, + // by peer + enable_file_transfer: bool, + // by peer + audio_sender: MediaSender, + // audio by the remote peer/client + tx_input: std_mpsc::Sender, + // handle input messages video_ack_required: bool, peer_info: (String, String), server_audit_conn: String, @@ -107,6 +120,8 @@ pub struct Connection { #[cfg(windows)] portable: PortableState, from_switch: bool, + voice_call_request_timestamp: Option, + audio_input_device_before_voice_call: Option, } impl ConnInner { @@ -216,6 +231,8 @@ impl Connection { portable: Default::default(), from_switch: false, audio_sender, + voice_call_request_timestamp: None, + audio_input_device_before_voice_call: None, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] tokio::spawn(async move { @@ -380,6 +397,12 @@ impl Connection { msg.set_misc(misc); conn.send(msg).await; } + ipc::Data::VoiceCallResponse(accepted) => { + conn.start_voice_call().await; + } + ipc::Data::CloseVoiceCall(_reason) => { + conn.close_voice_call().await; + } _ => {} } }, @@ -650,15 +673,15 @@ impl Connection { .collect(); if !whitelist.is_empty() && whitelist - .iter() - .filter(|x| x == &"0.0.0.0") - .next() - .is_none() + .iter() + .filter(|x| x == &"0.0.0.0") + .next() + .is_none() && whitelist - .iter() - .filter(|x| IpCidr::from_str(x).map_or(false, |y| y.contains(addr.ip()))) - .next() - .is_none() + .iter() + .filter(|x| IpCidr::from_str(x).map_or(false, |y| y.contains(addr.ip()))) + .next() + .is_none() { self.send_login_error("Your ip is blocked by the peer") .await; @@ -784,7 +807,7 @@ impl Connection { }; self.post_conn_audit(json!({"peer": self.peer_info, "type": conn_type})); #[allow(unused_mut)] - let mut username = crate::platform::get_active_username(); + let mut username = crate::platform::get_active_username(); let mut res = LoginResponse::new(); let mut pi = PeerInfo { username: username.clone(), @@ -811,7 +834,7 @@ impl Connection { h265, ..Default::default() }) - .into(); + .into(); } if self.port_forward_socket.is_some() { @@ -855,7 +878,7 @@ impl Connection { privacy_mode: video_service::is_privacy_mode_supported(), ..Default::default() }) - .into(); + .into(); let mut sub_service = false; if self.file_transfer.is_some() { @@ -1138,7 +1161,7 @@ impl Connection { "Failed to access remote {}, please make sure if it is open", addr )) - .await; + .await; return false; } } @@ -1302,12 +1325,12 @@ impl Connection { } } Some(message::Union::Clipboard(cb)) => - { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if self.clipboard { - update_clipboard(cb, None); + { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if self.clipboard { + update_clipboard(cb, None); + } } - } Some(message::Union::Cliprdr(_clip)) => { if self.file_transfer_enabled() { #[cfg(windows)] @@ -1490,15 +1513,15 @@ impl Connection { } Some(misc::Union::RestartRemoteDevice(_)) => - { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if self.restart { - match system_shutdown::reboot() { - Ok(_) => log::info!("Restart by the peer"), - Err(e) => log::error!("Failed to restart:{}", e), + { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if self.restart { + match system_shutdown::reboot() { + Ok(_) => log::info!("Restart by the peer"), + Err(e) => log::error!("Failed to restart:{}", e), + } } } - } Some(misc::Union::ElevationRequest(r)) => match r.union { Some(elevation_request::Union::Direct(_)) => { #[cfg(windows)] @@ -1508,8 +1531,8 @@ impl Connection { err = portable_client::start_portable_service( portable_client::StartPara::Direct, ) - .err() - .map_or("".to_string(), |e| e.to_string()); + .err() + .map_or("".to_string(), |e| e.to_string()); } self.portable.elevation_requested = err.is_empty(); let mut misc = Misc::new(); @@ -1527,8 +1550,8 @@ impl Connection { err = portable_client::start_portable_service( portable_client::StartPara::Logon(_r.username, _r.password), ) - .err() - .map_or("".to_string(), |e| e.to_string()); + .err() + .map_or("".to_string(), |e| e.to_string()); } self.portable.elevation_requested = err.is_empty(); let mut misc = Misc::new(); @@ -1541,12 +1564,7 @@ impl Connection { _ => {} }, Some(misc::Union::AudioFormat(format)) => { - if !self.disable_audio { - // Switch to default input device - let default_sound_device = get_default_sound_input(); - if let Some(device) = default_sound_device { - set_sound_input(device); - } + if !self.disable_audio { allow_err!(self.audio_sender.send(MediaData::AudioFormat(format))); } } @@ -1559,7 +1577,7 @@ impl Connection { "--switch_uuid", uuid.to_string().as_ref(), ]) - .ok(); + .ok(); self.send_close_reason_no_retry("Closed as expected").await; self.on_close("switch sides", false).await; return false; @@ -1573,10 +1591,19 @@ impl Connection { } } Some(message::Union::VoiceCallRequest(request)) => { - // TODO + if request.is_connect { + self.voice_call_request_timestamp = Some( + NonZeroI64::new(request.req_timestamp) + .unwrap_or(NonZeroI64::new(get_time()).unwrap()), + ); + // Call cm. + self.send_to_cm(Data::VoiceCallIncoming); + } else { + self.close_voice_call().await; + } } - Some(message::Union::VoiceCallResponse(response)) => { - // TODO + Some(message::Union::VoiceCallResponse(_response)) => { + // TODO: Maybe we can do a voice call from cm directly. } _ => {} } @@ -1584,6 +1611,34 @@ impl Connection { true } + pub async fn start_voice_call(&self) { + if let Some(ts) = conn.voice_call_request_timestamp.take() { + let msg = new_voice_call_response(ts.get(), accepted); + conn.send(msg).await; + if accepted { + // Backup the default input device. + let audio_input_device = Config::get_option("audio-input"); + conn.audio_input_device_before_voice_call = Some(audio_input_device); + // Switch to default input device + let default_sound_device = get_default_sound_input(); + if let Some(device) = default_sound_device { + set_sound_input(device); + } + } + } else { + log::warn!("Possible a voice call attack."); + } + } + + pub async fn close_voice_call(&mut self) { + // Restore to the prior audio device. + if let Some(sound_input) = std::mem::replace(&mut self.audio_input_device_before_voice_call, None) { + set_sound_input(sound_input); + // Notify the connection manager. + self.send_to_cm(Data::CloseVoiceCall("Closed manually by the peer".to_owned())); + } + } + async fn update_option(&mut self, o: &OptionMessage) { log::info!("Option update: {:?}", o); if let Ok(q) = o.image_quality.enum_value() { @@ -1752,13 +1807,13 @@ impl Connection { lock_screen().await; } #[cfg(not(any(target_os = "android", target_os = "ios")))] - let data = if self.chat_unanswered { + let data = if self.chat_unanswered { ipc::Data::Disconnected } else { ipc::Data::Close }; #[cfg(any(target_os = "android", target_os = "ios"))] - let data = ipc::Data::Close; + let data = ipc::Data::Close; self.tx_to_cm.send(data).ok(); self.port_forward_socket.take(); } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 5d6692c3b..eb83890d4 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -266,6 +266,22 @@ impl InvokeUiSession for SciterHandler { } fn switch_back(&self, _id: &str) {} + + fn on_voice_call_start(&self) { + self.call("onVoiceCallStart", &make_args!()); + } + + fn on_voice_call_stop(&self, reason: &str) { + self.call("onVoiceCallStop", &make_args!(reason)); + } + + fn on_voice_call_waiting(&self) { + self.call("onVoiceCallWaiting", &make_args!()); + } + + fn on_voice_call_incoming(&self) { + self.call("onVoiceCallIncoming", &make_args!()); + } } pub struct SciterSession(Session); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 2f6827523..a740b373e 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -705,6 +705,10 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn clipboard(&self, content: String); fn cancel_msgbox(&self, tag: &str); fn switch_back(&self, id: &str); + fn on_voice_call_start(&self); + fn on_voice_call_stop(&self, reason: &str); + fn on_voice_call_waiting(&self); + fn on_voice_call_incoming(&self); } impl Deref for Session { From 11c60088111ba9d9312fd974896afee688a3a722 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 6 Feb 2023 11:53:37 +0800 Subject: [PATCH 030/199] fix: rust conn build --- src/server/connection.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index 1007c71ca..87b3f74ea 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -398,7 +398,9 @@ impl Connection { conn.send(msg).await; } ipc::Data::VoiceCallResponse(accepted) => { - conn.start_voice_call().await; + if accepted { + conn.start_voice_call().await; + } } ipc::Data::CloseVoiceCall(_reason) => { conn.close_voice_call().await; @@ -1611,19 +1613,17 @@ impl Connection { true } - pub async fn start_voice_call(&self) { - if let Some(ts) = conn.voice_call_request_timestamp.take() { + pub async fn start_voice_call(&mut self) { + if let Some(ts) = self.voice_call_request_timestamp.take() { let msg = new_voice_call_response(ts.get(), accepted); conn.send(msg).await; - if accepted { - // Backup the default input device. - let audio_input_device = Config::get_option("audio-input"); - conn.audio_input_device_before_voice_call = Some(audio_input_device); - // Switch to default input device - let default_sound_device = get_default_sound_input(); - if let Some(device) = default_sound_device { - set_sound_input(device); - } + // Backup the default input device. + let audio_input_device = Config::get_option("audio-input"); + self.audio_input_device_before_voice_call = Some(audio_input_device); + // Switch to default input device + let default_sound_device = get_default_sound_input(); + if let Some(device) = default_sound_device { + set_sound_input(device); } } else { log::warn!("Possible a voice call attack."); From a601e3b241eddc3f5a104fee89a8518be79ca34a Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 6 Feb 2023 12:10:15 +0800 Subject: [PATCH 031/199] fix: compile --- src/flutter_ffi.rs | 1 + src/server/connection.rs | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index e28332943..588733c37 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1288,6 +1288,7 @@ pub fn main_start_ipc_url_server() { /// Send a url scheme throught the ipc. /// /// * macOS only +#[allow(unused_variables)] pub fn send_url_scheme(url: String) { #[cfg(target_os = "macos")] thread::spawn(move || crate::ui::macos::handle_url_scheme(url)); diff --git a/src/server/connection.rs b/src/server/connection.rs index 87b3f74ea..c4c9ec168 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -398,9 +398,7 @@ impl Connection { conn.send(msg).await; } ipc::Data::VoiceCallResponse(accepted) => { - if accepted { - conn.start_voice_call().await; - } + conn.handle_voice_call(accepted).await; } ipc::Data::CloseVoiceCall(_reason) => { conn.close_voice_call().await; @@ -1613,17 +1611,19 @@ impl Connection { true } - pub async fn start_voice_call(&mut self) { + pub async fn handle_voice_call(&mut self, accepted: bool) { if let Some(ts) = self.voice_call_request_timestamp.take() { let msg = new_voice_call_response(ts.get(), accepted); - conn.send(msg).await; - // Backup the default input device. - let audio_input_device = Config::get_option("audio-input"); - self.audio_input_device_before_voice_call = Some(audio_input_device); - // Switch to default input device - let default_sound_device = get_default_sound_input(); - if let Some(device) = default_sound_device { - set_sound_input(device); + self.send(msg).await; + if accepted { + // Backup the default input device. + let audio_input_device = Config::get_option("audio-input"); + self.audio_input_device_before_voice_call = Some(audio_input_device); + // Switch to default input device + let default_sound_device = get_default_sound_input(); + if let Some(device) = default_sound_device { + set_sound_input(device); + } } } else { log::warn!("Possible a voice call attack."); From 850c4bcbbf5bfbf152ccda3e876330e5f7286f7e Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 6 Feb 2023 12:14:20 +0800 Subject: [PATCH 032/199] opt: uniform name --- src/client/io_loop.rs | 3 ++- src/flutter.rs | 4 ++-- src/ui/remote.rs | 4 ++-- src/ui_session_interface.rs | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 8f2b45321..e34df30b4 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -749,6 +749,7 @@ impl Remote { .unwrap_or(NonZeroI64::new(get_time()).unwrap()), ); allow_err!(peer.send(&msg).await); + self.handler.on_voice_call_waiting(); } Data::CloseVoiceCall => { self.stop_voice_call(); @@ -1262,7 +1263,7 @@ impl Remote { self.stop_voice_call_sender = self.start_voice_call(); } else { // The peer refused the voice call. - self.handler.on_voice_call_stop("Refused"); + self.handler.on_voice_call_closed("Refused"); } } } diff --git a/src/flutter.rs b/src/flutter.rs index 7062d85df..f8d8569ba 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -399,8 +399,8 @@ impl InvokeUiSession for FlutterHandler { self.push_event("on_voice_call_start", [].into()); } - fn on_voice_call_stop(&self, reason: &str) { - self.push_event("on_voice_call_stop", [("reason", reason)].into()) + fn on_voice_call_closed(&self, reason: &str) { + self.push_event("on_voice_call_closed", [("reason", reason)].into()) } fn on_voice_call_waiting(&self) { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index eb83890d4..9888e5831 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -271,8 +271,8 @@ impl InvokeUiSession for SciterHandler { self.call("onVoiceCallStart", &make_args!()); } - fn on_voice_call_stop(&self, reason: &str) { - self.call("onVoiceCallStop", &make_args!(reason)); + fn on_voice_call_closed(&self, reason: &str) { + self.call("onVoiceCallClosed", &make_args!(reason)); } fn on_voice_call_waiting(&self) { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index a740b373e..4b47608f9 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -706,7 +706,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn cancel_msgbox(&self, tag: &str); fn switch_back(&self, id: &str); fn on_voice_call_start(&self); - fn on_voice_call_stop(&self, reason: &str); + fn on_voice_call_closed(&self, reason: &str); fn on_voice_call_waiting(&self); fn on_voice_call_incoming(&self); } From 040396b3f8421075adce6762010bd74b964d407f Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 6 Feb 2023 12:53:57 +0800 Subject: [PATCH 033/199] feat: cm interface --- src/flutter.rs | 12 ++++++++++++ src/ipc.rs | 1 + src/server/connection.rs | 1 + src/ui/cm.rs | 12 ++++++++++++ src/ui_cm_interface.rs | 27 +++++++++++++++++++++++++++ 5 files changed, 53 insertions(+) diff --git a/src/flutter.rs b/src/flutter.rs index f8d8569ba..e83beb03b 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -537,6 +537,18 @@ pub mod connection_manager { fn show_elevation(&self, show: bool) { self.push_event("show_elevation", vec![("show", &show.to_string())]); } + + fn voice_call_started(&self, id: i32) { + self.push_event("voice_call_started", vec![("show", &id.to_string())]); + } + + fn voice_call_incoming(&self, id: i32) { + self.push_event("voice_call_incoming", vec![("id", &id.to_string())]); + } + + fn voice_call_closed(&self, id: i32, reason: &str) { + self.push_event("voice_call_closed", vec![("id", &id.to_string()), ("reason", &reason.to_string())]); + } } impl FlutterHandler { diff --git a/src/ipc.rs b/src/ipc.rs index 18f618847..0ede560fc 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -212,6 +212,7 @@ pub enum Data { SwitchSidesBack, UrlLink(String), VoiceCallIncoming, + StartVoiceCall, VoiceCallResponse(bool), CloseVoiceCall(String), } diff --git a/src/server/connection.rs b/src/server/connection.rs index c4c9ec168..da0126213 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1624,6 +1624,7 @@ impl Connection { if let Some(device) = default_sound_device { set_sound_input(device); } + self.send_to_cm(Data::StartVoiceCall); } } else { log::warn!("Possible a voice call attack."); diff --git a/src/ui/cm.rs b/src/ui/cm.rs index 2bd8824db..dc941c3d0 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -55,6 +55,18 @@ impl InvokeUiCM for SciterHandler { fn show_elevation(&self, show: bool) { self.call("showElevation", &make_args!(show)); } + + fn voice_call_started(&self, id: i32) { + self.call("voice_call_started", &make_args!(id)); + } + + fn voice_call_incoming(&self, id: i32) { + self.call("voice_call_incoming", &make_args!(id)); + } + + fn voice_call_closed(&self, id: i32, reason: &str) { + self.call("voice_call_incoming", &make_args!(id, reason)); + } } impl SciterHandler { diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 5d451e4d4..1120a1731 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -88,6 +88,12 @@ pub trait InvokeUiCM: Send + Clone + 'static + Sized { fn change_language(&self); fn show_elevation(&self, show: bool); + + fn voice_call_started(&self, id: i32); + + fn voice_call_incoming(&self, id: i32); + + fn voice_call_closed(&self, id: i32, reason: &str); } impl Deref for ConnectionManager { @@ -180,6 +186,18 @@ impl ConnectionManager { fn show_elevation(&self, show: bool) { self.ui_handler.show_elevation(show); } + + fn voice_call_started(&self, id: i32) { + self.ui_handler.voice_call_started(id); + } + + fn voice_call_incoming(&self, id: i32) { + self.ui_handler.voice_call_incoming(id); + } + + fn voice_call_closed(&self, id: i32, reason: &str) { + self.ui_handler.voice_call_closed(id, reason); + } } #[inline] @@ -389,6 +407,15 @@ impl IpcTaskRunner { Data::DataPortableService(ipc::DataPortableService::CmShowElevation(show)) => { self.cm.show_elevation(show); } + Data::StartVoiceCall => { + self.cm.voice_call_started(self.conn_id); + } + Data::VoiceCallIncoming => { + self.cm.voice_call_incoming(self.conn_id); + } + Data::CloseVoiceCall(reason) => { + self.cm.voice_call_closed(self.conn_id, reason.as_str()); + } _ => { } From ea391542fcf607619631c63505df28fd84ec7c67 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 6 Feb 2023 15:36:36 +0800 Subject: [PATCH 034/199] opt: rename to on_voice_call_started --- src/client/io_loop.rs | 2 +- src/flutter.rs | 4 ++-- src/ui/remote.rs | 2 +- src/ui_session_interface.rs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index e34df30b4..d49227864 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1259,7 +1259,7 @@ impl Remote { } else { if response.accepted { // The peer accepts the voice call. - self.handler.on_voice_call_start(); + self.handler.on_voice_call_started(); self.stop_voice_call_sender = self.start_voice_call(); } else { // The peer refused the voice call. diff --git a/src/flutter.rs b/src/flutter.rs index e83beb03b..4249e4d94 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -395,8 +395,8 @@ impl InvokeUiSession for FlutterHandler { self.push_event("switch_back", [("peer_id", peer_id)].into()); } - fn on_voice_call_start(&self) { - self.push_event("on_voice_call_start", [].into()); + fn on_voice_call_started(&self) { + self.push_event("on_voice_call_started", [].into()); } fn on_voice_call_closed(&self, reason: &str) { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 9888e5831..999b409e0 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -267,7 +267,7 @@ impl InvokeUiSession for SciterHandler { fn switch_back(&self, _id: &str) {} - fn on_voice_call_start(&self) { + fn on_voice_call_started(&self) { self.call("onVoiceCallStart", &make_args!()); } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 4b47608f9..f63bbd081 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -705,7 +705,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn clipboard(&self, content: String); fn cancel_msgbox(&self, tag: &str); fn switch_back(&self, id: &str); - fn on_voice_call_start(&self); + fn on_voice_call_started(&self); fn on_voice_call_closed(&self, reason: &str); fn on_voice_call_waiting(&self); fn on_voice_call_incoming(&self); From 5e21a81a5cc6aca17ba9a4726a626b14b06a67cc Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 6 Feb 2023 20:10:39 +0800 Subject: [PATCH 035/199] wip: implement flutter ui --- Cargo.lock | 5 +-- Cargo.toml | 2 +- flutter/assets/voice_call.svg | 1 + flutter/assets/voice_call_waiting.svg | 1 + .../lib/desktop/widgets/remote_menubar.dart | 32 ++++++++++++++++++- flutter/lib/models/chat_model.dart | 32 +++++++++++++++++++ flutter/lib/models/model.dart | 15 +++++++++ 7 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 flutter/assets/voice_call.svg create mode 100644 flutter/assets/voice_call_waiting.svg diff --git a/Cargo.lock b/Cargo.lock index e15641363..52fcc76cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1334,8 +1334,9 @@ checksum = "f578e8e2c440e7297e008bb5486a3a8a194775224bbc23729b0dbdfaeebf162e" [[package]] name = "default-net" -version = "0.11.0" -source = "git+https://github.com/Kingtous/default-net#bdaad8dd5b08efcba303e71729d3d0b1d5ccdb25" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14e349ed1e06fb344a7dd8b5a676375cf671b31e8900075dd2be816efc063a63" dependencies = [ "libc", "memalloc", diff --git a/Cargo.toml b/Cargo.toml index 936b9e349..b315024e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,7 +59,7 @@ base64 = "0.13" sysinfo = "0.24" num_cpus = "1.13" bytes = { version = "1.2", features = ["serde"] } -default-net = { git = "https://github.com/Kingtous/default-net" } +default-net = "0.12.0" wol-rs = "0.9.1" flutter_rust_bridge = { version = "1.61.1", optional = true } errno = "0.2.8" diff --git a/flutter/assets/voice_call.svg b/flutter/assets/voice_call.svg new file mode 100644 index 000000000..0637b58d9 --- /dev/null +++ b/flutter/assets/voice_call.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter/assets/voice_call_waiting.svg b/flutter/assets/voice_call_waiting.svg new file mode 100644 index 000000000..fd8334f92 --- /dev/null +++ b/flutter/assets/voice_call_waiting.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 0004c65fe..d06be52fa 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -426,6 +426,7 @@ class _RemoteMenubarState extends State { menubarItems.add(_buildKeyboard(context)); if (!isWeb) { menubarItems.add(_buildChat(context)); + menubarItems.add(_buildVoiceCall(context)); } menubarItems.add(_buildRecording(context)); menubarItems.add(_buildClose(context)); @@ -707,6 +708,32 @@ class _RemoteMenubarState extends State { ); } + Widget _buildVoiceCall(BuildContext context) { + return Obx( + () { + switch (widget.ffi.chatModel.voiceCallStatus.value) { + case VoiceCallStatus.waitingForResponse: + return SvgPicture.asset( + "assets/voice_call_waiting.svg", + color: _MenubarTheme.commonColor, + width: Theme.of(context).iconTheme.size ?? 24.0, + height: Theme.of(context).iconTheme.size ?? 24.0, + ); + break; + case VoiceCallStatus.connected: + return SvgPicture.asset( + "assets/voice_call.svg", + color: Colors.red, + width: Theme.of(context).iconTheme.size ?? 24.0, + height: Theme.of(context).iconTheme.size ?? 24.0, + ); + default: + return const Offstage(); + } + }, + ); + } + List> _getChatMenu(BuildContext context) { final List> chatMenu = []; const EdgeInsets padding = EdgeInsets.only(left: 14.0, right: 5.0); @@ -728,7 +755,10 @@ class _RemoteMenubarState extends State { translate('Voice call'), style: style, ), - proc: () {}, + proc: () { + // Request a voice call. + bind.sessionRequestVoiceCall(id: widget.id); + }, padding: padding, dismissOnClicked: true, ), diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index 18a0be279..61602c5b4 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -2,6 +2,7 @@ import 'package:dash_chat_2/dash_chat_2.dart'; import 'package:draggable_float_widget/draggable_float_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get.dart'; import 'package:window_manager/window_manager.dart'; import '../consts.dart'; @@ -33,8 +34,13 @@ class ChatModel with ChangeNotifier { OverlayState? _overlayState; OverlayEntry? chatIconOverlayEntry; OverlayEntry? chatWindowOverlayEntry; + bool isConnManager = false; + final Rx _voiceCallStatus = Rx(VoiceCallStatus.notStarted); + + Rx get voiceCallStatus => _voiceCallStatus; + final ChatUser me = ChatUser( id: "", firstName: "Me", @@ -292,4 +298,30 @@ class ChatModel with ChangeNotifier { resetClientMode() { _messages[clientModeID]?.clear(); } + + void onVoiceCallWaiting() { + _voiceCallStatus.value = VoiceCallStatus.waitingForResponse; + } + + void onVoiceCallStarted() { + _voiceCallStatus.value = VoiceCallStatus.connected; + } + + void onVoiceCallClosed(String reason) { + _voiceCallStatus.value = VoiceCallStatus.notStarted; + } + + void onVoiceCallIncoming() { + if (isConnManager) { + _voiceCallStatus.value = VoiceCallStatus.incoming; + } + } } + +enum VoiceCallStatus { + notStarted, + waitingForResponse, + connected, + // Connection manager only. + incoming +} \ No newline at end of file diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index daf7bfe34..2a4c68839 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -203,6 +203,21 @@ class FfiModel with ChangeNotifier { } else if (name == "on_url_scheme_received") { final url = evt['url'].toString(); parseRustdeskUri(url); + } else if (name == "on_voice_call_waiting") { + // Waiting for the response from the peer. + parent.target?.chatModel.onVoiceCallWaiting(); + } else if (name == "on_voice_call_started") { + // Voice call is connected. + parent.target?.chatModel.onVoiceCallStarted(); + } else if (name == "on_voice_call_closed") { + // Voice call is closed with reason. + final reason = evt['reason'].toString(); + parent.target?.chatModel.onVoiceCallClosed(reason); + } else if (name == "on_voice_call_incoming") { + // Voice call is requested by the peer. + parent.target?.chatModel.onVoiceCallIncoming(); + } else { + debugPrint("Unknown event name: $name"); } }; } From 2943d2d0ccaad9ffe580b98979af95cf44100fb5 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 16:11:55 +0800 Subject: [PATCH 036/199] feat: cm interface --- flutter/lib/desktop/pages/server_page.dart | 42 +++++++++++++++++- .../lib/desktop/widgets/remote_menubar.dart | 32 +++++++++----- flutter/lib/models/chat_model.dart | 4 ++ flutter/lib/models/model.dart | 2 + flutter/lib/models/server_model.dart | 18 ++++++++ src/client/io_loop.rs | 1 + src/flutter.rs | 13 ++---- src/flutter_ffi.rs | 8 ++++ src/server/connection.rs | 2 +- src/ui/cm.rs | 19 ++++---- src/ui_cm_interface.rs | 44 +++++++++++++++---- src/ui_session_interface.rs | 2 +- 12 files changed, 143 insertions(+), 44 deletions(-) diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 521413647..b2f70cdd5 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -521,6 +521,38 @@ class _CmControlPanel extends StatelessWidget { return Column( mainAxisAlignment: MainAxisAlignment.end, children: [ + Offstage( + offstage: !client.inVoiceCall, + child: buildButton(context, + color: Colors.purple, + onClick: () => closeVoiceCall(), + icon: Icon(Icons.reply, color: Colors.white), + text: "Stop voice call", + textColor: Colors.white), + ), + Offstage( + offstage: !client.incomingVoiceCall, + child: Row( + children: [ + Expanded( + child: buildButton(context, + color: MyTheme.accent, + onClick: () => handleVoiceCall(true), + icon: Icon(Icons.phone, color: Colors.white), + text: "Accept", + textColor: Colors.white), + ), + Expanded( + child: buildButton(context, + color: Colors.red, + onClick: () => handleVoiceCall(false), + icon: Icon(Icons.phone, color: Colors.white), + text: "Deny", + textColor: Colors.white), + ) + ], + ), + ), Offstage( offstage: !client.fromSwitch, child: buildButton(context, @@ -626,7 +658,7 @@ class _CmControlPanel extends StatelessWidget { .marginSymmetric(horizontal: showElevation ? 0 : bigMargin); } - buildButton( + Widget buildButton( BuildContext context, { required Color? color, required Function() onClick, @@ -692,6 +724,14 @@ class _CmControlPanel extends StatelessWidget { void handleSwitchBack(BuildContext context) { bind.cmSwitchBack(connId: client.id); } + + void handleVoiceCall(bool accept) { + bind.cmHandleIncomingVoiceCall(id: client.id, accept: accept); + } + + void closeVoiceCall() { + bind.cmCloseVoiceCall(id: client.id); + } } void checkClickTime(int id, Function() callback) async { diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index d06be52fa..653ff37b1 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -713,19 +713,27 @@ class _RemoteMenubarState extends State { () { switch (widget.ffi.chatModel.voiceCallStatus.value) { case VoiceCallStatus.waitingForResponse: - return SvgPicture.asset( - "assets/voice_call_waiting.svg", - color: _MenubarTheme.commonColor, - width: Theme.of(context).iconTheme.size ?? 24.0, - height: Theme.of(context).iconTheme.size ?? 24.0, - ); - break; + return IconButton( + onPressed: () { + widget.ffi.chatModel.closeVoiceCall(widget.id); + }, + icon: SvgPicture.asset( + "assets/voice_call_waiting.svg", + color: Colors.red, + width: Theme.of(context).iconTheme.size ?? 24.0, + height: Theme.of(context).iconTheme.size ?? 24.0, + )); case VoiceCallStatus.connected: - return SvgPicture.asset( - "assets/voice_call.svg", - color: Colors.red, - width: Theme.of(context).iconTheme.size ?? 24.0, - height: Theme.of(context).iconTheme.size ?? 24.0, + return IconButton( + onPressed: () { + widget.ffi.chatModel.closeVoiceCall(widget.id); + }, + icon: SvgPicture.asset( + "assets/voice_call.svg", + color: Colors.red, + width: Theme.of(context).iconTheme.size ?? 24.0, + height: Theme.of(context).iconTheme.size ?? 24.0, + ), ); default: return const Offstage(); diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index 61602c5b4..14af96570 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -316,6 +316,10 @@ class ChatModel with ChangeNotifier { _voiceCallStatus.value = VoiceCallStatus.incoming; } } + + void closeVoiceCall(String id) { + bind.sessionCloseVoiceCall(id: id); + } } enum VoiceCallStatus { diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 2a4c68839..a2fe205af 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -216,6 +216,8 @@ class FfiModel with ChangeNotifier { } else if (name == "on_voice_call_incoming") { // Voice call is requested by the peer. parent.target?.chatModel.onVoiceCallIncoming(); + } else if (name == "update_voice_call_state") { + parent.target?.serverModel.updateVoiceCallState(evt); } else { debugPrint("Unknown event name: $name"); } diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 56dca4cdf..6cd905c37 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -579,6 +579,20 @@ class ServerModel with ChangeNotifier { notifyListeners(); } } + + void updateVoiceCallState(Map evt) { + try { + final client = Client.fromJson(jsonDecode(evt["client"])); + final index = _clients.indexWhere((element) => element.id == client.id); + if (index != -1) { + _clients[index].inVoiceCall = evt['in_voice_call']; + _clients[index].incomingVoiceCall = evt['incoming_voice_call']; + notifyListeners(); + } + } catch (e) { + debugPrint("updateVoiceCallState failed: $e"); + } + } } enum ClientType { @@ -602,6 +616,8 @@ class Client { bool recording = false; bool disconnected = false; bool fromSwitch = false; + bool inVoiceCall = false; + bool incomingVoiceCall = false; RxBool hasUnreadChatMessage = false.obs; @@ -623,6 +639,8 @@ class Client { recording = json['recording']; disconnected = json['disconnected']; fromSwitch = json['from_switch']; + inVoiceCall = json['in_voice_call']; + incomingVoiceCall = json['incoming_voice_call']; } Map toJson() { diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index d49227864..aa51df378 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -754,6 +754,7 @@ impl Remote { Data::CloseVoiceCall => { self.stop_voice_call(); let msg = new_voice_call_request(false); + self.handler.on_voice_call_closed("Closed manually by the peer"); allow_err!(peer.send(&msg).await); } _ => {} diff --git a/src/flutter.rs b/src/flutter.rs index 4249e4d94..a27a9d4e1 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -538,16 +538,9 @@ pub mod connection_manager { self.push_event("show_elevation", vec![("show", &show.to_string())]); } - fn voice_call_started(&self, id: i32) { - self.push_event("voice_call_started", vec![("show", &id.to_string())]); - } - - fn voice_call_incoming(&self, id: i32) { - self.push_event("voice_call_incoming", vec![("id", &id.to_string())]); - } - - fn voice_call_closed(&self, id: i32, reason: &str) { - self.push_event("voice_call_closed", vec![("id", &id.to_string()), ("reason", &reason.to_string())]); + fn update_voice_call_state(&self, client: &crate::ui_cm_interface::Client) { + let client_json = serde_json::to_string(&client).unwrap_or("".into()); + self.push_event("update_voice_call_state", vec![("client", &client_json)]); } } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 588733c37..cfca0e082 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -838,6 +838,14 @@ pub fn session_close_voice_call(id: String) { } } +pub fn cm_handle_incoming_voice_call(id: i32, accept: bool) { + crate::ui_cm_interface::handle_incoming_voice_call(id, accept); +} + +pub fn cm_close_voice_call(id: i32) { + crate::ui_cm_interface::close_voice_call(id); +} + pub fn main_get_last_remote_id() -> String { LocalConfig::get_remote_id() } diff --git a/src/server/connection.rs b/src/server/connection.rs index da0126213..1e88b9b05 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1636,7 +1636,7 @@ impl Connection { if let Some(sound_input) = std::mem::replace(&mut self.audio_input_device_before_voice_call, None) { set_sound_input(sound_input); // Notify the connection manager. - self.send_to_cm(Data::CloseVoiceCall("Closed manually by the peer".to_owned())); + self.send_to_cm(Data::CloseVoiceCall("".to_owned())); } } diff --git a/src/ui/cm.rs b/src/ui/cm.rs index dc941c3d0..cce553154 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -56,16 +56,15 @@ impl InvokeUiCM for SciterHandler { self.call("showElevation", &make_args!(show)); } - fn voice_call_started(&self, id: i32) { - self.call("voice_call_started", &make_args!(id)); - } - - fn voice_call_incoming(&self, id: i32) { - self.call("voice_call_incoming", &make_args!(id)); - } - - fn voice_call_closed(&self, id: i32, reason: &str) { - self.call("voice_call_incoming", &make_args!(id, reason)); + fn update_voice_call_state(&self, client: &crate::ui_cm_interface::Client) { + self.call( + "updateVoiceCallState", + &make_args!( + client.id, + client.in_voice_call, + client.incoming_voice_call + ), + ); } } diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 1120a1731..ccddab0ee 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -49,6 +49,8 @@ pub struct Client { pub restart: bool, pub recording: bool, pub from_switch: bool, + pub in_voice_call: bool, + pub incoming_voice_call: bool, #[serde(skip)] tx: UnboundedSender, } @@ -89,11 +91,7 @@ pub trait InvokeUiCM: Send + Clone + 'static + Sized { fn show_elevation(&self, show: bool); - fn voice_call_started(&self, id: i32); - - fn voice_call_incoming(&self, id: i32); - - fn voice_call_closed(&self, id: i32, reason: &str); + fn update_voice_call_state(&self, client: &Client); } impl Deref for ConnectionManager { @@ -144,6 +142,8 @@ impl ConnectionManager { recording, from_switch, tx, + in_voice_call: false, + incoming_voice_call: false }; CLIENTS .write() @@ -188,15 +188,27 @@ impl ConnectionManager { } fn voice_call_started(&self, id: i32) { - self.ui_handler.voice_call_started(id); + if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { + client.incoming_voice_call = false; + client.in_voice_call = true; + self.ui_handler.update_voice_call_state(client); + } } fn voice_call_incoming(&self, id: i32) { - self.ui_handler.voice_call_incoming(id); + if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { + client.incoming_voice_call = true; + client.in_voice_call = false; + self.ui_handler.update_voice_call_state(client); + } } - fn voice_call_closed(&self, id: i32, reason: &str) { - self.ui_handler.voice_call_closed(id, reason); + fn voice_call_closed(&self, id: i32, _reason: &str) { + if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { + client.incoming_voice_call = false; + client.in_voice_call = false; + self.ui_handler.update_voice_call_state(client); + } } } @@ -832,3 +844,17 @@ pub fn elevate_portable(_id: i32) { } } } + +#[inline] +pub fn handle_incoming_voice_call(id: i32, accept: bool) { + if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { + allow_err!(client.tx.send(Data::VoiceCallResponse(accept))); + }; +} + +#[inline] +pub fn close_voice_call(id: i32) { + if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { + allow_err!(client.tx.send(Data::CloseVoiceCall("".to_owned()))); + }; +} \ No newline at end of file diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index f63bbd081..cd0bdcde2 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -661,7 +661,7 @@ impl Session { pub fn request_voice_call(&self) { self.send(Data::NewVoiceCall); } - + pub fn close_voice_call(&self) { self.send(Data::CloseVoiceCall); } From fc933ad7b4c8e88f035aea44694ff53721895a33 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 16:47:19 +0800 Subject: [PATCH 037/199] fix: voice call 1 --- flutter/lib/models/server_model.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 6cd905c37..eec424bfe 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -585,8 +585,8 @@ class ServerModel with ChangeNotifier { final client = Client.fromJson(jsonDecode(evt["client"])); final index = _clients.indexWhere((element) => element.id == client.id); if (index != -1) { - _clients[index].inVoiceCall = evt['in_voice_call']; - _clients[index].incomingVoiceCall = evt['incoming_voice_call']; + _clients[index].inVoiceCall = client.inVoiceCall; + _clients[index].incomingVoiceCall = client.incomingVoiceCall; notifyListeners(); } } catch (e) { From cd6cdbff8f9c9fb38d7ad9631634ba2b9bea328d Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 16:53:46 +0800 Subject: [PATCH 038/199] fix: close notify --- src/client/io_loop.rs | 2 +- src/server/connection.rs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index aa51df378..234f4f842 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1264,7 +1264,7 @@ impl Remote { self.stop_voice_call_sender = self.start_voice_call(); } else { // The peer refused the voice call. - self.handler.on_voice_call_closed("Refused"); + self.handler.on_voice_call_closed(""); } } } diff --git a/src/server/connection.rs b/src/server/connection.rs index 1e88b9b05..7a16df811 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1614,7 +1614,6 @@ impl Connection { pub async fn handle_voice_call(&mut self, accepted: bool) { if let Some(ts) = self.voice_call_request_timestamp.take() { let msg = new_voice_call_response(ts.get(), accepted); - self.send(msg).await; if accepted { // Backup the default input device. let audio_input_device = Config::get_option("audio-input"); @@ -1625,7 +1624,10 @@ impl Connection { set_sound_input(device); } self.send_to_cm(Data::StartVoiceCall); + } else { + self.send_to_cm(Data::CloseVoiceCall("".to_owned())); } + self.send(msg).await; } else { log::warn!("Possible a voice call attack."); } From 66aaf243cf7654c40628187a0249ac77b9452c7a Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 17:09:36 +0800 Subject: [PATCH 039/199] opt: notify cm --- flutter/lib/desktop/pages/server_page.dart | 7 ++++--- flutter/lib/models/server_model.dart | 6 ++++++ src/client/io_loop.rs | 2 +- src/server/connection.rs | 7 ++++--- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index b2f70cdd5..a253b9aa2 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -524,7 +524,7 @@ class _CmControlPanel extends StatelessWidget { Offstage( offstage: !client.inVoiceCall, child: buildButton(context, - color: Colors.purple, + color: Colors.red, onClick: () => closeVoiceCall(), icon: Icon(Icons.reply, color: Colors.white), text: "Stop voice call", @@ -538,7 +538,7 @@ class _CmControlPanel extends StatelessWidget { child: buildButton(context, color: MyTheme.accent, onClick: () => handleVoiceCall(true), - icon: Icon(Icons.phone, color: Colors.white), + icon: Icon(Icons.phone_enabled, color: Colors.white), text: "Accept", textColor: Colors.white), ), @@ -546,7 +546,8 @@ class _CmControlPanel extends StatelessWidget { child: buildButton(context, color: Colors.red, onClick: () => handleVoiceCall(false), - icon: Icon(Icons.phone, color: Colors.white), + icon: + Icon(Icons.phone_disabled_rounded, color: Colors.white), text: "Deny", textColor: Colors.white), ) diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index eec424bfe..aab12ab5d 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -587,6 +587,12 @@ class ServerModel with ChangeNotifier { if (index != -1) { _clients[index].inVoiceCall = client.inVoiceCall; _clients[index].incomingVoiceCall = client.incomingVoiceCall; + if (client.incomingVoiceCall) { + // Has incoming phone call, let's set the window on top. + Future.delayed(Duration.zero, () { + window_on_top(null); + }); + } notifyListeners(); } } catch (e) { diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 234f4f842..05eab692a 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1259,7 +1259,7 @@ impl Remote { log::debug!("Possible encountering a voice call attack."); } else { if response.accepted { - // The peer accepts the voice call. + // The peer accepted the voice call. self.handler.on_voice_call_started(); self.stop_voice_call_sender = self.start_voice_call(); } else { diff --git a/src/server/connection.rs b/src/server/connection.rs index 7a16df811..86d837619 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1596,9 +1596,11 @@ impl Connection { NonZeroI64::new(request.req_timestamp) .unwrap_or(NonZeroI64::new(get_time()).unwrap()), ); - // Call cm. + // Notify the connection manager. self.send_to_cm(Data::VoiceCallIncoming); } else { + // Notify the connection manager. + self.send_to_cm(Data::CloseVoiceCall("".to_owned())); self.close_voice_call().await; } } @@ -1617,6 +1619,7 @@ impl Connection { if accepted { // Backup the default input device. let audio_input_device = Config::get_option("audio-input"); + log::debug!("Backup the sound input device {}", audio_input_device); self.audio_input_device_before_voice_call = Some(audio_input_device); // Switch to default input device let default_sound_device = get_default_sound_input(); @@ -1637,8 +1640,6 @@ impl Connection { // Restore to the prior audio device. if let Some(sound_input) = std::mem::replace(&mut self.audio_input_device_before_voice_call, None) { set_sound_input(sound_input); - // Notify the connection manager. - self.send_to_cm(Data::CloseVoiceCall("".to_owned())); } } From 29b1d106aa8385b03a40ecfa7e125831a3920caf Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 17:16:06 +0800 Subject: [PATCH 040/199] opt: ui and message --- flutter/lib/desktop/pages/server_page.dart | 4 ++-- src/lang/ca.rs | 8 +++----- src/lang/cn.rs | 7 +------ src/lang/cs.rs | 8 +++----- src/lang/da.rs | 6 +++--- src/lang/de.rs | 8 +++----- src/lang/eo.rs | 8 +++----- src/lang/es.rs | 9 ++++----- src/lang/fa.rs | 8 +++----- src/lang/fr.rs | 8 +++----- src/lang/gr.rs | 8 +++----- src/lang/hu.rs | 8 +++----- src/lang/id.rs | 8 +++----- src/lang/it.rs | 8 +++----- src/lang/ja.rs | 8 +++----- src/lang/ko.rs | 8 +++----- src/lang/kz.rs | 8 +++----- src/lang/pl.rs | 8 +++----- src/lang/pt_PT.rs | 8 +++----- src/lang/ptbr.rs | 8 +++----- src/lang/ro.rs | 8 +++----- src/lang/ru.rs | 12 +++++------- src/lang/sk.rs | 8 +++----- src/lang/sl.rs | 6 +++--- src/lang/sq.rs | 8 +++----- src/lang/sr.rs | 8 +++----- src/lang/sv.rs | 8 +++----- src/lang/template.rs | 8 +++----- src/lang/th.rs | 8 +++----- src/lang/tr.rs | 8 +++----- src/lang/tw.rs | 8 +++----- src/lang/ua.rs | 8 +++----- src/lang/vn.rs | 8 +++----- 33 files changed, 99 insertions(+), 161 deletions(-) diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index a253b9aa2..66a043fef 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -526,7 +526,7 @@ class _CmControlPanel extends StatelessWidget { child: buildButton(context, color: Colors.red, onClick: () => closeVoiceCall(), - icon: Icon(Icons.reply, color: Colors.white), + icon: Icon(Icons.phone_disabled_rounded, color: Colors.white), text: "Stop voice call", textColor: Colors.white), ), @@ -548,7 +548,7 @@ class _CmControlPanel extends StatelessWidget { onClick: () => handleVoiceCall(false), icon: Icon(Icons.phone_disabled_rounded, color: Colors.white), - text: "Deny", + text: "Dismiss", textColor: Colors.white), ) ], diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 4404e178d..e98c6636a 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 5a9abba9c..64c37709a 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -446,13 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "帧率"), ("Auto", "自动"), ("Other Default Options", "其它默认选项"), - ("Guest to Host", "被控到主机"), - ("Dual way", "双向"), - ("Guest to host audio transmission", "被控到主机音频传输"), - ("Dual-way audio transmission", "双向音频传输"), ("Voice call", "语音通话"), ("Text chat", "文字聊天"), - ("Audio Transmission Mode", "音频传输模式"), - ("Refused", "已拒绝") + ("Stop voice call", "停止语音聊天"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index a2a19a37a..70a3eb6c7 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 905f4814e..ae943e1e8 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -437,9 +437,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), ("Closed as expected", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), @@ -449,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 4028e3337..44bbafdac 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "fps"), ("Auto", "Automatisch"), ("Other Default Options", "Weitere Standardoptionen"), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index fe3830b99..f457833f8 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index b9b31f109..220447454 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -436,7 +436,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", "Fuerte"), ("Switch Sides", "Intercambiar lados"), ("Please confirm if you want to share your desktop?", "Por favor, confirma si quieres compartir tu escritorio"), - ("Closed as expected", "Cerrado como se esperaba"), + ("Closed as expected", ""), ("Display", "Pantalla"), ("Default View Style", "Estilo de vista predeterminado"), ("Default Scroll Style", "Estilo de desplazamiento predeterminado"), @@ -446,9 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", "Otras opciones predeterminadas"), - ("Closed as expected", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 0b92c6658..c206f91ff 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "خودکار"), ("Other Default Options", "سایر گزینه های پیش فرض"), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 4965f6dab..39ee3bc7f 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "Auto"), ("Other Default Options", "Autres options par défaut"), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index e40151ccf..7cb678ecc 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 0e1887e48..25562f556 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 689ae98cf..68a80e540 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 65f91ecec..9730bbc2d 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "Auto"), ("Other Default Options", "Altre Opzioni Predefinite"), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 33fb2da05..7069c0daf 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index c874dd695..43eb552d3 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 01014bab0..49c7b9916 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 9dd005bdd..41239961a 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "Auto"), ("Other Default Options", "Inne opcje domyślne"), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 716d3df82..e69a140c9 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index c7d0cd6ec..0887a5915 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 2d48b91b4..304353d42 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 8224cd5eb..1e6c6962a 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -435,8 +435,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Medium", "Средний"), ("Strong", "Стойкий"), ("Switch Sides", "Переключить стороны"), - ("Please confirm if you want to share your desktop?", "Подтверждаете, что хотите поделиться своим рабочим столом?"), - ("Closed as expected", "Закрыто по ожиданию"), + ("Please confirm if you want to share your desktop?", "Подтвердите, что хотите поделиться своим рабочим столом?"), + ("Closed as expected", ""), ("Display", "Отображение"), ("Default View Style", "Стиль отображения по умолчанию"), ("Default Scroll Style", "Стиль прокрутки по умолчанию"), @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "Авто"), ("Other Default Options", "Другие параметры по умолчанию"), - ("Please confirm if you want to share your desktop?", "Подтвердите, что хотите поделиться своим рабочим столом?"), - ("Closed as expected", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 5e0330954..6f6f7a18e 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index a75da46bd..2fb74fa5d 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -446,8 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index d3964a2e9..5d4a6e1ad 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 78059645d..31a3ade8f 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index ca2257756..e30c09e44 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 4355d643a..b88618074 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 57dfe6e43..1c75aaae7 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 49a42af4a..a9e2c1715 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 50e684258..7c49a29a2 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "幀率"), ("Auto", "自動"), ("Other Default Options", "其它默認選項"), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index f37ed341e..92c99d90c 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 5788a7f3d..8bb1d45e9 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } From 4ea41b52d3066031f8ea8ac32942c7e67f36eada Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 18:01:54 +0800 Subject: [PATCH 041/199] fix: execution order of listening ipc thread --- flutter/lib/main.dart | 3 +++ src/client.rs | 2 -- src/client/io_loop.rs | 2 +- src/core_main.rs | 2 -- src/flutter_ffi.rs | 4 ++++ 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index c19adf753..b923a31e1 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -114,6 +114,9 @@ Future initEnv(String appType) async { _registerEventHandler(); // Update the system theme. updateSystemWindowTheme(); + if (appType == kAppTypeConnectionManager) { + await bind.cmStartListenIpcThread(); + } } void runMainApp(bool startService) async { diff --git a/src/client.rs b/src/client.rs index 2ea33b655..020bea1f0 100644 --- a/src/client.rs +++ b/src/client.rs @@ -773,7 +773,6 @@ impl AudioHandler { unsafe { std::slice::from_raw_parts::(buffer.as_ptr() as _, n * 4) }; self.simple.as_mut().map(|x| x.write(data_u8)); } - log::debug!("write Audio frame {} to system.", frame.timestamp); } }); } @@ -1595,7 +1594,6 @@ pub fn start_audio_thread( if let Ok(data) = audio_receiver.recv() { match data { MediaData::AudioFrame(af) => { - log::debug!("recved audio frame={}", af.timestamp); audio_handler.handle_frame(af); } MediaData::AudioFormat(f) => { diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 05eab692a..c8a0f2ca3 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -300,7 +300,7 @@ impl Remote { // check if client is closed match rx.try_recv() { Ok(_) | Err(std::sync::mpsc::TryRecvError::Disconnected) => { - log::debug!("Exit local audio service of client"); + log::debug!("Exit voice call audio service of client"); // unsubscribe CLIENT_SERVER.write().unwrap().subscribe( audio_service::NAME, diff --git a/src/core_main.rs b/src/core_main.rs index 99d0e888e..03d057eff 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -246,8 +246,6 @@ pub fn core_main() -> Option> { } else if args[0] == "--cm" { // call connection manager to establish connections // meanwhile, return true to call flutter window to show control panel - #[cfg(feature = "flutter")] - crate::flutter::connection_manager::start_listen_ipc_thread(); crate::ui_interface::start_option_status_sync(); } } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index cfca0e082..84407cd96 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1284,6 +1284,10 @@ pub fn main_hide_docker() -> SyncReturn { SyncReturn(true) } +pub fn cm_start_listen_ipc_thread() { + crate::flutter::connection_manager::start_listen_ipc_thread(); +} + /// Start an ipc server for receiving the url scheme. /// /// * Should only be called in the main flutter window. From 795b0068d0deefa1eeb99a52c8b6cef1fd1e30d5 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 18:17:31 +0800 Subject: [PATCH 042/199] opt: close voice call msg --- src/server/connection.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index 86d837619..1bacad124 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1599,8 +1599,6 @@ impl Connection { // Notify the connection manager. self.send_to_cm(Data::VoiceCallIncoming); } else { - // Notify the connection manager. - self.send_to_cm(Data::CloseVoiceCall("".to_owned())); self.close_voice_call().await; } } @@ -1641,6 +1639,7 @@ impl Connection { if let Some(sound_input) = std::mem::replace(&mut self.audio_input_device_before_voice_call, None) { set_sound_input(sound_input); } + self.send_to_cm(Data::CloseVoiceCall("".to_owned())); } async fn update_option(&mut self, o: &OptionMessage) { From db8b6d618f0d6b93b69f97dcfc42bca26063b2cf Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 19:09:22 +0800 Subject: [PATCH 043/199] fix: audio close status sync --- src/client/io_loop.rs | 11 +++++++++-- src/server/connection.rs | 4 ++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index c8a0f2ca3..96ddd51f0 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1249,8 +1249,15 @@ impl Remote { self.handler .msgbox(&msgbox.msgtype, &msgbox.title, &msgbox.text, &link); } - Some(message::Union::VoiceCallRequest(_request)) => { - // TODO: maybe we will do voice call from the peer. + Some(message::Union::VoiceCallRequest(request)) => { + if request.is_connect { + // TODO: maybe we will do voice call from the peer in the future. + } else { + if let Some(sender) = self.stop_voice_call_sender.take() { + allow_err!(sender.send(())); + self.handler.on_voice_call_closed(""); + } + } } Some(message::Union::VoiceCallResponse(response)) => { let ts = std::mem::replace(&mut self.voice_call_request_timestamp, None); diff --git a/src/server/connection.rs b/src/server/connection.rs index 1bacad124..17417cf61 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -402,6 +402,9 @@ impl Connection { } ipc::Data::CloseVoiceCall(_reason) => { conn.close_voice_call().await; + // Notify the peer that we closed the voice call. + let req = new_voice_call_request(false); + conn.send(req).await; } _ => {} } @@ -1639,6 +1642,7 @@ impl Connection { if let Some(sound_input) = std::mem::replace(&mut self.audio_input_device_before_voice_call, None) { set_sound_input(sound_input); } + // Notify the connection manager that the voice call has been closed. self.send_to_cm(Data::CloseVoiceCall("".to_owned())); } From c4b1c51e9e745f32037e04c3ae17fd4a6f0799a5 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 19:33:58 +0800 Subject: [PATCH 044/199] opt: more debug info --- flutter/lib/main.dart | 4 +--- src/flutter.rs | 4 +++- src/server/connection.rs | 5 +++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index b923a31e1..c61287d4f 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -114,9 +114,6 @@ Future initEnv(String appType) async { _registerEventHandler(); // Update the system theme. updateSystemWindowTheme(); - if (appType == kAppTypeConnectionManager) { - await bind.cmStartListenIpcThread(); - } } void runMainApp(bool startService) async { @@ -219,6 +216,7 @@ void runMultiWindow( void runConnectionManagerScreen(bool hide) async { await initEnv(kAppTypeConnectionManager); + await bind.cmStartListenIpcThread(); _runApp( '', const DesktopServerPage(), diff --git a/src/flutter.rs b/src/flutter.rs index a27a9d4e1..2d7d3fb86 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -549,9 +549,11 @@ pub mod connection_manager { let mut h: HashMap<&str, &str> = event.iter().cloned().collect(); assert!(h.get("name").is_none()); h.insert("name", name); - + if let Some(s) = GLOBAL_EVENT_STREAM.read().unwrap().get(super::APP_TYPE_CM) { s.add(serde_json::ser::to_string(&h).unwrap_or("".to_owned())); + } else { + println!("Push event {} failed. No {} event stream found.", name, super::APP_TYPE_CM); }; } } diff --git a/src/server/connection.rs b/src/server/connection.rs index 17417cf61..a8849b4e6 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -401,10 +401,11 @@ impl Connection { conn.handle_voice_call(accepted).await; } ipc::Data::CloseVoiceCall(_reason) => { + log::debug!("Close the voice call from the ipc."); conn.close_voice_call().await; // Notify the peer that we closed the voice call. - let req = new_voice_call_request(false); - conn.send(req).await; + let msg = new_voice_call_request(false); + conn.send(msg).await; } _ => {} } From 86b88c2927a0251dcd8cdbd90e799ced45bb5d04 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 19:40:50 +0800 Subject: [PATCH 045/199] opt: open audio when needed --- src/client/io_loop.rs | 3 ++- src/server/connection.rs | 18 ++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 96ddd51f0..f5792bce3 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1251,8 +1251,9 @@ impl Remote { } Some(message::Union::VoiceCallRequest(request)) => { if request.is_connect { - // TODO: maybe we will do voice call from the peer in the future. + // TODO: maybe we will do a voice call from the peer in the future. } else { + log::debug!("The remote has requested to close the voice call"); if let Some(sender) = self.stop_voice_call_sender.take() { allow_err!(sender.send(())); self.handler.on_voice_call_closed(""); diff --git a/src/server/connection.rs b/src/server/connection.rs index a8849b4e6..02888d1ea 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -106,7 +106,7 @@ pub struct Connection { // by peer enable_file_transfer: bool, // by peer - audio_sender: MediaSender, + audio_sender: Option, // audio by the remote peer/client tx_input: std_mpsc::Sender, // handle input messages @@ -184,11 +184,6 @@ impl Connection { let mut hbbs_rx = crate::hbbs_http::sync::signal_receiver(); let tx_cloned = tx.clone(); - // Start a audio thread to play the audio sent by peer. - let latency_controller = LatencyController::new(); - // No video frame will be sent here, so we need to disable latency controller, or audio check may fail. - latency_controller.lock().unwrap().set_audio_only(true); - let audio_sender = start_audio_thread(Some(latency_controller)); let mut conn = Self { inner: ConnInner { id, @@ -230,7 +225,7 @@ impl Connection { #[cfg(windows)] portable: Default::default(), from_switch: false, - audio_sender, + audio_sender: None, voice_call_request_timestamp: None, audio_input_device_before_voice_call: None, }; @@ -1569,7 +1564,14 @@ impl Connection { }, Some(misc::Union::AudioFormat(format)) => { if !self.disable_audio { - allow_err!(self.audio_sender.send(MediaData::AudioFormat(format))); + // Drop the audio sender previously. + std::mem::replace(&mut self.audio_sender, None); + // Start a audio thread to play the audio sent by peer. + let latency_controller = LatencyController::new(); + // No video frame will be sent here, so we need to disable latency controller, or audio check may fail. + latency_controller.lock().unwrap().set_audio_only(true); + self.audio_sender = Some(start_audio_thread(Some(latency_controller))); + allow_err!(self.audio_sender.unwrap().send(MediaData::AudioFormat(format))); } } #[cfg(feature = "flutter")] From 404915c97512cbb9a60d58f70ae9eb83c60c2733 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 19:49:42 +0800 Subject: [PATCH 046/199] fix: compile --- src/server/connection.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index 02888d1ea..9ce53c960 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1565,13 +1565,13 @@ impl Connection { Some(misc::Union::AudioFormat(format)) => { if !self.disable_audio { // Drop the audio sender previously. - std::mem::replace(&mut self.audio_sender, None); + drop(std::mem::replace(&mut self.audio_sender, None)); // Start a audio thread to play the audio sent by peer. let latency_controller = LatencyController::new(); // No video frame will be sent here, so we need to disable latency controller, or audio check may fail. latency_controller.lock().unwrap().set_audio_only(true); self.audio_sender = Some(start_audio_thread(Some(latency_controller))); - allow_err!(self.audio_sender.unwrap().send(MediaData::AudioFormat(format))); + allow_err!(self.audio_sender.as_ref().unwrap().send(MediaData::AudioFormat(format))); } } #[cfg(feature = "flutter")] @@ -1593,7 +1593,11 @@ impl Connection { }, Some(message::Union::AudioFrame(frame)) => { if !self.disable_audio { - allow_err!(self.audio_sender.send(MediaData::AudioFrame(frame))); + if let Some(sender) = &self.audio_sender { + allow_err!(sender.send(MediaData::AudioFrame(frame))); + } else { + log::warn!("Processing audio frame without the voice call audio sender."); + } } } Some(message::Union::VoiceCallRequest(request)) => { From 344d927ff8bbd090b02967ba0e1217cbdb1776f2 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 20:38:27 +0800 Subject: [PATCH 047/199] opt: optimize icon --- flutter/assets/record_screen.svg | 24 +++++ flutter/assets/voice_call.svg | 2 +- .../lib/desktop/pages/desktop_home_page.dart | 16 ++-- .../lib/desktop/widgets/remote_menubar.dart | 93 ++++++++++++------- flutter/lib/models/chat_model.dart | 2 +- 5 files changed, 95 insertions(+), 42 deletions(-) create mode 100644 flutter/assets/record_screen.svg diff --git a/flutter/assets/record_screen.svg b/flutter/assets/record_screen.svg new file mode 100644 index 000000000..e1b962124 --- /dev/null +++ b/flutter/assets/record_screen.svg @@ -0,0 +1,24 @@ + + + + + + + + + \ No newline at end of file diff --git a/flutter/assets/voice_call.svg b/flutter/assets/voice_call.svg index 0637b58d9..5654befc7 100644 --- a/flutter/assets/voice_call.svg +++ b/flutter/assets/voice_call.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 71dd2c96e..2986adc7a 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -358,14 +358,16 @@ class _DesktopHomePageState extends State return buildInstallCard("", "install_daemon_tip", "Install", () async { bind.mainIsInstalledDaemon(prompt: true); }); - } else if ((await osxCanRecordAudio() != - PermissionAuthorizeType.authorized)) { - return buildInstallCard("Permissions", "config_microphone", "Configure", - () async { - osxRequestAudio(); - watchIsCanRecordAudio = true; - }); } + //// Disable microphone configuration for macOS. We will request the permission when needed. + // else if ((await osxCanRecordAudio() != + // PermissionAuthorizeType.authorized)) { + // return buildInstallCard("Permissions", "config_microphone", "Configure", + // () async { + // osxRequestAudio(); + // watchIsCanRecordAudio = true; + // }); + // } } else if (Platform.isLinux) { if (bind.mainCurrentIsWayland()) { return buildInstallCard( diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 653ff37b1..dcc531408 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -657,12 +657,17 @@ class _RemoteMenubarState extends State { ? translate('Stop session recording') : translate('Start session recording'), onPressed: () => value.toggle(), - icon: Icon( - value.start - ? Icons.pause_circle_filled - : Icons.videocam_outlined, - color: _MenubarTheme.commonColor, - ), + icon: value.start + ? Icon( + Icons.pause_circle_filled, + color: _MenubarTheme.commonColor, + ) + : SvgPicture.asset( + "assets/record_screen.svg", + color: _MenubarTheme.commonColor, + width: Theme.of(context).iconTheme.size ?? 22.0, + height: Theme.of(context).iconTheme.size ?? 22.0, + ), )); } else { return Offstage(); @@ -708,36 +713,58 @@ class _RemoteMenubarState extends State { ); } + Widget _getVoiceCallIcon() { + switch (widget.ffi.chatModel.voiceCallStatus.value) { + case VoiceCallStatus.waitingForResponse: + return IconButton( + onPressed: () { + widget.ffi.chatModel.closeVoiceCall(widget.id); + }, + icon: SvgPicture.asset( + "assets/voice_call_waiting.svg", + color: Colors.red, + width: Theme.of(context).iconTheme.size ?? 20.0, + height: Theme.of(context).iconTheme.size ?? 20.0, + )); + case VoiceCallStatus.connected: + return IconButton( + onPressed: () { + widget.ffi.chatModel.closeVoiceCall(widget.id); + }, + icon: Icon( + Icons.phone_disabled_rounded, + color: Colors.red, + size: Theme.of(context).iconTheme.size ?? 22.0, + ), + ); + default: + return const Offstage(); + } + } + + String? _getVoiceCallTooltip() { + switch (widget.ffi.chatModel.voiceCallStatus.value) { + case VoiceCallStatus.waitingForResponse: + return "Waiting"; + case VoiceCallStatus.connected: + return "Disconnect"; + default: + return null; + } + } + Widget _buildVoiceCall(BuildContext context) { return Obx( () { - switch (widget.ffi.chatModel.voiceCallStatus.value) { - case VoiceCallStatus.waitingForResponse: - return IconButton( - onPressed: () { - widget.ffi.chatModel.closeVoiceCall(widget.id); - }, - icon: SvgPicture.asset( - "assets/voice_call_waiting.svg", - color: Colors.red, - width: Theme.of(context).iconTheme.size ?? 24.0, - height: Theme.of(context).iconTheme.size ?? 24.0, - )); - case VoiceCallStatus.connected: - return IconButton( - onPressed: () { - widget.ffi.chatModel.closeVoiceCall(widget.id); - }, - icon: SvgPicture.asset( - "assets/voice_call.svg", - color: Colors.red, - width: Theme.of(context).iconTheme.size ?? 24.0, - height: Theme.of(context).iconTheme.size ?? 24.0, - ), - ); - default: - return const Offstage(); - } + final tooltipText = _getVoiceCallTooltip(); + return tooltipText == null + ? const Offstage() + : IconButton( + padding: EdgeInsets.zero, + icon: _getVoiceCallIcon(), + tooltip: translate(tooltipText), + onPressed: () => bind.sessionRequestVoiceCall(id: widget.id), + ); }, ); } diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index 14af96570..bf7f8773d 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -328,4 +328,4 @@ enum VoiceCallStatus { connected, // Connection manager only. incoming -} \ No newline at end of file +} From c3b273a5add1f208a50c062f69906f45fc680156 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 20:48:09 +0800 Subject: [PATCH 048/199] fix: android compile --- src/flutter_ffi.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 84407cd96..2e6c450c1 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1285,6 +1285,7 @@ pub fn main_hide_docker() -> SyncReturn { } pub fn cm_start_listen_ipc_thread() { + #[cfg(not(any(target_os = "android", target_os = "ios")))] crate::flutter::connection_manager::start_listen_ipc_thread(); } From e944b776bc6ce9afe31ac3ce1b7e6f4520cd8f18 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 20:59:13 +0800 Subject: [PATCH 049/199] opt: remove unnecessary config field --- libs/hbb_common/src/config.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 6032ae9c7..71dd9a5c6 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -212,11 +212,6 @@ pub struct PeerConfig { deserialize_with = "PeerConfig::deserialize_image_quality" )] pub image_quality: String, - #[serde( - default = "PeerConfig::default_audio_mode", - deserialize_with = "PeerConfig::deserialize_audio_mode" - )] - pub audio_mode: String, #[serde( default = "PeerConfig::default_custom_image_quality", deserialize_with = "PeerConfig::deserialize_custom_image_quality" @@ -1001,11 +996,6 @@ impl PeerConfig { deserialize_image_quality, UserDefaultConfig::load().get("image_quality") ); - serde_field_string!( - default_audio_mode, - deserialize_audio_mode, - "guest-to-host".to_owned() - ); fn default_custom_image_quality() -> Vec { let f: f64 = UserDefaultConfig::load() From cf121bdf47550df9725b604e340e1fa4491cafd4 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 2 Feb 2023 22:27:11 +0800 Subject: [PATCH 050/199] win, translate mode, not debug yet Signed-off-by: fufesou --- Cargo.lock | 26 ++++++++- Cargo.toml | 2 +- src/keyboard.rs | 105 ++++++++++++++++++++++++++++-------- src/server/input_service.rs | 17 +++++- src/ui_session_interface.rs | 21 ++++++++ 5 files changed, 144 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e15641363..4ac2720be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1556,7 +1556,7 @@ dependencies = [ "log", "objc", "pkg-config", - "rdev", + "rdev 0.5.0-2 (git+https://github.com/fufesou/rdev)", "serde 1.0.149", "serde_derive", "tfc", @@ -4401,6 +4401,28 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "rdev" +version = "0.5.0-2" +dependencies = [ + "cocoa", + "core-foundation 0.9.3", + "core-foundation-sys 0.8.3", + "core-graphics 0.22.3", + "enum-map", + "epoll", + "inotify", + "lazy_static", + "libc", + "log", + "mio 0.8.5", + "strum 0.24.1", + "strum_macros 0.24.3", + "widestring 1.0.2", + "winapi 0.3.9", + "x11 2.20.1", +] + [[package]] name = "rdev" version = "0.5.0-2" @@ -4709,7 +4731,7 @@ dependencies = [ "objc", "objc_id", "parity-tokio-ipc", - "rdev", + "rdev 0.5.0-2", "repng", "reqwest", "rpassword 7.2.0", diff --git a/Cargo.toml b/Cargo.toml index 936b9e349..5d75b7a23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,7 +63,7 @@ default-net = { git = "https://github.com/Kingtous/default-net" } wol-rs = "0.9.1" flutter_rust_bridge = { version = "1.61.1", optional = true } errno = "0.2.8" -rdev = { git = "https://github.com/fufesou/rdev" } +rdev = { path = "../rdev" } url = { version = "2.1", features = ["serde"] } reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false } diff --git a/src/keyboard.rs b/src/keyboard.rs index 054a39580..bcb0650ac 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -92,7 +92,8 @@ pub mod client { if is_long_press(&event) { return; } - if let Some(key_event) = event_to_key_event(&event, lock_modes) { + + for key_event in event_to_key_events(&event, lock_modes) { send_key_event(&key_event); } } @@ -341,7 +342,7 @@ fn update_modifiers_state(event: &Event) { }; } -pub fn event_to_key_event(event: &Event, lock_modes: Option) -> Option { +pub fn event_to_key_events(event: &Event, lock_modes: Option) -> Vec { let mut key_event = KeyEvent::new(); update_modifiers_state(event); @@ -357,28 +358,38 @@ pub fn event_to_key_event(event: &Event, lock_modes: Option) -> Option map_keyboard_mode(event, key_event)?, - KeyboardMode::Translate => translate_keyboard_mode(event, key_event)?, + let mut key_events = match keyboard_mode { + KeyboardMode::Map => match map_keyboard_mode(event, key_event) { + Some(event) => [event].to_vec(), + None => Vec::new(), + }, + KeyboardMode::Translate => translate_keyboard_mode(event, key_event), _ => { #[cfg(not(any(target_os = "android", target_os = "ios")))] { - legacy_keyboard_mode(event, key_event)? + legacy_keyboard_mode(event, key_event) } #[cfg(any(target_os = "android", target_os = "ios"))] { - None? + Vec::new() } } }; - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if let Some(lock_modes) = lock_modes { - add_numlock_capslock_with_lock_modes(&mut key_event, lock_modes); - } else { - add_numlock_capslock_status(&mut key_event); + + if keyboard_mode != KeyboardMode::Translate { + for key_event in &mut key_events { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if let Some(lock_modes) = lock_modes { + add_numlock_capslock_with_lock_modes(key_event, lock_modes); + } else { + add_numlock_capslock_status(key_event); + } + } } - return Some(key_event); + println!("REMOVE ME ========================= key_events {:?}", &key_events); + + key_events } pub fn event_type_to_event(event_type: EventType) -> Event { @@ -386,6 +397,7 @@ pub fn event_type_to_event(event_type: EventType) -> Event { event_type, time: SystemTime::now(), name: None, + unicode: Vec::new(), code: 0, scan_code: 0, } @@ -423,13 +435,14 @@ pub fn get_peer_platform() -> String { } #[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option { +pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Vec { + let mut events = Vec::new(); // legacy mode(0): Generate characters locally, look for keycode on other side. let (mut key, down_or_up) = match event.event_type { EventType::KeyPress(key) => (key, true), EventType::KeyRelease(key) => (key, false), _ => { - return None; + return events; } }; @@ -475,7 +488,7 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option Option { if is_win && ctrl && alt { client::ctrl_alt_del(); - return None; + return events; } Some(ControlKey::Delete) } @@ -545,7 +558,7 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option Some(ControlKey::Subtract), Key::KpPlus => Some(ControlKey::Add), Key::CapsLock | Key::NumLock | Key::ScrollLock => { - return None; + return events; } Key::Home => Some(ControlKey::Home), Key::End => Some(ControlKey::End), @@ -628,12 +641,12 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option Option Option { @@ -703,6 +717,51 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option Option { - None +#[cfg(target_os = "windows")] +fn is_modifier_code(scan_code: u32) -> bool { + match scan_code { + // Alt | AltGr | ControlLeft | ControlRight | ShiftLeft | ShiftRight | MetaLeft | MetaRight + 0x38 | 0xE038 | 0x1D | 0xE01D | 0x2A | 0x36 | 0xE05B | 0xE05C => true, + _ => false, + } +} + +#[cfg(target_os = "linux")] +fn is_modifier_code(key_code: u32) -> bool { + match scan_code { + 64 | 108 | 37 | 105 | 50 | 62 | 133 | 134 => true, + _ => false, + } +} + +#[cfg(target_os = "macos")] +fn is_modifier_code(key_code: u32) -> bool { + match scan_code { + 0x3A | 0x3D | 0x3B | 0x3E | 0x38 | 0x3C | 0x37 | 0x36 => true, + _ => false, + } +} + +pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec { + #[cfg(target_os = "windows")] + let is_modifier = is_modifier_code(event.scan_code); + #[cfg(target_os = "linux")] + let is_modifier = is_modifier_code(event.key_code); + #[cfg(target_os = "macos")] + let is_modifier = is_modifier_code(event.key_code); + + let mut events: Vec = Vec::new(); + if is_modifier { + if let Some(evt) = map_keyboard_mode(event, key_event) { + events.push(evt); + } + return events; + } + + for unicode in &event.unicode { + let mut evt = key_event.clone(); + evt.set_unicode(*unicode as _); + events.push(evt); + } + events } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 2715a2643..072ef53fb 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -1067,6 +1067,21 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { release_keys(&mut en, &to_release); } +fn translate_keyboard_mode(evt: &KeyEvent) { + match evt.union { + Some(key_event::Union::Unicode(unicode)) => { + println!("REMOVE ME ========================= simulate_unicode {}", unicode); + allow_err!(rdev::simulate_unicode(unicode as _)); + }, + Some(key_event::Union::Chr(..)) => { + map_keyboard_mode(evt) + } + _ => { + log::debug!("Unreachable. Unexpected key event {:?}", &evt); + } + } +} + pub fn handle_key_(evt: &KeyEvent) { if EXITING.load(Ordering::SeqCst) { return; @@ -1080,7 +1095,7 @@ pub fn handle_key_(evt: &KeyEvent) { map_keyboard_mode(evt); } KeyboardMode::Translate => { - legacy_keyboard_mode(evt); + translate_keyboard_mode(evt); } _ => { legacy_keyboard_mode(evt); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 4fc5db743..95b8cdbd0 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -361,11 +361,31 @@ impl Session { } pub fn enter(&self) { + #[cfg(target_os = "windows")] + { + match &self.lc.read().unwrap().keyboard_mode as _ { + "legacy" => { + println!("REMOVE ME =========================== enter legacy "); + rdev::set_get_key_name(true); + } + "translate" => { + println!("REMOVE ME =========================== enter translate "); + rdev::set_get_key_name(true); + } + _ => {} + } + } + IS_IN.store(true, Ordering::SeqCst); keyboard::client::change_grab_status(GrabState::Run); } pub fn leave(&self) { + #[cfg(target_os = "windows")] + { + println!("REMOVE ME =========================== leave "); + rdev::set_get_key_name(false); + } IS_IN.store(false, Ordering::SeqCst); keyboard::client::change_grab_status(GrabState::Wait); } @@ -429,6 +449,7 @@ impl Session { let event = Event { time: std::time::SystemTime::now(), name: Option::Some(name.to_owned()), + unicode: Vec::new(), code: keycode as _, scan_code: scancode as _, event_type: event_type, From 6e54cd2e6b7a7e207b13e31f2668db4df98f13ee Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 3 Feb 2023 10:41:47 +0800 Subject: [PATCH 051/199] win, translate mode, check dead code Signed-off-by: fufesou --- .../lib/desktop/widgets/remote_menubar.dart | 29 +++++---- src/common.rs | 4 +- src/keyboard.rs | 63 ++++++------------- src/ui_session_interface.rs | 5 +- 4 files changed, 41 insertions(+), 60 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 36b9504c0..4fd702ad8 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1382,25 +1382,23 @@ class _RemoteMenubarState extends State { text: translate('Ratio'), optionsGetter: () { List list = []; - List modes = ["legacy"]; + List modes = [ + KeyboardModeMenu(key: 'legacy', menu: 'Legacy mode'), + KeyboardModeMenu(key: 'map', menu: 'Map mode'), + KeyboardModeMenu(key: 'translate', menu: 'Translate mode'), + ]; - if (bind.sessionIsKeyboardModeSupported(id: widget.id, mode: "map")) { - modes.add("map"); - } - - for (String mode in modes) { - if (mode == "legacy") { + for (KeyboardModeMenu mode in modes) { + if (bind.sessionIsKeyboardModeSupported( + id: widget.id, mode: mode.key)) { list.add(MenuEntryRadioOption( - text: translate('Legacy mode'), value: 'legacy')); - } else if (mode == "map") { - list.add(MenuEntryRadioOption( - text: translate('Map mode'), value: 'map')); + text: translate(mode.menu), value: mode.key)); } } return list; }, curOptionGetter: () async { - return await bind.sessionGetKeyboardMode(id: widget.id) ?? "legacy"; + return await bind.sessionGetKeyboardMode(id: widget.id) ?? 'legacy'; }, optionSetter: (String oldValue, String newValue) async { await bind.sessionSetKeyboardMode(id: widget.id, value: newValue); @@ -1689,3 +1687,10 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { ); } } + +class KeyboardModeMenu { + final String key; + final String menu; + + KeyboardModeMenu({required this.key, required this.menu}); +} diff --git a/src/common.rs b/src/common.rs index c2d5a81f0..8f8ce8dec 100644 --- a/src/common.rs +++ b/src/common.rs @@ -671,8 +671,8 @@ pub fn is_keyboard_mode_supported(keyboard_mode: &KeyboardMode, version_number: match keyboard_mode { KeyboardMode::Legacy => true, KeyboardMode::Map => version_number >= hbb_common::get_version_number("1.2.0"), - KeyboardMode::Translate => false, - KeyboardMode::Auto => false, + KeyboardMode::Translate => version_number >= hbb_common::get_version_number("1.2.0"), + KeyboardMode::Auto => version_number >= hbb_common::get_version_number("1.2.0"), } } diff --git a/src/keyboard.rs b/src/keyboard.rs index bcb0650ac..7d5f36af2 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -387,7 +387,10 @@ pub fn event_to_key_events(event: &Event, lock_modes: Option) -> Vec Event { Event { event_type, time: SystemTime::now(), - name: None, - unicode: Vec::new(), + unicode: None, code: 0, scan_code: 0, } @@ -571,7 +573,8 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Vec { if s.len() <= 2 { // exclude chinese characters @@ -717,51 +720,25 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option bool { - match scan_code { - // Alt | AltGr | ControlLeft | ControlRight | ShiftLeft | ShiftRight | MetaLeft | MetaRight - 0x38 | 0xE038 | 0x1D | 0xE01D | 0x2A | 0x36 | 0xE05B | 0xE05C => true, - _ => false, - } -} - -#[cfg(target_os = "linux")] -fn is_modifier_code(key_code: u32) -> bool { - match scan_code { - 64 | 108 | 37 | 105 | 50 | 62 | 133 | 134 => true, - _ => false, - } -} - -#[cfg(target_os = "macos")] -fn is_modifier_code(key_code: u32) -> bool { - match scan_code { - 0x3A | 0x3D | 0x3B | 0x3E | 0x38 | 0x3C | 0x37 | 0x36 => true, - _ => false, - } -} - pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec { - #[cfg(target_os = "windows")] - let is_modifier = is_modifier_code(event.scan_code); - #[cfg(target_os = "linux")] - let is_modifier = is_modifier_code(event.key_code); - #[cfg(target_os = "macos")] - let is_modifier = is_modifier_code(event.key_code); - let mut events: Vec = Vec::new(); - if is_modifier { + match &event.unicode { + Some(unicode_info) => { + if !unicode_info.is_dead { + for code in &unicode_info.unicode { + let mut evt = key_event.clone(); + evt.set_unicode(*code as _); + events.push(evt); + } + } + } + None => {} + } + if events.is_empty() { if let Some(evt) = map_keyboard_mode(event, key_event) { events.push(evt); } return events; } - - for unicode in &event.unicode { - let mut evt = key_event.clone(); - evt.set_unicode(*unicode as _); - events.push(evt); - } events } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 95b8cdbd0..3801eda67 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -423,7 +423,7 @@ impl Session { pub fn handle_flutter_key_event( &self, - name: &str, + _name: &str, keycode: i32, scancode: i32, lock_modes: i32, @@ -448,8 +448,7 @@ impl Session { }; let event = Event { time: std::time::SystemTime::now(), - name: Option::Some(name.to_owned()), - unicode: Vec::new(), + unicode: None, code: keycode as _, scan_code: scancode as _, event_type: event_type, From 6eec0041bd7038a062a250b7f6dbd8bb3b4569d1 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 6 Feb 2023 16:26:27 +0800 Subject: [PATCH 052/199] win, tranlsate mode, handle shift Signed-off-by: fufesou --- src/keyboard.rs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index 7d5f36af2..08ab23b1b 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -88,14 +88,17 @@ pub mod client { } } - pub fn process_event(event: &Event, lock_modes: Option) { + pub fn process_event(event: &Event, lock_modes: Option) -> KeyboardMode { + let keyboard_mode = get_keyboard_mode_enum(); + if is_long_press(&event) { - return; + return keyboard_mode; } - for key_event in event_to_key_events(&event, lock_modes) { + for key_event in event_to_key_events(&event, keyboard_mode, lock_modes) { send_key_event(&key_event); } + keyboard_mode } pub fn get_modifiers_state( @@ -205,7 +208,14 @@ pub fn start_grab_loop() { return Some(event); } if KEYBOARD_HOOKED.load(Ordering::SeqCst) { - client::process_event(&event, None); + let keyboard_mode = client::process_event(&event, None); + if keyboard_mode == KeyboardMode::Translate { + // shift + if event.scan_code == 0x2A { + return Some(event); + } + } + if is_press { return None; } else { @@ -342,7 +352,7 @@ fn update_modifiers_state(event: &Event) { }; } -pub fn event_to_key_events(event: &Event, lock_modes: Option) -> Vec { +pub fn event_to_key_events(event: &Event, keyboard_mode: KeyboardMode, lock_modes: Option) -> Vec { let mut key_event = KeyEvent::new(); update_modifiers_state(event); @@ -356,7 +366,6 @@ pub fn event_to_key_events(event: &Event, lock_modes: Option) -> Vec {} } - let keyboard_mode = get_keyboard_mode_enum(); key_event.mode = keyboard_mode.into(); let mut key_events = match keyboard_mode { KeyboardMode::Map => match map_keyboard_mode(event, key_event) { From ddc9792d15420a2a3e56cc6b37cc89a064c5013b Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 6 Feb 2023 18:13:17 +0800 Subject: [PATCH 053/199] win, translate mode, debug almost done Signed-off-by: fufesou --- Cargo.lock | 28 ++----------------- Cargo.toml | 2 +- .../lib/desktop/widgets/remote_menubar.dart | 6 ++++ src/keyboard.rs | 9 ++---- src/server/input_service.rs | 1 - src/ui_session_interface.rs | 11 ++------ 6 files changed, 14 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4ac2720be..988363019 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1556,7 +1556,7 @@ dependencies = [ "log", "objc", "pkg-config", - "rdev 0.5.0-2 (git+https://github.com/fufesou/rdev)", + "rdev", "serde 1.0.149", "serde_derive", "tfc", @@ -4404,29 +4404,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -dependencies = [ - "cocoa", - "core-foundation 0.9.3", - "core-foundation-sys 0.8.3", - "core-graphics 0.22.3", - "enum-map", - "epoll", - "inotify", - "lazy_static", - "libc", - "log", - "mio 0.8.5", - "strum 0.24.1", - "strum_macros 0.24.3", - "widestring 1.0.2", - "winapi 0.3.9", - "x11 2.20.1", -] - -[[package]] -name = "rdev" -version = "0.5.0-2" -source = "git+https://github.com/fufesou/rdev#238c9778da40056e2efda1e4264355bc89fb6358" +source = "git+https://github.com/fufesou/rdev#77b45e9e43f713851874c7fbb8e7149ab4f2e6a1" dependencies = [ "cocoa", "core-foundation 0.9.3", @@ -4731,7 +4709,7 @@ dependencies = [ "objc", "objc_id", "parity-tokio-ipc", - "rdev 0.5.0-2", + "rdev", "repng", "reqwest", "rpassword 7.2.0", diff --git a/Cargo.toml b/Cargo.toml index 5d75b7a23..936b9e349 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,7 +63,7 @@ default-net = { git = "https://github.com/Kingtous/default-net" } wol-rs = "0.9.1" flutter_rust_bridge = { version = "1.61.1", optional = true } errno = "0.2.8" -rdev = { path = "../rdev" } +rdev = { git = "https://github.com/fufesou/rdev" } url = { version = "2.1", features = ["serde"] } reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false } diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 4fd702ad8..c3c8ce3fe 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1391,6 +1391,12 @@ class _RemoteMenubarState extends State { for (KeyboardModeMenu mode in modes) { if (bind.sessionIsKeyboardModeSupported( id: widget.id, mode: mode.key)) { + if (mode.key == 'translate') { + if (!Platform.isWindows || + widget.ffi.ffiModel.pi.platform != kPeerPlatformWindows) { + continue; + } + } list.add(MenuEntryRadioOption( text: translate(mode.menu), value: mode.key)); } diff --git a/src/keyboard.rs b/src/keyboard.rs index 08ab23b1b..fd9514427 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -210,8 +210,8 @@ pub fn start_grab_loop() { if KEYBOARD_HOOKED.load(Ordering::SeqCst) { let keyboard_mode = client::process_event(&event, None); if keyboard_mode == KeyboardMode::Translate { - // shift - if event.scan_code == 0x2A { + // SHIFT(0x2A) RSHIFT(0x36) + if event.scan_code == 0x2A || event.scan_code == 0x36 { return Some(event); } } @@ -396,11 +396,6 @@ pub fn event_to_key_events(event: &Event, keyboard_mode: KeyboardMode, lock_mode } } - println!( - "REMOVE ME ========================= key_events {:?}", - &key_events - ); - key_events } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 072ef53fb..1d7d4773d 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -1070,7 +1070,6 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { fn translate_keyboard_mode(evt: &KeyEvent) { match evt.union { Some(key_event::Union::Unicode(unicode)) => { - println!("REMOVE ME ========================= simulate_unicode {}", unicode); allow_err!(rdev::simulate_unicode(unicode as _)); }, Some(key_event::Union::Chr(..)) => { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 3801eda67..12412d7cd 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -364,14 +364,8 @@ impl Session { #[cfg(target_os = "windows")] { match &self.lc.read().unwrap().keyboard_mode as _ { - "legacy" => { - println!("REMOVE ME =========================== enter legacy "); - rdev::set_get_key_name(true); - } - "translate" => { - println!("REMOVE ME =========================== enter translate "); - rdev::set_get_key_name(true); - } + "legacy" => rdev::set_get_key_name(true), + "translate" => rdev::set_get_key_name(true), _ => {} } } @@ -383,7 +377,6 @@ impl Session { pub fn leave(&self) { #[cfg(target_os = "windows")] { - println!("REMOVE ME =========================== leave "); rdev::set_get_key_name(false); } IS_IN.store(false, Ordering::SeqCst); From 347add18744358b9d07247bee458f2a4ca17c0f8 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 8 Feb 2023 09:48:04 +0800 Subject: [PATCH 054/199] win, translate mode, to debug Signed-off-by: fufesou --- Cargo.lock | 26 +++++++++- Cargo.toml | 2 +- src/keyboard.rs | 100 ++++++++++++++++++++++++++++++++---- src/server/input_service.rs | 13 ++++- 4 files changed, 128 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 988363019..f5ffa7f9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1556,7 +1556,7 @@ dependencies = [ "log", "objc", "pkg-config", - "rdev", + "rdev 0.5.0-2 (git+https://github.com/fufesou/rdev)", "serde 1.0.149", "serde_derive", "tfc", @@ -4401,6 +4401,28 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "rdev" +version = "0.5.0-2" +dependencies = [ + "cocoa", + "core-foundation 0.9.3", + "core-foundation-sys 0.8.3", + "core-graphics 0.22.3", + "enum-map", + "epoll", + "inotify", + "lazy_static", + "libc", + "log", + "mio 0.8.5", + "strum 0.24.1", + "strum_macros 0.24.3", + "widestring 1.0.2", + "winapi 0.3.9", + "x11 2.20.1", +] + [[package]] name = "rdev" version = "0.5.0-2" @@ -4709,7 +4731,7 @@ dependencies = [ "objc", "objc_id", "parity-tokio-ipc", - "rdev", + "rdev 0.5.0-2", "repng", "reqwest", "rpassword 7.2.0", diff --git a/Cargo.toml b/Cargo.toml index 936b9e349..5d75b7a23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,7 +63,7 @@ default-net = { git = "https://github.com/Kingtous/default-net" } wol-rs = "0.9.1" flutter_rust_bridge = { version = "1.61.1", optional = true } errno = "0.2.8" -rdev = { git = "https://github.com/fufesou/rdev" } +rdev = { path = "../rdev" } url = { version = "2.1", features = ["serde"] } reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false } diff --git a/src/keyboard.rs b/src/keyboard.rs index fd9514427..492314ab6 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -199,6 +199,9 @@ pub fn update_grab_get_key_name() { }; } +#[cfg(target_os = "windows")] +static mut IS_LAST_0X021D: bool = false; + pub fn start_grab_loop() { #[cfg(any(target_os = "windows", target_os = "macos"))] std::thread::spawn(move || { @@ -210,12 +213,22 @@ pub fn start_grab_loop() { if KEYBOARD_HOOKED.load(Ordering::SeqCst) { let keyboard_mode = client::process_event(&event, None); if keyboard_mode == KeyboardMode::Translate { - // SHIFT(0x2A) RSHIFT(0x36) - if event.scan_code == 0x2A || event.scan_code == 0x36 { - return Some(event); + #[cfg(target_os = "windows")] + match event.scan_code { + 0x2A => rdev::set_modifier(Key::ShiftLeft, is_press), + 0x36 => rdev::set_modifier(Key::ShiftRight, is_press), + 0x38 => rdev::set_modifier(Key::Alt, is_press), + 0xE038 => rdev::set_modifier(Key::AltGr, is_press), + 0xE05B => rdev::set_modifier(Key::MetaLeft, is_press), + 0xE05C => rdev::set_modifier(Key::MetaRight, is_press), + _ => {} + } + #[cfg(target_os = "windows")] + unsafe { + IS_LAST_0X021D = event.scan_code == 0x021D; } } - + if is_press { return None; } else { @@ -352,7 +365,11 @@ fn update_modifiers_state(event: &Event) { }; } -pub fn event_to_key_events(event: &Event, keyboard_mode: KeyboardMode, lock_modes: Option) -> Vec { +pub fn event_to_key_events( + event: &Event, + keyboard_mode: KeyboardMode, + lock_modes: Option, +) -> Vec { let mut key_event = KeyEvent::new(); update_modifiers_state(event); @@ -577,7 +594,10 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Vec { if s.len() <= 2 { @@ -724,8 +744,7 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option Vec { - let mut events: Vec = Vec::new(); +fn try_fill_unicode(event: &Event, key_event: &KeyEvent, events: &mut Vec) { match &event.unicode { Some(unicode_info) => { if !unicode_info.is_dead { @@ -738,8 +757,71 @@ pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec {} } +} + +#[cfg(target_os = "windows")] +fn is_hot_key_modifiers_down() -> bool { + if rdev::get_modifier(Key::ControlLeft) || rdev::get_modifier(Key::ControlRight) { + return true; + } + if rdev::get_modifier(Key::Alt) || rdev::get_modifier(Key::AltGr) { + return true; + } + if rdev::get_modifier(Key::MetaLeft) || rdev::get_modifier(Key::MetaRight) { + return true; + } + return false; +} + +pub fn translate_virtual_keycode(event: &Event, mut key_event: KeyEvent) -> Option { + match event.event_type { + EventType::KeyPress(..) => { + key_event.down = true; + } + EventType::KeyRelease(..) => { + key_event.down = false; + } + _ => return None, + }; + + let mut peer = get_peer_platform().to_lowercase(); + peer.retain(|c| !c.is_whitespace()); + + // #[cfg(target_os = "windows")] + // let keycode = match peer.as_str() { + // "windows" => event.code, + // "macos" => { + // if hbb_common::config::LocalConfig::get_kb_layout_type() == "ISO" { + // rdev::win_scancode_to_macos_iso_code(event.scan_code)? + // } else { + // rdev::win_scancode_to_macos_code(event.scan_code)? + // } + // } + // _ => rdev::win_scancode_to_linux_code(event.scan_code)?, + // }; + + key_event.set_chr(event.code as _); + Some(key_event) +} + +pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec { + let mut events: Vec = Vec::new(); + #[cfg(target_os = "windows")] + unsafe { + if IS_LAST_0X021D { + if event.scan_code == 0xE038 { + return events; + } + } + } + + #[cfg(target_os = "windows")] + if !is_hot_key_modifiers_down() { + try_fill_unicode(event, &key_event, &mut events); + } + if events.is_empty() { - if let Some(evt) = map_keyboard_mode(event, key_event) { + if let Some(evt) = translate_virtual_keycode(event, key_event) { events.push(evt); } return events; diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 1d7d4773d..133b9a830 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -1067,13 +1067,24 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { release_keys(&mut en, &to_release); } +#[cfg(target_os = "windows")] +fn translate_process_virtual_keycode(vk: u32, down: bool) { + let scancode = rdev::vk_to_scancode(vk); + // map mode(1): Send keycode according to the peer platform. + record_pressed_key(scancode as u64 + KEY_CHAR_START, down); + + crate::platform::windows::try_change_desktop(); + sim_rdev_rawkey(scancode, down); +} + fn translate_keyboard_mode(evt: &KeyEvent) { match evt.union { Some(key_event::Union::Unicode(unicode)) => { allow_err!(rdev::simulate_unicode(unicode as _)); }, Some(key_event::Union::Chr(..)) => { - map_keyboard_mode(evt) + #[cfg(target_os = "windows")] + translate_process_virtual_keycode(evt.chr(), evt.down) } _ => { log::debug!("Unreachable. Unexpected key event {:?}", &evt); From 1294103ba778016a118ba51cbf9a3d61f1db5212 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 8 Feb 2023 13:31:49 +0800 Subject: [PATCH 055/199] win, translate mode, debug Signed-off-by: fufesou --- src/keyboard.rs | 62 +++++++++++++++++++------------- src/server/input_service.rs | 71 ++++++++++++++++++++++--------------- 2 files changed, 80 insertions(+), 53 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index 492314ab6..bdf1c5c1b 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -200,7 +200,7 @@ pub fn update_grab_get_key_name() { } #[cfg(target_os = "windows")] -static mut IS_LAST_0X021D: bool = false; +static mut IS_0X021D_DOWN: bool = false; pub fn start_grab_loop() { #[cfg(any(target_os = "windows", target_os = "macos"))] @@ -210,33 +210,43 @@ pub fn start_grab_loop() { if key == Key::CapsLock || key == Key::NumLock { return Some(event); } - if KEYBOARD_HOOKED.load(Ordering::SeqCst) { - let keyboard_mode = client::process_event(&event, None); - if keyboard_mode == KeyboardMode::Translate { - #[cfg(target_os = "windows")] - match event.scan_code { - 0x2A => rdev::set_modifier(Key::ShiftLeft, is_press), - 0x36 => rdev::set_modifier(Key::ShiftRight, is_press), - 0x38 => rdev::set_modifier(Key::Alt, is_press), - 0xE038 => rdev::set_modifier(Key::AltGr, is_press), - 0xE05B => rdev::set_modifier(Key::MetaLeft, is_press), - 0xE05C => rdev::set_modifier(Key::MetaRight, is_press), - _ => {} - } - #[cfg(target_os = "windows")] - unsafe { - IS_LAST_0X021D = event.scan_code == 0x021D; - } - } + let mut _keyboard_mode = KeyboardMode::Map; + let scan_code = event.scan_code; + let res = if KEYBOARD_HOOKED.load(Ordering::SeqCst) { + _keyboard_mode = client::process_event(&event, None); if is_press { - return None; + None } else { - return Some(event); + Some(event) } } else { - return Some(event); + Some(event) + }; + + #[cfg(target_os = "windows")] + match scan_code { + 0x1D | 0x021D => rdev::set_modifier(Key::ControlLeft, is_press), + 0xE01D => rdev::set_modifier(Key::ControlRight, is_press), + 0x2A => rdev::set_modifier(Key::ShiftLeft, is_press), + 0x36 => rdev::set_modifier(Key::ShiftRight, is_press), + 0x38 => rdev::set_modifier(Key::Alt, is_press), + // Right Alt + 0xE038 => rdev::set_modifier(Key::AltGr, is_press), + 0xE05B => rdev::set_modifier(Key::MetaLeft, is_press), + 0xE05C => rdev::set_modifier(Key::MetaRight, is_press), + _ => {} } + + #[cfg(target_os = "windows")] + unsafe { + // AltGr + if scan_code == 0x021D { + IS_0X021D_DOWN = is_press; + } + } + + return res; }; let func = move |event: Event| match event.event_type { EventType::KeyPress(key) => try_handle_keyboard(event, key, true), @@ -808,7 +818,11 @@ pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec = Vec::new(); #[cfg(target_os = "windows")] unsafe { - if IS_LAST_0X021D { + if event.scan_code == 0x021D { + return events; + } + + if IS_0X021D_DOWN { if event.scan_code == 0xE038 { return events; } @@ -816,7 +830,7 @@ pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec ResultType<()> Ok(()) } +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +enum KeysDown { + RdevKey(RawKey), + EnigoKey(u64) +} + lazy_static::lazy_static! { static ref ENIGO: Arc> = { Arc::new(Mutex::new(Enigo::new())) }; - static ref KEYS_DOWN: Arc>> = Default::default(); + static ref KEYS_DOWN: Arc>> = Default::default(); static ref LATEST_PEER_INPUT_CURSOR: Arc> = Default::default(); static ref LATEST_SYS_CURSOR_POS: Arc> = Arc::new(Mutex::new((Instant::now().sub(MOUSE_MOVE_PROTECTION_TIMEOUT), (0, 0)))); } @@ -375,12 +380,7 @@ fn record_key_is_control_key(record_key: u64) -> bool { #[inline] fn record_key_is_chr(record_key: u64) -> bool { - KEY_RDEV_START <= record_key && record_key < KEY_CHAR_START -} - -#[inline] -fn record_key_is_rdev_layout(record_key: u64) -> bool { - KEY_CHAR_START <= record_key + record_key < KEY_CHAR_START } #[inline] @@ -396,15 +396,18 @@ fn record_key_to_key(record_key: u64) -> Option { } #[inline] -fn release_record_key(record_key: u64) { +fn release_record_key(record_key: KeysDown) { let func = move || { - if record_key_is_rdev_layout(record_key) { - simulate_(&EventType::KeyRelease(RdevKey::Unknown( - (record_key - KEY_RDEV_START) as _, - ))); - } else if let Some(key) = record_key_to_key(record_key) { - ENIGO.lock().unwrap().key_up(key); - log::debug!("Fixed {:?} timeout", key); + match record_key { + KeysDown::RdevKey(raw_key) => { + simulate_(&EventType::KeyRelease(RdevKey::RawKey(raw_key))); + } + KeysDown::EnigoKey(key) => { + if let Some(key) = record_key_to_key(key) { + ENIGO.lock().unwrap().key_up(key); + log::debug!("Fixed {:?} timeout", key); + } + } } }; @@ -733,7 +736,7 @@ pub fn reset_input_ondisconn() { } } -fn sim_rdev_rawkey(code: u32, keydown: bool) { +fn sim_rdev_rawkey_position(code: u32, keydown: bool) { #[cfg(target_os = "windows")] let rawkey = RawKey::ScanCode(code); #[cfg(target_os = "linux")] @@ -744,6 +747,23 @@ fn sim_rdev_rawkey(code: u32, keydown: bool) { #[cfg(target_os = "macos")] let rawkey = RawKey::MacVirtualKeycode(code); + // map mode(1): Send keycode according to the peer platform. + record_pressed_key(KeysDown::RdevKey(rawkey), keydown); + + let event_type = if keydown { + EventType::KeyPress(RdevKey::RawKey(rawkey)) + } else { + EventType::KeyRelease(RdevKey::RawKey(rawkey)) + }; + simulate_(&event_type); +} + +fn sim_rdev_rawkey_virtual(code: u32, keydown: bool) { + #[cfg(target_os = "windows")] + let rawkey = RawKey::WinVirtualKeycode(code); + + record_pressed_key(KeysDown::RdevKey(rawkey), keydown); + let event_type = if keydown { EventType::KeyPress(RdevKey::RawKey(rawkey)) } else { @@ -874,9 +894,6 @@ fn sync_numlock_capslock_status(key_event: &KeyEvent) { } fn map_keyboard_mode(evt: &KeyEvent) { - // map mode(1): Send keycode according to the peer platform. - record_pressed_key(evt.chr() as u64 + KEY_CHAR_START, evt.down); - #[cfg(windows)] crate::platform::windows::try_change_desktop(); @@ -894,7 +911,7 @@ fn map_keyboard_mode(evt: &KeyEvent) { return; } - sim_rdev_rawkey(evt.chr(), evt.down); + sim_rdev_rawkey_position(evt.chr(), evt.down); } #[cfg(target_os = "macos")] @@ -1011,7 +1028,7 @@ fn release_keys(en: &mut Enigo, to_release: &Vec) { } } -fn record_pressed_key(record_key: u64, down: bool) { +fn record_pressed_key(record_key: KeysDown, down: bool) { let mut key_down = KEYS_DOWN.lock().unwrap(); if down { key_down.insert(record_key, Instant::now()); @@ -1050,12 +1067,12 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { return; } let record_key = ck.value() as u64; - record_pressed_key(record_key, down); + record_pressed_key(KeysDown::EnigoKey(record_key), down); process_control_key(&mut en, &ck, down) } Some(key_event::Union::Chr(chr)) => { let record_key = chr as u64 + KEY_CHAR_START; - record_pressed_key(record_key, down); + record_pressed_key(KeysDown::EnigoKey(record_key), down); process_chr(&mut en, chr, down) } Some(key_event::Union::Unicode(chr)) => process_unicode(&mut en, chr), @@ -1069,12 +1086,8 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { #[cfg(target_os = "windows")] fn translate_process_virtual_keycode(vk: u32, down: bool) { - let scancode = rdev::vk_to_scancode(vk); - // map mode(1): Send keycode according to the peer platform. - record_pressed_key(scancode as u64 + KEY_CHAR_START, down); - crate::platform::windows::try_change_desktop(); - sim_rdev_rawkey(scancode, down); + sim_rdev_rawkey_virtual(vk, down); } fn translate_keyboard_mode(evt: &KeyEvent) { From 5c7f2678fa870427975bdc98c29c7126bb35ba58 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 8 Feb 2023 14:23:05 +0800 Subject: [PATCH 056/199] update rdev Signed-off-by: fufesou --- Cargo.lock | 26 ++------------------------ Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f5ffa7f9d..988363019 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1556,7 +1556,7 @@ dependencies = [ "log", "objc", "pkg-config", - "rdev 0.5.0-2 (git+https://github.com/fufesou/rdev)", + "rdev", "serde 1.0.149", "serde_derive", "tfc", @@ -4401,28 +4401,6 @@ dependencies = [ "num_cpus", ] -[[package]] -name = "rdev" -version = "0.5.0-2" -dependencies = [ - "cocoa", - "core-foundation 0.9.3", - "core-foundation-sys 0.8.3", - "core-graphics 0.22.3", - "enum-map", - "epoll", - "inotify", - "lazy_static", - "libc", - "log", - "mio 0.8.5", - "strum 0.24.1", - "strum_macros 0.24.3", - "widestring 1.0.2", - "winapi 0.3.9", - "x11 2.20.1", -] - [[package]] name = "rdev" version = "0.5.0-2" @@ -4731,7 +4709,7 @@ dependencies = [ "objc", "objc_id", "parity-tokio-ipc", - "rdev 0.5.0-2", + "rdev", "repng", "reqwest", "rpassword 7.2.0", diff --git a/Cargo.toml b/Cargo.toml index 5d75b7a23..936b9e349 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,7 +63,7 @@ default-net = { git = "https://github.com/Kingtous/default-net" } wol-rs = "0.9.1" flutter_rust_bridge = { version = "1.61.1", optional = true } errno = "0.2.8" -rdev = { path = "../rdev" } +rdev = { git = "https://github.com/fufesou/rdev" } url = { version = "2.1", features = ["serde"] } reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false } From 01f762ffdb57d7dee4a17110d293252b1621c49a Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 8 Feb 2023 14:14:13 +0800 Subject: [PATCH 057/199] build linux Signed-off-by: fufesou --- src/keyboard.rs | 5 ++++- src/server/input_service.rs | 33 ++++++++++++++++----------------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index bdf1c5c1b..91480ba30 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -830,10 +830,13 @@ pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec ResultType<()> #[derive(Copy, Clone, PartialEq, Eq, Hash)] enum KeysDown { RdevKey(RawKey), - EnigoKey(u64) + EnigoKey(u64), } lazy_static::lazy_static! { @@ -397,16 +397,14 @@ fn record_key_to_key(record_key: u64) -> Option { #[inline] fn release_record_key(record_key: KeysDown) { - let func = move || { - match record_key { - KeysDown::RdevKey(raw_key) => { - simulate_(&EventType::KeyRelease(RdevKey::RawKey(raw_key))); - } - KeysDown::EnigoKey(key) => { - if let Some(key) = record_key_to_key(key) { - ENIGO.lock().unwrap().key_up(key); - log::debug!("Fixed {:?} timeout", key); - } + let func = move || match record_key { + KeysDown::RdevKey(raw_key) => { + simulate_(&EventType::KeyRelease(RdevKey::RawKey(raw_key))); + } + KeysDown::EnigoKey(key) => { + if let Some(key) = record_key_to_key(key) { + ENIGO.lock().unwrap().key_up(key); + log::debug!("Fixed {:?} timeout", key); } } }; @@ -758,12 +756,10 @@ fn sim_rdev_rawkey_position(code: u32, keydown: bool) { simulate_(&event_type); } +#[cfg(target_os = "windows")] fn sim_rdev_rawkey_virtual(code: u32, keydown: bool) { - #[cfg(target_os = "windows")] let rawkey = RawKey::WinVirtualKeycode(code); - record_pressed_key(KeysDown::RdevKey(rawkey), keydown); - let event_type = if keydown { EventType::KeyPress(RdevKey::RawKey(rawkey)) } else { @@ -941,10 +937,11 @@ fn release_unpressed_modifiers(en: &mut Enigo, key_event: &KeyEvent) { #[cfg(target_os = "linux")] fn is_altgr_pressed() -> bool { + let altgr_rawkey = RawKey::LinuxXorgKeycode(ControlKey::RAlt.value() as _); KEYS_DOWN .lock() .unwrap() - .get(&(ControlKey::RAlt.value() as _)) + .get(&KeysDown::RdevKey(altgr_rawkey)) .is_some() } @@ -1093,9 +1090,11 @@ fn translate_process_virtual_keycode(vk: u32, down: bool) { fn translate_keyboard_mode(evt: &KeyEvent) { match evt.union { Some(key_event::Union::Unicode(unicode)) => { + #[cfg(target_os = "windows")] allow_err!(rdev::simulate_unicode(unicode as _)); - }, - Some(key_event::Union::Chr(..)) => { + } + Some(key_event::Union::Chr(..)) => + { #[cfg(target_os = "windows")] translate_process_virtual_keycode(evt.chr(), evt.down) } From 586f0a272663222c6d013aca3129c4be0dfd0fae Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 8 Feb 2023 14:17:37 +0800 Subject: [PATCH 058/199] compile macos Signed-off-by: fufesou --- src/server/input_service.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 7b6130ad1..edf0ef497 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -1089,9 +1089,9 @@ fn translate_process_virtual_keycode(vk: u32, down: bool) { fn translate_keyboard_mode(evt: &KeyEvent) { match evt.union { - Some(key_event::Union::Unicode(unicode)) => { + Some(key_event::Union::Unicode(_unicode)) => { #[cfg(target_os = "windows")] - allow_err!(rdev::simulate_unicode(unicode as _)); + allow_err!(rdev::simulate_unicode(_unicode as _)); } Some(key_event::Union::Chr(..)) => { From d263d1892bf6fc1258c5b922b8cbd3b0922deebe Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 8 Feb 2023 14:34:52 +0800 Subject: [PATCH 059/199] update rdev Signed-off-by: fufesou --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 988363019..93b40ca3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4404,7 +4404,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/fufesou/rdev#77b45e9e43f713851874c7fbb8e7149ab4f2e6a1" +source = "git+https://github.com/fufesou/rdev#4d8231f05e14c5a04cd7d2c1288e87ad52d39e4c" dependencies = [ "cocoa", "core-foundation 0.9.3", From 948f9f28dbbd2846e8026f595021d7fb2a7c0b73 Mon Sep 17 00:00:00 2001 From: NicKoehler <53040044+NicKoehler@users.noreply.github.com> Date: Wed, 8 Feb 2023 10:15:08 +0100 Subject: [PATCH 060/199] Update it.rs --- src/lang/it.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 9730bbc2d..a4ea58304 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -446,8 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "Auto"), ("Other Default Options", "Altre Opzioni Predefinite"), - ("Voice call", ""), - ("Text chat", ""), - ("Stop voice call", ""), + ("Voice call", "Chiamata vocale"), + ("Text chat", "Chat testuale"), + ("Stop voice call", "Interrompi la chiamata vocale"), ].iter().cloned().collect(); } From 7c13be587638c61ffee6fe09a82c2e114ffd2531 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 8 Feb 2023 17:26:44 +0800 Subject: [PATCH 061/199] update issue template and clippy for hbb_common --- .github/ISSUE_TEMPLATE/bug_report.yaml | 15 ++++-- libs/hbb_common/build.rs | 4 +- libs/hbb_common/src/bytes_codec.rs | 40 ++++++++-------- libs/hbb_common/src/config.rs | 6 +-- libs/hbb_common/src/lib.rs | 60 +++++++++++------------- libs/hbb_common/src/password_security.rs | 48 +++++++++---------- libs/hbb_common/src/platform/linux.rs | 2 +- libs/hbb_common/src/protos/mod.rs | 2 +- libs/hbb_common/src/socket_client.rs | 22 ++++----- libs/hbb_common/src/tcp.rs | 2 +- 10 files changed, 103 insertions(+), 98 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 16509a3be..c2d92097c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -30,13 +30,22 @@ body: description: A clear and concise description of what you expected to happen validations: required: true + - type: input + id: os + attributes: + label: Operating system(s) on local side and remote side + description: What operating system(s) do you see this bug on? local side -> remote side. + placeholder: | + Windows 10 -> osx + validations: + required: true - type: input id: version attributes: - label: Operating System(s) and RustDesk Version(s) on local side and remote side - description: What Operatiing System(s) and version(s) of RustDesk do you see this bug on? local side / remote side. + label: RustDesk Version(s) on local side and remote side + description: What RustDesk version(s) do you see this bug on? local side -> remote side. placeholder: | - Windows 10, 1.1.9 / osx 13.1, 1.1.8 + 1.1.9 -> 1.1.8 validations: required: true - type: textarea diff --git a/libs/hbb_common/build.rs b/libs/hbb_common/build.rs index fe0d31076..5ebc3a287 100644 --- a/libs/hbb_common/build.rs +++ b/libs/hbb_common/build.rs @@ -2,11 +2,11 @@ fn main() { let out_dir = format!("{}/protos", std::env::var("OUT_DIR").unwrap()); std::fs::create_dir_all(&out_dir).unwrap(); - + protobuf_codegen::Codegen::new() .pure() .out_dir(out_dir) - .inputs(&["protos/rendezvous.proto", "protos/message.proto"]) + .inputs(["protos/rendezvous.proto", "protos/message.proto"]) .include("protos") .customize(protobuf_codegen::Customize::default().tokio_bytes(true)) .run() diff --git a/libs/hbb_common/src/bytes_codec.rs b/libs/hbb_common/src/bytes_codec.rs index 699aa9bff..bfc798715 100644 --- a/libs/hbb_common/src/bytes_codec.rs +++ b/libs/hbb_common/src/bytes_codec.rs @@ -143,32 +143,32 @@ mod tests { let mut buf = BytesMut::new(); let mut bytes: Vec = Vec::new(); bytes.resize(0x3F, 1); - assert!(!codec.encode(bytes.into(), &mut buf).is_err()); + assert!(codec.encode(bytes.into(), &mut buf).is_ok()); let buf_saved = buf.clone(); assert_eq!(buf.len(), 0x3F + 1); if let Ok(Some(res)) = codec.decode(&mut buf) { assert_eq!(res.len(), 0x3F); assert_eq!(res[0], 1); } else { - assert!(false); + panic!(); } let mut codec2 = BytesCodec::new(); let mut buf2 = BytesMut::new(); if let Ok(None) = codec2.decode(&mut buf2) { } else { - assert!(false); + panic!(); } buf2.extend(&buf_saved[0..1]); if let Ok(None) = codec2.decode(&mut buf2) { } else { - assert!(false); + panic!(); } buf2.extend(&buf_saved[1..]); if let Ok(Some(res)) = codec2.decode(&mut buf2) { assert_eq!(res.len(), 0x3F); assert_eq!(res[0], 1); } else { - assert!(false); + panic!(); } } @@ -177,21 +177,21 @@ mod tests { let mut codec = BytesCodec::new(); let mut buf = BytesMut::new(); let mut bytes: Vec = Vec::new(); - assert!(!codec.encode("".into(), &mut buf).is_err()); + assert!(codec.encode("".into(), &mut buf).is_ok()); assert_eq!(buf.len(), 1); bytes.resize(0x3F + 1, 2); - assert!(!codec.encode(bytes.into(), &mut buf).is_err()); + assert!(codec.encode(bytes.into(), &mut buf).is_ok()); assert_eq!(buf.len(), 0x3F + 2 + 2); if let Ok(Some(res)) = codec.decode(&mut buf) { assert_eq!(res.len(), 0); } else { - assert!(false); + panic!(); } if let Ok(Some(res)) = codec.decode(&mut buf) { assert_eq!(res.len(), 0x3F + 1); assert_eq!(res[0], 2); } else { - assert!(false); + panic!(); } } @@ -201,13 +201,13 @@ mod tests { let mut buf = BytesMut::new(); let mut bytes: Vec = Vec::new(); bytes.resize(0x3F - 1, 3); - assert!(!codec.encode(bytes.into(), &mut buf).is_err()); + assert!(codec.encode(bytes.into(), &mut buf).is_ok()); assert_eq!(buf.len(), 0x3F + 1 - 1); if let Ok(Some(res)) = codec.decode(&mut buf) { assert_eq!(res.len(), 0x3F - 1); assert_eq!(res[0], 3); } else { - assert!(false); + panic!(); } } #[test] @@ -216,13 +216,13 @@ mod tests { let mut buf = BytesMut::new(); let mut bytes: Vec = Vec::new(); bytes.resize(0x3FFF, 4); - assert!(!codec.encode(bytes.into(), &mut buf).is_err()); + assert!(codec.encode(bytes.into(), &mut buf).is_ok()); assert_eq!(buf.len(), 0x3FFF + 2); if let Ok(Some(res)) = codec.decode(&mut buf) { assert_eq!(res.len(), 0x3FFF); assert_eq!(res[0], 4); } else { - assert!(false); + panic!(); } } @@ -232,13 +232,13 @@ mod tests { let mut buf = BytesMut::new(); let mut bytes: Vec = Vec::new(); bytes.resize(0x3FFFFF, 5); - assert!(!codec.encode(bytes.into(), &mut buf).is_err()); + assert!(codec.encode(bytes.into(), &mut buf).is_ok()); assert_eq!(buf.len(), 0x3FFFFF + 3); if let Ok(Some(res)) = codec.decode(&mut buf) { assert_eq!(res.len(), 0x3FFFFF); assert_eq!(res[0], 5); } else { - assert!(false); + panic!(); } } @@ -248,33 +248,33 @@ mod tests { let mut buf = BytesMut::new(); let mut bytes: Vec = Vec::new(); bytes.resize(0x3FFFFF + 1, 6); - assert!(!codec.encode(bytes.into(), &mut buf).is_err()); + assert!(codec.encode(bytes.into(), &mut buf).is_ok()); let buf_saved = buf.clone(); assert_eq!(buf.len(), 0x3FFFFF + 4 + 1); if let Ok(Some(res)) = codec.decode(&mut buf) { assert_eq!(res.len(), 0x3FFFFF + 1); assert_eq!(res[0], 6); } else { - assert!(false); + panic!(); } let mut codec2 = BytesCodec::new(); let mut buf2 = BytesMut::new(); buf2.extend(&buf_saved[0..1]); if let Ok(None) = codec2.decode(&mut buf2) { } else { - assert!(false); + panic!(); } buf2.extend(&buf_saved[1..6]); if let Ok(None) = codec2.decode(&mut buf2) { } else { - assert!(false); + panic!(); } buf2.extend(&buf_saved[6..]); if let Ok(Some(res)) = codec2.decode(&mut buf2) { assert_eq!(res.len(), 0x3FFFFF + 1); assert_eq!(res[0], 6); } else { - assert!(false); + panic!(); } } } diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 71dd9a5c6..1e4d80c9f 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -288,7 +288,7 @@ fn patch(path: PathBuf) -> PathBuf { .trim() .to_owned(); if user != "root" { - return format!("/home/{}", user).into(); + return format!("/home/{user}").into(); } } } @@ -525,7 +525,7 @@ impl Config { let mut path: PathBuf = format!("/tmp/{}", *APP_NAME.read().unwrap()).into(); fs::create_dir(&path).ok(); fs::set_permissions(&path, fs::Permissions::from_mode(0o0777)).ok(); - path.push(format!("ipc{}", postfix)); + path.push(format!("ipc{postfix}")); path.to_str().unwrap_or("").to_owned() } } @@ -562,7 +562,7 @@ impl Config { .unwrap_or_default(); } if !rendezvous_server.contains(':') { - rendezvous_server = format!("{}:{}", rendezvous_server, RENDEZVOUS_PORT); + rendezvous_server = format!("{rendezvous_server}:{RENDEZVOUS_PORT}"); } rendezvous_server } diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs index c9f9e90d7..1c49adfb7 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -211,11 +211,7 @@ pub fn gen_version() { // generate build date let build_date = format!("{}", chrono::Local::now().format("%Y-%m-%d %H:%M")); file.write_all( - format!( - "#[allow(dead_code)]\npub const BUILD_DATE: &str = \"{}\";", - build_date - ) - .as_bytes(), + format!("#[allow(dead_code)]\npub const BUILD_DATE: &str = \"{build_date}\";\n").as_bytes(), ) .ok(); file.sync_all().ok(); @@ -342,39 +338,39 @@ mod test { #[test] fn test_ipv6() { - assert_eq!(is_ipv6_str("1:2:3"), true); - assert_eq!(is_ipv6_str("[ab:2:3]:12"), true); - assert_eq!(is_ipv6_str("[ABEF:2a:3]:12"), true); - assert_eq!(is_ipv6_str("[ABEG:2a:3]:12"), false); - assert_eq!(is_ipv6_str("1[ab:2:3]:12"), false); - assert_eq!(is_ipv6_str("1.1.1.1"), false); - assert_eq!(is_ip_str("1.1.1.1"), true); - assert_eq!(is_ipv6_str("1:2:"), false); - assert_eq!(is_ipv6_str("1:2::0"), true); - assert_eq!(is_ipv6_str("[1:2::0]:1"), true); - assert_eq!(is_ipv6_str("[1:2::0]:"), false); - assert_eq!(is_ipv6_str("1:2::0]:1"), false); + assert!(is_ipv6_str("1:2:3")); + assert!(is_ipv6_str("[ab:2:3]:12")); + assert!(is_ipv6_str("[ABEF:2a:3]:12")); + assert!(!is_ipv6_str("[ABEG:2a:3]:12")); + assert!(!is_ipv6_str("1[ab:2:3]:12")); + assert!(!is_ipv6_str("1.1.1.1")); + assert!(is_ip_str("1.1.1.1")); + assert!(!is_ipv6_str("1:2:")); + assert!(is_ipv6_str("1:2::0")); + assert!(is_ipv6_str("[1:2::0]:1")); + assert!(!is_ipv6_str("[1:2::0]:")); + assert!(!is_ipv6_str("1:2::0]:1")); } #[test] fn test_hostname_port() { - assert_eq!(is_domain_port_str("a:12"), false); - assert_eq!(is_domain_port_str("a.b.c:12"), false); - assert_eq!(is_domain_port_str("test.com:12"), true); - assert_eq!(is_domain_port_str("test-UPPER.com:12"), true); - assert_eq!(is_domain_port_str("some-other.domain.com:12"), true); - assert_eq!(is_domain_port_str("under_score:12"), false); - assert_eq!(is_domain_port_str("a@bc:12"), false); - assert_eq!(is_domain_port_str("1.1.1.1:12"), false); - assert_eq!(is_domain_port_str("1.2.3:12"), false); - assert_eq!(is_domain_port_str("1.2.3.45:12"), false); - assert_eq!(is_domain_port_str("a.b.c:123456"), false); - assert_eq!(is_domain_port_str("---:12"), false); - assert_eq!(is_domain_port_str(".:12"), false); + assert!(!is_domain_port_str("a:12")); + assert!(!is_domain_port_str("a.b.c:12")); + assert!(is_domain_port_str("test.com:12")); + assert!(is_domain_port_str("test-UPPER.com:12")); + assert!(is_domain_port_str("some-other.domain.com:12")); + assert!(!is_domain_port_str("under_score:12")); + assert!(!is_domain_port_str("a@bc:12")); + assert!(!is_domain_port_str("1.1.1.1:12")); + assert!(!is_domain_port_str("1.2.3:12")); + assert!(!is_domain_port_str("1.2.3.45:12")); + assert!(!is_domain_port_str("a.b.c:123456")); + assert!(!is_domain_port_str("---:12")); + assert!(!is_domain_port_str(".:12")); // todo: should we also check for these edge cases? // out-of-range port - assert_eq!(is_domain_port_str("test.com:0"), true); - assert_eq!(is_domain_port_str("test.com:98989"), true); + assert!(is_domain_port_str("test.com:0")); + assert!(is_domain_port_str("test.com:98989")); } #[test] diff --git a/libs/hbb_common/src/password_security.rs b/libs/hbb_common/src/password_security.rs index 0b66107fc..ddfe28baa 100644 --- a/libs/hbb_common/src/password_security.rs +++ b/libs/hbb_common/src/password_security.rs @@ -192,51 +192,51 @@ mod test { let data = "Hello World"; let encrypted = encrypt_str_or_original(data, version); let (decrypted, succ, store) = decrypt_str_or_original(&encrypted, version); - println!("data: {}", data); - println!("encrypted: {}", encrypted); - println!("decrypted: {}", decrypted); + println!("data: {data}"); + println!("encrypted: {encrypted}"); + println!("decrypted: {decrypted}"); assert_eq!(data, decrypted); assert_eq!(version, &encrypted[..2]); - assert_eq!(succ, true); - assert_eq!(store, false); + assert!(succ); + assert!(!store); let (_, _, store) = decrypt_str_or_original(&encrypted, "99"); - assert_eq!(store, true); - assert_eq!(decrypt_str_or_original(&decrypted, version).1, false); + assert!(store); + assert!(!decrypt_str_or_original(&decrypted, version).1); assert_eq!(encrypt_str_or_original(&encrypted, version), encrypted); println!("test vec"); let data: Vec = vec![1, 2, 3, 4, 5, 6]; let encrypted = encrypt_vec_or_original(&data, version); let (decrypted, succ, store) = decrypt_vec_or_original(&encrypted, version); - println!("data: {:?}", data); - println!("encrypted: {:?}", encrypted); - println!("decrypted: {:?}", decrypted); + println!("data: {data:?}"); + println!("encrypted: {encrypted:?}"); + println!("decrypted: {decrypted:?}"); assert_eq!(data, decrypted); assert_eq!(version.as_bytes(), &encrypted[..2]); - assert_eq!(store, false); - assert_eq!(succ, true); + assert!(!store); + assert!(succ); let (_, _, store) = decrypt_vec_or_original(&encrypted, "99"); - assert_eq!(store, true); - assert_eq!(decrypt_vec_or_original(&decrypted, version).1, false); + assert!(store); + assert!(!decrypt_vec_or_original(&decrypted, version).1); assert_eq!(encrypt_vec_or_original(&encrypted, version), encrypted); println!("test original"); let data = version.to_string() + "Hello World"; let (decrypted, succ, store) = decrypt_str_or_original(&data, version); assert_eq!(data, decrypted); - assert_eq!(store, true); - assert_eq!(succ, false); + assert!(store); + assert!(!succ); let verbytes = version.as_bytes(); - let data: Vec = vec![verbytes[0] as u8, verbytes[1] as u8, 1, 2, 3, 4, 5, 6]; + let data: Vec = vec![verbytes[0], verbytes[1], 1, 2, 3, 4, 5, 6]; let (decrypted, succ, store) = decrypt_vec_or_original(&data, version); assert_eq!(data, decrypted); - assert_eq!(store, true); - assert_eq!(succ, false); + assert!(store); + assert!(!succ); let (_, succ, store) = decrypt_str_or_original("", version); - assert_eq!(store, false); - assert_eq!(succ, false); - let (_, succ, store) = decrypt_vec_or_original(&vec![], version); - assert_eq!(store, false); - assert_eq!(succ, false); + assert!(!store); + assert!(!succ); + let (_, succ, store) = decrypt_vec_or_original(&[], version); + assert!(!store); + assert!(!succ); } } diff --git a/libs/hbb_common/src/platform/linux.rs b/libs/hbb_common/src/platform/linux.rs index 716025dc7..7c107d11c 100644 --- a/libs/hbb_common/src/platform/linux.rs +++ b/libs/hbb_common/src/platform/linux.rs @@ -60,7 +60,7 @@ fn get_display_server_of_session(session: &str) -> String { .replace("TTY=", "") .trim_end() .into(); - if let Ok(xorg_results) = run_cmds(format!("ps -e | grep \"{}.\\\\+Xorg\"", tty)) + if let Ok(xorg_results) = run_cmds(format!("ps -e | grep \"{tty}.\\\\+Xorg\"")) // And check if Xorg is running on that tty { if xorg_results.trim_end() != "" { diff --git a/libs/hbb_common/src/protos/mod.rs b/libs/hbb_common/src/protos/mod.rs index c001c58fb..57d9b68fe 100644 --- a/libs/hbb_common/src/protos/mod.rs +++ b/libs/hbb_common/src/protos/mod.rs @@ -1 +1 @@ -include!(concat!(env!("OUT_DIR"), "/protos/mod.rs")); \ No newline at end of file +include!(concat!(env!("OUT_DIR"), "/protos/mod.rs")); diff --git a/libs/hbb_common/src/socket_client.rs b/libs/hbb_common/src/socket_client.rs index a034b4e12..2d9b5a984 100644 --- a/libs/hbb_common/src/socket_client.rs +++ b/libs/hbb_common/src/socket_client.rs @@ -13,22 +13,22 @@ use tokio_socks::{IntoTargetAddr, TargetAddr}; pub fn check_port(host: T, port: i32) -> String { let host = host.to_string(); if crate::is_ipv6_str(&host) { - if host.starts_with("[") { + if host.starts_with('[') { return host; } - return format!("[{}]:{}", host, port); + return format!("[{host}]:{port}"); } - if !host.contains(":") { - return format!("{}:{}", host, port); + if !host.contains(':') { + return format!("{host}:{port}"); } - return host; + host } #[inline] pub fn increase_port(host: T, offset: i32) -> String { let host = host.to_string(); if crate::is_ipv6_str(&host) { - if host.starts_with("[") { + if host.starts_with('[') { let tmp: Vec<&str> = host.split("]:").collect(); if tmp.len() == 2 { let port: i32 = tmp[1].parse().unwrap_or(0); @@ -37,8 +37,8 @@ pub fn increase_port(host: T, offset: i32) -> String { } } } - } else if host.contains(":") { - let tmp: Vec<&str> = host.split(":").collect(); + } else if host.contains(':') { + let tmp: Vec<&str> = host.split(':').collect(); if tmp.len() == 2 { let port: i32 = tmp[1].parse().unwrap_or(0); if port > 0 { @@ -46,7 +46,7 @@ pub fn increase_port(host: T, offset: i32) -> String { } } } - return host; + host } pub fn test_if_valid_server(host: &str) -> String { @@ -148,7 +148,7 @@ pub async fn query_nip_io(addr: &SocketAddr) -> ResultType { pub fn ipv4_to_ipv6(addr: String, ipv4: bool) -> String { if !ipv4 && crate::is_ipv4_str(&addr) { if let Some(ip) = addr.split(':').next() { - return addr.replace(ip, &format!("{}.nip.io", ip)); + return addr.replace(ip, &format!("{ip}.nip.io")); } } addr @@ -163,7 +163,7 @@ async fn test_target(target: &str) -> ResultType { tokio::net::lookup_host(target) .await? .next() - .context(format!("Failed to look up host for {}", target)) + .context(format!("Failed to look up host for {target}")) } #[inline] diff --git a/libs/hbb_common/src/tcp.rs b/libs/hbb_common/src/tcp.rs index a7ac4eb3a..f574e8309 100644 --- a/libs/hbb_common/src/tcp.rs +++ b/libs/hbb_common/src/tcp.rs @@ -100,7 +100,7 @@ impl FramedStream { } } } - bail!(format!("Failed to connect to {}", remote_addr)); + bail!(format!("Failed to connect to {remote_addr}")); } pub async fn connect<'a, 't, P, T>( From 4134b77680126f408e5ce88f7eb4c3ad5711b749 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 8 Feb 2023 19:17:59 +0800 Subject: [PATCH 062/199] improve ffi enum data size, fix compile warning on mac --- src/client/io_loop.rs | 2 +- src/common.rs | 7 +---- src/core_main.rs | 4 +-- src/ipc.rs | 28 +++++++++++++++---- src/keyboard.rs | 6 ++-- src/server.rs | 62 +++++++++++++++++++++--------------------- src/ui/macos.rs | 9 ++---- src/ui_cm_interface.rs | 4 ++- 8 files changed, 65 insertions(+), 57 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index f5792bce3..5186aff4d 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -25,7 +25,7 @@ use hbb_common::{allow_err, get_time, message_proto::*, sleep}; use hbb_common::{fs, log, Stream}; use crate::client::{ - new_voice_call_request, Client, CodecFormat, LoginConfigHandler, MediaData, MediaSender, + new_voice_call_request, Client, CodecFormat, MediaData, MediaSender, QualityStatus, MILLI1, SEC30, SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED, }; diff --git a/src/common.rs b/src/common.rs index 2142d973d..79a4664db 100644 --- a/src/common.rs +++ b/src/common.rs @@ -30,7 +30,7 @@ use hbb_common::{ // #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] use hbb_common::{config::RENDEZVOUS_PORT, futures::future::join_all}; -use crate::ui_interface::{set_option, get_option}; +use crate::ui_interface::{get_option, set_option}; pub type NotifyMessageBox = fn(String, String, String, String) -> dyn Future; @@ -762,8 +762,3 @@ pub fn make_fd_to_json(id: i32, path: String, entries: &Vec) -> Strin fd_json.insert("entries".into(), json!(entries_out)); serde_json::to_string(&fd_json).unwrap_or("".into()) } - -#[cfg(test)] -mod test_common { - -} diff --git a/src/core_main.rs b/src/core_main.rs index 03d057eff..0af7026e9 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -1,6 +1,4 @@ -use std::future::Future; - -use hbb_common::{log, ResultType}; +use hbb_common::log; /// shared by flutter and sciter main function /// diff --git a/src/ipc.rs b/src/ipc.rs index 0ede560fc..699b0bcd7 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -16,10 +16,10 @@ use hbb_common::{ config::{self, Config, Config2}, futures::StreamExt as _, futures_util::sink::SinkExt, - log, password_security as password, ResultType, timeout, - tokio, + log, password_security as password, timeout, tokio, tokio::io::{AsyncRead, AsyncWrite}, tokio_util::codec::Framed, + ResultType, }; use crate::rendezvous_mediator::RendezvousMediator; @@ -190,7 +190,7 @@ pub enum Data { Socks(Option), FS(FS), Test, - SyncConfig(Option<(Config, Config2)>), + SyncConfig(Option>), #[cfg(not(any(target_os = "android", target_os = "ios")))] ClipboardFile(ClipboardFile), ClipboardFileEnabled(bool), @@ -419,7 +419,8 @@ async fn handle(data: Data, stream: &mut Connection) { let t = Config::get_nat_type(); allow_err!(stream.send(&Data::NatType(Some(t))).await); } - Data::SyncConfig(Some((config, config2))) => { + Data::SyncConfig(Some(configs)) => { + let (config, config2) = *configs; let _chk = CheckIfRestart::new(); Config::set(config); Config2::set(config2); @@ -428,7 +429,9 @@ async fn handle(data: Data, stream: &mut Connection) { Data::SyncConfig(None) => { allow_err!( stream - .send(&Data::SyncConfig(Some((Config::get(), Config2::get())))) + .send(&Data::SyncConfig(Some( + (Config::get(), Config2::get()).into() + ))) .await ); } @@ -840,6 +843,19 @@ pub async fn test_rendezvous_server() -> ResultType<()> { #[tokio::main(flavor = "current_thread")] pub async fn send_url_scheme(url: String) -> ResultType<()> { - connect(1_000, "_url").await?.send(&Data::UrlLink(url)).await?; + connect(1_000, "_url") + .await? + .send(&Data::UrlLink(url)) + .await?; Ok(()) } + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn verify_ffi_enum_data_size() { + println!("{}", std::mem::size_of::()); + assert!(std::mem::size_of::() < 96); + } +} diff --git a/src/keyboard.rs b/src/keyboard.rs index 91480ba30..17c52abf7 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -212,7 +212,7 @@ pub fn start_grab_loop() { } let mut _keyboard_mode = KeyboardMode::Map; - let scan_code = event.scan_code; + let _scan_code = event.scan_code; let res = if KEYBOARD_HOOKED.load(Ordering::SeqCst) { _keyboard_mode = client::process_event(&event, None); if is_press { @@ -225,7 +225,7 @@ pub fn start_grab_loop() { }; #[cfg(target_os = "windows")] - match scan_code { + match _scan_code { 0x1D | 0x021D => rdev::set_modifier(Key::ControlLeft, is_press), 0xE01D => rdev::set_modifier(Key::ControlRight, is_press), 0x2A => rdev::set_modifier(Key::ShiftLeft, is_press), @@ -241,7 +241,7 @@ pub fn start_grab_loop() { #[cfg(target_os = "windows")] unsafe { // AltGr - if scan_code == 0x021D { + if _scan_code == 0x021D { IS_0X021D_DOWN = is_press; } } diff --git a/src/server.rs b/src/server.rs index 616d92375..7807c4fac 100644 --- a/src/server.rs +++ b/src/server.rs @@ -8,6 +8,9 @@ use std::{ use bytes::Bytes; pub use connection::*; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use hbb_common::config::Config2; +use hbb_common::tcp::new_listener; use hbb_common::{ allow_err, anyhow::{anyhow, Context}, @@ -17,18 +20,15 @@ use hbb_common::{ message_proto::*, protobuf::{Enum, Message as _}, rendezvous_proto::*, - ResultType, socket_client, - sodiumoxide::crypto::{box_, secretbox, sign}, Stream, timeout, tokio, + sodiumoxide::crypto::{box_, secretbox, sign}, + timeout, tokio, ResultType, Stream, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use hbb_common::config::Config2; -use hbb_common::tcp::new_listener; -use service::{GenericService, Service, Subscriber}; -#[cfg(not(any(target_os = "android", target_os = "ios")))] use service::ServiceTmpl; +use service::{GenericService, Service, Subscriber}; -use crate::ipc::{connect, Data}; +use crate::ipc::Data; pub mod audio_service; cfg_if::cfg_if! { @@ -65,7 +65,7 @@ type ConnMap = HashMap; lazy_static::lazy_static! { pub static ref CHILD_PROCESS: Childs = Default::default(); pub static ref CONN_COUNT: Arc> = Default::default(); - // A client server used to provide local services(audio, video, clipboard, etc.) + // A client server used to provide local services(audio, video, clipboard, etc.) // for all initiative connections. // // [Note] @@ -420,7 +420,8 @@ pub async fn start_server(is_server: bool) { if conn.send(&Data::SyncConfig(None)).await.is_ok() { if let Ok(Some(data)) = conn.next_timeout(1000).await { match data { - Data::SyncConfig(Some((config, config2))) => { + Data::SyncConfig(Some(configs)) => { + let (config, config2) = *configs; if Config::set(config) { log::info!("config synced"); } @@ -450,28 +451,26 @@ pub async fn start_ipc_url_server() { while let Some(Ok(conn)) = incoming.next().await { let mut conn = crate::ipc::Connection::new(conn); match conn.next_timeout(1000).await { - Ok(Some(data)) => { - match data { - Data::UrlLink(url) => { - #[cfg(feature = "flutter")] - { - if let Some(stream) = crate::flutter::GLOBAL_EVENT_STREAM.read().unwrap().get( - crate::flutter::APP_TYPE_MAIN - ) { - let mut m = HashMap::new(); - m.insert("name", "on_url_scheme_received"); - m.insert("url", url.as_str()); - stream.add(serde_json::to_string(&m).unwrap()); - } else { - log::warn!("No main window app found!"); - } - } - } - _ => { - log::warn!("An unexpected data was sent to the ipc url server.") + Ok(Some(data)) => match data { + #[cfg(feature = "flutter")] + Data::UrlLink(url) => { + if let Some(stream) = crate::flutter::GLOBAL_EVENT_STREAM + .read() + .unwrap() + .get(crate::flutter::APP_TYPE_MAIN) + { + let mut m = HashMap::new(); + m.insert("name", "on_url_scheme_received"); + m.insert("url", url.as_str()); + stream.add(serde_json::to_string(&m).unwrap()); + } else { + log::warn!("No main window app found!"); } } - } + _ => { + log::warn!("An unexpected data was sent to the ipc url server.") + } + }, Err(err) => { log::error!("{}", err); } @@ -509,7 +508,8 @@ async fn sync_and_watch_config_dir() { if conn.send(&Data::SyncConfig(None)).await.is_ok() { if let Ok(Some(data)) = conn.next_timeout(1000).await { match data { - Data::SyncConfig(Some((config, config2))) => { + Data::SyncConfig(Some(configs)) => { + let (config, config2) = *configs; let _chk = crate::ipc::CheckIfRestart::new(); if cfg0.0 != config { cfg0.0 = config.clone(); @@ -534,7 +534,7 @@ async fn sync_and_watch_config_dir() { let cfg = (Config::get(), Config2::get()); if cfg != cfg0 { log::info!("config updated, sync to root"); - match conn.send(&Data::SyncConfig(Some(cfg.clone()))).await { + match conn.send(&Data::SyncConfig(Some(cfg.clone().into()))).await { Err(e) => { log::error!("sync config to root failed: {}", e); break; diff --git a/src/ui/macos.rs b/src/ui/macos.rs index 98e355dc1..f34b7c2c1 100644 --- a/src/ui/macos.rs +++ b/src/ui/macos.rs @@ -14,12 +14,9 @@ use objc::{ sel, sel_impl, }; use objc::runtime::Class; -use objc_id::WeakId; use sciter::{Host, make_args}; -use hbb_common::{log, tokio}; - -use crate::ui_cm_interface::start_ipc; +use hbb_common::log; static APP_HANDLER_IVAR: &str = "GoDeskAppHandler"; @@ -141,7 +138,7 @@ extern "C" fn application_should_handle_open_untitled_file( if !LAUNCHED { return YES; } - hbb_common::log::debug!("icon clicked on finder"); + log::debug!("icon clicked on finder"); if std::env::args().nth(1) == Some("--server".to_owned()) { crate::platform::macos::check_main_window(); } @@ -267,4 +264,4 @@ pub fn make_tray() { set_delegate(None); } crate::tray::make_tray(); -} \ No newline at end of file +} diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index ccddab0ee..de33b0169 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -845,6 +845,7 @@ pub fn elevate_portable(_id: i32) { } } +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] #[inline] pub fn handle_incoming_voice_call(id: i32, accept: bool) { if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { @@ -852,9 +853,10 @@ pub fn handle_incoming_voice_call(id: i32, accept: bool) { }; } +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] #[inline] pub fn close_voice_call(id: i32) { if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { allow_err!(client.tx.send(Data::CloseVoiceCall("".to_owned()))); }; -} \ No newline at end of file +} From 1588e44d61b255ed3eb99529e90dd7e18e4779d9 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 8 Feb 2023 19:17:43 +0800 Subject: [PATCH 063/199] win, translate mode, fix dead key Signed-off-by: fufesou --- Cargo.lock | 2 +- src/keyboard.rs | 21 +++++++++++++-------- src/ui_session_interface.rs | 6 +++--- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c53c573f2..83f623ca7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4405,7 +4405,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/fufesou/rdev#4d8231f05e14c5a04cd7d2c1288e87ad52d39e4c" +source = "git+https://github.com/fufesou/rdev#cedc4e62744566775026af4b434ef799804c1130" dependencies = [ "cocoa", "core-foundation 0.9.3", diff --git a/src/keyboard.rs b/src/keyboard.rs index 17c52abf7..5b9920714 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -193,8 +193,8 @@ pub mod client { #[cfg(windows)] pub fn update_grab_get_key_name() { match get_keyboard_mode_enum() { - KeyboardMode::Map => rdev::set_get_key_name(false), - KeyboardMode::Translate => rdev::set_get_key_name(true), + KeyboardMode::Map => rdev::set_get_key_unicode(false), + KeyboardMode::Translate => rdev::set_get_key_unicode(true), _ => {} }; } @@ -256,6 +256,7 @@ pub fn start_grab_loop() { if let Err(error) = rdev::grab(func) { log::error!("rdev Error: {:?}", error) } + rdev::set_event_popup(false); }); #[cfg(target_os = "linux")] @@ -757,12 +758,10 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option) { match &event.unicode { Some(unicode_info) => { - if !unicode_info.is_dead { - for code in &unicode_info.unicode { - let mut evt = key_event.clone(); - evt.set_unicode(*code as _); - events.push(evt); - } + for code in &unicode_info.unicode { + let mut evt = key_event.clone(); + evt.set_unicode(*code as _); + events.push(evt); } } None => {} @@ -816,6 +815,12 @@ pub fn translate_virtual_keycode(event: &Event, mut key_event: KeyEvent) -> Opti pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec { let mut events: Vec = Vec::new(); + if let Some(unicode_info) = &event.unicode { + if unicode_info.is_dead { + return events; + } + } + #[cfg(target_os = "windows")] unsafe { if event.scan_code == 0x021D { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index dc0e365ab..87ea8e9eb 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -368,8 +368,8 @@ impl Session { #[cfg(target_os = "windows")] { match &self.lc.read().unwrap().keyboard_mode as _ { - "legacy" => rdev::set_get_key_name(true), - "translate" => rdev::set_get_key_name(true), + "legacy" => rdev::set_get_key_unicode(true), + "translate" => rdev::set_get_key_unicode(true), _ => {} } } @@ -381,7 +381,7 @@ impl Session { pub fn leave(&self) { #[cfg(target_os = "windows")] { - rdev::set_get_key_name(false); + rdev::set_get_key_unicode(false); } IS_IN.store(false, Ordering::SeqCst); keyboard::client::change_grab_status(GrabState::Wait); From c049e728fd3f49db3b99338c377131294ce90473 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 8 Feb 2023 19:25:25 +0800 Subject: [PATCH 064/199] suppress warns Signed-off-by: fufesou --- src/flutter_ffi.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 2e6c450c1..bb1b8b8b9 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -928,7 +928,7 @@ pub fn main_start_dbus_server() { { use crate::dbus::start_dbus_server; // spawn new thread to start dbus server - std::thread::spawn(|| { + thread::spawn(|| { let _ = start_dbus_server(); }); } @@ -1275,7 +1275,7 @@ pub fn main_is_login_wayland() -> SyncReturn { pub fn main_start_pa() { #[cfg(target_os = "linux")] - std::thread::spawn(crate::ipc::start_pa); + thread::spawn(crate::ipc::start_pa); } pub fn main_hide_docker() -> SyncReturn { @@ -1302,9 +1302,9 @@ pub fn main_start_ipc_url_server() { /// /// * macOS only #[allow(unused_variables)] -pub fn send_url_scheme(url: String) { +pub fn send_url_scheme(_url: String) { #[cfg(target_os = "macos")] - thread::spawn(move || crate::ui::macos::handle_url_scheme(url)); + thread::spawn(move || crate::ui::macos::handle_url_scheme(_url)); } #[cfg(target_os = "android")] @@ -1324,7 +1324,7 @@ pub mod server_side { _class: JClass, ) { log::debug!("startServer from java"); - std::thread::spawn(move || start_server(true)); + thread::spawn(move || start_server(true)); } #[no_mangle] From 3a0137a3f71bef19fa3a0e440f3025c860cfcaf3 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 8 Feb 2023 19:45:15 +0800 Subject: [PATCH 065/199] suppress warns Signed-off-by: fufesou --- src/flutter_ffi.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index bb1b8b8b9..ec4a90973 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,6 +1,9 @@ -use std::{collections::HashMap, ffi::{CStr, CString}, os::raw::c_char, thread}; +use std::{collections::HashMap, ffi::{CStr, CString}, os::raw::c_char}; use std::str::FromStr; +#[cfg(any(target_os = "linux", target_os = "macos"))] +use std::thread; + use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; use serde_json::json; From 2feed1cdaf26c0f1a0f4f07819bfcb86b0e3934e Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 8 Feb 2023 20:00:16 +0800 Subject: [PATCH 066/199] though this change exe name to rustdesk, but it also change the name used in the other place --- flutter/macos/Runner.xcodeproj/project.pbxproj | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj index 18166c8ff..066560203 100644 --- a/flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter/macos/Runner.xcodeproj/project.pbxproj @@ -64,7 +64,7 @@ 295AD07E63F13855C270A0E0 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* rustdesk.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = rustdesk.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* RustDesk.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RustDesk.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -127,7 +127,7 @@ 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( - 33CC10ED2044A3C60003C045 /* rustdesk.app */, + 33CC10ED2044A3C60003C045 /* RustDesk.app */, ); name = Products; sourceTree = ""; @@ -212,7 +212,7 @@ ); name = Runner; productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* rustdesk.app */; + productReference = 33CC10ED2044A3C60003C045 /* RustDesk.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -462,7 +462,6 @@ ); MACOSX_DEPLOYMENT_TARGET = 10.14; PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk; - PRODUCT_NAME = rustdesk; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -608,7 +607,6 @@ ); MACOSX_DEPLOYMENT_TARGET = 10.14; PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk; - PRODUCT_NAME = rustdesk; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; "SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h; @@ -646,7 +644,6 @@ /dev/null, ); PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk; - PRODUCT_NAME = rustdesk; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; "SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h; From 80da209be8226a0af25b64511e6c35f36cfb8829 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 8 Feb 2023 20:09:07 +0800 Subject: [PATCH 067/199] change executable name from RustDesk to rustdesk in mac deployment --- .github/workflows/flutter-nightly.yml | 1 - build.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 5ca284cee..f03cd0be8 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -242,7 +242,6 @@ jobs: security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain # start sign the rustdesk.app and dmg rm rustdesk-${{ env.VERSION }}.dmg || true - mv ./flutter/build/macos/Build/Products/Release/rustdesk.app ./flutter/build/macos/Build/Products/Release/RustDesk.app codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep ./flutter/build/macos/Build/Products/Release/RustDesk.app -v create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep rustdesk-${{ env.VERSION }}.dmg -v diff --git a/build.py b/build.py index 6b107ff4b..dce434720 100755 --- a/build.py +++ b/build.py @@ -322,8 +322,9 @@ def build_flutter_dmg(version, features): os.system('sed -i "" "s/char \*\*rustdesk_core_main(int \*args_len);//" flutter/macos/Runner/bridge_generated.h') os.chdir('flutter') os.system('flutter build macos --release') + os.system('mv ./build/macos/Build/Products/Release/RustDesk.app/Contents/MacOS/RustDesk ./build/macos/Build/Products/Release/RustDesk.app/Contents/MacOS/rustdesk') os.system( - "create-dmg rustdesk.dmg ./build/macos/Build/Products/Release/rustdesk.app") + "create-dmg rustdesk.dmg ./build/macos/Build/Products/Release/RustDesk.app") os.rename("rustdesk.dmg", f"../rustdesk-{version}.dmg") os.chdir("..") From 3ae53a5d577b944baffb33da4ef9c959fefa1c72 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 8 Feb 2023 20:41:19 +0800 Subject: [PATCH 068/199] fix build Signed-off-by: fufesou --- src/keyboard.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/keyboard.rs b/src/keyboard.rs index 5b9920714..28e151580 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -256,6 +256,7 @@ pub fn start_grab_loop() { if let Err(error) = rdev::grab(func) { log::error!("rdev Error: {:?}", error) } + #[cfg(target_os = "windows")] rdev::set_event_popup(false); }); From 0dba0130893b54951fe3df3b9ce4997037a0a7ca Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 8 Feb 2023 21:54:48 +0900 Subject: [PATCH 069/199] remove unused Overlay in desktop_tab_page.dart and server_page.dart --- .../lib/desktop/pages/desktop_tab_page.dart | 28 +++++++--------- flutter/lib/desktop/pages/server_page.dart | 33 ++++++++----------- 2 files changed, 24 insertions(+), 37 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index c1965921c..35d5a61ef 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -64,23 +64,17 @@ class _DesktopTabPageState extends State { @override Widget build(BuildContext context) { final tabWidget = Container( - child: Overlay(initialEntries: [ - OverlayEntry(builder: (context) { - gFFI.dialogManager.setOverlayState(Overlay.of(context)); - return Scaffold( - backgroundColor: Theme.of(context).backgroundColor, - body: DesktopTab( - controller: tabController, - tail: ActionIcon( - message: 'Settings', - icon: IconFont.menu, - onTap: DesktopTabPage.onAddSetting, - isClose: false, - ), - )); - }) - ]), - ); + child: Scaffold( + backgroundColor: Theme.of(context).backgroundColor, + body: DesktopTab( + controller: tabController, + tail: ActionIcon( + message: 'Settings', + icon: IconFont.menu, + onTap: DesktopTabPage.onAddSetting, + isClose: false, + ), + ))); return Platform.isMacOS ? tabWidget : Obx( diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 521413647..b4d7f4fac 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -68,26 +68,19 @@ class _DesktopServerPageState extends State ], child: Consumer( builder: (context, serverModel, child) => Container( - decoration: BoxDecoration( - border: - Border.all(color: MyTheme.color(context).border!)), - child: Overlay(initialEntries: [ - OverlayEntry(builder: (context) { - gFFI.dialogManager.setOverlayState(Overlay.of(context)); - return Scaffold( - backgroundColor: Theme.of(context).backgroundColor, - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Expanded(child: ConnectionManager()), - ], - ), - ), - ); - }) - ]), - ))); + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: Scaffold( + backgroundColor: Theme.of(context).backgroundColor, + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded(child: ConnectionManager()), + ], + ), + ), + )))); } @override From 3d5aca18d690235ec1fb361b8526a25af7d46672 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 8 Feb 2023 22:01:15 +0900 Subject: [PATCH 070/199] refactor OverlayKeyState for OverlayDialogManager and ChatModel --- flutter/lib/common.dart | 30 +++++++++++-------- flutter/lib/common/widgets/overlay.dart | 24 ++++----------- .../lib/desktop/pages/file_manager_page.dart | 7 +++-- flutter/lib/desktop/pages/remote_page.dart | 16 ++++++---- flutter/lib/models/chat_model.dart | 18 +++++------ 5 files changed, 47 insertions(+), 48 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index a2623ff15..04e29eaa0 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -369,20 +369,25 @@ class Dialog { } } +class OverlayKeyState { + final _overlayKey = GlobalKey(); + + /// use global overlay by default + OverlayState? get state => + _overlayKey.currentState ?? globalKey.currentState?.overlay; + + GlobalKey? get key => _overlayKey; +} + class OverlayDialogManager { - OverlayState? _overlayState; final Map _dialogs = {}; + var _overlayKeyState = OverlayKeyState(); int _tagCount = 0; OverlayEntry? _mobileActionsOverlayEntry; - /// By default OverlayDialogManager use global overlay - OverlayDialogManager() { - _overlayState = globalKey.currentState?.overlay; - } - - void setOverlayState(OverlayState? overlayState) { - _overlayState = overlayState; + void setOverlayState(OverlayKeyState overlayKeyState) { + _overlayKeyState = overlayKeyState; } void dismissAll() { @@ -406,7 +411,7 @@ class OverlayDialogManager { bool useAnimation = true, bool forceGlobal = false}) { final overlayState = - forceGlobal ? globalKey.currentState?.overlay : _overlayState; + forceGlobal ? globalKey.currentState?.overlay : _overlayKeyState.state; if (overlayState == null) { return Future.error( @@ -510,7 +515,8 @@ class OverlayDialogManager { void showMobileActionsOverlay({FFI? ffi}) { if (_mobileActionsOverlayEntry != null) return; - if (_overlayState == null) return; + final overlayState = _overlayKeyState.state; + if (overlayState == null) return; // compute overlay position final screenW = MediaQuery.of(globalKey.currentContext!).size.width; @@ -536,7 +542,7 @@ class OverlayDialogManager { onHidePressed: () => hideMobileActionsOverlay(), ); }); - _overlayState!.insert(overlay); + overlayState.insert(overlay); _mobileActionsOverlayEntry = overlay; } @@ -1701,4 +1707,4 @@ Future updateSystemWindowTheme() async { : SystemWindowTheme.dark); } } -} \ No newline at end of file +} diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart index 3e248700f..32dced02a 100644 --- a/flutter/lib/common/widgets/overlay.dart +++ b/flutter/lib/common/widgets/overlay.dart @@ -372,25 +372,12 @@ class QualityMonitor extends StatelessWidget { : const SizedBox.shrink())); } -class PenetrableOverlayState { +class BlockableOverlayState extends OverlayKeyState { final _middleBlocked = false.obs; - final _overlayKey = GlobalKey(); VoidCallback? onMiddleBlockedClick; // to-do use listener RxBool get middleBlocked => _middleBlocked; - GlobalKey get overlayKey => _overlayKey; - OverlayState? get overlayState => _overlayKey.currentState; - - OverlayState? getOverlayStateOrGlobal() { - if (overlayState == null) { - if (globalKey.currentState == null || - globalKey.currentState!.overlay == null) return null; - return globalKey.currentState!.overlay; - } else { - return overlayState; - } - } void addMiddleBlockedListener(void Function(bool) cb) { _middleBlocked.listen(cb); @@ -403,13 +390,13 @@ class PenetrableOverlayState { } } -class PenetrableOverlay extends StatelessWidget { +class BlockableOverlay extends StatelessWidget { final Widget underlying; final List? upperLayer; - final PenetrableOverlayState state; + final BlockableOverlayState state; - PenetrableOverlay( + BlockableOverlay( {required this.underlying, required this.state, this.upperLayer}); @override @@ -433,6 +420,7 @@ class PenetrableOverlay extends StatelessWidget { initialEntries.addAll(upperLayer!); } - return Overlay(key: state.overlayKey, initialEntries: initialEntries); + /// set key + return Overlay(key: state.key, initialEntries: initialEntries); } } diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index b6a9e5fed..9955c2768 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -80,6 +80,7 @@ class _FileManagerPageState extends State Entry? _lastClickEntry; final _dropMaskVisible = false.obs; // TODO impl drop mask + final _overlayKeyState = OverlayKeyState(); ScrollController getBreadCrumbScrollController(bool isLocal) { return isLocal ? _breadCrumbScrollerLocal : _breadCrumbScrollerRemote; @@ -115,6 +116,7 @@ class _FileManagerPageState extends State // register location listener _locationNodeLocal.addListener(onLocalLocationFocusChanged); _locationNodeRemote.addListener(onRemoteLocationFocusChanged); + _ffi.dialogManager.setOverlayState(_overlayKeyState); } @override @@ -137,9 +139,8 @@ class _FileManagerPageState extends State @override Widget build(BuildContext context) { super.build(context); - return Overlay(initialEntries: [ - OverlayEntry(builder: (context) { - _ffi.dialogManager.setOverlayState(Overlay.of(context)); + return Overlay(key: _overlayKeyState.key, initialEntries: [ + OverlayEntry(builder: (_) { return ChangeNotifierProvider.value( value: _ffi.fileModel, child: Consumer(builder: (context, model, child) { diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 4bda68c2d..c444d1f5f 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -62,7 +62,7 @@ class _RemotePageState extends State late RxBool _remoteCursorMoved; late RxBool _keyboardEnabled; - final overlayState = PenetrableOverlayState(); + final _blockableOverlayState = BlockableOverlayState(); final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode"); @@ -136,10 +136,11 @@ class _RemotePageState extends State // _isCustomCursorInited = true; // } - _ffi.chatModel.setPenetrableOverlayState(overlayState); + _ffi.dialogManager.setOverlayState(_blockableOverlayState); + _ffi.chatModel.setOverlayState(_blockableOverlayState); // make remote page penetrable automatically, effective for chat over remote - overlayState.onMiddleBlockedClick = () { - overlayState.setMiddleBlocked(false); + _blockableOverlayState.onMiddleBlockedClick = () { + _blockableOverlayState.setMiddleBlocked(false); }; } @@ -201,8 +202,11 @@ class _RemotePageState extends State Widget buildBody(BuildContext context) { return Scaffold( backgroundColor: Theme.of(context).backgroundColor, - body: PenetrableOverlay( - state: overlayState, + + /// the Overlay key will be set with _blockableOverlayState in BlockableOverlay + /// see override build() in [BlockableOverlay] + body: BlockableOverlay( + state: _blockableOverlayState, underlying: Container( color: Colors.black, child: RawKeyFocusScope( diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index b61ce79a7..8320d08dd 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -34,7 +34,7 @@ class ChatModel with ChangeNotifier { bool isConnManager = false; RxBool isWindowFocus = true.obs; - PenetrableOverlayState? pOverlayState; + BlockableOverlayState? _blockableOverlayState; final ChatUser me = ChatUser( id: "", @@ -53,10 +53,10 @@ class ChatModel with ChangeNotifier { bool get isShowCMChatPage => _isShowCMChatPage; - void setPenetrableOverlayState(PenetrableOverlayState state) { - pOverlayState = state; + void setOverlayState(BlockableOverlayState blockableOverlayState) { + _blockableOverlayState = blockableOverlayState; - pOverlayState!.addMiddleBlockedListener((v) { + _blockableOverlayState!.addMiddleBlockedListener((v) { if (!v) { isWindowFocus.value = false; if (isWindowFocus.value) { @@ -94,7 +94,7 @@ class ChatModel with ChangeNotifier { } } - final overlayState = pOverlayState?.getOverlayStateOrGlobal(); + final overlayState = _blockableOverlayState?.state; if (overlayState == null) return; final overlay = OverlayEntry(builder: (context) { @@ -129,16 +129,16 @@ class ChatModel with ChangeNotifier { showChatWindowOverlay({Offset? chatInitPos}) { if (chatWindowOverlayEntry != null) return; isWindowFocus.value = true; - pOverlayState?.setMiddleBlocked(true); + _blockableOverlayState?.setMiddleBlocked(true); - final overlayState = pOverlayState?.getOverlayStateOrGlobal(); + final overlayState = _blockableOverlayState?.state; if (overlayState == null) return; final overlay = OverlayEntry(builder: (context) { return Listener( onPointerDown: (_) { if (!isWindowFocus.value) { isWindowFocus.value = true; - pOverlayState?.setMiddleBlocked(true); + _blockableOverlayState?.setMiddleBlocked(true); } }, child: DraggableChatWindow( @@ -154,7 +154,7 @@ class ChatModel with ChangeNotifier { hideChatWindowOverlay() { if (chatWindowOverlayEntry != null) { - pOverlayState?.setMiddleBlocked(false); + _blockableOverlayState?.setMiddleBlocked(false); chatWindowOverlayEntry!.remove(); chatWindowOverlayEntry = null; return; From ac1ae9fc3bbfb7c7cd343222f59618957637093c Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 8 Feb 2023 10:11:53 +0900 Subject: [PATCH 071/199] workaround: PageView reload --- .../lib/desktop/widgets/tabbar_widget.dart | 33 +++++++++++++++---- flutter/lib/models/model.dart | 4 +-- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 598b2cc4c..ddc51eddb 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -327,14 +327,32 @@ class DesktopTab extends StatelessWidget { )); } + List _tabWidgets = []; Widget _buildPageView() { return _buildBlock( child: Obx(() => PageView( controller: state.value.pageController, physics: NeverScrollableScrollPhysics(), - children: state.value.tabs - .map((tab) => tab.page) - .toList(growable: false)))); + children: () { + /// to-do refactor, separate connection state and UI state for remote session. + /// [workaround] PageView children need an immutable list, after it has been passed into PageView + final tabLen = state.value.tabs.length; + if (tabLen == _tabWidgets.length) { + return _tabWidgets; + } else if (_tabWidgets.isNotEmpty && + tabLen == _tabWidgets.length + 1) { + /// On add. Use the previous list(pointer) to prevent item's state init twice. + /// *[_tabWidgets.isNotEmpty] means TabsWindow(remote_tab_page or file_manager_tab_page) opened before, but was hidden. In this case, we have to reload, otherwise the child can't be built. + _tabWidgets.add(state.value.tabs.last.page); + return _tabWidgets; + } else { + /// On remove or change. Use new list(pointer) to reload list children so that items loading order is normal. + /// the Widgets in list must enable [AutomaticKeepAliveClientMixin] + final newList = state.value.tabs.map((v) => v.page).toList(); + _tabWidgets = newList; + return newList; + } + }()))); } /// Check whether to show ListView @@ -765,7 +783,8 @@ class _ListView extends StatelessWidget { tabBuilder: tabBuilder, tabMenuBuilder: tabMenuBuilder, maxLabelWidth: maxLabelWidth, - selectedTabBackgroundColor: selectedTabBackgroundColor ?? MyTheme.tabbar(context).selectedTabBackgroundColor, + selectedTabBackgroundColor: selectedTabBackgroundColor ?? + MyTheme.tabbar(context).selectedTabBackgroundColor, unSelectedTabBackgroundColor: unSelectedTabBackgroundColor, ); }).toList())); @@ -1119,7 +1138,8 @@ class TabbarTheme extends ThemeExtension { dividerColor: dividerColor ?? this.dividerColor, hoverColor: hoverColor ?? this.hoverColor, closeHoverColor: closeHoverColor ?? this.closeHoverColor, - selectedTabBackgroundColor: selectedTabBackgroundColor ?? this.selectedTabBackgroundColor, + selectedTabBackgroundColor: + selectedTabBackgroundColor ?? this.selectedTabBackgroundColor, ); } @@ -1145,7 +1165,8 @@ class TabbarTheme extends ThemeExtension { dividerColor: Color.lerp(dividerColor, other.dividerColor, t), hoverColor: Color.lerp(hoverColor, other.hoverColor, t), closeHoverColor: Color.lerp(closeHoverColor, other.closeHoverColor, t), - selectedTabBackgroundColor: Color.lerp(selectedTabBackgroundColor, other.selectedTabBackgroundColor, t), + selectedTabBackgroundColor: Color.lerp( + selectedTabBackgroundColor, other.selectedTabBackgroundColor, t), ); } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 1eac1be39..5e4693ccc 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -17,7 +17,6 @@ import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/models/user_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/common/shared_state.dart'; -import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:tuple/tuple.dart'; import 'package:image/image.dart' as img2; import 'package:flutter_custom_cursor/cursor_manager.dart'; @@ -25,7 +24,6 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import '../common.dart'; -import '../common/shared_state.dart'; import '../utils/image.dart' as img; import '../mobile/widgets/dialog.dart'; import 'input_model.dart'; @@ -1348,13 +1346,13 @@ class FFI { canvasModel.y, canvasModel.scale, ffiModel.pi.currentDisplay); } bind.sessionClose(id: id); - id = ''; imageModel.update(null); cursorModel.clear(); ffiModel.clear(); canvasModel.clear(); inputModel.resetModifiers(); debugPrint('model $id closed'); + id = ''; } void setMethodCallHandler(FMethod callback) { From 552e45b320a6e1361580764332f8401801e7c160 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 8 Feb 2023 22:05:11 +0900 Subject: [PATCH 072/199] BlockableOverlay blocked layer transparent color --- flutter/lib/common/widgets/overlay.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart index 32dced02a..ba7b8a059 100644 --- a/flutter/lib/common/widgets/overlay.dart +++ b/flutter/lib/common/widgets/overlay.dart @@ -411,9 +411,8 @@ class BlockableOverlay extends StatelessWidget { state.onMiddleBlockedClick?.call(); }, child: Container( - color: state.middleBlocked.value - ? Colors.red.withOpacity(0.3) - : null)))), + color: + state.middleBlocked.value ? Colors.transparent : null)))), ]; if (upperLayer != null) { From 38d26ec47b2d44e351661b71451afcc6ebfaa275 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 8 Feb 2023 21:50:18 +0800 Subject: [PATCH 073/199] fix altgr Signed-off-by: fufesou --- src/keyboard.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index 28e151580..105b84400 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -840,6 +840,13 @@ pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec Vec Date: Wed, 8 Feb 2023 22:06:18 +0800 Subject: [PATCH 074/199] fix CI --- src/flutter_ffi.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index ec4a90973..ad0d119d7 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,7 +1,7 @@ use std::{collections::HashMap, ffi::{CStr, CString}, os::raw::c_char}; use std::str::FromStr; -#[cfg(any(target_os = "linux", target_os = "macos"))] +#[cfg(any(target_os = "linux", target_os = "macos", target_os = "android"))] use std::thread; use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; From 974fa86b8abb2fc90f43a069bc22ad71429d53c6 Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Wed, 8 Feb 2023 22:47:41 +0100 Subject: [PATCH 075/199] Update de.rs --- src/lang/de.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 44bbafdac..1743505cc 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -392,7 +392,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", "oder"), ("Continue with", "Fortfahren mit"), ("Elevate", "Erheben"), - ("Zoom cursor", "Cursor zoomen"), + ("Zoom cursor", "Cursor vergrößern"), ("Accept sessions via password", "Sitzung mit Passwort bestätigen"), ("Accept sessions via click", "Sitzung mit einem Klick bestätigen"), ("Accept sessions via both", "Sitzung mit Klick und Passwort bestätigen"), @@ -414,8 +414,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", "Lokalen Tastaturtyp auswählen"), ("software_render_tip", "Wenn Sie eine Nvidia-Grafikkarte haben und sich das entfernte Fenster sofort nach dem Herstellen der Verbindung schließt, kann es helfen, den Nouveau-Treiber zu installieren und Software-Rendering zu verwenden. Ein Neustart der Software ist erforderlich."), ("Always use software rendering", "Software-Rendering immer verwenden"), - ("config_input", "Um den entfernten Desktop mit der Tastatur steuern zu können, müssen Sie RustDesk \"Input Monitoring\"-Rechte erteilen."), - ("config_microphone", ""), + ("config_input", "Um den entfernten Desktop mit der Tastatur steuern zu können, müssen Sie RustDesk die Berechtigung \"Input Monitoring\" erteilen."), + ("config_microphone", "Um aus der Ferne sprechen zu können, müssen Sie RustDesk die Berechtigung \"Audio aufzeichnen\" erteilen."), ("request_elevation_tip", "Sie können auch erhöhte Rechte anfordern, wenn sich jemand auf der Gegenseite befindet."), ("Wait", "Warten"), ("Elevation Error", "Berechtigungsfehler"), @@ -445,9 +445,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", "Bitrate"), ("FPS", "fps"), ("Auto", "Automatisch"), - ("Other Default Options", "Weitere Standardoptionen"), - ("Voice call", ""), - ("Text chat", ""), - ("Stop voice call", ""), + ("Other Default Options", "Weitere Standardeinstellungen"), + ("Voice call", "Sprachanruf"), + ("Text chat", "Text-Chat"), + ("Stop voice call", "Sprachanruf beenden"), ].iter().cloned().collect(); } From 244cfa25f14f9ee86ac8fc371f5ff1f0823c35eb Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 9 Feb 2023 10:29:35 +0900 Subject: [PATCH 076/199] opt dark theme in gesture_help.dart --- flutter/lib/mobile/widgets/gesture_help.dart | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/flutter/lib/mobile/widgets/gesture_help.dart b/flutter/lib/mobile/widgets/gesture_help.dart index 37cc77c8f..bc31ae2c4 100644 --- a/flutter/lib/mobile/widgets/gesture_help.dart +++ b/flutter/lib/mobile/widgets/gesture_help.dart @@ -2,8 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:toggle_switch/toggle_switch.dart'; -import '../../models/model.dart'; - class GestureIcons { static const String _family = 'gestureicons'; @@ -79,7 +77,10 @@ class _GestureHelpState extends State { children: [ ToggleSwitch( initialLabelIndex: _selectedIndex, - inactiveBgColor: MyTheme.darkGray, + activeFgColor: Colors.white, + inactiveFgColor: Colors.white60, + activeBgColor: [MyTheme.accent], + inactiveBgColor: Theme.of(context).hintColor, totalSwitches: 2, minWidth: 150, fontSize: 15, @@ -188,7 +189,7 @@ class GestureInfo extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - width: this.width, + width: width, child: Column( children: [ Icon( @@ -199,11 +200,14 @@ class GestureInfo extends StatelessWidget { SizedBox(height: 6), Text(fromText, textAlign: TextAlign.center, - style: TextStyle(fontSize: 9, color: Colors.grey)), + style: + TextStyle(fontSize: 9, color: Theme.of(context).hintColor)), SizedBox(height: 3), Text(toText, textAlign: TextAlign.center, - style: TextStyle(fontSize: 12, color: Colors.black)) + style: TextStyle( + fontSize: 12, + color: Theme.of(context).textTheme.bodySmall?.color)) ], )); } From 4f25b03a10a41adf3220a35944b99cfc6ff6ea61 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 9 Feb 2023 16:54:26 +0800 Subject: [PATCH 077/199] fix CI --- src/flutter.rs | 15 +++++++++++---- src/flutter_ffi.rs | 48 ++++++++++++++++++++++------------------------ 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/src/flutter.rs b/src/flutter.rs index 2d7d3fb86..bf5746c13 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1,5 +1,8 @@ -use crate::ui_session_interface::{io_loop, InvokeUiSession, Session}; -use crate::{client::*, flutter_ffi::EventToUI}; +use crate::{ + client::*, + flutter_ffi::EventToUI, + ui_session_interface::{io_loop, InvokeUiSession, Session}, +}; use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; use hbb_common::{ bail, config::LocalConfig, get_version_number, message_proto::*, rendezvous_proto::ConnType, @@ -549,11 +552,15 @@ pub mod connection_manager { let mut h: HashMap<&str, &str> = event.iter().cloned().collect(); assert!(h.get("name").is_none()); h.insert("name", name); - + if let Some(s) = GLOBAL_EVENT_STREAM.read().unwrap().get(super::APP_TYPE_CM) { s.add(serde_json::ser::to_string(&h).unwrap_or("".to_owned())); } else { - println!("Push event {} failed. No {} event stream found.", name, super::APP_TYPE_CM); + println!( + "Push event {} failed. No {} event stream found.", + name, + super::APP_TYPE_CM + ); }; } } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index ad0d119d7..a7e32d0b2 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,27 +1,25 @@ -use std::{collections::HashMap, ffi::{CStr, CString}, os::raw::c_char}; -use std::str::FromStr; - -#[cfg(any(target_os = "linux", target_os = "macos", target_os = "android"))] -use std::thread; - -use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; -use serde_json::json; - -use hbb_common::{ - config::{self, LocalConfig, ONLINE, PeerConfig}, - fs, log, -}; -use hbb_common::message_proto::KeyboardMode; -use hbb_common::ResultType; - use crate::{ client::file_trait::FileManager, common::make_fd_to_json, + common::{get_default_sound_input, is_keyboard_mode_supported}, + flutter::{self, SESSIONS}, flutter::{session_add, session_start_}, + ui_interface::{self, *}, +}; +use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; +use hbb_common::{ + config::{self, LocalConfig, PeerConfig, ONLINE}, + fs, log, + message_proto::KeyboardMode, + ResultType, +}; +use serde_json::json; +use std::{ + collections::HashMap, + ffi::{CStr, CString}, + os::raw::c_char, + str::FromStr, }; -use crate::common::{get_default_sound_input, is_keyboard_mode_supported}; -use crate::flutter::{self, SESSIONS}; -use crate::ui_interface::{self, *}; // use crate::hbbs_http::account::AuthResult; @@ -931,7 +929,7 @@ pub fn main_start_dbus_server() { { use crate::dbus::start_dbus_server; // spawn new thread to start dbus server - thread::spawn(|| { + std::thread::spawn(|| { let _ = start_dbus_server(); }); } @@ -1278,7 +1276,7 @@ pub fn main_is_login_wayland() -> SyncReturn { pub fn main_start_pa() { #[cfg(target_os = "linux")] - thread::spawn(crate::ipc::start_pa); + std::thread::spawn(crate::ipc::start_pa); } pub fn main_hide_docker() -> SyncReturn { @@ -1298,7 +1296,7 @@ pub fn cm_start_listen_ipc_thread() { /// * macOS only pub fn main_start_ipc_url_server() { #[cfg(target_os = "macos")] - thread::spawn(move || crate::server::start_ipc_url_server()); + std::thread::spawn(move || crate::server::start_ipc_url_server()); } /// Send a url scheme throught the ipc. @@ -1307,16 +1305,16 @@ pub fn main_start_ipc_url_server() { #[allow(unused_variables)] pub fn send_url_scheme(_url: String) { #[cfg(target_os = "macos")] - thread::spawn(move || crate::ui::macos::handle_url_scheme(_url)); + std::thread::spawn(move || crate::ui::macos::handle_url_scheme(_url)); } #[cfg(target_os = "android")] pub mod server_side { use hbb_common::log; use jni::{ - JNIEnv, objects::{JClass, JString}, sys::jstring, + JNIEnv, }; use crate::start_server; @@ -1327,7 +1325,7 @@ pub mod server_side { _class: JClass, ) { log::debug!("startServer from java"); - thread::spawn(move || start_server(true)); + std::thread::spawn(move || start_server(true)); } #[no_mangle] From fcd1f9b4a3758112098108e563f429a7897576d8 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 9 Feb 2023 18:11:32 +0800 Subject: [PATCH 078/199] refactor handle_applicationShouldOpenUntitledFile --- src/flutter.rs | 6 +----- src/platform/macos.rs | 15 +++++++++++++-- src/ui/macos.rs | 18 +++++++++--------- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/flutter.rs b/src/flutter.rs index bf5746c13..f60d9b30e 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -42,11 +42,7 @@ pub extern "C" fn rustdesk_core_main() -> bool { #[cfg(target_os = "macos")] #[no_mangle] pub extern "C" fn handle_applicationShouldOpenUntitledFile() { - hbb_common::log::debug!("icon clicked on finder"); - let x = std::env::args().nth(1).unwrap_or_default(); - if x == "--server" || x == "--cm" { - crate::platform::macos::check_main_window(); - } + crate::platform::macos::handle_applicationShouldOpenUntitledFile(); } #[cfg(windows)] diff --git a/src/platform/macos.rs b/src/platform/macos.rs index c7dbd9b73..b61f51732 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -557,7 +557,7 @@ pub fn hide_dock() { } } -pub fn check_main_window() { +fn check_main_window() -> bool { use sysinfo::{ProcessExt, System, SystemExt}; let mut sys = System::new(); sys.refresh_processes(); @@ -568,11 +568,22 @@ pub fn check_main_window() { .unwrap_or_default(); for (_, p) in sys.processes().iter() { if p.cmd().len() == 1 && p.user_id() == my_uid && p.cmd()[0].contains(&app) { - return; + return true; } } std::process::Command::new("open") .args(["-n", &app]) .status() .ok(); + false +} + +pub fn handle_applicationShouldOpenUntitledFile() { + hbb_common::log::debug!("icon clicked on finder"); + let x = std::env::args().nth(1).unwrap_or_default(); + if x == "--server" || x == "--cm" { + if crate::platform::macos::check_main_window() { + crate::ipc::send_url_scheme("rustdesk:".into()); + } + } } diff --git a/src/ui/macos.rs b/src/ui/macos.rs index f34b7c2c1..c6600608b 100644 --- a/src/ui/macos.rs +++ b/src/ui/macos.rs @@ -6,15 +6,15 @@ use cocoa::{ base::{id, nil, YES}, foundation::{NSAutoreleasePool, NSString}, }; +use objc::runtime::Class; use objc::{ class, declare::ClassDecl, msg_send, - runtime::{BOOL, Object, Sel}, + runtime::{Object, Sel, BOOL}, sel, sel_impl, }; -use objc::runtime::Class; -use sciter::{Host, make_args}; +use sciter::{make_args, Host}; use hbb_common::log; @@ -102,7 +102,10 @@ unsafe fn set_delegate(handler: Option>) { sel!(handleMenuItem:), handle_menu_item as extern "C" fn(&mut Object, Sel, id), ); - decl.add_method(sel!(handleEvent:withReplyEvent:), handle_apple_event as extern fn(&Object, Sel, u64, u64)); + decl.add_method( + sel!(handleEvent:withReplyEvent:), + handle_apple_event as extern "C" fn(&Object, Sel, u64, u64), + ); let decl = decl.register(); let delegate: id = msg_send![decl, alloc]; let () = msg_send![delegate, init]; @@ -138,10 +141,7 @@ extern "C" fn application_should_handle_open_untitled_file( if !LAUNCHED { return YES; } - log::debug!("icon clicked on finder"); - if std::env::args().nth(1) == Some("--server".to_owned()) { - crate::platform::macos::check_main_window(); - } + crate::platform::macos::handle_applicationShouldOpenUntitledFile(); let inner: *mut c_void = *this.get_ivar(APP_HANDLER_IVAR); let inner = &mut *(inner as *mut DelegateState); (*inner).command(AWAKE); @@ -191,7 +191,7 @@ pub fn handle_url_scheme(url: String) { } } -extern fn handle_apple_event(_this: &Object, _cmd: Sel, event: u64, _reply: u64) { +extern "C" fn handle_apple_event(_this: &Object, _cmd: Sel, event: u64, _reply: u64) { let event = event as *mut Object; let url = fruitbasket::parse_url_event(event); log::debug!("an event was received: {}", url); From c03adf53347ac519e2c4bb4327bbcca6599d06ab Mon Sep 17 00:00:00 2001 From: mehdi-song Date: Thu, 9 Feb 2023 15:30:41 +0330 Subject: [PATCH 079/199] Update fa.rs --- src/lang/fa.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/fa.rs b/src/lang/fa.rs index c206f91ff..8413673a1 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -446,8 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "خودکار"), ("Other Default Options", "سایر گزینه های پیش فرض"), - ("Voice call", ""), - ("Text chat", ""), - ("Stop voice call", ""), + ("Voice call", "تماس صوتی"), + ("Text chat", "گفتگو متنی (چت متنی)"), + ("Stop voice call", "توقف تماس صوتی"), ].iter().cloned().collect(); } From 15a8460fcd36a690915fa52f984377a9e9570e65 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Thu, 9 Feb 2023 13:36:48 +0100 Subject: [PATCH 080/199] removed SizedBox --- .../lib/desktop/pages/port_forward_page.dart | 57 +++++++++---------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart index f513a1c6a..2385813eb 100644 --- a/flutter/lib/desktop/pages/port_forward_page.dart +++ b/flutter/lib/desktop/pages/port_forward_page.dart @@ -179,36 +179,33 @@ class _PortForwardPageState extends State buildTunnelInputCell(context, controller: remotePortController, inputFormatters: portInputFormatter), - SizedBox( - width: _kColumn4Width, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - elevation: 0, side: const BorderSide(color: MyTheme.border)), - onPressed: () async { - int? localPort = int.tryParse(localPortController.text); - int? remotePort = int.tryParse(remotePortController.text); - if (localPort != null && - remotePort != null && - (remoteHostController.text.isEmpty || - remoteHostController.text.trim().isNotEmpty)) { - await bind.sessionAddPortForward( - id: 'pf_${widget.id}', - localPort: localPort, - remoteHost: remoteHostController.text.trim().isEmpty - ? 'localhost' - : remoteHostController.text.trim(), - remotePort: remotePort); - localPortController.clear(); - remoteHostController.clear(); - remotePortController.clear(); - refreshTunnelConfig(); - } - }, - child: Text( - translate('Add'), - ), - ).marginAll(10), - ), + ElevatedButton( + style: ElevatedButton.styleFrom( + elevation: 0, side: const BorderSide(color: MyTheme.border)), + onPressed: () async { + int? localPort = int.tryParse(localPortController.text); + int? remotePort = int.tryParse(remotePortController.text); + if (localPort != null && + remotePort != null && + (remoteHostController.text.isEmpty || + remoteHostController.text.trim().isNotEmpty)) { + await bind.sessionAddPortForward( + id: 'pf_${widget.id}', + localPort: localPort, + remoteHost: remoteHostController.text.trim().isEmpty + ? 'localhost' + : remoteHostController.text.trim(), + remotePort: remotePort); + localPortController.clear(); + remoteHostController.clear(); + remotePortController.clear(); + refreshTunnelConfig(); + } + }, + child: Text( + translate('Add'), + ), + ).marginAll(10), ]), ); } From f7643077d339accf11896d9be4e1d876e0c88f99 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 9 Feb 2023 21:28:42 +0800 Subject: [PATCH 081/199] new tray --- Cargo.lock | 670 +++++++++++++----- Cargo.toml | 13 +- .../macos/Runner.xcodeproj/project.pbxproj | 16 - res/mac-tray-dark-x2.png | Bin 1585 -> 703 bytes res/mac-tray-dark.png | Bin 535 -> 0 bytes res/mac-tray-light-x2.png | Bin 1193 -> 728 bytes res/mac-tray-light.png | Bin 415 -> 0 bytes src/core_main.rs | 25 +- src/flutter.rs | 2 +- src/platform/macos.rs | 8 +- src/tray.rs | 224 ++---- src/ui/macos.rs | 9 +- 12 files changed, 569 insertions(+), 398 deletions(-) delete mode 100644 res/mac-tray-dark.png delete mode 100644 res/mac-tray-light.png diff --git a/Cargo.lock b/Cargo.lock index 83f623ca7..f0f66e287 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -153,7 +153,7 @@ checksum = "dc120354d1b5ec6d7aaf4876b602def75595937b5e15d356eb554ab5177e08bb" dependencies = [ "clipboard-win", "core-graphics 0.22.3", - "image", + "image 0.23.14", "log", "objc", "objc-foundation", @@ -278,24 +278,24 @@ dependencies = [ [[package]] name = "atk" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c3d816ce6f0e2909a96830d6911c2aff044370b1ef92d7f267b43bae5addedd" +checksum = "39991bc421ddf72f70159011b323ff49b0f783cc676a7287c59453da2e2531cf" dependencies = [ "atk-sys", "bitflags", - "glib 0.15.12", + "glib 0.16.5", "libc", ] [[package]] name = "atk-sys" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58aeb089fb698e06db8089971c7ee317ab9644bade33383f63631437b03aafb6" +checksum = "11ad703eb64dc058024f0e57ccfa069e15a413b98dbd50a1a950e743b7f11148" dependencies = [ - "glib-sys 0.15.10", - "gobject-sys 0.15.10", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", "libc", "system-deps 6.0.3", ] @@ -405,6 +405,12 @@ dependencies = [ "syn", ] +[[package]] +name = "bit_field" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb6dd1c2376d2e096796e234a70e17e94cc2d5d54ff8ce42b28cef1d0d359a4" + [[package]] name = "bitflags" version = "1.3.2" @@ -508,24 +514,25 @@ dependencies = [ [[package]] name = "cairo-rs" -version = "0.15.12" +version = "0.16.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c76ee391b03d35510d9fa917357c7f1855bd9a6659c95a1b392e33f49b3369bc" +checksum = "f3125b15ec28b84c238f6f476c6034016a5f6cc0221cb514ca46c532139fc97d" dependencies = [ "bitflags", "cairo-sys-rs", - "glib 0.15.12", + "glib 0.16.5", "libc", + "once_cell", "thiserror", ] [[package]] name = "cairo-sys-rs" -version = "0.15.1" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8" +checksum = "7c48f4af05fabdcfa9658178e1326efa061853f040ce7d72e33af6885196f421" dependencies = [ - "glib-sys 0.15.10", + "glib-sys 0.16.3", "libc", "system-deps 6.0.3", ] @@ -972,7 +979,7 @@ dependencies = [ "alsa", "core-foundation-sys 0.8.3", "coreaudio-rs", - "jni", + "jni 0.19.0", "js-sys", "lazy_static", "libc", @@ -1059,6 +1066,12 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -1131,9 +1144,9 @@ dependencies = [ [[package]] name = "dark-light" -version = "0.2.3" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413487ef345ab5cdfbf23e66070741217a701bce70f2f397a54221b4f2b6056a" +checksum = "a62007a65515b3cd88c733dd3464431f05d2ad066999a824259d8edc3cf6f645" dependencies = [ "dconf_rs", "detect-desktop-environment", @@ -1712,6 +1725,22 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "exr" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8af5ef47e2ed89d23d0ecbc1b681b30390069de70260937877514377fc24feb" +dependencies = [ + "bit_field", + "flume", + "half", + "lebe", + "miniz_oxide 0.6.2", + "smallvec", + "threadpool", + "zune-inflate", +] + [[package]] name = "extend" version = "1.1.2" @@ -1794,6 +1823,19 @@ dependencies = [ "time 0.3.9", ] +[[package]] +name = "flume" +version = "0.10.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "pin-project", + "spin 0.9.5", +] + [[package]] name = "flutter_rust_bridge" version = "1.61.1" @@ -2040,63 +2082,90 @@ dependencies = [ [[package]] name = "gdk" -version = "0.15.4" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6e05c1f572ab0e1f15be94217f0dc29088c248b14f792a5ff0af0d84bcda9e8" +checksum = "aa9cb33da481c6c040404a11f8212d193889e9b435db2c14fd86987f630d3ce1" dependencies = [ "bitflags", "cairo-rs", "gdk-pixbuf", "gdk-sys", "gio", - "glib 0.15.12", + "glib 0.16.5", "libc", "pango", ] [[package]] name = "gdk-pixbuf" -version = "0.15.11" +version = "0.16.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad38dd9cc8b099cceecdf41375bb6d481b1b5a7cd5cd603e10a69a9383f8619a" +checksum = "c3578c60dee9d029ad86593ed88cb40f35c1b83360e12498d055022385dd9a05" dependencies = [ "bitflags", "gdk-pixbuf-sys", "gio", - "glib 0.15.12", + "glib 0.16.5", "libc", ] [[package]] name = "gdk-pixbuf-sys" -version = "0.15.10" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "140b2f5378256527150350a8346dbdb08fadc13453a7a2d73aecd5fab3c402a7" +checksum = "3092cf797a5f1210479ea38070d9ae8a5b8e9f8f1be9f32f4643c529c7d70016" dependencies = [ - "gio-sys 0.15.10", - "glib-sys 0.15.10", - "gobject-sys 0.15.10", + "gio-sys", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", "libc", "system-deps 6.0.3", ] [[package]] name = "gdk-sys" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e7a08c1e8f06f4177fb7e51a777b8c1689f743a7bc11ea91d44d2226073a88" +checksum = "d76354f97a913e55b984759a997b693aa7dc71068c9e98bcce51aa167a0a5c5a" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", - "gio-sys 0.15.10", - "glib-sys 0.15.10", - "gobject-sys 0.15.10", + "gio-sys", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", "libc", "pango-sys", "pkg-config", "system-deps 6.0.3", ] +[[package]] +name = "gdkwayland-sys" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4511710212ed3020b61a8622a37aa6f0dd2a84516575da92e9b96928dcbe83ba" +dependencies = [ + "gdk-sys", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", + "libc", + "pkg-config", + "system-deps 6.0.3", +] + +[[package]] +name = "gdkx11-sys" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa2bf8b5b8c414bc5d05e48b271896d0fd3ddb57464a3108438082da61de6af" +dependencies = [ + "gdk-sys", + "glib-sys 0.16.3", + "libc", + "system-deps 6.0.3", + "x11 2.20.1", +] + [[package]] name = "generic-array" version = "0.14.6" @@ -2124,8 +2193,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" dependencies = [ "cfg-if 1.0.0", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "gif" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06" +dependencies = [ + "color_quant", + "weezl", ] [[package]] @@ -2136,34 +2217,24 @@ checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" [[package]] name = "gio" -version = "0.15.12" +version = "0.16.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68fdbc90312d462781a395f7a16d96a2b379bb6ef8cd6310a2df272771c4283b" +checksum = "2a1c84b4534a290a29160ef5c6eff2a9c95833111472e824fc5cb78b513dd092" dependencies = [ "bitflags", "futures-channel", "futures-core", "futures-io", - "gio-sys 0.15.10", - "glib 0.15.12", + "futures-util", + "gio-sys", + "glib 0.16.5", "libc", "once_cell", + "pin-project-lite", + "smallvec", "thiserror", ] -[[package]] -name = "gio-sys" -version = "0.15.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32157a475271e2c4a023382e9cab31c4584ee30a97da41d3c4e9fdd605abcf8d" -dependencies = [ - "glib-sys 0.15.10", - "gobject-sys 0.15.10", - "libc", - "system-deps 6.0.3", - "winapi 0.3.9", -] - [[package]] name = "gio-sys" version = "0.16.3" @@ -2196,26 +2267,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "glib" -version = "0.15.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb0306fbad0ab5428b0ca674a23893db909a98582969c9b537be4ced78c505d" -dependencies = [ - "bitflags", - "futures-channel", - "futures-core", - "futures-executor", - "futures-task", - "glib-macros 0.15.11", - "glib-sys 0.15.10", - "gobject-sys 0.15.10", - "libc", - "once_cell", - "smallvec", - "thiserror", -] - [[package]] name = "glib" version = "0.16.5" @@ -2228,7 +2279,7 @@ dependencies = [ "futures-executor", "futures-task", "futures-util", - "gio-sys 0.16.3", + "gio-sys", "glib-macros 0.16.3", "glib-sys 0.16.3", "gobject-sys 0.16.3", @@ -2254,21 +2305,6 @@ dependencies = [ "syn", ] -[[package]] -name = "glib-macros" -version = "0.15.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25a68131a662b04931e71891fb14aaf65ee4b44d08e8abc10f49e77418c86c64" -dependencies = [ - "anyhow", - "heck 0.4.0", - "proc-macro-crate 1.2.1", - "proc-macro-error", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "glib-macros" version = "0.16.3" @@ -2294,16 +2330,6 @@ dependencies = [ "system-deps 1.3.2", ] -[[package]] -name = "glib-sys" -version = "0.15.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4" -dependencies = [ - "libc", - "system-deps 6.0.3", -] - [[package]] name = "glib-sys" version = "0.16.3" @@ -2331,17 +2357,6 @@ dependencies = [ "system-deps 1.3.2", ] -[[package]] -name = "gobject-sys" -version = "0.15.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a" -dependencies = [ - "glib-sys 0.15.10", - "libc", - "system-deps 6.0.3", -] - [[package]] name = "gobject-sys" version = "0.16.3" @@ -2370,7 +2385,7 @@ dependencies = [ "gstreamer-sys", "libc", "muldiv", - "num-rational", + "num-rational 0.3.2", "once_cell", "paste", "pretty-hex", @@ -2488,9 +2503,9 @@ dependencies = [ [[package]] name = "gtk" -version = "0.15.5" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e3004a2d5d6d8b5057d2b57b3712c9529b62e82c77f25c1fecde1fd5c23bd0" +checksum = "e4d3507d43908c866c805f74c9dd593c0ce7ba5c38e576e41846639cdcd4bee6" dependencies = [ "atk", "bitflags", @@ -2500,7 +2515,7 @@ dependencies = [ "gdk", "gdk-pixbuf", "gio", - "glib 0.15.12", + "glib 0.16.5", "gtk-sys", "gtk3-macros", "libc", @@ -2511,17 +2526,17 @@ dependencies = [ [[package]] name = "gtk-sys" -version = "0.15.3" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5bc2f0587cba247f60246a0ca11fe25fb733eabc3de12d1965fc07efab87c84" +checksum = "89b5f8946685d5fe44497007786600c2f368ff6b1e61a16251c89f72a97520a3" dependencies = [ "atk-sys", "cairo-sys-rs", "gdk-pixbuf-sys", "gdk-sys", - "gio-sys 0.15.10", - "glib-sys 0.15.10", - "gobject-sys 0.15.10", + "gio-sys", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", "libc", "pango-sys", "system-deps 6.0.3", @@ -2529,9 +2544,9 @@ dependencies = [ [[package]] name = "gtk3-macros" -version = "0.15.4" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24f518afe90c23fba585b2d7697856f9e6a7bbc62f65588035e66f6afb01a2e9" +checksum = "8cfd6557b1018b773e43c8de9d0d13581d6b36190d0501916cbec4731db5ccff" dependencies = [ "anyhow", "proc-macro-crate 1.2.1", @@ -2560,6 +2575,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0" +dependencies = [ + "crunchy", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -2781,10 +2805,29 @@ dependencies = [ "byteorder", "color_quant", "num-iter", - "num-rational", + "num-rational 0.3.2", "num-traits 0.2.15", - "png", - "tiff", + "png 0.16.8", + "tiff 0.6.1", +] + +[[package]] +name = "image" +version = "0.24.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b7ea949b537b0fd0af141fff8c77690f2ce96f4f41f042ccb6c69c6c965945" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "jpeg-decoder 0.3.0", + "num-rational 0.4.1", + "num-traits 0.2.15", + "png 0.17.7", + "scoped_threadpool", + "tiff 0.8.1", ] [[package]] @@ -2915,6 +2958,20 @@ dependencies = [ "walkdir", ] +[[package]] +name = "jni" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + [[package]] name = "jni-sys" version = "0.3.0" @@ -2936,6 +2993,15 @@ version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" +[[package]] +name = "jpeg-decoder" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" +dependencies = [ + "rayon", +] + [[package]] name = "js-sys" version = "0.3.60" @@ -2955,6 +3021,17 @@ dependencies = [ "winapi-build", ] +[[package]] +name = "keyboard-types" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7668b7cff6a51fe61cdde64cd27c8a220786f399501b57ebe36f7d8112fd68" +dependencies = [ + "bitflags", + "serde 1.0.149", + "unicode-segmentation", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -2968,12 +3045,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] -name = "libappindicator" -version = "0.7.1" +name = "lebe" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2d3cb96d092b4824cb306c9e544c856a4cb6210c1081945187f7f1924b47e8" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + +[[package]] +name = "libappindicator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e1edfdc9b0853358306c6dfb4b77c79c779174256fe93d80c0b5ebca451a2f" dependencies = [ - "glib 0.15.12", + "glib 0.16.5", "gtk", "gtk-sys", "libappindicator-sys", @@ -2982,9 +3065,9 @@ dependencies = [ [[package]] name = "libappindicator-sys" -version = "0.7.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1b3b6681973cea8cc3bce7391e6d7d5502720b80a581c9a95c9cbaf592826aa" +checksum = "08fcb2bea89cee9613982501ec83eaa2d09256b24540ae463c52a28906163918" dependencies = [ "gtk-sys", "libloading", @@ -3085,6 +3168,25 @@ dependencies = [ "walkdir", ] +[[package]] +name = "libxdo" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00333b8756a3d28e78def82067a377de7fa61b24909000aeaa2b446a948d14db" +dependencies = [ + "libxdo-sys", +] + +[[package]] +name = "libxdo-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db23b9e7e2b7831bbd8aac0bbeeeb7b68cbebc162b227e7052e8e55829a09212" +dependencies = [ + "libc", + "x11 2.20.1", +] + [[package]] name = "link-cplusplus" version = "1.0.7" @@ -3340,12 +3442,41 @@ dependencies = [ "glob", ] +[[package]] +name = "muda" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66365a21dc5e322c6b6ba25c735d00153c57dd2eb377926aa50e3caf547b6f6" +dependencies = [ + "cocoa", + "crossbeam-channel", + "gdk", + "gdk-pixbuf", + "gtk", + "keyboard-types", + "libxdo", + "objc", + "once_cell", + "png 0.17.7", + "thiserror", + "windows-sys 0.45.0", +] + [[package]] name = "muldiv" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0419348c027fa7be448d2ae7ea0e4e04c2334c31dc4e74ab29f00a2a7ca69204" +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom", +] + [[package]] name = "ndk" version = "0.5.0" @@ -3616,6 +3747,17 @@ dependencies = [ "num-traits 0.2.15", ] +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg 1.1.0", + "num-integer", + "num-traits 0.2.15", +] + [[package]] name = "num-traits" version = "0.1.43" @@ -3728,7 +3870,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27f63c358b4fa0fbcfefd7c8be5cfc39c08ce2389f5325687e7762a48d30a5c1" dependencies = [ - "jni", + "jni 0.19.0", "ndk 0.6.0", "ndk-context", "num-derive", @@ -3747,9 +3889,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" [[package]] name = "openssl-probe" @@ -3783,20 +3925,15 @@ version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" -[[package]] -name = "padlock" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c10569378a1dacd9f30dbe7ae49e054d2c45dc2f8ee49899903e09c3924e8b6f" - [[package]] name = "pango" -version = "0.15.10" +version = "0.16.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e4045548659aee5313bde6c582b0d83a627b7904dd20dc2d9ef0895d414e4f" +checksum = "cdff66b271861037b89d028656184059e03b0b6ccb36003820be19f7200b1e94" dependencies = [ "bitflags", - "glib 0.15.12", + "gio", + "glib 0.16.5", "libc", "once_cell", "pango-sys", @@ -3804,12 +3941,12 @@ dependencies = [ [[package]] name = "pango-sys" -version = "0.15.10" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2a00081cde4661982ed91d80ef437c20eacaf6aa1a5962c0279ae194662c3aa" +checksum = "9e134909a9a293e04d2cc31928aa95679c5e4df954d0b85483159bd20d8f047f" dependencies = [ - "glib-sys 0.15.10", - "gobject-sys 0.15.10", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", "libc", "system-deps 6.0.3", ] @@ -4005,6 +4142,18 @@ dependencies = [ "miniz_oxide 0.3.7", ] +[[package]] +name = "png" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638" +dependencies = [ + "bitflags", + "crc32fast", + "flate2", + "miniz_oxide 0.6.2", +] + [[package]] name = "polling" version = "2.5.1" @@ -4547,7 +4696,7 @@ dependencies = [ "cc", "libc", "once_cell", - "spin", + "spin 0.5.2", "untrusted", "web-sys", "winapi 0.3.9", @@ -4690,15 +4839,13 @@ dependencies = [ "flutter_rust_bridge", "flutter_rust_bridge_codegen", "fruitbasket", - "glib 0.16.5", - "gtk", "hbb_common", "hound", + "image 0.24.5", "impersonate_system", "include_dir", - "jni", + "jni 0.19.0", "lazy_static", - "libappindicator", "libc", "libpulse-binding", "libpulse-simple-binding", @@ -4730,7 +4877,8 @@ dependencies = [ "sys-locale", "sysinfo", "system_shutdown", - "tray-item", + "tao", + "tray-icon", "trayicon", "url", "uuid", @@ -4868,6 +5016,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +[[package]] +name = "scoped_threadpool" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" + [[package]] name = "scopeguard" version = "1.1.0" @@ -4889,7 +5043,7 @@ dependencies = [ "gstreamer-video", "hbb_common", "hwcodec", - "jni", + "jni 0.19.0", "lazy_static", "libc", "log", @@ -5127,6 +5281,12 @@ version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +[[package]] +name = "simd-adler32" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14a5df39617d7c8558154693a1bb8157a4aab8179209540cc0b10e5dc24e0b18" + [[package]] name = "simple_rc" version = "0.1.0" @@ -5217,6 +5377,15 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "spin" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dccf47db1b41fa1573ed27ccf5e08e3ca771cb994f776668c5ebda893b248fc" +dependencies = [ + "lock_api", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -5399,6 +5568,61 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "tao" +version = "0.17.0" +source = "git+https://github.com/tauri-apps/tao?branch=muda#676bd90a80286b893d8850cc4e3813a0c4a27dcf" +dependencies = [ + "bitflags", + "cairo-rs", + "cc", + "cocoa", + "core-foundation 0.9.3", + "core-graphics 0.22.3", + "crossbeam-channel", + "dispatch", + "gdk", + "gdk-pixbuf", + "gdk-sys", + "gdkwayland-sys", + "gdkx11-sys", + "gio", + "glib 0.16.5", + "glib-sys 0.16.3", + "gtk", + "image 0.24.5", + "instant", + "jni 0.20.0", + "lazy_static", + "libc", + "log", + "ndk 0.6.0", + "ndk-context", + "ndk-sys 0.3.0", + "objc", + "once_cell", + "parking_lot 0.12.1", + "png 0.17.7", + "raw-window-handle 0.5.0", + "scopeguard", + "tao-macros", + "unicode-segmentation", + "uuid", + "windows 0.44.0", + "windows-implement", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.0" +source = "git+https://github.com/tauri-apps/tao?branch=muda#676bd90a80286b893d8850cc4e3813a0c4a27dcf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tap" version = "1.0.1" @@ -5509,11 +5733,22 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437" dependencies = [ - "jpeg-decoder", + "jpeg-decoder 0.1.22", "miniz_oxide 0.4.4", "weezl", ] +[[package]] +name = "tiff" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7449334f9ff2baf290d55d73983a7d6fa15e01198faef72af07e2a8db851e471" +dependencies = [ + "flate2", + "jpeg-decoder 0.3.0", + "weezl", +] + [[package]] name = "time" version = "0.1.45" @@ -5698,21 +5933,22 @@ dependencies = [ ] [[package]] -name = "tray-item" -version = "0.7.1" +name = "tray-icon" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0914b62e00e8f51241806cb9f9c4ea6b10c75d94cae02c89278de6f4b98c7d0f" +checksum = "d62801a4da61bb100b8d3174a5a46fed7b6ea03cc2ae93ee7340793b09a94ce3" dependencies = [ "cocoa", "core-graphics 0.22.3", - "gtk", + "crossbeam-channel", + "dirs-next", "libappindicator", - "libc", + "muda", "objc", - "objc-foundation", - "objc_id", - "padlock", - "winapi 0.3.9", + "once_cell", + "png 0.17.7", + "thiserror", + "windows-sys 0.45.0", ] [[package]] @@ -5811,9 +6047,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.2.2" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" +checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" dependencies = [ "getrandom", ] @@ -6242,6 +6478,39 @@ dependencies = [ "windows_x86_64_msvc 0.34.0", ] +[[package]] +name = "windows" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-targets", +] + +[[package]] +name = "windows-implement" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce87ca8e3417b02dc2a8a22769306658670ec92d78f1bd420d6310a67c245c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "853f69a591ecd4f810d29f17e902d40e349fb05b0b11fff63b08b826bfe39c7f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-service" version = "0.4.0" @@ -6287,19 +6556,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ "windows_aarch64_gnullvm", - "windows_aarch64_msvc 0.42.0", - "windows_i686_gnu 0.42.0", - "windows_i686_msvc 0.42.0", - "windows_x86_64_gnu 0.42.0", + "windows_aarch64_msvc 0.42.1", + "windows_i686_gnu 0.42.1", + "windows_i686_msvc 0.42.1", + "windows_x86_64_gnu 0.42.1", "windows_x86_64_gnullvm", - "windows_x86_64_msvc 0.42.0", + "windows_x86_64_msvc 0.42.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc 0.42.1", + "windows_i686_gnu 0.42.1", + "windows_i686_msvc 0.42.1", + "windows_x86_64_gnu 0.42.1", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc 0.42.1", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" [[package]] name = "windows_aarch64_msvc" @@ -6327,9 +6620,9 @@ checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" [[package]] name = "windows_aarch64_msvc" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" [[package]] name = "windows_i686_gnu" @@ -6357,9 +6650,9 @@ checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" [[package]] name = "windows_i686_gnu" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" [[package]] name = "windows_i686_msvc" @@ -6387,9 +6680,9 @@ checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" [[package]] name = "windows_i686_msvc" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" [[package]] name = "windows_x86_64_gnu" @@ -6417,15 +6710,15 @@ checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" [[package]] name = "windows_x86_64_gnu" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" [[package]] name = "windows_x86_64_msvc" @@ -6453,9 +6746,9 @@ checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" [[package]] name = "windows_x86_64_msvc" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" [[package]] name = "winit" @@ -6566,12 +6859,12 @@ dependencies = [ [[package]] name = "x11-dl" -version = "2.20.1" +version = "2.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1536d6965a5d4e573c7ef73a2c15ebcd0b2de3347bdf526c34c297c00ac40f0" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" dependencies = [ - "lazy_static", "libc", + "once_cell", "pkg-config", ] @@ -6703,6 +6996,15 @@ dependencies = [ "libc", ] +[[package]] +name = "zune-inflate" +version = "0.2.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c473377c11c4a3ac6a2758f944cd336678e9c977aa0abf54f6450cf77e902d6d" +dependencies = [ + "simd-adler32", +] + [[package]] name = "zvariant" version = "3.9.0" diff --git a/Cargo.toml b/Cargo.toml index b315024e9..9588d10b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,7 +86,6 @@ arboard = "2.0" system_shutdown = "3.0.0" [target.'cfg(target_os = "windows")'.dependencies] -#systray = { git = "https://github.com/open-trade/systray-rs" } trayicon = { git = "https://github.com/open-trade/trayicon-rs", features = ["winit"] } winit = "0.26" winapi = { version = "0.3", features = ["winuser"] } @@ -104,11 +103,15 @@ dispatch = "0.2" core-foundation = "0.9" core-graphics = "0.22" include_dir = "0.7.2" -tray-item = "0.7" # looks better than trayicon -dark-light = "0.2" +dark-light = "1.0" fruitbasket = "0.10.0" objc_id = "0.1.1" +[target.'cfg(any(target_os = "macos", target_os = "linux"))'.dependencies] +tray-icon = "0.4" +tao = { git = "https://github.com/tauri-apps/tao", branch = "muda" } +image = "0.24" + [target.'cfg(target_os = "linux")'.dependencies] psimple = { package = "libpulse-simple-binding", version = "2.25" } pulse = { package = "libpulse-binding", version = "2.26" } @@ -118,9 +121,6 @@ mouce = { git="https://github.com/fufesou/mouce.git" } evdev = { git="https://github.com/fufesou/evdev" } dbus = "0.9" dbus-crossroads = "0.5" -gtk = "0.15" -libappindicator = "0.7" -glib = "0.16.5" backtrace = "0.3" [target.'cfg(target_os = "android")'.dependencies] @@ -157,7 +157,6 @@ 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", "curl", "libvdpau1", "libva2"] osx_minimum_system_version = "10.14" -resources = ["res/mac-tray-light.png","res/mac-tray-dark.png", "res/mac-tray-light-x2.png","res/mac-tray-dark-x2.png"] #https://github.com/johnthagen/min-sized-rust [profile.release] diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj index 066560203..0019335ef 100644 --- a/flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter/macos/Runner.xcodeproj/project.pbxproj @@ -26,10 +26,6 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; - 7E4BCD762966B0EC006D24E2 /* mac-tray-light.png in Resources */ = {isa = PBXBuildFile; fileRef = 7E4BCD742966B0EC006D24E2 /* mac-tray-light.png */; }; - 7E4BCD772966B0EC006D24E2 /* mac-tray-dark.png in Resources */ = {isa = PBXBuildFile; fileRef = 7E4BCD752966B0EC006D24E2 /* mac-tray-dark.png */; }; - 7E881462296E98EE00A0C54F /* mac-tray-light-x2.png in Resources */ = {isa = PBXBuildFile; fileRef = 7E881461296E98ED00A0C54F /* mac-tray-light-x2.png */; }; - 7E881464296E991200A0C54F /* mac-tray-dark-x2.png in Resources */ = {isa = PBXBuildFile; fileRef = 7E881463296E991200A0C54F /* mac-tray-dark-x2.png */; }; 84010BA8292CF66600152837 /* liblibrustdesk.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 84010BA7292CF66600152837 /* liblibrustdesk.dylib */; settings = {ATTRIBUTES = (Weak, ); }; }; 84010BA9292CF68300152837 /* liblibrustdesk.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 84010BA7292CF66600152837 /* liblibrustdesk.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; C5E54335B73C89F72DB1B606 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 26C84465887F29AE938039CB /* Pods_Runner.framework */; }; @@ -78,10 +74,6 @@ 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; 7436B85D94E8F7B5A9324869 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 7E4BCD742966B0EC006D24E2 /* mac-tray-light.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "mac-tray-light.png"; path = "../../res/mac-tray-light.png"; sourceTree = ""; }; - 7E4BCD752966B0EC006D24E2 /* mac-tray-dark.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "mac-tray-dark.png"; path = "../../res/mac-tray-dark.png"; sourceTree = ""; }; - 7E881461296E98ED00A0C54F /* mac-tray-light-x2.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "mac-tray-light-x2.png"; path = "../../res/mac-tray-light-x2.png"; sourceTree = ""; }; - 7E881463296E991200A0C54F /* mac-tray-dark-x2.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "mac-tray-dark-x2.png"; path = "../../res/mac-tray-dark-x2.png"; sourceTree = ""; }; 84010BA7292CF66600152837 /* liblibrustdesk.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = liblibrustdesk.dylib; path = ../../target/release/liblibrustdesk.dylib; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; C3BB669FF6190AE1B11BCAEA /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; @@ -135,10 +127,6 @@ 33CC11242044D66E0003C045 /* Resources */ = { isa = PBXGroup; children = ( - 7E881463296E991200A0C54F /* mac-tray-dark-x2.png */, - 7E881461296E98ED00A0C54F /* mac-tray-light-x2.png */, - 7E4BCD752966B0EC006D24E2 /* mac-tray-dark.png */, - 7E4BCD742966B0EC006D24E2 /* mac-tray-light.png */, 33CC10F22044A3C60003C045 /* Assets.xcassets */, 33CC10F42044A3C60003C045 /* MainMenu.xib */, 33CC10F72044A3C60003C045 /* Info.plist */, @@ -265,12 +253,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 7E881462296E98EE00A0C54F /* mac-tray-light-x2.png in Resources */, - 7E4BCD762966B0EC006D24E2 /* mac-tray-light.png in Resources */, - 7E4BCD772966B0EC006D24E2 /* mac-tray-dark.png in Resources */, 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, - 7E881464296E991200A0C54F /* mac-tray-dark-x2.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/res/mac-tray-dark-x2.png b/res/mac-tray-dark-x2.png index bdd48ad15ade67946a7c45b5ff1896ce96878ca3..595b850aef971e9e756aa55222318de018782cad 100644 GIT binary patch literal 703 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sT>(BJu0UD}24rMpfJ_Mq35WoM z3uH@5N+OFxxDYiES-2Lspt!g=L`qgx7A^>3NJ~pY7(jC%YzP_1Z50M|jc!ShUogX; zPhSLOC0_OOy}g+iuK4}l*`4VIlcIDUmnQ|Oi*B36aqQ@-1wBRP2P)&eYL+XTdN43B zZufL?4DmQVb=vi5O$GuE?H>AC3JaK$)?5GoU!AQiX~whbJLkEboF}(1XWW=Ln}sRR z=X_+|LH08R&9aXcTQ0c%j>A-~_v}yZMZs=~>+Sj`K4q~oZ?swZF20ws{#9mO7xS6p zv*M-xf7X8cr|q%C9{xh}55NDsj0jYI_dLJWc-Gc`{^~EpCq?P|-`<|ox1&a!|7`f7 z5dVh?i~dK=6|`r*x4*bzUT@8U#6LGryMOdJ5%^R4*!DTsJQy9WZr-qC)s$0`2i#6F zNleqSH+`bDAZK-ja!}j4ntRNrjlzREhrX;Y`fbcAG9%*mI0B4JkNw{^9Q zbb@w#(7L-<*v-B;1yA-gmehKveC*NN{)O{p|Gi}R{xtf`KkHn#D@h0RldLx9+-!7w z9X)+YOzNDKzc0zH%gZ+MSKFQXka5QH$Ey}6-w8I}r{h{XueFVdQ&MBb@04s+K-~a#s delta 1579 zcmV+`2Gse#1+ff}8Gi-<00374`G)`i010qNS#tmY8$kd78$kiGsWprM000?uMObuG zZ)S9NVRB^vcXxL#X>MzCV_|S*E^l&Yo9;Xs000HPNkl3Nt`OY^(%nZK-gTaul z>(3EUH#08<&EDYq5v*4^LbTO&%|Oe%kR0V(f}fnNYm8R)CMAY2S7If zs-T;TM05zizJGW;K3<6r&jEx&p`fN|pD^<#0F9NLv;eo6dB3J-kN9|bVqw<}4A=-Tx3^l@2z(SHEi+S)cV^Iia2Rh-E&oKB}- zP9~F+vJDE(0mCrf0B{(9T19i^7%P2MmvC~0K5d??YwggI*#)~ zB9Vv|>wh>Bi8M`5Pp<}W)v~Nh`N!_M1Ey(y%FH`G8lPn5-MX&t85$b;N2WnA7;Mya z{dH!ZH)qbA-e@#>y_hx>3N2Gr^#n4h2?N+A3%c#^FI*Lrg%Ia z_s~X~tohRbf_Z;mC!!^})MI89vTb_@yqW((2!HWvt;~nR;YUx^)1cw70jn z5r5Hg$^LiMYRu;`Dye(gFpPzY<2XH#+l<`OH0{gEeaVqZrH%l&B_UOas7E296_S1T z%C7T%7#|!G=G``kdlx#Dv(=&%5FXXFBAYWyPpNMHj+LjH9D#&a)%Ap&E5%y$q#`B_P6R3V}(lAXTn z!;@E5$%u&PiXyw_0$3Le25VK3n;|B_f5H_XBuBvdLs15I6v20~i<>xIskU6cG@Ng)j_bu3;G85z$*7nty$pE5*#D2#(|I25@thFv`q5LI~O9$`j#m_)!2q zGxM7snoXwD>0P-WGvjS!WMs@RjI98^MkYV{1HdXF#Bi~mOw%-7*If@_OxN|pvO}!3 zwY5`Gl&t_hg7@O_uF=uanQdpm+9)%3HZ(N+)ZgDfRl@w^%zOfOHsC1$$A18vb{wbA zah##Cv9XD)w%K*v2!LmZXcd50P+5Zbn-F3-GpCCNAVb#tDFDr7q`B%vHLBbb0G3;p zbv6G_&$WLK=7H*!=UryrAcVL&`+%q0=+S&N*(!uMTIhiE5DJAtK~+_MheXk-ia12H zB_5CODJGB}LaM5+!TmLV6Mw*lcszcrl*r2*_|u;sPh0}9ZuW#Ng!CM+EbAPAk5M}1 z@XI8C?LvqZGUfq)pBlUm|L@UqOaj=OPN#S0Kcr-+_+yX7ix8Jnz diff --git a/res/mac-tray-dark.png b/res/mac-tray-dark.png deleted file mode 100644 index a98fe63b0930e9e9358059dd6661e1eaadfd73fe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 535 zcmV+y0_gpTP);W&wBJ!Z>At9 zUc^FsksPeMS(97nWkF(EkrOS0N(DabN?WuIt^u z;B$9>BO)IF-2JYIY?;}!s;a6qO@pf5EX(qpWLefTv#(Lx$7c3;JRYAdYaxWNWoAb} z$KAVXW;+plz6u6tn&tq=09Dl;1@1-p*Q?-}zD4K0a`9En?)qsBjH-T){F@3$l=pv; zMY;QeU%40(07T^Wx&pwR$p1ru_faxmMTQV^;Azy&K+S9)FyL%9!b_E9*>y8}3tX9n zOjTcryOXBrgCt2#i=y}$Ldct@c_|`8cmJZQ;_kPAClL&=S5?&uiI=I<>0A|&k3hQS z9gbS9*2Cd&_-kQlM5G6_SAYiW&0&bPajtf|eIrSdZJ_V&*MQ$5Qn>q|X_|w{WO5q& Z{{Yw=yte_XgkS&w002ovPDHLkV1j~<^7a4# diff --git a/res/mac-tray-light-x2.png b/res/mac-tray-light-x2.png index 253450ecbc102a345187896f2b65ab1900d8fcac..2e27118884994f5573334acc2cd962e373903619 100644 GIT binary patch literal 728 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sGXs1=T!HleK?$zPvV;H~XH*j8 z7tHYa&;1DkuOch-WyHd`et&qoYkO|8lDq`p_pdk39xKpMpWN#&eCy<@nHdS@j~C6& zJm4{9{Q_xkK2AHGgg-)V!0xPnYml1&)oC>@L8Y!wu-I= z_u`*gZa0|f#k4^q$y}Rv;>*<62@Td2S10A&2)BOWvQDx4`gf<#&MQyrhOYj%Nqwqu z&Nj`LN4C{VmHpUzr}bp-(~L}e`MA@|6so{@4lW7dl#JlIeizW zt$}C3o_|rb0={}jm6@fquWtU-sqr^nE&Jj6g}>`>{yC!d??LRx-~UwaNj7*LS^R_f ze^w3i4fE%rkHjN?vWgsO39#7s=#7BM&+iKiszp;@eEP+$HD4!p!bhuyQ=U(r_Fk9e zjo%#7l%L2kx5d2YK=0>9zGvateD7HPALMdj}EVtg> zvr;tv;7z;d!FfL?-v9bXd0tiS(&!s=O6vm`_vT+-H~Yg%fA+_2v3KX`=YLIo_mD4e k&&g9$GD7;yH~#%#KWoL|&dIu_pj5!%>FVdQ&MBb@03sKs`Tzg` delta 1184 zcmV;R1Yi5u1*r*;8Gi-<00374`G)`i010qNS#tmY8$kd78$kiGsWprM000?uMObuG zZ)S9NVRB^vcXxL#X>MzCV_|S*E^l&Yo9;Xs000CwNklZK`teW5`?A@Y>|d7 zW>hM)X;SYUCncSKi|1VDdS>o9&->nUryrcnJ^%B*|L1+*^L{+{v`L}_ZUr6$`hk@| z53m^MNbgsHzkng&81Mt|9q@az6}L4JKo{^d@FZruEuxGgz+T`(U?j?gq|j@jq}L=> zRUTuK-jcK=)PGNic&4NmOE#Y6qNL|T0`Lx@6~HIJT~XZ}&RXC{k1%m)Heh3Qy37MC z^9qZ4U@PzrFtbTRAq)bmfKiV)uLrgR?=!vTPXp_5nuo?c@Hp@}Fryq_E=~gvVLjkx zssL`oUZ9T2@*u{6VXT+ulHV9n=7CAm=qyP`Bd5SINq-+n+9Ih}Qj#fqnWT-9-j!6T zdu}Mz=S`Pg>9cFH_p-uQB8*ohy(DQ)frVXeQXg~pMzO44vn z{S`@_l7FtP16zTco$vhtU`Oz5;inro2+YsXbO4)SVja93m9IoJX$aOgIH z1+WCT+gbQ-;I}wmZq7P&_W|=0U>&~0$QZCE(o_gvq;(aM0P7Oq9%sS(fOD~~0;pg? zc<4{CGpvV$aekus#;IGG0KHD};Yc$v3^;YYZGVy~6mQ(F05<@oq%a)Z2>j#FjU>QL z4&gAn5~k)FiP3@0O7wusrlr@#{d&wK!!2d>LXERb{| z6X~F&b3%4->aR{0Te1U#a~N z`zPOO;Oo>_9<}U68CmWsX8uI|k1@G_uX!jhJwJ~9BYGlxBeWYs%$Lbd;LE9Qgnta7 zg}~4Fl1^oD6K8}c8Qimf_#n!3pV0000JM1D&kPZ zKeHVam-;#MTs&TCZ+{R-Lh?&;a)L@m=G9&GM*UE~)H`)gt!3k5V>|@){2$-y+8~0W zdNL$2#iKeI6BOuU8>sV(E^q{#2YVSP16#l=Fa^|r<8D3DAigiz5&$Mfy_$ zoJNjHPI3j|bip%rV7MRU2wc{ZzX_-%;nX@js(a3259AKP&(M002ov JPDHLkV1ggSrT_o{ diff --git a/src/core_main.rs b/src/core_main.rs index 0af7026e9..e2f3f80e0 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -164,9 +164,6 @@ pub fn core_main() -> Option> { #[cfg(feature = "with_rc")] hbb_common::allow_err!(crate::rc::extract_resources(&args[1])); return None; - } else if args[0] == "--tray" { - crate::tray::start_tray(); - return None; } else if args[0] == "--portable-service" { crate::platform::elevate_or_run_as_system( click_setup, @@ -183,34 +180,24 @@ pub fn core_main() -> Option> { std::fs::remove_file(&args[1]).ok(); return None; } + } else if args[0] == "--tray" { + crate::tray::start_tray(); + return None; } else if args[0] == "--service" { log::info!("start --service"); crate::start_os_service(); return None; } else if args[0] == "--server" { log::info!("start --server with user {}", crate::username()); - #[cfg(target_os = "windows")] + #[cfg(any(target_os = "linux", target_os = "windows"))] { crate::start_server(true); return None; } #[cfg(target_os = "macos")] - { - std::thread::spawn(move || crate::start_server(true)); - crate::platform::macos::hide_dock(); - crate::ui::macos::make_tray(); - return None; - } - #[cfg(target_os = "linux")] { let handler = std::thread::spawn(move || crate::start_server(true)); - // Show the tray in linux only when current user is a normal user - // [Note] - // As for GNOME, the tray cannot be shown in user's status bar. - // As for KDE, the tray can be shown without user's theme. - if !crate::platform::is_root() { - crate::tray::start_tray(); - } + crate::tray::start_tray(); // prevent server exit when encountering errors from tray hbb_common::allow_err!(handler.join()); } @@ -349,6 +336,6 @@ fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option bool { #[cfg(target_os = "macos")] #[no_mangle] pub extern "C" fn handle_applicationShouldOpenUntitledFile() { - crate::platform::macos::handle_applicationShouldOpenUntitledFile(); + crate::platform::macos::handle_application_should_open_untitled_file(); } #[cfg(windows)] diff --git a/src/platform/macos.rs b/src/platform/macos.rs index b61f51732..0c8c51455 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -17,7 +17,7 @@ use core_graphics::{ display::{kCGNullWindowID, kCGWindowListOptionOnScreenOnly, CGWindowListCopyWindowInfo}, window::{kCGWindowName, kCGWindowOwnerPID}, }; -use hbb_common::{bail, log}; +use hbb_common::{allow_err, bail, log}; use include_dir::{include_dir, Dir}; use objc::{class, msg_send, sel, sel_impl}; use scrap::{libc::c_void, quartz::ffi::*}; @@ -578,12 +578,12 @@ fn check_main_window() -> bool { false } -pub fn handle_applicationShouldOpenUntitledFile() { +pub fn handle_application_should_open_untitled_file() { hbb_common::log::debug!("icon clicked on finder"); let x = std::env::args().nth(1).unwrap_or_default(); - if x == "--server" || x == "--cm" { + if x == "--server" || x == "--cm" || x == "--tray" { if crate::platform::macos::check_main_window() { - crate::ipc::send_url_scheme("rustdesk:".into()); + allow_err!(crate::ipc::send_url_scheme("rustdesk:".into())); } } } diff --git a/src/tray.rs b/src/tray.rs index e41a616de..b449bbbd3 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -1,11 +1,5 @@ -#[cfg(any(target_os = "linux", target_os = "windows"))] +#[cfg(any(target_os = "windows"))] use super::ui_interface::get_option_opt; -#[cfg(target_os = "linux")] -use hbb_common::log::{debug, error, info}; -#[cfg(target_os = "linux")] -use libappindicator::AppIndicator; -#[cfg(target_os = "linux")] -use std::env::temp_dir; #[cfg(target_os = "windows")] use std::sync::{Arc, Mutex}; #[cfg(target_os = "windows")] @@ -83,119 +77,10 @@ pub fn start_tray() { }); } -/// Start a tray icon in Linux -/// -/// [Block] -/// This function will block current execution, show the tray icon and handle events. -#[cfg(target_os = "linux")] -pub fn start_tray() { - use std::time::Duration; - - use glib::{clone, Continue}; - use gtk::traits::{GtkMenuItemExt, MenuShellExt, WidgetExt}; - - info!("configuring tray"); - // init gtk context - if let Err(err) = gtk::init() { - error!("Error when starting the tray: {}", err); - return; - } - if let Some(mut appindicator) = get_default_app_indicator() { - let mut menu = gtk::Menu::new(); - let stoped = is_service_stopped(); - // start/stop service - let label = if stoped { - crate::client::translate("Start Service".to_owned()) - } else { - crate::client::translate("Stop service".to_owned()) - }; - let menu_item_service = gtk::MenuItem::with_label(label.as_str()); - menu_item_service.connect_activate(move |_| { - let _lock = crate::ui_interface::SENDER.lock().unwrap(); - change_service_state(); - }); - menu.append(&menu_item_service); - // show tray item - menu.show_all(); - appindicator.set_menu(&mut menu); - // start event loop - info!("Setting tray event loop"); - // check the connection status for every second - glib::timeout_add_local( - Duration::from_secs(1), - clone!(@strong menu_item_service as item => move || { - let _lock = crate::ui_interface::SENDER.lock().unwrap(); - update_tray_service_item(&item); - // continue to trigger the next status check - Continue(true) - }), - ); - gtk::main(); - } else { - error!("Tray process exit now"); - } -} - -#[cfg(target_os = "linux")] -fn change_service_state() { - if is_service_stopped() { - debug!("Now try to start service"); - crate::ipc::set_option("stop-service", ""); - } else { - debug!("Now try to stop service"); - crate::ipc::set_option("stop-service", "Y"); - } -} - -#[cfg(target_os = "linux")] -#[inline] -fn update_tray_service_item(item: >k::MenuItem) { - use gtk::traits::GtkMenuItemExt; - - if is_service_stopped() { - item.set_label(&crate::client::translate("Start Service".to_owned())); - } else { - item.set_label(&crate::client::translate("Stop service".to_owned())); - } -} - -#[cfg(target_os = "linux")] -fn get_default_app_indicator() -> Option { - use libappindicator::AppIndicatorStatus; - use std::io::Write; - - let icon = include_bytes!("../res/icon.png"); - // appindicator does not support icon buffer, so we write it to tmp folder - let mut icon_path = temp_dir(); - icon_path.push("RustDesk"); - icon_path.push("rustdesk.png"); - match std::fs::File::create(icon_path.clone()) { - Ok(mut f) => { - f.write_all(icon).unwrap(); - // set .png icon file to be writable - // this ensures successful file rewrite when switching between x11 and wayland. - let mut perm = f.metadata().unwrap().permissions(); - if perm.readonly() { - perm.set_readonly(false); - f.set_permissions(perm).unwrap(); - } - } - Err(err) => { - error!("Error when writing icon to {:?}: {}", icon_path, err); - return None; - } - } - debug!("write temp icon complete"); - let mut appindicator = AppIndicator::new("RustDesk", icon_path.to_str().unwrap_or("rustdesk")); - appindicator.set_label("RustDesk", "A remote control software."); - appindicator.set_status(AppIndicatorStatus::Active); - Some(appindicator) -} - /// Check if service is stoped. /// Return [`true`] if service is stoped, [`false`] otherwise. #[inline] -#[cfg(any(target_os = "linux", target_os = "windows"))] +#[cfg(any(target_os = "windows"))] fn is_service_stopped() -> bool { if let Some(v) = get_option_opt("stop-service") { v == "Y" @@ -204,47 +89,68 @@ fn is_service_stopped() -> bool { } } -#[cfg(target_os = "macos")] -pub fn make_tray() { - extern "C" { - fn BackingScaleFactor() -> f32; - } - let f = unsafe { BackingScaleFactor() }; - use tray_item::TrayItem; - let mode = dark_light::detect(); - let icon_path = match mode { - dark_light::Mode::Dark => { - // still show big overflow icon in my test, so still use x1 png. - // let's do it with objc with svg support later. - // or use another tray crate, or find out in tauri (it has tray support) - if f > 2. { - "mac-tray-light-x2.png" - } else { - "mac-tray-light.png" - } - } - dark_light::Mode::Light => { - if f > 2. { - "mac-tray-dark-x2.png" - } else { - "mac-tray-dark.png" - } - } - }; - if let Ok(mut tray) = TrayItem::new(&crate::get_app_name(), icon_path) { - tray.add_label(&format!( - "{} {}", - crate::get_app_name(), - crate::lang::translate("Service is running".to_owned()) - )) - .ok(); +/// Start a tray icon in Linux +/// +/// [Block] +/// This function will block current execution, show the tray icon and handle events. +#[cfg(target_os = "linux")] +pub fn start_tray() {} - let inner = tray.inner_mut(); - inner.add_quit_item(&crate::lang::translate("Quit".to_owned())); - inner.display(); - } else { - loop { - std::thread::sleep(std::time::Duration::from_secs(3)); - } - } +#[cfg(target_os = "macos")] +pub fn start_tray() { + use hbb_common::{allow_err, log}; + allow_err!(make_tray()); +} + +#[cfg(target_os = "macos")] +pub fn make_tray() -> hbb_common::ResultType<()> { + // https://github.com/tauri-apps/tray-icon/blob/dev/examples/tao.rs + use hbb_common::anyhow::Context; + use tao::event_loop::{ControlFlow, EventLoopBuilder}; + use tray_icon::{TrayEvent, TrayIconBuilder}; + let mode = dark_light::detect(); + const LIGHT: &[u8] = include_bytes!("../res/mac-tray-light-x2.png"); + const DARK: &[u8] = include_bytes!("../res/mac-tray-dark-x2.png"); + let icon = match mode { + dark_light::Mode::Dark => DARK, + _ => LIGHT, + }; + let (icon_rgba, icon_width, icon_height) = { + let image = image::load_from_memory(icon) + .context("Failed to open icon path")? + .into_rgba8(); + let (width, height) = image.dimensions(); + let rgba = image.into_raw(); + (rgba, width, height) + }; + let icon = tray_icon::icon::Icon::from_rgba(icon_rgba, icon_width, icon_height) + .context("Failed to open icon")?; + + let event_loop = EventLoopBuilder::new().build(); + + let _tray_icon = Some( + TrayIconBuilder::new() + .with_tooltip(format!( + "{} {}", + crate::get_app_name(), + crate::lang::translate("Service is running".to_owned()) + )) + .with_icon(icon) + .build()?, + ); + + let tray_channel = TrayEvent::receiver(); + let mut docker_hiden = false; + + event_loop.run(move |_event, _, control_flow| { + if !docker_hiden { + crate::platform::macos::hide_dock(); + docker_hiden = true; + } + *control_flow = ControlFlow::Poll; + + if tray_channel.try_recv().is_ok() { + crate::platform::macos::handle_application_should_open_untitled_file(); + } + }); } diff --git a/src/ui/macos.rs b/src/ui/macos.rs index c6600608b..8a1fc990c 100644 --- a/src/ui/macos.rs +++ b/src/ui/macos.rs @@ -141,7 +141,7 @@ extern "C" fn application_should_handle_open_untitled_file( if !LAUNCHED { return YES; } - crate::platform::macos::handle_applicationShouldOpenUntitledFile(); + crate::platform::macos::handle_application_should_open_untitled_file(); let inner: *mut c_void = *this.get_ivar(APP_HANDLER_IVAR); let inner = &mut *(inner as *mut DelegateState); (*inner).command(AWAKE); @@ -258,10 +258,3 @@ pub fn show_dock() { NSApp().setActivationPolicy_(NSApplicationActivationPolicyRegular); } } - -pub fn make_tray() { - unsafe { - set_delegate(None); - } - crate::tray::make_tray(); -} From 1f5d68ef224ccdbb2ba00eed37419156ed640615 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 9 Feb 2023 22:55:56 +0900 Subject: [PATCH 082/199] workaround for https://github.com/rustdesk/rustdesk/issues/3131 --- flutter/lib/mobile/pages/remote_page.dart | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index c4b07b375..853f3168c 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -228,13 +228,18 @@ class _RemotePageState extends State { return false; }, child: getRawPointerAndKeyBody(Scaffold( - // resizeToAvoidBottomInset: true, + // workaround for https://github.com/rustdesk/rustdesk/issues/3131 + floatingActionButtonLocation: hideKeyboard + ? FABLocation(FloatingActionButtonLocation.endFloat, 0, -35) + : null, floatingActionButton: !showActionButton ? null : FloatingActionButton( mini: !hideKeyboard, child: Icon( - hideKeyboard ? Icons.expand_more : Icons.expand_less), + hideKeyboard ? Icons.expand_more : Icons.expand_less, + color: Colors.white, + ), backgroundColor: MyTheme.accent, onPressed: () { setState(() { @@ -1134,3 +1139,16 @@ void sendPrompt(bool isMac, String key) { gFFI.inputModel.ctrl = old; } } + +class FABLocation extends FloatingActionButtonLocation { + FloatingActionButtonLocation location; + double offsetX; + double offsetY; + FABLocation(this.location, this.offsetX, this.offsetY); + + @override + Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) { + final offset = location.getOffset(scaffoldGeometry); + return Offset(offset.dx + offsetX, offset.dy + offsetY); + } +} From 2a0c9699e8bf7c2393fcd863b256c976cde8e4dc Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 9 Feb 2023 23:00:34 +0900 Subject: [PATCH 083/199] move ImagePainter, and fix mobile drawImage quality --- flutter/lib/desktop/pages/remote_page.dart | 38 +-------------------- flutter/lib/mobile/pages/remote_page.dart | 27 +-------------- flutter/lib/utils/image.dart | 39 ++++++++++++++++++++++ 3 files changed, 41 insertions(+), 63 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index a7289335f..211d36c39 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -21,6 +21,7 @@ import '../../mobile/widgets/dialog.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; import '../../common/shared_state.dart'; +import '../../utils/image.dart'; import '../widgets/remote_menubar.dart'; import '../widgets/kb_layout_type_chooser.dart'; @@ -685,40 +686,3 @@ class CursorPaint extends StatelessWidget { ); } } - -class ImagePainter extends CustomPainter { - ImagePainter({ - required this.image, - required this.x, - required this.y, - required this.scale, - }); - - ui.Image? image; - double x; - double y; - double scale; - - @override - void paint(Canvas canvas, Size size) { - if (image == null) return; - if (x.isNaN || y.isNaN) return; - canvas.scale(scale, scale); - // https://github.com/flutter/flutter/issues/76187#issuecomment-784628161 - // https://api.flutter-io.cn/flutter/dart-ui/FilterQuality.html - var paint = Paint(); - if ((scale - 1.0).abs() > 0.001) { - paint.filterQuality = FilterQuality.medium; - if (scale > 10.00000) { - paint.filterQuality = FilterQuality.high; - } - } - canvas.drawImage( - image!, Offset(x.toInt().toDouble(), y.toInt().toDouble()), paint); - } - - @override - bool shouldRepaint(CustomPainter oldDelegate) { - return oldDelegate != this; - } -} diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 853f3168c..956b985a7 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -17,6 +17,7 @@ import '../../common/widgets/remote_input.dart'; import '../../models/input_model.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; +import '../../utils/image.dart'; import '../widgets/dialog.dart'; import '../widgets/gestures.dart'; @@ -898,32 +899,6 @@ class CursorPaint extends StatelessWidget { } } -class ImagePainter extends CustomPainter { - ImagePainter({ - required this.image, - required this.x, - required this.y, - required this.scale, - }); - - ui.Image? image; - double x; - double y; - double scale; - - @override - void paint(Canvas canvas, Size size) { - if (image == null) return; - canvas.scale(scale, scale); - canvas.drawImage(image!, Offset(x, y), Paint()); - } - - @override - bool shouldRepaint(CustomPainter oldDelegate) { - return oldDelegate != this; - } -} - void showOptions( BuildContext context, String id, OverlayDialogManager dialogManager) async { String quality = diff --git a/flutter/lib/utils/image.dart b/flutter/lib/utils/image.dart index 1f0d5b0cd..7a6bcbc15 100644 --- a/flutter/lib/utils/image.dart +++ b/flutter/lib/utils/image.dart @@ -1,6 +1,8 @@ import 'dart:typed_data'; import 'dart:ui' as ui; +import 'package:flutter/widgets.dart'; + Future decodeImageFromPixels( Uint8List pixels, int width, @@ -47,3 +49,40 @@ Future decodeImageFromPixels( descriptor.dispose(); return frameInfo.image; } + +class ImagePainter extends CustomPainter { + ImagePainter({ + required this.image, + required this.x, + required this.y, + required this.scale, + }); + + ui.Image? image; + double x; + double y; + double scale; + + @override + void paint(Canvas canvas, Size size) { + if (image == null) return; + if (x.isNaN || y.isNaN) return; + canvas.scale(scale, scale); + // https://github.com/flutter/flutter/issues/76187#issuecomment-784628161 + // https://api.flutter-io.cn/flutter/dart-ui/FilterQuality.html + var paint = Paint(); + if ((scale - 1.0).abs() > 0.001) { + paint.filterQuality = FilterQuality.medium; + if (scale > 10.00000) { + paint.filterQuality = FilterQuality.high; + } + } + canvas.drawImage( + image!, Offset(x.toInt().toDouble(), y.toInt().toDouble()), paint); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return oldDelegate != this; + } +} From 58f67481344524fafa2e041e7214744efe16c7b5 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 9 Feb 2023 23:14:24 +0900 Subject: [PATCH 084/199] fix physical keyboard on mobile does not work --- flutter/lib/common/widgets/remote_input.dart | 11 ++++- flutter/lib/mobile/pages/remote_page.dart | 52 ++++++++++---------- flutter/lib/models/input_model.dart | 14 +++--- 3 files changed, 44 insertions(+), 33 deletions(-) diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index 2fb409970..5833e760d 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/models/state_model.dart'; +import '../../common.dart'; import '../../models/input_model.dart'; class RawKeyFocusScope extends StatelessWidget { @@ -19,6 +20,13 @@ class RawKeyFocusScope extends StatelessWidget { @override Widget build(BuildContext context) { + final FocusOnKeyCallback? onKey; + if (isAndroid) { + onKey = inputModel.handleRawKeyEvent; + } else { + onKey = stateGlobal.grabKeyboard ? inputModel.handleRawKeyEvent : null; + } + return FocusScope( autofocus: true, child: Focus( @@ -26,8 +34,7 @@ class RawKeyFocusScope extends StatelessWidget { canRequestFocus: true, focusNode: focusNode, onFocusChange: onFocusChange, - onKey: - stateGlobal.grabKeyboard ? inputModel.handleRawKeyEvent : null, + onKey: onKey, child: child)); } } diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 956b985a7..9ae856250 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -581,9 +581,10 @@ class _RemotePageState extends State { child: Text(translate('Reset canvas')), value: 'reset_canvas')); } if (perms['keyboard'] != false) { - more.add(PopupMenuItem( - child: Text(translate('Physical Keyboard Input Mode')), - value: 'input-mode')); + // * Currently mobile does not enable map mode + // more.add(PopupMenuItem( + // child: Text(translate('Physical Keyboard Input Mode')), + // value: 'input-mode')); if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) { more.add(PopupMenuItem( child: Text('${translate('Insert')} Ctrl + Alt + Del'), @@ -638,8 +639,9 @@ class _RemotePageState extends State { ); if (value == 'cad') { bind.sessionCtrlAltDel(id: widget.id); - } else if (value == 'input-mode') { - changePhysicalKeyboardInputMode(); + // * Currently mobile does not enable map mode + // } else if (value == 'input-mode') { + // changePhysicalKeyboardInputMode(); } else if (value == 'lock') { bind.sessionLockScreen(id: widget.id); } else if (value == 'block-input') { @@ -701,26 +703,26 @@ class _RemotePageState extends State { })); } - void changePhysicalKeyboardInputMode() async { - var current = await bind.sessionGetKeyboardMode(id: widget.id) ?? "legacy"; - gFFI.dialogManager.show((setState, close) { - void setMode(String? v) async { - await bind.sessionPeerOption( - id: widget.id, name: "keyboard-mode", value: v ?? ""); - setState(() => current = v ?? ''); - Future.delayed(Duration(milliseconds: 300), close); - } - - return CustomAlertDialog( - title: Text(translate('Physical Keyboard Input Mode')), - content: Column(mainAxisSize: MainAxisSize.min, children: [ - getRadio('Legacy mode', 'legacy', current, setMode, - contentPadding: EdgeInsets.zero), - getRadio('Map mode', 'map', current, setMode, - contentPadding: EdgeInsets.zero), - ])); - }, clickMaskDismiss: true); - } + // * Currently mobile does not enable map mode + // void changePhysicalKeyboardInputMode() async { + // var current = await bind.sessionGetKeyboardMode(id: widget.id) ?? "legacy"; + // gFFI.dialogManager.show((setState, close) { + // void setMode(String? v) async { + // await bind.sessionSetKeyboardMode(id: widget.id, value: v ?? ""); + // setState(() => current = v ?? ''); + // Future.delayed(Duration(milliseconds: 300), close); + // } + // + // return CustomAlertDialog( + // title: Text(translate('Physical Keyboard Input Mode')), + // content: Column(mainAxisSize: MainAxisSize.min, children: [ + // getRadio('Legacy mode', 'legacy', current, setMode, + // contentPadding: EdgeInsets.zero), + // getRadio('Map mode', 'map', current, setMode, + // contentPadding: EdgeInsets.zero), + // ])); + // }, clickMaskDismiss: true); + // } Widget getHelpTools() { final keyboard = isKeyboardShown(); diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 8c37f50bd..c37d01860 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -58,9 +58,12 @@ class InputModel { InputModel(this.parent); KeyEventResult handleRawKeyEvent(FocusNode data, RawKeyEvent e) { - bind.sessionGetKeyboardMode(id: id).then((result) { - keyboardMode = result.toString(); - }); + // * Currently mobile does not enable map mode + if (isDesktop) { + bind.sessionGetKeyboardMode(id: id).then((result) { + keyboardMode = result.toString(); + }); + } final key = e.logicalKey; if (e is RawKeyDownEvent) { @@ -93,10 +96,9 @@ class InputModel { } } - if (keyboardMode == 'map') { + // * Currently mobile does not enable map mode + if (isDesktop && keyboardMode == 'map') { mapKeyboardMode(e); - } else if (keyboardMode == 'translate') { - legacyKeyboardMode(e); } else { legacyKeyboardMode(e); } From 628fa513f7402550c23ac96e63bd17958fc1f6d5 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 9 Feb 2023 23:36:24 +0900 Subject: [PATCH 085/199] mobile remote_page.dart HelpTools add 'Insert' --- flutter/lib/mobile/pages/remote_page.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 9ae856250..54b6f1d47 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -814,6 +814,9 @@ class _RemotePageState extends State { wrap('End', () { inputModel.inputKey('VK_END'); }), + wrap('Ins', () { + inputModel.inputKey('VK_INSERT'); + }), wrap('Del', () { inputModel.inputKey('VK_DELETE'); }), From 73a2f41794a81603f8b603ac3a3c92e3f39fbe57 Mon Sep 17 00:00:00 2001 From: "Miguel F. G" <116861809+flusheDData@users.noreply.github.com> Date: Thu, 9 Feb 2023 16:18:36 +0100 Subject: [PATCH 086/199] Update es.rs New terms added --- src/lang/es.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 220447454..939a4831f 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -446,8 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", "Otras opciones predeterminadas"), - ("Voice call", ""), - ("Text chat", ""), - ("Stop voice call", ""), + ("Voice call", "Llamada de voz"), + ("Text chat", "Chat de texto"), + ("Stop voice call", "Detener llamada de voz"), ].iter().cloned().collect(); } From 37a3185c1c92c7fc69c016ada8d24f5dda8eea10 Mon Sep 17 00:00:00 2001 From: solokot Date: Thu, 9 Feb 2023 20:17:34 +0300 Subject: [PATCH 087/199] Update ru.rs --- src/lang/ru.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 1e6c6962a..1792eccce 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -415,7 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Если у вас видеокарта Nvidia и удалённое окно закрывается сразу после подключения, может помочь установка драйвера Nouveau и выбор использования программной визуализации. Потребуется перезапуск."), ("Always use software rendering", "Использовать программную визуализацию"), ("config_input", "Чтобы управлять удалённым рабочим столом с помощью клавиатуры, необходимо предоставить RustDesk разрешения \"Мониторинг ввода\"."), - ("config_microphone", ""), + ("config_microphone", "Чтобы разговаривать с удалённой стороной, необходимо предоставить RustDesk разрешение \"Запись аудио\"."), ("request_elevation_tip", "Также можно запросить повышение прав, если кто-то есть на удалённой стороне."), ("Wait", "Ждите"), ("Elevation Error", "Ошибка повышения прав"), @@ -435,19 +435,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Medium", "Средний"), ("Strong", "Стойкий"), ("Switch Sides", "Переключить стороны"), - ("Please confirm if you want to share your desktop?", "Подтвердите, что хотите поделиться своим рабочим столом?"), - ("Closed as expected", ""), + ("Please confirm if you want to share your desktop?", "Подтверждаете, что хотите поделиться своим рабочим столом?"), + ("Closed as expected", "Закрыто по ожиданию"), ("Display", "Отображение"), ("Default View Style", "Стиль отображения по умолчанию"), ("Default Scroll Style", "Стиль прокрутки по умолчанию"), ("Default Image Quality", "Качество изображения по умолчанию"), ("Default Codec", "Кодек по умолчанию"), ("Bitrate", "Битрейт"), - ("FPS", "FPS"), + ("FPS", "Частота кадров"), ("Auto", "Авто"), ("Other Default Options", "Другие параметры по умолчанию"), - ("Voice call", ""), - ("Text chat", ""), - ("Stop voice call", ""), + ("Voice call", "Голосовой вызов"), + ("Text chat", "Текстовый чат"), + ("Stop voice call", "Завершить голосовой вызов"), ].iter().cloned().collect(); } From 9d88a06cdfffde6d28612799420479e2177a8bfa Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 10 Feb 2023 15:05:35 +0800 Subject: [PATCH 088/199] showTitle default to false, change titlebar logo --- flutter/assets/logo.ico | Bin 270398 -> 0 bytes flutter/assets/logo.png | Bin 8643 -> 0 bytes flutter/assets/logo.svg | 2 +- .../lib/desktop/widgets/tabbar_widget.dart | 2 +- .../lib/desktop/widgets/titlebar_widget.dart | 41 +----------------- res/logo.svg | 2 +- 6 files changed, 4 insertions(+), 43 deletions(-) delete mode 100644 flutter/assets/logo.ico delete mode 100644 flutter/assets/logo.png diff --git a/flutter/assets/logo.ico b/flutter/assets/logo.ico deleted file mode 100644 index d5080c1f778ffb5ee61fc8429f558bbc7050aade..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 270398 zcmeHQ2e?$#wLWN!(HMy(wx}d(il)6tOrA-6#3V0jVoWh6)|l#xqJs3!1p!f-$OQ{1 z2neEr2%F{?_3jN19jp{oWQ$6Quy2rVLUp@KXWBGkP zzgF?k97o`g1$i z^9%jqA^x|H>&p2wa{kCR8!jBG=EnLASHKx?Co_XDCu_rb7BzHE<#yENHcjF8UHpDz zyY(}biDDo>1`KDwox0#sIIN8JF4UIC`@5ZUxy}vvwUjxpb9=>ietfH3N|g&Z1THnq zaEmz>EsOo#ovw2}%bdT7>p6p82l-WM`}B7zE5$%@41iNJG8|*B`D1DLS7pZhhE8p+ z+faUOSKe#Crx=J71K`+Do(t3l_rhm%>38IM9LN394g8L?4ei5K28x05VPI7?_l_%_ zvEluaZg);%&iCZkR<4Kc<(E(I$}yESRda4D^Y1}$F+64$zY{lzES?9XP7pKnk+1lrP8UxwA!SldT{|qmDWrq9r zIF~W^VV|q=R~aY<A;$aQMhkz4#{?yFO#vgEPkT%2C^{y0;et+j#80!H=C_#=a1C zeQMjI__r7T>DC>%Ti;pDwgI(^kNBVbf^Kt;;p>LzxUcgb#XtfuP~UkP{&}A;yzpO{ zdH+4mjjVU|<N4%R#F=1Sr&i&S-+aU4 zenWogSzp@mqU}#wpNTWUMsEDCj@gA+BS~w!wnufU7(g7M0C{7du$;ZHeT>kTX;6$GDAvcHWUS*&dNInKOu#I2MY#+e9|1-akmq&H4 zGEfX89|H&29_TONfAzk9H|D;^^+>+nmD{RgVBiq%_jS*3zn*hC|9c|u{c8+g#fJ6| zX~2Nt`b6#nPOtX!Td8w@UHd2oQiOr}&N~_I*K@96z3*4ur%3l{7~`~!#pZIb^ZPB& z`|ncjEB6%xsl>pYyawp4x)-kd21gS${X9{NFA}vn)x0-4rda+b-aLR&S0Y5TeuGVynN;0e)Ij7l|=8nMl`Xr=?^+^e}MT4 z<}PXAlAUb?_p4MtG4Q`BkE;*;lxTQeqIpA!-hU?_yGs9TGxryRZ)U#n+#w7scKjOi zd2;E`Km3<_wh}EFL-bNBqCUUib<3B+saF_n(|crf0q$=J1urVotW5L2H{fv|Yv}vF zLG(f+qUEm;ZC^`-@iVM~`2yDGL3M#~d)5;~9~}%{vqK;8JZ3%7x_P{2>B@cHPk6rb z1)UcJ)lnGN*UxZY?S8|MWubduP27n&|LlK=-g=4X!@a>d<#vbRcLwMGpc}*OmvZZi zb;PQvW?t~%rKV3TBs={)xGvpg2=1%hpSz6w>7dbPi5~b4(X@w&)-Ph-@5^ODbbl{= zKG;40*6S-KMOVMI!@0e$|JExGoi+|qZM`k}S_nuMzjlSSrt~cf`oxZMXgRn8F_bZP3pfbXE z|Jbj2T|bd%@Aks`JI8fP@ zCOhnv4fqq$+Bb+Wjukp5D;J;8_dM~(((0a1$HQ)mevhJ2)f*&@jMzRM^jloy)Gx`E7HG;X2CiF?Nn>%wwj{y|fpb^SR%LQP$Agz0n@j zXVvRO{dr!~^aRCnXjqQH?#1T1jvG7re7ki3 z-;^;ejBkz|=s&WSGavUi|8HKCV+%Zcl{X6%RQj~f?T{PG_$hn9|ytJdK> z7iSmyc5rN>ht#*wMjn>C&e-$r`C~rQXZzixlB){eLluZQp(g`{$g{2|0`ZiiWE%zk~Oo5aE@aQCOw|nXuJIB7U$zn z|FitQuH|6Y#QWI(?-=iL$&CLA;(u1wo0f8nfh$=aK5e3vW8SeOU1m@Hm1yUtEJBj? z{&H)7K67nOH#UyRl7>0Mg`EImk4eX+)-PHeHD()^q5qKOvllksMmF#_*0nsDXyM4P zYa{1=h_{H?W_mstEypDHVFRx>_TzF;8E#Fmj{$pSKRSTz17Z;;x$Jn?#3%P7AHBvO zt|Z6}{^R|xP3C$(m#;X#gpZ;hnZsug=Vp@qe{$RE_wQ|093$lq{(U6<$X2iMhsm(y z2H$b$J?Yp+{2#Q{uiQp8VH*gJ!`D>-SAe+%#|>oJFkhAm`nPU%YKi z8rW~f1H^4q8=>gmp979?(BuKe`x`1%mw|H(hcAHzNjPM2~Ij9qKz z5cR6VGUv93g+18FAnrD7B-65Yf<6Gb9CSZ0JRF6E$5QVDU%riRmBJi*e*->ZPh;ms z&Xv<7*nXdzTrm%L_?K)G_`JzwmLwmkeA(Tc&M2J-q#ya1t{?QL~ynyqYvW>vJr-}A#`=owJkvZCdxz$Up z*oWGO52aU`>hFX1v#?uDnokeLSe#8y<5(V#T;VUCZ`9aAE*|(%zB!Btc}FGX4GP*H z?cQSck)oEN-ak11JDg|iUV`de!u5!2KW_-p;F~y}?N{xvQEze_QI8+;UPM!l$Gt*= z6vL?lauI7B!QlLivX2Ay5VIQdWLpdJZ6XHI*b0;H*4B9^)?he8ANFsel~e3Le+8x{ zcb7T$ijq^-+#6?oe_>z#9kw#x_$0m1FnKnpZO-I1+26Szm};+XQ1hJ|)L&R?e}Fu^ z>*m=v_G4UuFXH$PW{!`2HK{TVR4?wIF&{wxwqY^r$-=!^=xWVO&NX;yLUrWW+pbsc zw~zbqtzGm&wT~Y2BM`rA(H*z*{%^C5Z9jYCE{(W5CAzW%Qx9Hh>|bn|U1IFpzK(NK z+*y6LoEGlq;0gJ~D(e^<8{5tbU{eNP(p2=7 zIN#(|wJFXBYyze|qI-c7`hzF_$aZi$?HKuSp7+K;Ugv*B$Bta=Al4w}LYbIMmh1}v zbFeSZZHN9{UU}fWuWE)J+~3bOJY!lCH9xVuGO`AfyasykM<#El9UA4FLeFt-CB~pN zGWIYp6W=Dr%b*)LJ{_NzOE0_MCmPjQ=ZmFastfzuESqRYoEG>)W^#!niC31irQJDp zoG0($9JHA@^GQ9|ivTZe*~YRJXxo(2nN;%O=RS?fPInQTI!NwNr6Y zn*1;(EE*{n4~Gt#^X%p|OKXqny||p<`}p=)xwAd&Qy^Co@<^*bqm=>jwn~mkziuwa z;^DnX#X-0-=<+?;{=~>D9wgU-;-xrVu$G?OP3C$K`+bOe0b69%Wdt(d^Iqg5!dzQo zf&D4Ycvbh&$pCX|*v(5Uu|pz&il)WZSV_gE$~fRfj^4+clqaeJ{%#w z^>cM)=Awtb+nf<@d-KUaKl`+doAEl}-TBhkZ*oobz53I2Row-X2mJird`6zGVdsOG zgR0+nWH9I^jw{N3xYEjN55^;pq~fscSw!%mlU87p(&8l5bzHcE?BORQE%wXiWqJ+} zCMF*Ly}W&Y#2FrZOFX%E!j!G;_v-aO9RG!NA*sOkL-PiPH!jZVFG;(mF0e_5Ewb(W zNkaYMgDa_EUlM!5Nsw*aZNMCF&1^~9HFFX8_Qfq5eZJ8F_S+=+f@kbOxKGmYAvi~1 zPqE`&S^P(S9>g70{>hL*t5ZxIbxF2i$Z>H0w`9o2W}H5Ao5?pXr3b)X)3|%p#zPdQ z_Z8DTpU-ap_Hp|kf?)%BbbpLB*#f93n zN#nl-=}&OBD``&$KE4%SmW%%&vQEOa9WUTDz$!W81@?+Ch9yC^akSz2dUAA$n&8Y| zWBSR(GtS%Q43;xyfdA?X;2$s3ACbd<=yG%uwfBo-94Psjd=7y8;!<)6X7&J^`Nx0B z?T#P^L+sCHUA{9}4*z#XwlSR zE!!X>4z1UA=B4T+gVJ;=r2>9{8@DIKSw}=Rc=we_JrTc$A!RX7#?^- zJaVuVf7mY_-$70vj+_Iw$})}hhK<(y??_<3sl$l789#iUl}26oX>;RaiKbS{(cj|H zsyjb=jO!Zsjlfn*N}1GE?SQ>t0XZJtcv6b?Okd=s{`%W^`^%=ECz?G#4t{3#Q-5(U zoIwAL9KTWue9OJmUw<2~ypSJYVkar+;oMLh3#j2S@yf&|OtwCa2zC<^3haK7cg!aF zX$X#xu9OI}n@+WRuDue&)7ZG*7NOczn4uU)rYc|Z?B$S<859NR99D<)6 z=1daGy17IhzgBwPgJ4##4NuO!E+c~7mV|;BcFOn{P%(nN$daBd=A;Z zmYp2%BJ-sERucb_pH#>H(mB+g_y45CaLJ7SydRM?ek8p;II|VA{^xjPy8bVnL+#EX zLfjMy1$GF!|5v*1r1UF@11M$xe+$O|QT~^Xp|)ocRn3&Ze(s0n6W#aC((5h-|7ovB z-`~iIABB9eYX4I@huWMjC-2wBw}>A2PU&?|JMW~lf54yE{*U+FBoxGs)w~g&MbLP1W+9zQI-?o#ld_IeDtmFU232?*ji_OB`bz2LFgi9~aU$j}|)w^ja3`R_%4 z5`GJxZNzPpP~gk1{=cPj2(p^oO@ii3PWeZ#D!uL!u%GriG(4K)$K5LfKd8lwej5M7 zD<;u@e{>*z9U}Jc;G32IUiBB}?{U^X{n7ZbldZvoj+z6=D;}QwiyZ&Wms%_Tz3MN{ z-?#W8(UKRl?2G>$d2BWQr#BA4e&d7n@#_vXKrFd9@vP7GCjT~P$nhUPbV%oqe((z) z04Xt?SH12He}3Al*MuiyU`I;sn0-4n2e=<>!(K{C43ItBbpOVW4&%C}vW}eG5!==f z!51MeS=jvCB^-BOLctzjw~JMWwqWUnMiNkC>hSJ7&Hw2aH`q6oP{9A^?oj^w)nmD@ zoAZe-(kJ>nYVag?%; zvySI+>Idk*FZYW9uh${h_Kx@D@PA@w`7-k=<8uAJ)hR?Pr^?XfezqkaaGmP1T)OxB z9GtZu+%E$&$UmZ(@z0rz9oJ(rbctF_?yh+p{qnLL*EKnw2)XvKUXxJ3fAt;r&wpTM z_?VtfaU=O77l z7$e{>{^33;C^5b(YBL&6|A#ez6gvPQ408bZm`UP5n(G2xESn&Q`>4s=FB7#oRWT9< zE}Nb}v~-*t{buZe>#}Zn%^61f^d7xLH@c~;z1{Z~ z5Mj+3RvYy`{$vRok=4`Xa33|nI;8J!{k2DNkY8?({EE*3H_Fj7bUdoD`US_0SJ>Ab z*dqrQs0sYV5yLue*(B-ttxqFD91ckZ@k#FchUz&koV7WV*ZmXa*!yL%xp0JHB6Ljf zT6=u^tb}E}V;>-66U^(A(A&$lk9tpc89aGC5&SYF6<}i6zufDsdi5ut4(G|6>%qpe z@73HM-3u&he}x=T@2`-=d{Y49(i!?*Se?s7v8rKRI#=^Q>Dh&8KcJ?^>-6WhSS z_>R0E%(9nOF!xsh2_o{65;GkTEW>w@5`fYdss5G`94iQPqg+8Y1%jSfu9=Is5Z-2dfj0Q@Y;j& z`hJW>IEP06BwdEK)~_l1_fPC3Pxsm0Nb^LL2mA29k&@dnt9!g-F93NZd?26DC-R@yfsFO=09BztN~wcON3Z7QOY1y_vW*ehkin| zi|5zU*8ExRBgc2!vsCBh!sQcxkdq%Ui-GJr_NJcsKgB{=Sm=BaQPs?>bmhGt#u=HKZePCa?nDc{39S?SI;rZaN)jqfsUAp}SW9p(8c)fQ}9`Ca?+PX^P{JH5c zs_$U?EhW}#77N*T^h>Z0e(Wlp2l&;0Tek0g|;_Y3)zq!#o6 z+p0|dw+3n(;Auak;>*03`V{x?JhzryFwg5&TlpV^$3dxdAaw&7(cRjqK$9cgCwk1K4QC;#cbcw z?FT%b93y8wm6d7O?+uvu@R6Isef3_RL))-s%S9CQg+4oMx#<1Ekty;&OY{xO#n9n@ zV?BVpm7|&@`z#52d;@PVIo)mJeO717Umr03;$r)WZPQM4=i|&2=>j0 zbrrR|qPjoqGoSc9(fns+RPIO3*|6{5gKYvf zF3HuT-GAfUdHY&(UVwcxjI-$H;)*@q>6F{t3b3|l?Kx!L#@KXa#Bt?d5}a(!m`Q?dNLGUF{dI4tFGiD$B?{Mvkk$?T!+l`F^o{@$E`+kk*S$?iY)jz`KY|Bdf z?9Kde4KY!AUK)<>U|0Qen`E5dl`2cD`C;F0!+A?od`aSc`~1Sh<4MC9wq{m1c52AA zW1D?`sXWj3JNO|@dszK5lgNMA(mcy{0k+u(=fi2S&#*s;xClY@h}?#0DmceL?6IJ7 z)9dBR@Wp27n9nX59n7Z)IZb!Gn-2D)zpR?U=P2jP#jW-!;pnD$P5XS#{jd;wfgLaK zFSGl*kgPL{PKUut!z8BJA1cezQcy< zi9hOIzh8{lejRKB7CxUWd-W_+F}97Y?|)nyad&d(N01F{7yDhqbN-`~Ki{>Tr%~3w z9ow~G9sv9Lq!suK`NcQ(No|VD=S6q$qkwI=L|>{Xv99;d-`8WW8@U{kcGf+yGh_Ui z{B16Vg>mhZcUFpVAO3s~|5CYMO#f;7zWEn9E?v)Lt$C+b`j0OPzX|XqNdgTei--}k>BN;>wneWp*u+29KeIiCQZ zpV?mEqYE2;ue!y$7(i5gOYL9EtH&b$` zG5=>+#{WHAiAJ-{5^OrXwpV|zGAk7W*iRqZ&cxr8z8=6Ho*(|>{O|SNc8vcY?j?Hb zC893hWelqikM?~|^nDrT$MBC^Gg~%CCU^HAyZW^r^6h^x=D*mSsLh$t>%7$QL4TJX zUBHKMLPw4*xJo8h#P>ikNJE)qc;5M;hV(16?5fYm$8k>FnM-^ zuw(K&Cq~>)*v&~QSZ}SHL-h1N{hez{#E;e~8U0r?j+2d;(W|G+=m%i;_AH-S_{G=A z26FNyASUmIC7c7Twb|dpUaa4C>2)f*C^686ZOw+?RUI$?P<*2&d>N6a$8TG4ZQSuc zFy8N2NAzkJw%`3aw?+5&eCyLM4)i+j9KxCavG-tqws~1RdGE0|79<8R&N}yQkEj1a z+<~nelMnuwh(qY-3`2429k(j8@MVU0(!=lM__8z08&3lY|?%=<26Up<}Y>v&&=^Sr40J;ar5UyEqS-%Kv~eIM}NTEuIEb#vJc?p*);Z*el) zJFbbI|Cr-L9>^R1d_6PIZ=`m+%KHTHK3~5ATVlkZc)gFYQ^(#`6lGG^;N$0_=cB0E zC-+27Huxy@<$Y82)A8R|=fjzPCR?6Z({|=L?y!IJnZY!Z#~1V8pf-iL9`Wt&{c7;` z_rEs0m}saM+J`;GRoq^jO+LUjdpI{ytf~ysi~;NyV!b`C9T9SFAx53wv-hp5`5f{p z#uKkOg31)Re1Hlp_y z@b_cP9OjWLIR9|XXzG#{y=yxaKe-sd8A8R`M16kFc|GnSn)Nu*{AY>a<1^qo<{QT^ z&i#E9f5WzE&`ri32k`+hpMUBW;|qoJI;c&^^F9CH0wb!W?&1NQ~+dqHe}>>ZSw zA9!X%e(Cvq`DGuCuEG5SWyeu8ZApqwv>l3%a$x}6-=h4tW&GEADpo_pK>ZB&t5l{T z%2da$a*bX3oZwiiqWaDv<-hV@F_20OEM&b;P~E3e_d0$CA3yDuVf_sEdn^By|B8VW zVxXSWo9F*`sqRy#`?QaJ+J4G#zn*gq^MAkg0on&B22zEA{RHmc?bK$yuT2LIiS`+!3_2hcfy zVjvY50N1;N`wkcW`_KGRJ7DSkw$@MQ7U5vv0FU`Mfcq$Pn^Tkdzn)(zUzLGkAo&>B zKuw*RhW+OM%>4n%f9?Mi11Z2jedozs?&ELP`;|N=*robUf&P;}zUdsMkd6(?qL{p@GF2hfOhe^B+GNd4>BpZqbtkdDFW##x>hdZ)Th zE$#zmD*u)Lih)F7U=}rWY8B#r_H$~>`v47`n_2feRR4+6f1zJ`;686Qoo?&j}JNrNTQAncdK~M z|B}P`UF_ePb-+E&jokll(=i|sV}Nozi5z!t(>5NzZz^@(e|Vq3yg!bwuhcn!@?XaQ z%P?Skd@I?v=lH{U?d9LB_mlbcBER%Z(7mtK@m1v`1q0wJID4`et{3_(-{)IA2bj(8 zQrf5WP+cen3S(dvI9rI%em>_|L0{v0F4VPv_JKMM2*rTe?^_72`r)|SHJLa;_c(Pj zeL&%UNBd-zjbb1^3@l{(-a6U1K5l&W^IdfxZ~^xLm|y6AKzw>uj;ZW(F#s;YX7_@q zxS!i!ux-Fa{H)Qs77#iQRBq&QBiVnOIsYgg_bGf;Z7e(Iy{{rrhZsEG=-k|mYadRYz&&M9$Eav@Rz54P* z!S{Uk%*+K4KY-&5@M{_O2bxz<`+_8Lv7Bw%$2?ljuO{%{mo(<*>jSc%Q6uLx?yK(R zdcV$n!ESz)Q)c=smACB}*qz}H@^dqnvb?hCozEhVVq2thxgA$A{|EAG1HX`G=n#LV zX9mhq+c_Fe{S6n8lMCDcM+Rm%1MU=v)oD{dkgY3nj&R$q=T~=b-z)sSl3%(;(0(8s z9@!^HV1H#b7rOJ`*B_pjC(E5Ay=UwQk$V)m$B}~^bDVlkZ+@S^GFZqjoF{JK7y3h; zC)kJUwDS4zb_2V>FtCjCx<&jBj05Yy{591a$mHRTgX2W>QK9G7;V-#V75e_z%-0_m z`r2{Y;s*t9@35sZmx8xPSy!ToOcujN|(Oaol1 z?>O9mV&7M|J->!~^H~6{&1V6)HlGDh?0Z-7I8Ln{Zq;|3nnAy>a=-tvlij~RI_mE$ z+@D{g*!S+2>$fTam4He>C7=>e38(~A0xAKOfJ#6mpb}6Cs0363Dgl*%NN@K z1j;Xg3in4AJ^z^OetqQUH&yP>KQ`$3Rk7=fov#%;pDXr#vGYOK<5e86R=5d>&nG9l zeP6=)XO-Lad_e3fAU>aU6+oTh=fkc7;PYj-24>EuT^f)%-*%}WSI@PAZSg%7e1JKh LFD#*2eDVJS=SPuN diff --git a/flutter/assets/logo.png b/flutter/assets/logo.png deleted file mode 100644 index ede0e00c4447d6f08e3013ebc3d69365df0b0688..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8643 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 diff --git a/flutter/assets/logo.svg b/flutter/assets/logo.svg index 0001d0762..965218c95 100644 --- a/flutter/assets/logo.svg +++ b/flutter/assets/logo.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 5c37900f2..9ba7a6315 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -234,7 +234,7 @@ class DesktopTab extends StatelessWidget { Key? key, required this.controller, this.showLogo = true, - this.showTitle = true, + this.showTitle = false, this.showMinimize = true, this.showMaximize = true, this.showClose = true, diff --git a/flutter/lib/desktop/widgets/titlebar_widget.dart b/flutter/lib/desktop/widgets/titlebar_widget.dart index 475b4cb86..38e4d917b 100644 --- a/flutter/lib/desktop/widgets/titlebar_widget.dart +++ b/flutter/lib/desktop/widgets/titlebar_widget.dart @@ -24,47 +24,8 @@ class DesktopTitleBar extends StatelessWidget { Expanded( child: child ?? Offstage(), ) - // const WindowButtons() ], ), ); } -} - -// final buttonColors = WindowButtonColors( -// iconNormal: const Color(0xFF805306), -// mouseOver: const Color(0xFFF6A00C), -// mouseDown: const Color(0xFF805306), -// iconMouseOver: const Color(0xFF805306), -// iconMouseDown: const Color(0xFFFFD500)); -// -// final closeButtonColors = WindowButtonColors( -// mouseOver: const Color(0xFFD32F2F), -// mouseDown: const Color(0xFFB71C1C), -// iconNormal: const Color(0xFF805306), -// iconMouseOver: Colors.white); -// -// class WindowButtons extends StatelessWidget { -// const WindowButtons({Key? key}) : super(key: key); -// -// @override -// Widget build(BuildContext context) { -// return Row( -// children: [ -// MinimizeWindowButton(colors: buttonColors, onPressed: () { -// windowManager.minimize(); -// },), -// MaximizeWindowButton(colors: buttonColors, onPressed: () async { -// if (await windowManager.isMaximized()) { -// windowManager.restore(); -// } else { -// windowManager.maximize(); -// } -// },), -// CloseWindowButton(colors: closeButtonColors, onPressed: () { -// windowManager.close(); -// },), -// ], -// ); -// } -// } +} \ No newline at end of file diff --git a/res/logo.svg b/res/logo.svg index 0001d0762..965218c95 100644 --- a/res/logo.svg +++ b/res/logo.svg @@ -1 +1 @@ - + \ No newline at end of file From 3c9e70d3a42b6038abf39050e4db2feefbe8ac5f Mon Sep 17 00:00:00 2001 From: grummbeer Date: Fri, 10 Feb 2023 09:31:43 +0100 Subject: [PATCH 089/199] fix autofocus --- flutter/lib/desktop/pages/desktop_setting_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 4b6cf2a62..366fb2ed7 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1861,7 +1861,7 @@ void changeSocks5Proxy() async { border: const OutlineInputBorder(), errorText: proxyMsg.isNotEmpty ? proxyMsg : null), controller: proxyController, - focusNode: FocusNode()..requestFocus(), + autofocus: true, ), ), ], From be09728bf584030c1e79457bfd0e311b45548bee Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 10 Feb 2023 17:09:31 +0800 Subject: [PATCH 090/199] exclude ui module (sciter) for flutter --- src/cli.rs | 2 +- src/client/file_trait.rs | 4 +- src/common.rs | 11 +++ src/flutter_ffi.rs | 9 +-- src/lib.rs | 2 +- src/main.rs | 13 +++- src/ui.rs | 109 ++++++++++++++++++++++---- src/ui/macos.rs | 13 +--- src/ui_interface.rs | 161 +-------------------------------------- 9 files changed, 123 insertions(+), 201 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 117486ee4..40ab21188 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -36,7 +36,7 @@ impl Session { .lc .write() .unwrap() - .initialize(id.to_owned(), ConnType::PORT_FORWARD); + .initialize(id.to_owned(), ConnType::PORT_FORWARD, None); session } } diff --git a/src/client/file_trait.rs b/src/client/file_trait.rs index 2ecfca837..49e3f2358 100644 --- a/src/client/file_trait.rs +++ b/src/client/file_trait.rs @@ -7,7 +7,7 @@ pub trait FileManager: Interface { fs::get_home_as_string() } - #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] + #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli", feature = "flutter")))] fn read_dir(&self, path: String, include_hidden: bool) -> sciter::Value { match fs::read_dir(&fs::get_path(&path), include_hidden) { Err(_) => sciter::Value::null(), @@ -20,7 +20,7 @@ pub trait FileManager: Interface { } } - #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] + #[cfg(any(target_os = "android", target_os = "ios", feature = "cli", feature = "flutter"))] fn read_dir(&self, path: &str, include_hidden: bool) -> String { use crate::common::make_fd_to_json; match fs::read_dir(&fs::get_path(path), include_hidden) { diff --git a/src/common.rs b/src/common.rs index 79a4664db..b66261ebe 100644 --- a/src/common.rs +++ b/src/common.rs @@ -762,3 +762,14 @@ pub fn make_fd_to_json(id: i32, path: String, entries: &Vec) -> Strin fd_json.insert("entries".into(), json!(entries_out)); serde_json::to_string(&fd_json).unwrap_or("".into()) } + +/// The function to handle the url scheme sent by the system. +/// +/// 1. Try to send the url scheme from ipc. +/// 2. If failed to send the url scheme, we open a new main window to handle this url scheme. +pub fn handle_url_scheme(url: String) { + if let Err(err) = crate::ipc::send_url_scheme(url.clone()) { + log::debug!("Send the url to the existing flutter process failed, {}. Let's open a new program to handle this.", err); + let _ = crate::run_me(vec![url]); + } +} \ No newline at end of file diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index a7e32d0b2..a79ef2de8 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1119,13 +1119,6 @@ pub fn cm_switch_back(conn_id: i32) { crate::ui_cm_interface::switch_back(conn_id); } -pub fn main_get_icon() -> String { - #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] - return ui_interface::get_icon(); - #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] - return String::new(); -} - pub fn main_get_build_date() -> String { crate::BUILD_DATE.to_string() } @@ -1305,7 +1298,7 @@ pub fn main_start_ipc_url_server() { #[allow(unused_variables)] pub fn send_url_scheme(_url: String) { #[cfg(target_os = "macos")] - std::thread::spawn(move || crate::ui::macos::handle_url_scheme(_url)); + std::thread::spawn(move || crate::handle_url_scheme(_url)); } #[cfg(target_os = "android")] diff --git a/src/lib.rs b/src/lib.rs index 7b94c8a2c..748d375b4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,7 +20,7 @@ pub use self::rendezvous_mediator::*; pub mod common; #[cfg(not(any(target_os = "ios")))] pub mod ipc; -#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] +#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli", feature = "flutter")))] pub mod ui; mod version; pub use version::*; diff --git a/src/main.rs b/src/main.rs index 6500a8e4a..8bc375841 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ // Requires Rust 1.18. //#![windows_subsystem = "windows"] +#[cfg(not(feature = "flutter"))] use librustdesk::*; #[cfg(any(target_os = "android", target_os = "ios"))] @@ -16,7 +17,12 @@ fn main() { common::global_clean(); } -#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] +#[cfg(not(any( + target_os = "android", + target_os = "ios", + feature = "cli", + feature = "flutter" +)))] fn main() { if !common::global_init() { return; @@ -27,6 +33,11 @@ fn main() { common::global_clean(); } +#[cfg(feature = "flutter")] +fn main() { + hbb_common::log::info!("Hello world!"); +} + #[cfg(feature = "cli")] fn main() { if !common::global_init() { diff --git a/src/ui.rs b/src/ui.rs index 7973a0ba4..aede5fe7a 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -9,7 +9,7 @@ use sciter::Value; use hbb_common::{ allow_err, - config::{self, PeerConfig}, + config::{self, LocalConfig, PeerConfig}, log, }; @@ -38,6 +38,7 @@ lazy_static::lazy_static! { #[cfg(not(any(feature = "flutter", feature = "cli")))] lazy_static::lazy_static! { pub static ref CUR_SESSION: Arc>>> = Default::default(); + static ref CHILDREN : Children = Default::default(); } struct UIHostHandler; @@ -190,11 +191,11 @@ impl UI { } fn get_remote_id(&mut self) -> String { - get_remote_id() + LocalConfig::get_remote_id() } fn set_remote_id(&mut self, id: String) { - set_remote_id(id); + LocalConfig::set_remote_id(&id); } fn goto_install(&mut self) { @@ -309,7 +310,10 @@ impl UI { } fn is_release(&self) -> bool { - is_release() + #[cfg(not(debug_assertions))] + return true; + #[cfg(debug_assertions)] + return false; } fn is_rdp_service_open(&self) -> bool { @@ -329,11 +333,18 @@ impl UI { } fn closing(&mut self, x: i32, y: i32, w: i32, h: i32) { - closing(x, y, w, h) + crate::server::input_service::fix_key_down_timeout_at_exit(); + LocalConfig::set_size(x, y, w, h); } fn get_size(&mut self) -> Value { - Value::from_iter(get_size()) + let s = LocalConfig::get_size(); + let mut v = Vec::new(); + v.push(s.0); + v.push(s.1); + v.push(s.2); + v.push(s.3); + Value::from_iter(v) } fn get_mouse_time(&self) -> f64 { @@ -388,7 +399,7 @@ impl UI { fn get_recent_sessions(&mut self) -> Value { // to-do: limit number of recent sessions, and remove old peer file - let peers: Vec = get_recent_sessions() + let peers: Vec = PeerConfig::peers() .drain(..) .map(|p| Self::get_peer_value(p.0, p.2)) .collect(); @@ -396,11 +407,11 @@ impl UI { } fn get_icon(&mut self) -> String { - get_icon() + crate::get_icon() } fn remove_peer(&mut self, id: String) { - remove_peer(id) + PeerConfig::remove(&id); } fn remove_discovered(&mut self, id: String) { @@ -442,7 +453,7 @@ impl UI { } fn get_software_update_url(&self) -> String { - get_software_update_url() + crate::SOFTWARE_UPDATE_URL.lock().unwrap().clone() } fn get_new_version(&self) -> String { @@ -458,14 +469,30 @@ impl UI { } fn get_software_ext(&self) -> String { - get_software_ext() + #[cfg(windows)] + let p = "exe"; + #[cfg(target_os = "macos")] + let p = "dmg"; + #[cfg(target_os = "linux")] + let p = "deb"; + p.to_owned() } fn get_software_store_path(&self) -> String { - get_software_store_path() + let mut p = std::env::temp_dir(); + let name = crate::SOFTWARE_UPDATE_URL + .lock() + .unwrap() + .split("/") + .last() + .map(|x| x.to_owned()) + .unwrap_or(crate::get_app_name()); + p.push(name); + format!("{}.{}", p.to_string_lossy(), self.get_software_ext()) } fn create_shortcut(&self, _id: String) { + #[cfg(windows)] create_shortcut(_id) } @@ -495,7 +522,17 @@ impl UI { } fn open_url(&self, url: String) { - open_url(url) + #[cfg(windows)] + let p = "explorer"; + #[cfg(target_os = "macos")] + let p = "open"; + #[cfg(target_os = "linux")] + let p = if std::path::Path::new("/usr/bin/firefox").exists() { + "firefox" + } else { + "xdg-open" + }; + allow_err!(std::process::Command::new(p).arg(url).spawn()); } fn change_id(&self, id: String) { @@ -508,7 +545,7 @@ impl UI { } fn is_ok_change_id(&self) -> bool { - is_ok_change_id() + machine_uid::get().is_ok() } fn get_async_job_status(&self) -> String { @@ -516,11 +553,11 @@ impl UI { } fn t(&self, name: String) -> String { - t(name) + crate::client::translate(name) } fn is_xfce(&self) -> bool { - is_xfce() + crate::platform::is_xfce() } fn get_api_server(&self) -> String { @@ -683,3 +720,43 @@ pub fn value_crash_workaround(values: &[Value]) -> Arc> { STUPID_VALUES.lock().unwrap().push(persist.clone()); persist } + +#[inline] +pub fn new_remote(id: String, remote_type: String) { + let mut lock = CHILDREN.lock().unwrap(); + let args = vec![format!("--{}", remote_type), id.clone()]; + let key = (id.clone(), remote_type.clone()); + if let Some(c) = lock.1.get_mut(&key) { + if let Ok(Some(_)) = c.try_wait() { + lock.1.remove(&key); + } else { + if remote_type == "rdp" { + allow_err!(c.kill()); + std::thread::sleep(std::time::Duration::from_millis(30)); + c.try_wait().ok(); + lock.1.remove(&key); + } else { + return; + } + } + } + match crate::run_me(args) { + Ok(child) => { + lock.1.insert(key, child); + } + Err(err) => { + log::error!("Failed to spawn remote: {}", err); + } + } +} + +#[inline] +pub fn recent_sessions_updated() -> bool { + let mut children = CHILDREN.lock().unwrap(); + if children.0 { + children.0 = false; + true + } else { + false + } +} diff --git a/src/ui/macos.rs b/src/ui/macos.rs index 8a1fc990c..cd0e5871b 100644 --- a/src/ui/macos.rs +++ b/src/ui/macos.rs @@ -180,22 +180,11 @@ extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) { } } -/// The function to handle the url scheme sent by the system. -/// -/// 1. Try to send the url scheme from ipc. -/// 2. If failed to send the url scheme, we open a new main window to handle this url scheme. -pub fn handle_url_scheme(url: String) { - if let Err(err) = crate::ipc::send_url_scheme(url.clone()) { - log::debug!("Send the url to the existing flutter process failed, {}. Let's open a new program to handle this.", err); - let _ = crate::run_me(vec![url]); - } -} - extern "C" fn handle_apple_event(_this: &Object, _cmd: Sel, event: u64, _reply: u64) { let event = event as *mut Object; let url = fruitbasket::parse_url_event(event); log::debug!("an event was received: {}", url); - std::thread::spawn(move || handle_url_scheme(url)); + std::thread::spawn(move || crate::handle_url_scheme(url)); } unsafe fn make_menu_item(title: &str, key: &str, tag: u32) -> *mut Object { diff --git a/src/ui_interface.rs b/src/ui_interface.rs index d357c9cef..6576c340c 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -2,7 +2,6 @@ use std::{ collections::HashMap, process::Child, sync::{Arc, Mutex}, - time::SystemTime, }; #[cfg(any(target_os = "android", target_os = "ios"))] @@ -31,7 +30,6 @@ pub type Children = Arc)>>; type Status = (i32, bool, i64, String); // (status_num, key_confirmed, mouse_time, id) lazy_static::lazy_static! { - static ref CHILDREN : Children = Default::default(); static ref UI_STATUS : Arc> = Arc::new(Mutex::new((0, false, 0, "".to_owned()))); static ref OPTIONS : Arc>> = Arc::new(Mutex::new(Config::get_options())); static ref ASYNC_JOB_STATUS : Arc> = Default::default(); @@ -44,17 +42,6 @@ lazy_static::lazy_static! { pub static ref SENDER : Mutex> = Mutex::new(check_connect_status(true)); } -#[inline] -pub fn recent_sessions_updated() -> bool { - let mut children = CHILDREN.lock().unwrap(); - if children.0 { - children.0 = false; - true - } else { - false - } -} - #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] #[inline] pub fn get_id() -> String { @@ -64,16 +51,6 @@ pub fn get_id() -> String { return ipc::get_id(); } -#[inline] -pub fn get_remote_id() -> String { - LocalConfig::get_remote_id() -} - -#[inline] -pub fn set_remote_id(id: String) { - LocalConfig::set_remote_id(&id); -} - #[inline] pub fn goto_install() { allow_err!(crate::run_me(vec!["--install"])); @@ -419,24 +396,6 @@ pub fn is_installed_lower_version() -> bool { } } -#[inline] -pub fn closing(x: i32, y: i32, w: i32, h: i32) { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - crate::server::input_service::fix_key_down_timeout_at_exit(); - LocalConfig::set_size(x, y, w, h); -} - -#[inline] -pub fn get_size() -> Vec { - let s = LocalConfig::get_size(); - let mut v = Vec::new(); - v.push(s.0); - v.push(s.1); - v.push(s.2); - v.push(s.3); - v -} - #[inline] pub fn get_mouse_time() -> f64 { let ui_status = UI_STATUS.lock().unwrap(); @@ -507,51 +466,6 @@ pub fn store_fav(fav: Vec) { LocalConfig::set_fav(fav); } -#[inline] -pub fn get_recent_sessions() -> Vec<(String, SystemTime, PeerConfig)> { - PeerConfig::peers() -} - -#[inline] -#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] -pub fn get_icon() -> String { - crate::get_icon() -} - -#[inline] -pub fn remove_peer(id: String) { - PeerConfig::remove(&id); -} - -#[inline] -pub fn new_remote(id: String, remote_type: String) { - let mut lock = CHILDREN.lock().unwrap(); - let args = vec![format!("--{}", remote_type), id.clone()]; - let key = (id.clone(), remote_type.clone()); - if let Some(c) = lock.1.get_mut(&key) { - if let Ok(Some(_)) = c.try_wait() { - lock.1.remove(&key); - } else { - if remote_type == "rdp" { - allow_err!(c.kill()); - std::thread::sleep(std::time::Duration::from_millis(30)); - c.try_wait().ok(); - lock.1.remove(&key); - } else { - return; - } - } - } - match crate::run_me(args) { - Ok(child) => { - lock.1.insert(key, child); - } - Err(err) => { - log::error!("Failed to spawn remote: {}", err); - } - } -} - #[inline] pub fn is_process_trusted(_prompt: bool) -> bool { #[cfg(target_os = "macos")] @@ -622,11 +536,6 @@ pub fn current_is_wayland() -> bool { return false; } -#[inline] -pub fn get_software_update_url() -> String { - SOFTWARE_UPDATE_URL.lock().unwrap().clone() -} - #[inline] pub fn get_new_version() -> String { hbb_common::get_version_from_url(&*SOFTWARE_UPDATE_URL.lock().unwrap()) @@ -643,36 +552,9 @@ pub fn get_app_name() -> String { crate::get_app_name() } -#[inline] -#[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn get_software_ext() -> String { - #[cfg(windows)] - let p = "exe"; - #[cfg(target_os = "macos")] - let p = "dmg"; - #[cfg(target_os = "linux")] - let p = "deb"; - p.to_owned() -} - -#[inline] -#[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn get_software_store_path() -> String { - let mut p = std::env::temp_dir(); - let name = SOFTWARE_UPDATE_URL - .lock() - .unwrap() - .split("/") - .last() - .map(|x| x.to_owned()) - .unwrap_or(crate::get_app_name()); - p.push(name); - format!("{}.{}", p.to_string_lossy(), get_software_ext()) -} - +#[cfg(windows)] #[inline] pub fn create_shortcut(_id: String) { - #[cfg(windows)] crate::platform::windows::create_shortcut(&_id).ok(); } @@ -719,22 +601,6 @@ pub fn get_uuid() -> String { base64::encode(hbb_common::get_uuid()) } -#[inline] -#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] -pub fn open_url(url: String) { - #[cfg(windows)] - let p = "explorer"; - #[cfg(target_os = "macos")] - let p = "open"; - #[cfg(target_os = "linux")] - let p = if std::path::Path::new("/usr/bin/firefox").exists() { - "firefox" - } else { - "xdg-open" - }; - allow_err!(std::process::Command::new(p).arg(url).spawn()); -} - #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] #[inline] pub fn change_id(id: String) { @@ -756,23 +622,11 @@ pub fn post_request(url: String, body: String, header: String) { }); } -#[inline] -#[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn is_ok_change_id() -> bool { - machine_uid::get().is_ok() -} - #[inline] pub fn get_async_job_status() -> String { ASYNC_JOB_STATUS.lock().unwrap().clone() } -#[inline] -#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] -pub fn t(name: String) -> String { - crate::client::translate(name) -} - #[inline] pub fn get_langs() -> String { crate::lang::LANGS.to_string() @@ -813,11 +667,6 @@ pub fn default_video_save_directory() -> String { "".to_owned() } -#[inline] -pub fn is_xfce() -> bool { - crate::platform::is_xfce() -} - #[inline] pub fn get_api_server() -> String { crate::get_api_server( @@ -834,14 +683,6 @@ pub fn has_hwcodec() -> bool { return true; } -#[inline] -pub fn is_release() -> bool { - #[cfg(not(debug_assertions))] - return true; - #[cfg(debug_assertions)] - return false; -} - #[cfg(not(any(target_os = "android", target_os = "ios")))] #[inline] pub fn is_root() -> bool { From 930faecb13fbf3761f66aeeea7371903b5e741f3 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 10 Feb 2023 17:38:08 +0800 Subject: [PATCH 091/199] fix ci --- src/ui_interface.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 6576c340c..26038218e 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -511,9 +511,9 @@ pub fn get_error() -> String { if dtype != "x11" { return format!( "{} {}, {}", - t("Unsupported display server ".to_owned()), + crate::client::translate("Unsupported display server ".to_owned()), dtype, - t("x11 expected".to_owned()), + crate::client::translate("x11 expected".to_owned()), ); } } From 7edb3e6e92a90ba520edc52d8b66354c0f9a0378 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 10 Feb 2023 17:48:53 +0800 Subject: [PATCH 092/199] CI --- src/lib.rs | 2 ++ src/main.rs | 8 +------- src/platform/windows.rs | 12 ++++++------ src/server/connection.rs | 4 ++-- src/server/video_service.rs | 10 +++++----- src/ui.rs | 2 -- src/ui_cm_interface.rs | 2 +- src/{ui => }/win_privacy.rs | 0 8 files changed, 17 insertions(+), 23 deletions(-) rename src/{ui => }/win_privacy.rs (100%) diff --git a/src/lib.rs b/src/lib.rs index 748d375b4..5dcd6389c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -56,3 +56,5 @@ pub mod clipboard_file; #[cfg(all(windows, feature = "with_rc"))] pub mod rc; +#[cfg(target_os = "windows")] +pub mod win_privacy; diff --git a/src/main.rs b/src/main.rs index 8bc375841..169515425 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,10 +2,9 @@ // Requires Rust 1.18. //#![windows_subsystem = "windows"] -#[cfg(not(feature = "flutter"))] use librustdesk::*; -#[cfg(any(target_os = "android", target_os = "ios"))] +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] fn main() { if !common::global_init() { return; @@ -33,11 +32,6 @@ fn main() { common::global_clean(); } -#[cfg(feature = "flutter")] -fn main() { - hbb_common::log::info!("Hello world!"); -} - #[cfg(feature = "cli")] fn main() { if !common::global_init() { diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 17f275c2a..bd6a1fc4c 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -833,8 +833,8 @@ fn get_default_install_path() -> String { pub fn check_update_broker_process() -> ResultType<()> { // let (_, path, _, _) = get_install_info(); - let process_exe = crate::ui::win_privacy::INJECTED_PROCESS_EXE; - let origin_process_exe = crate::ui::win_privacy::ORIGIN_PROCESS_EXE; + let process_exe = crate::win_privacy::INJECTED_PROCESS_EXE; + let origin_process_exe = crate::win_privacy::ORIGIN_PROCESS_EXE; let exe_file = std::env::current_exe()?; if exe_file.parent().is_none() { @@ -919,8 +919,8 @@ pub fn copy_exe_cmd(src_exe: &str, _exe: &str, path: &str) -> String { ", main_exe = main_exe, path = path, - ORIGIN_PROCESS_EXE = crate::ui::win_privacy::ORIGIN_PROCESS_EXE, - broker_exe = crate::ui::win_privacy::INJECTED_PROCESS_EXE, + ORIGIN_PROCESS_EXE = crate::win_privacy::ORIGIN_PROCESS_EXE, + broker_exe = crate::win_privacy::INJECTED_PROCESS_EXE, ); } @@ -938,7 +938,7 @@ pub fn update_me() -> ResultType<()> { {lic} ", copy_exe = copy_exe_cmd(&src_exe, &exe, &path), - broker_exe = crate::ui::win_privacy::INJECTED_PROCESS_EXE, + broker_exe = crate::win_privacy::INJECTED_PROCESS_EXE, app_name = crate::get_app_name(), lic = register_licence(), cur_pid = get_current_pid(), @@ -1203,7 +1203,7 @@ fn get_before_uninstall() -> String { netsh advfirewall firewall delete rule name=\"{app_name} Service\" ", app_name = app_name, - broker_exe = crate::ui::win_privacy::INJECTED_PROCESS_EXE, + broker_exe = crate::win_privacy::INJECTED_PROCESS_EXE, ext = ext, cur_pid = get_current_pid(), ) diff --git a/src/server/connection.rs b/src/server/connection.rs index 9ce53c960..53ccd7008 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -2045,7 +2045,7 @@ mod privacy_mode { pub(super) fn turn_off_privacy(_conn_id: i32) -> Message { #[cfg(windows)] { - use crate::ui::win_privacy::*; + use crate::win_privacy::*; let res = turn_off_privacy(_conn_id, None); match res { @@ -2069,7 +2069,7 @@ mod privacy_mode { pub(super) fn turn_on_privacy(_conn_id: i32) -> ResultType { #[cfg(windows)] { - let plugin_exist = crate::ui::win_privacy::turn_on_privacy(_conn_id)?; + let plugin_exist = crate::win_privacy::turn_on_privacy(_conn_id)?; Ok(plugin_exist) } #[cfg(not(windows))] diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 57fdf2c22..bc9c5ff6f 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -207,7 +207,7 @@ fn create_capturer( if privacy_mode_id > 0 { #[cfg(windows)] { - use crate::ui::win_privacy::*; + use crate::win_privacy::*; match scrap::CapturerMag::new( display.origin(), @@ -308,11 +308,11 @@ pub fn test_create_capturer(privacy_mode_id: i32, timeout_millis: u64) -> bool { fn check_uac_switch(privacy_mode_id: i32, capturer_privacy_mode_id: i32) -> ResultType<()> { if capturer_privacy_mode_id != 0 { if privacy_mode_id != capturer_privacy_mode_id { - if !crate::ui::win_privacy::is_process_consent_running()? { + if !crate::win_privacy::is_process_consent_running()? { bail!("consent.exe is running"); } } - if crate::ui::win_privacy::is_process_consent_running()? { + if crate::win_privacy::is_process_consent_running()? { bail!("consent.exe is running"); } } @@ -372,7 +372,7 @@ fn get_capturer(use_yuv: bool, portable_service_running: bool) -> ResultType)>>; #[allow(dead_code)] diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index de33b0169..f5c575d43 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -494,7 +494,7 @@ pub async fn start_ipc(cm: ConnectionManager) { e ); } - allow_err!(crate::ui::win_privacy::start()); + allow_err!(crate::win_privacy::start()); }); match ipc::new_listener("_cm").await { diff --git a/src/ui/win_privacy.rs b/src/win_privacy.rs similarity index 100% rename from src/ui/win_privacy.rs rename to src/win_privacy.rs From 23f133b83674347f8bd7f9e61f6c764e0dda23cc Mon Sep 17 00:00:00 2001 From: grummbeer Date: Fri, 10 Feb 2023 10:50:48 +0100 Subject: [PATCH 093/199] unify padding of dialogs --- flutter/lib/common.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index a731f0b08..4ad4a9927 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -648,8 +648,6 @@ class CustomAlertDialog extends StatelessWidget { child: AlertDialog( scrollable: true, title: title, - contentPadding: EdgeInsets.fromLTRB( - contentPadding ?? padding, 25, contentPadding ?? padding, 10), content: ConstrainedBox( constraints: contentBoxConstraints, child: Theme( From 07b86bee8e521872048e159bdd213f09335b22a2 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 10 Feb 2023 18:26:23 +0800 Subject: [PATCH 094/199] try fix memory issue when decoding is too slow Signed-off-by: fufesou --- flutter/lib/models/model.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index ca99a5bd1..feab5bdc8 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -415,6 +415,8 @@ class ImageModel with ChangeNotifier { String id = ''; + int decodeCount = 0; + WeakReference parent; final List _callbacksOnFirstImage = []; @@ -434,7 +436,13 @@ class ImageModel with ChangeNotifier { } } } + + if (decodeCount >= 1) { + return; + } + final pid = parent.target?.id; + decodeCount += 1; ui.decodeImageFromPixels( rgba, parent.target?.ffiModel.display.width ?? 0, @@ -442,6 +450,7 @@ class ImageModel with ChangeNotifier { isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888, (image) { if (parent.target?.id != pid) return; try { + decodeCount -= 1; // my throw exception, because the listener maybe already dispose update(image); } catch (e) { From a73514c35b9b7403b743628c1e5e3cb111217bee Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 10 Feb 2023 18:35:02 +0800 Subject: [PATCH 095/199] fix counter logic Signed-off-by: fufesou --- flutter/lib/models/model.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index feab5bdc8..add1289e2 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -448,9 +448,9 @@ class ImageModel with ChangeNotifier { parent.target?.ffiModel.display.width ?? 0, parent.target?.ffiModel.display.height ?? 0, isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888, (image) { + decodeCount -= 1; if (parent.target?.id != pid) return; try { - decodeCount -= 1; // my throw exception, because the listener maybe already dispose update(image); } catch (e) { From 5b36555faa97a48d26cfda2bc95c58e71ef91294 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 10 Feb 2023 18:42:08 +0800 Subject: [PATCH 096/199] flutter option enable share rdp Signed-off-by: 21pages --- .../desktop/pages/desktop_setting_page.dart | 28 +++++++++++++++++++ src/flutter_ffi.rs | 4 +++ 2 files changed, 32 insertions(+) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 4b6cf2a62..5d524523a 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -701,6 +701,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { child: _OptionCheckBox(context, 'Enable RDP', 'enable-rdp', enabled: enabled), ), + shareRdp(context, enabled), _OptionCheckBox(context, 'Deny LAN Discovery', 'enable-lan-discovery', reverse: true, enabled: enabled), ...directIp(context), @@ -708,6 +709,33 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { ]); } + shareRdp(BuildContext context, bool enabled) { + onChanged(bool b) async { + await bind.mainSetShareRdp(enable: b); + setState(() {}); + } + + bool value = bind.mainIsShareRdp(); + return Offstage( + offstage: !(Platform.isWindows && bind.mainIsRdpServiceOpen()), + child: GestureDetector( + child: Row( + children: [ + Checkbox( + value: value, + onChanged: enabled ? (_) => onChanged(!value) : null) + .marginOnly(right: 5), + Expanded( + child: Text(translate('Enable RDP session sharing'), + style: + TextStyle(color: _disabledTextColor(context, enabled))), + ) + ], + ).marginOnly(left: _kCheckBoxLeftMargin), + onTap: enabled ? () => onChanged(!value) : null), + ); + } + List directIp(BuildContext context) { TextEditingController controller = TextEditingController(); update() => setState(() {}); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index a7e32d0b2..3611b5dbf 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1210,6 +1210,10 @@ pub fn main_is_rdp_service_open() -> SyncReturn { SyncReturn(is_rdp_service_open()) } +pub fn main_set_share_rdp(enable: bool) { + set_share_rdp(enable) +} + pub fn main_goto_install() -> SyncReturn { goto_install(); SyncReturn(true) From b4357e1e000f4914953385dd23982aceb776a863 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Fri, 10 Feb 2023 12:51:49 +0100 Subject: [PATCH 097/199] fix icon name --- flutter/assets/{Github.svg => GitHub.svg} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename flutter/assets/{Github.svg => GitHub.svg} (100%) diff --git a/flutter/assets/Github.svg b/flutter/assets/GitHub.svg similarity index 100% rename from flutter/assets/Github.svg rename to flutter/assets/GitHub.svg From 554b8bd0324a58ddf07a16caeb1f205dc933ee30 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Fri, 10 Feb 2023 14:14:49 +0100 Subject: [PATCH 098/199] Addressbook login. Button instead of text --- flutter/lib/common/widgets/address_book.dart | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index 5c1e1218c..5cd2af2be 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -43,13 +43,10 @@ class _AddressBookState extends State { return Obx(() { if (gFFI.userModel.userName.value.isEmpty) { return Center( - child: InkWell( - onTap: loginDialog, - child: Text( - translate("Login"), - style: const TextStyle(decoration: TextDecoration.underline), - ), - ), + child: ElevatedButton( + onPressed: loginDialog, + child: Text(translate("Login")) + ) ); } else { if (gFFI.abModel.abLoading.value) { From 19c7cd99d57f91b4697eed912961ac53f9410250 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 10 Feb 2023 21:18:55 +0800 Subject: [PATCH 099/199] fix: --cm cannot exit on macOS --- flutter/lib/common.dart | 5 +++++ flutter/lib/desktop/pages/server_page.dart | 15 +++++++++++++-- flutter/lib/models/model.dart | 2 +- flutter/lib/models/server_model.dart | 6 ++---- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 4ad4a9927..d86960a0d 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -49,6 +49,11 @@ int androidVersion = 0; int windowsBuildNumber = 0; DesktopType? desktopType; +/// Check if the app is running with single view mode. +bool isSingleViewApp() { + return desktopType == DesktopType.cm; +} + /// * debug or test only, DO NOT enable in release build bool isTest = false; diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index b66a08e74..252e1cd12 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -1,11 +1,13 @@ // original cm window in Sciter version. import 'dart:async'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/models/chat_model.dart'; +import 'package:flutter_hbb/utils/platform_channel.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:window_manager/window_manager.dart'; @@ -47,8 +49,17 @@ class _DesktopServerPageState extends State @override void onWindowClose() { - gFFI.serverModel.closeAll(); - gFFI.close(); + Future.wait([ + gFFI.serverModel.closeAll(), + gFFI.close() + ]).then((_) { + if (Platform.isMacOS) { + RdPlatformChannel.instance.terminate(); + } else { + windowManager.setPreventClose(false); + windowManager.close(); + } + }); super.onWindowClose(); } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index add1289e2..eb837ba70 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1399,12 +1399,12 @@ class FFI { await setCanvasConfig(id, cursorModel.x, cursorModel.y, canvasModel.x, canvasModel.y, canvasModel.scale, ffiModel.pi.currentDisplay); } - bind.sessionClose(id: id); imageModel.update(null); cursorModel.clear(); ffiModel.clear(); canvasModel.clear(); inputModel.resetModifiers(); + await bind.sessionClose(id: id); debugPrint('model $id closed'); id = ''; } diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index aab12ab5d..b2043f3c2 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -560,10 +560,8 @@ class ServerModel with ChangeNotifier { } } - closeAll() { - for (var client in _clients) { - bind.cmCloseConnection(connId: client.id); - } + Future closeAll() async { + await Future.wait(_clients.map((client) => bind.cmCloseConnection(connId: client.id))); _clients.clear(); tabController.state.value.tabs.clear(); } From cfc6f4b88a5c362226e029df5f0c8cc9a78b638b Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 10 Feb 2023 21:32:51 +0800 Subject: [PATCH 100/199] mouse do not control in black blank area Signed-off-by: fufesou --- flutter/lib/models/input_model.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index c37d01860..b1491d526 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -485,10 +485,19 @@ class InputModel { y /= canvasModel.scale; x += d.x; y += d.y; + + if (x < d.x || y < d.y || x > (d.x + d.width) || y > (d.y + d.height)) { + // If left mouse up, no early return. + if (evt['buttons'] != kPrimaryMouseButton || type != 'up') { + return; + } + } + if (type != '') { x = 0; y = 0; } + evt['x'] = '${x.round()}'; evt['y'] = '${y.round()}'; var buttons = ''; From 3e17fd372b21a6cbfa7188a03d0a5ffd030c6e80 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Fri, 10 Feb 2023 23:33:52 +0800 Subject: [PATCH 101/199] Revert "unify padding of dialogs" --- flutter/lib/common.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index d86960a0d..a295ad4f8 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -653,6 +653,8 @@ class CustomAlertDialog extends StatelessWidget { child: AlertDialog( scrollable: true, title: title, + contentPadding: EdgeInsets.fromLTRB( + contentPadding ?? padding, 25, contentPadding ?? padding, 10), content: ConstrainedBox( constraints: contentBoxConstraints, child: Theme( From d416d7d9658abfd5cd3ab954c9cb34d1a3e41b99 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sat, 11 Feb 2023 00:21:19 +0800 Subject: [PATCH 102/199] base64 icon only for sciter --- libs/hbb_common/src/config.rs | 8 +------- src/common.rs | 7 +------ src/ui.rs | 15 ++++++++++++++- src/ui/cm.rs | 2 +- src/ui/remote.rs | 2 +- 5 files changed, 18 insertions(+), 16 deletions(-) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 1e4d80c9f..3bfc885c5 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -30,13 +30,7 @@ pub const REG_INTERVAL: i64 = 12_000; pub const COMPRESS_LEVEL: i32 = 3; const SERIAL: i32 = 3; const PASSWORD_ENC_VERSION: &str = "00"; -// 128x128 -#[cfg(target_os = "macos")] // 128x128 on 160x160 canvas, then shrink to 128, mac looks better with padding -pub const ICON: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAyVBMVEUAAAAAcf8Acf8Acf8Acv8Acf8Acf8Acf8Acf8AcP8Acf8Ab/8AcP8Acf////8AaP/z+f/o8v/k7v/5/v/T5f8AYP/u9v/X6f+hx/+Kuv95pP8Aef/B1/+TwP9xoP8BdP/g6P+Irv9ZmP8Bgf/E3f98q/9sn/+01f+Es/9nm/9Jif8hhv8off/M4P+syP+avP86iP/c7f+xy/9yqf9Om/9hk/9Rjv+60P99tv9fpf88lv8yjf8Tgf8deP+kvP8BiP8NeP8hkP80gP8oj2VLAAAADXRSTlMA7o7qLvnaxZ1FOxYPjH9HWgAABHJJREFUeNrtm+tW4jAQgBfwuu7MtIUWsOUiCCioIIgLiqvr+z/UHq/LJKVkmwTcc/r9E2nzlU4mSTP9lpGRkZGR8VX5cZjfL+yCEXYL+/nDH//U/Pd8DgyTy39Xbv7oIAcWyB0cqbW/sweW2NtRaj8H1sgpGOwUIAH7Bkd7YJW9dXFwAJY5WNP/cmCZQnJvzIN18on5LwfWySXlxEPYAIcad8D6PdiHDbCfIFCADVBIENiFDbCbIACKPPXrZ+cP8E6/0znvP4EymgIEravIRcTxu8HxNSJ60a8W0AYECKrlAN+YwAthCd9wm1Ug6wKzIn5SgRduXfwkqDasCjx0XFzi9PV6zwNcIuhcWBOg+ikySq8C9UD4dEKWBCoOcspvAuLHTo9sCDQiFPHotRM48j8G5gVur1FdAN2uaYEuiz7xFsgEJ2RUoMUakXuBTHHoGxQYOBhHjeUBAefEnMAowFhaLBOKuOemBBbxLRQrH2PBCgMvNCPQGMeevTb9zLrPxz2Mo+QbEaijzPUcOOHMQZkKGRAIPem39+bypREMPTkQW/oCfk866zAkiIFG4yIKRE/aAnfiSd0WrORY6pFdXQEqi9mvAQm0RIOSnoCcZ8vJoz3diCnjRk+g8VP4/fuQDJ2Lxr6WwG0gXs9aTpDzW0vgDBlVUpixR8gYk44AD8FrUKHr8JQJGgIDnoDqoALxmWPQSi9AVVzm8gKUuEPGr/QCvptwJkbSYT/TC4S8C96DGjTj86aHtAI0x2WaBIq0eSYYpRa4EsdWVVwWu9O0Aj6f6dyBMnwEraeOgSYu0wZlauzA47QCbT7DgAQSE+hZWoEBF/BBmWOewNMK3BsSqKUW4MGcWqCSVmDkbvkXGKQOwg6PAUO9oL3xXhA20yaiCjuwYygRVQlUOTWTCf2SuNJTxeFjgaHByGuAIvd8ItdPLTDhS7IuqEE1YSKVOgbayLhSFQhMzYh8hwfBs1r7c505YVIQYEdNoKwxK06MJiyrpUFHiF0NAfCQUVHoiRclIXJIR6C2fqG37pBHvcWpgwzvAtYwkR5UGV2e42UISdBJETl3mg8ouo54Rcnti1/vaT+iuUQBt500Cgo4U10BeHSkk57FB0JjWkKRMWgLUA0lLodtImAQdaMiiri3+gIAPZQoutHNsgKF1aaDMhMyIdBf8Th+Bh8MTjGWCpl5Wv43tDmnF+IUVMrcZgRoiAxhtrloYizNkZaAnF5leglbNhj0wYCAbCDvGb0mP4nib7O7ZlcYQ2m1gPtIZgVgGNNMeaVAaWR+57TrqgtUnm3sHQ+kYeE6fufUubG1ez50FXbPnWgBlgSABmN3TTcsRl2yWkHRrwbiunvk/W2+Mg1hPZplPDeXRbZzStFH15s1QIVd3UImP5z/bHpeeQLvRJ7XLFUffQIlCvqlXETQbgN9/rlYABGosv+Vi9m2Xs639YLGrZd0br+odetlvdsvbN56abfd4vbCzv9Q3v/ygoOV21A4OPpfXvH4Ai+5ZGRkZGRkbJA/t/I0QMzoMiEAAAAASUVORK5CYII= -"; -#[cfg(not(target_os = "macos"))] // 128x128 no padding -pub const ICON: &str = "data:image/png;base64,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 -"; + #[cfg(target_os = "macos")] lazy_static::lazy_static! { pub static ref ORG: Arc> = Arc::new(RwLock::new("com.carriez".to_owned())); diff --git a/src/common.rs b/src/common.rs index b66261ebe..ee44cf4f2 100644 --- a/src/common.rs +++ b/src/common.rs @@ -588,11 +588,6 @@ async fn check_software_update_() -> hbb_common::ResultType<()> { Ok(()) } -#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] -pub fn get_icon() -> String { - hbb_common::config::ICON.to_owned() -} - pub fn get_app_name() -> String { hbb_common::config::APP_NAME.read().unwrap().clone() } @@ -772,4 +767,4 @@ pub fn handle_url_scheme(url: String) { log::debug!("Send the url to the existing flutter process failed, {}. Let's open a new program to handle this.", err); let _ = crate::run_me(vec![url]); } -} \ No newline at end of file +} diff --git a/src/ui.rs b/src/ui.rs index ce97745fb..1b6838e46 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -405,7 +405,7 @@ impl UI { } fn get_icon(&mut self) -> String { - crate::get_icon() + get_icon() } fn remove_peer(&mut self, id: String) { @@ -758,3 +758,16 @@ pub fn recent_sessions_updated() -> bool { false } } + +pub fn get_icon() -> String { + // 128x128 + #[cfg(target_os = "macos")] + // 128x128 on 160x160 canvas, then shrink to 128, mac looks better with padding + { + "data:image/png;base64,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".into() + } + #[cfg(not(target_os = "macos"))] // 128x128 no padding + { + "data:image/png;base64,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".into() + } +} diff --git a/src/ui/cm.rs b/src/ui/cm.rs index cce553154..a574b5e88 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -100,7 +100,7 @@ impl SciterConnectionManager { } fn get_icon(&mut self) -> String { - crate::get_icon() + super::get_icon() } fn check_click_time(&mut self, id: i32) { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 999b409e0..fdb6b2df8 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -486,7 +486,7 @@ impl SciterSession { } pub fn get_icon(&self) -> String { - crate::get_icon() + super::get_icon() } fn supported_hwcodec(&self) -> Value { From 7514a067d378f74a75b206fe86e0f1ed76f61a5b Mon Sep 17 00:00:00 2001 From: Carsten Date: Fri, 10 Feb 2023 21:32:21 +0100 Subject: [PATCH 103/199] Update README-DE.md fix grammar and improve readability --- docs/README-DE.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/README-DE.md b/docs/README-DE.md index 0b51d8fdd..e537d41f3 100644 --- a/docs/README-DE.md +++ b/docs/README-DE.md @@ -6,24 +6,24 @@ DateistrukturScreenshots
    [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    - Wir brauchen deine Hilfe um diese README Datei zu verbessern und aktualisieren + Wir brauchen deine Hilfe, um diese README Datei zu verbessern und zu aktualisieren

    -Rede mit uns: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) +Rede mit uns auf: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) -Das hier ist ein Programm was, man nutzen kann, um einen Computer fernzusteuern, es wurde in Rust geschrieben. Es funktioniert ohne Konfiguration oder ähnliches, man kann es einfach direkt nutzen. Du hast volle Kontrolle über deine Daten und brauchst dir daher auch keine Sorgen um die Sicherheit dieser Daten zu machen. Du kannst unseren Rendezvous/Relay Server nutzen, [einen eigenen Server eröffnen](https://rustdesk.com/server) oder [einen neuen eigenen Server programmieren](https://github.com/rustdesk/rustdesk-server-demo). +RustDesk ist eine in Rust geschriebene Remote-Desktop-Software, die out-of-the-box ohne besondere Konfiguration funktioniert. Du hast die volle Kontrolle über deine Daten und musst dir keine Sorgen um die Sicherheit machen. Du kannst unseren Rendezvous/Relay Server nutzen, [einen eigenen Server aufsetzen](https://rustdesk.com/server) oder [einen eigenen Server programmieren](https://github.com/rustdesk/rustdesk-server-demo). -RustDesk heißt jegliche Mitarbeit willkommen. Schau dir [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) an, wenn du Hilfe brauchst für den Start. +RustDesk heißt jegliche Mitarbeit willkommen. Schau dir [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) an, wenn du Unterstützung beim Start brauchst. [**PROGRAMM DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) ## Kostenlose öffentliche Server -Hier sind die Server, die du kostenlos nutzen kannst, es kann sein das sich diese Liste immer mal wieder ändert. Falls du nicht in der Nähe einer dieser Server bist, kann es sein, dass deine Verbindung langsam sein wird. +Nachfolgend sind die Server gelistet, die du kostenlos nutzen kannst. Es kann sein, dass sich diese Liste immer mal wieder ändert. Falls du nicht in der Nähe einer dieser Server bist, kann es sein, dass deine Verbindung langsam sein wird. -| Standort | Serverart | Spezifikationen | Kommentare | +| Standort | Anbieter | Spezifikationen | Kommentar | | --------- | ------------- | ------------------ | ---------- | | Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | | | Germany | Codext | 2 vCPU / 4GB RAM | @@ -33,7 +33,7 @@ Hier sind die Server, die du kostenlos nutzen kannst, es kann sein das sich dies ## Abhängigkeiten -Die Desktop-Versionen nutzen [Sciter](https://sciter.com/) für die Oberfläche, bitte lade die dynamische Sciter Bibliothek selbst herunter. +Die Desktop-Versionen nutzen [Sciter](https://sciter.com/) oder Flutter für die GUI. Bitte lade die dynamische Sciter Bibliothek selbst herunter. [Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | [Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | @@ -41,7 +41,7 @@ Die Desktop-Versionen nutzen [Sciter](https://sciter.com/) für die Oberfläche, ## Die groben Schritte zum Kompilieren -- Bereite deine Rust Entwicklungsumgebung und C++ Entwicklungsumgebung vor +- Bereite deine Rust Entwicklungsumgebung und C++ Build-Umgebung vor - Installiere [vcpkg](https://github.com/microsoft/vcpkg) und füge die `VCPKG_ROOT` Systemumgebungsvariable hinzu @@ -110,11 +110,11 @@ cargo run ### Ändere Wayland zu X11 (Xorg) -RustDesk unterstützt "Wayland" nicht. Siehe [hier](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) um Xorg als Standard GNOME Session zu nutzen. +RustDesk unterstützt "Wayland" nicht. Siehe [hier](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/), um Xorg als Standard GNOME Session zu nutzen. -## Auf Docker Kompilieren +## Auf Docker kompilieren -Beginne damit das Repository zu klonen und den Docker Container zu bauen: +Beginne damit, das Repository zu klonen und den Docker Container zu bauen: ```sh git clone https://github.com/rustdesk/rustdesk @@ -122,13 +122,13 @@ cd rustdesk docker build -t "rustdesk-builder" . ``` -Jedes Mal, wenn du das Programm Kompilieren musst, nutze diesen Befehl: +Jedes Mal, wenn du das Programm kompilieren musst, nutze diesen Befehl: ```sh docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder ``` -Bedenke, dass das erste Mal Kompilieren länger dauern kann, da die Abhängigkeiten erst kompiliert werden müssen bevor sie zwischengespeichert werden können. Darauf folgende Kompiliervorgänge werden schneller sein. Falls du zusätzliche oder andere Argumente für den Kompilierbefehl angeben musst, kannst du diese am Ende des Befehls an der `` Position machen. Wenn du zum Beispiel eine optimierte Releaseversion kompilieren willst, kannst du das tun, indem du `--release` am Ende des Befehls anhängst. Das daraus entstehende Programm kannst du im “target” Ordner auf deinem System finden. Du kannst es mit folgenden Befehlen ausführen: +Bedenke, dass das erste Mal Kompilieren länger dauern kann, da die Abhängigkeiten erst kompiliert werden müssen bevor sie zwischengespeichert werden können. Nachfolgende Kompiliervorgänge werden schneller sein. Falls du zusätzliche oder andere Argumente für den Kompilierbefehl angeben musst, kannst du diese am Ende des Befehls an der `` Position machen. Wenn du zum Beispiel eine optimierte Releaseversion kompilieren willst, kannst du das tun, indem du `--release` am Ende des Befehls anhängst. Das daraus entstehende Programm kannst du im “target” Ordner auf deinem System finden. Du kannst es mit folgenden Befehlen ausführen: ```sh target/debug/rustdesk @@ -140,13 +140,13 @@ Oder, wenn du eine Releaseversion benutzt: target/release/rustdesk ``` -Bitte gehe sicher, dass du diese Befehle vom Stammverzeichnis vom RustDesk Repository nutzt, sonst kann es passieren, dass das Programm die Ressourcen nicht finden kann. Bitte bedenke auch, dass Unterbefehle von Cargo, wie z. B. `install` oder `run` aktuell noch nicht unterstützt werden, da sie das Programm innerhalb des Containers starten oder installieren würden, anstatt auf deinem eigentlichen System. +Bitte stelle sicher, dass du diese Befehle vom Stammverzeichnis vom RustDesk Repository nutzt. Ansonsten kann es passieren, dass das Programm die Ressourcen nicht finden kann. Bitte bedenke auch, dass Unterbefehle von Cargo, wie z. B. `install` oder `run` aktuell noch nicht unterstützt werden, da sie das Programm innerhalb des Containers starten oder installieren würden, anstatt auf deinem eigentlichen System. ## Dateistruktur -- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: Video Codec, Konfiguration, TCP/UDP Wrapper, Protokoll Puffer, fs Funktionen für Dateitransfer, und ein paar andere nützliche Funktionen +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: Video Codec, Konfiguration, TCP/UDP Wrapper, Protokoll Puffer, fs Funktionen für Dateitransfer und ein paar andere nützliche Funktionen - **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: Bildschirmaufnahme -- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: Plattformspezifische Maus und Tastatur Steuerung +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: Plattformspezifische Maus- und Tastatur-Steuerung - **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI - **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: Audio/Zwischenablage/Eingabe/Videodienste und Netzwerk Verbindungen - **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: Starten einer Peer-Verbindung From 491932cda104b517ef236b27e026a603831f1400 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 11 Feb 2023 09:57:27 +0800 Subject: [PATCH 104/199] opt: fetch rgba positively for sessions on flutter --- flutter/lib/models/model.dart | 7 ++++++- src/flutter.rs | 8 +++++++- src/flutter_ffi.rs | 13 ++++++++++++- src/ui/remote.rs | 5 +++++ src/ui_session_interface.rs | 1 + 5 files changed, 31 insertions(+), 3 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index eb837ba70..f30209a60 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1376,7 +1376,12 @@ class FFI { debugPrint('json.decode fail1(): $e, ${message.field0}'); } } else if (message is EventToUI_Rgba) { - imageModel.onRgba(message.field0); + // Fetch the image buffer from rust codes. + bind.sessionGetRgba(id: id).then((rgba) { + if (rgba != null) { + imageModel.onRgba(rgba); + } + }); } } }(); diff --git a/src/flutter.rs b/src/flutter.rs index 7533244eb..8ef451397 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -110,6 +110,7 @@ pub unsafe extern "C" fn free_c_args(ptr: *mut *mut c_char, len: c_int) { #[derive(Default, Clone)] pub struct FlutterHandler { pub event_stream: Arc>>>, + pub rgba: Arc>>> } impl FlutterHandler { @@ -290,7 +291,8 @@ impl InvokeUiSession for FlutterHandler { fn on_rgba(&self, data: &[u8]) { if let Some(stream) = &*self.event_stream.read().unwrap() { - stream.add(EventToUI::Rgba(ZeroCopyBuffer(data.to_owned()))); + drop(self.rgba.write().unwrap().replace(data.to_owned())); + stream.add(EventToUI::Rgba); } } @@ -409,6 +411,10 @@ impl InvokeUiSession for FlutterHandler { fn on_voice_call_incoming(&self) { self.push_event("on_voice_call_incoming", [].into()); } + + fn get_rgba(&self) -> Option> { + self.rgba.write().unwrap().take() + } } /// Create a new remote session with the given id. diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index a12d5acab..3a0fcc5fa 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -20,6 +20,7 @@ use std::{ os::raw::c_char, str::FromStr, }; +use crate::ui_session_interface::InvokeUiSession; // use crate::hbbs_http::account::AuthResult; @@ -47,7 +48,7 @@ fn initialize(app_dir: &str) { pub enum EventToUI { Event(String), - Rgba(ZeroCopyBuffer>), + Rgba, } pub fn start_global_event_stream(s: StreamSink, app_type: String) -> ResultType<()> { @@ -103,6 +104,16 @@ pub fn session_get_remember(id: String) -> Option { } } +pub fn session_get_rgba(id: String) -> Option>> { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + return match session.get_rgba() { + Some(buf) => Some(ZeroCopyBuffer(buf)), + _ => None + }; + } + None +} + pub fn session_get_toggle_option(id: String, arg: String) -> Option { if let Some(session) = SESSIONS.read().unwrap().get(&id) { Some(session.get_toggle_option(arg)) diff --git a/src/ui/remote.rs b/src/ui/remote.rs index fdb6b2df8..06af70eae 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -282,6 +282,11 @@ impl InvokeUiSession for SciterHandler { fn on_voice_call_incoming(&self) { self.call("onVoiceCallIncoming", &make_args!()); } + + /// RGBA is directly rendered by [on_rgba]. No need to store the rgba for the sciter ui. + fn get_rgba(&self) -> Option> { + None + } } pub struct SciterSession(Session); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 87ea8e9eb..2944a76d1 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -722,6 +722,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn on_voice_call_closed(&self, reason: &str); fn on_voice_call_waiting(&self); fn on_voice_call_incoming(&self); + fn get_rgba(&self) -> Option>; } impl Deref for Session { From f8c78a6bf2ca029d7b4fdc3523bb0b9ad4e3fbde Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 11 Feb 2023 10:14:09 +0800 Subject: [PATCH 105/199] opt: remove unnecessary rgba events to decrease memory usage --- src/flutter.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/flutter.rs b/src/flutter.rs index 8ef451397..a2dcbdbcf 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -291,8 +291,12 @@ impl InvokeUiSession for FlutterHandler { fn on_rgba(&self, data: &[u8]) { if let Some(stream) = &*self.event_stream.read().unwrap() { - drop(self.rgba.write().unwrap().replace(data.to_owned())); - stream.add(EventToUI::Rgba); + let former_rgba = self.rgba.write().unwrap().replace(data.to_owned()); + if former_rgba.is_none() { + // The [former_rgba] is none, which means the latest rgba had taken from flutter. + // We need to send a signal to flutter for notifying there's a new rgba buffer here. + stream.add(EventToUI::Rgba); + } } } From f521b1665a81f0e7dc11356fae993d7f26d3e4fb Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 11 Feb 2023 12:25:13 +0800 Subject: [PATCH 106/199] opt: no copy during transmitting the decoded frame --- src/client.rs | 4 ++-- src/flutter.rs | 6 +++--- src/ui/remote.rs | 4 ++-- src/ui_session_interface.rs | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/client.rs b/src/client.rs index 020bea1f0..ecfc59749 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1545,7 +1545,7 @@ pub type MediaSender = mpsc::Sender; /// * `video_callback` - The callback for video frame. Being called when a video frame is ready. pub fn start_video_audio_threads(video_callback: F) -> (MediaSender, MediaSender) where - F: 'static + FnMut(&[u8]) + Send, + F: 'static + FnMut(Vec) + Send, { let (video_sender, video_receiver) = mpsc::channel::(); let mut video_callback = video_callback; @@ -1560,7 +1560,7 @@ where match data { MediaData::VideoFrame(vf) => { if let Ok(true) = video_handler.handle_frame(vf) { - video_callback(&video_handler.rgb); + video_callback(std::mem::replace(&mut video_handler.rgb, vec![])); } } MediaData::Reset => { diff --git a/src/flutter.rs b/src/flutter.rs index a2dcbdbcf..bee4dd7a5 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -3,7 +3,7 @@ use crate::{ flutter_ffi::EventToUI, ui_session_interface::{io_loop, InvokeUiSession, Session}, }; -use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; +use flutter_rust_bridge::{StreamSink}; use hbb_common::{ bail, config::LocalConfig, get_version_number, message_proto::*, rendezvous_proto::ConnType, ResultType, @@ -289,9 +289,9 @@ impl InvokeUiSession for FlutterHandler { // unused in flutter fn adapt_size(&self) {} - fn on_rgba(&self, data: &[u8]) { + fn on_rgba(&self, data: Vec) { if let Some(stream) = &*self.event_stream.read().unwrap() { - let former_rgba = self.rgba.write().unwrap().replace(data.to_owned()); + let former_rgba = self.rgba.write().unwrap().replace(data); if former_rgba.is_none() { // The [former_rgba] is none, which means the latest rgba had taken from flutter. // We need to send a signal to flutter for notifying there's a new rgba buffer here. diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 06af70eae..b6663ad7e 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -201,12 +201,12 @@ impl InvokeUiSession for SciterHandler { self.call("adaptSize", &make_args!()); } - fn on_rgba(&self, data: &[u8]) { + fn on_rgba(&self, data: Vec) { VIDEO .lock() .unwrap() .as_mut() - .map(|v| v.render_frame(data).ok()); + .map(|v| v.render_frame(&data).ok()); } fn set_peer_info(&self, pi: &PeerInfo) { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 2944a76d1..cbf6d0171 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -712,7 +712,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn update_block_input_state(&self, on: bool); fn job_progress(&self, id: i32, file_num: i32, speed: f64, finished_size: f64); fn adapt_size(&self); - fn on_rgba(&self, data: &[u8]); + fn on_rgba(&self, data: Vec); fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str, retry: bool); #[cfg(any(target_os = "android", target_os = "ios"))] fn clipboard(&self, content: String); @@ -957,7 +957,7 @@ pub async fn io_loop(handler: Session) { let frame_count = Arc::new(AtomicUsize::new(0)); let frame_count_cl = frame_count.clone(); let ui_handler = handler.ui_handler.clone(); - let (video_sender, audio_sender) = start_video_audio_threads(move |data: &[u8]| { + let (video_sender, audio_sender) = start_video_audio_threads(move |data: Vec| { frame_count_cl.fetch_add(1, Ordering::Relaxed); ui_handler.on_rgba(data); }); From bf38fb7118321986b0cf502ab0809f742d74c3fb Mon Sep 17 00:00:00 2001 From: grummbeer Date: Sat, 11 Feb 2023 12:32:30 +0100 Subject: [PATCH 107/199] Dialog. Unify padding. --- flutter/lib/common.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index a295ad4f8..6c1245a7d 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -653,6 +653,7 @@ class CustomAlertDialog extends StatelessWidget { child: AlertDialog( scrollable: true, title: title, + titlePadding: EdgeInsets.fromLTRB(padding, 24, padding, 0), contentPadding: EdgeInsets.fromLTRB( contentPadding ?? padding, 25, contentPadding ?? padding, 10), content: ConstrainedBox( From 01d30bce9e4509b6129843bf2d460d0351c28638 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 12 Feb 2023 01:52:11 +0800 Subject: [PATCH 108/199] opt: reduce copy and malloc times for both of flutter and rust --- flutter/lib/models/model.dart | 30 +++++++++++++--- flutter/lib/models/native_model.dart | 26 +++++++++++++- src/client.rs | 8 ++--- src/flutter.rs | 53 ++++++++++++++++++++++------ src/flutter_ffi.rs | 10 ------ src/ui/remote.rs | 10 +++--- src/ui_session_interface.rs | 6 ++-- 7 files changed, 105 insertions(+), 38 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index f30209a60..e09a99875 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1,10 +1,12 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:ffi' hide Size; import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; import 'dart:ui' as ui; +import 'package:ffi/ffi.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/consts.dart'; @@ -1367,6 +1369,9 @@ class FFI { final stream = bind.sessionStart(id: id); final cb = ffiModel.startEventListener(id); () async { + // Preserved for the rgba data. + Pointer? buffer; + int? bufferSize; await for (final message in stream) { if (message is EventToUI_Event) { try { @@ -1377,13 +1382,30 @@ class FFI { } } else if (message is EventToUI_Rgba) { // Fetch the image buffer from rust codes. - bind.sessionGetRgba(id: id).then((rgba) { - if (rgba != null) { - imageModel.onRgba(rgba); + final sz = platformFFI.getRgbaSize(id); + if (sz == null) { + return; + } + // The buffer does not exists or the bufferSize is not + // equal to the required size. + if (buffer == null || bufferSize != sz) { + // reallocate buffer + if (buffer != null) { + malloc.free(buffer); } - }); + buffer = malloc.allocate(sz); + bufferSize = sz; + } + final rgba = platformFFI.getRgba(id, buffer, bufferSize!); + if (rgba != null) { + imageModel.onRgba(rgba); + } } } + // Free the buffer allocated on the heap. + if (buffer != null) { + malloc.free(buffer); + } }(); // every instance will bind a stream this.id = id; diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 34a673953..588c3646f 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -23,7 +23,10 @@ class RgbaFrame extends Struct { } typedef F2 = Pointer Function(Pointer, Pointer); -typedef F3 = void Function(Pointer, Pointer); +typedef F3 = Void Function(Pointer, Pointer); +typedef F3Dart = void Function(Pointer, Pointer); +typedef F4 = Uint64 Function(Pointer); +typedef F4Dart = int Function(Pointer); typedef HandleEvent = Future Function(Map evt); /// FFI wrapper around the native Rust core. @@ -44,6 +47,8 @@ class PlatformFFI { final _toAndroidChannel = const MethodChannel('mChannel'); RustdeskImpl get ffiBind => _ffiBind; + F3Dart? _session_get_rgba; + F4Dart? _session_get_rgba_size; static get localeName => Platform.localeName; @@ -92,6 +97,23 @@ class PlatformFFI { return res; } + Uint8List? getRgba(String id, Pointer buffer, int bufSize) { + if (_session_get_rgba == null) return null; + var a = id.toNativeUtf8(); + _session_get_rgba!(a, buffer); + final data = buffer.asTypedList(bufSize); + malloc.free(a); + return data; + } + + int? getRgbaSize(String id) { + if (_session_get_rgba_size == null) return null; + var a = id.toNativeUtf8(); + final bufferSize = _session_get_rgba_size!(a); + malloc.free(a); + return bufferSize; + } + /// Init the FFI class, loads the native Rust core library. Future init(String appType) async { _appType = appType; @@ -107,6 +129,8 @@ class PlatformFFI { debugPrint('initializing FFI $_appType'); try { _translate = dylib.lookupFunction('translate'); + _session_get_rgba = dylib.lookupFunction("session_get_rgba"); + _session_get_rgba_size = dylib.lookupFunction("session_get_rgba_size"); try { // SYSTEM user failed _dir = (await getApplicationDocumentsDirectory()).path; diff --git a/src/client.rs b/src/client.rs index ecfc59749..c6e0a759f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -817,7 +817,7 @@ impl AudioHandler { pub struct VideoHandler { decoder: Decoder, latency_controller: Arc>, - pub rgb: Vec, + pub rgb: Arc>>, recorder: Arc>>, record: bool, } @@ -850,7 +850,7 @@ impl VideoHandler { } match &vf.union { Some(frame) => { - let res = self.decoder.handle_video_frame(frame, &mut self.rgb); + let res = self.decoder.handle_video_frame(frame, &mut self.rgb.write().unwrap()); if self.record { self.recorder .lock() @@ -1545,7 +1545,7 @@ pub type MediaSender = mpsc::Sender; /// * `video_callback` - The callback for video frame. Being called when a video frame is ready. pub fn start_video_audio_threads(video_callback: F) -> (MediaSender, MediaSender) where - F: 'static + FnMut(Vec) + Send, + F: 'static + FnMut(Arc>>) + Send, { let (video_sender, video_receiver) = mpsc::channel::(); let mut video_callback = video_callback; @@ -1560,7 +1560,7 @@ where match data { MediaData::VideoFrame(vf) => { if let Ok(true) = video_handler.handle_frame(vf) { - video_callback(std::mem::replace(&mut video_handler.rgb, vec![])); + video_callback(video_handler.rgb.clone()); } } MediaData::Reset => { diff --git a/src/flutter.rs b/src/flutter.rs index bee4dd7a5..bb6f85bb9 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -15,6 +15,7 @@ use std::{ os::raw::{c_char, c_int}, sync::{Arc, RwLock}, }; +use libc::memcpy; pub(super) const APP_TYPE_MAIN: &str = "main"; pub(super) const APP_TYPE_CM: &str = "cm"; @@ -110,7 +111,8 @@ pub unsafe extern "C" fn free_c_args(ptr: *mut *mut c_char, len: c_int) { #[derive(Default, Clone)] pub struct FlutterHandler { pub event_stream: Arc>>>, - pub rgba: Arc>>> + pub rgba: Arc>>, + pub rgba_valid: Arc> } impl FlutterHandler { @@ -289,15 +291,18 @@ impl InvokeUiSession for FlutterHandler { // unused in flutter fn adapt_size(&self) {} - fn on_rgba(&self, data: Vec) { - if let Some(stream) = &*self.event_stream.read().unwrap() { - let former_rgba = self.rgba.write().unwrap().replace(data); - if former_rgba.is_none() { - // The [former_rgba] is none, which means the latest rgba had taken from flutter. - // We need to send a signal to flutter for notifying there's a new rgba buffer here. - stream.add(EventToUI::Rgba); - } + fn on_rgba(&self, data: Arc>>) { + // If the current rgba is not fetched by flutter, i.e., is valid. + // We give up sending a new event to flutter. + if *self.rgba_valid.read().unwrap() { + return; } + // Return the rgba buffer to the video handler for reusing allocated rgba buffer. + std::mem::swap::>(data.write().unwrap().as_mut(), self.rgba.write().unwrap().as_mut()); + if let Some(stream) = &*self.event_stream.read().unwrap() { + stream.add(EventToUI::Rgba); + } + let _ = std::mem::replace(&mut *self.rgba_valid.write().unwrap(), true); } fn set_peer_info(&self, pi: &PeerInfo) { @@ -416,8 +421,13 @@ impl InvokeUiSession for FlutterHandler { self.push_event("on_voice_call_incoming", [].into()); } - fn get_rgba(&self) -> Option> { - self.rgba.write().unwrap().take() + fn get_rgba(&mut self, buffer: *mut u8) { + // [Safety] + // * It must be ensures the buffer has enough space to place the whole rgba. + let max_len = self.rgba.read().unwrap().len(); + unsafe { std::ptr::copy_nonoverlapping(self.rgba.read().unwrap().as_ptr(), buffer, max_len)}; + // mark the rgba has been taken from flutter. + let _ = std::mem::replace(&mut *self.rgba_valid.write().unwrap(), false); } } @@ -645,3 +655,24 @@ pub fn set_cur_session_id(id: String) { *CUR_SESSION_ID.write().unwrap() = id; } } + +#[no_mangle] +pub fn session_get_rgba_size(id: *const char) -> usize { + let id = unsafe { std::ffi::CStr::from_ptr(id as _) }; + if let Ok(id) = id.to_str() { + if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { + return session.rgba.read().unwrap().len(); + } + } + 0 +} + +#[no_mangle] +pub fn session_get_rgba(id: *const char, buffer: *mut u8) { + let id = unsafe { std::ffi::CStr::from_ptr(id as _) }; + if let Ok(id) = id.to_str() { + if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { + return session.get_rgba(buffer); + } + } +} \ No newline at end of file diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 3a0fcc5fa..b4e79b361 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -104,16 +104,6 @@ pub fn session_get_remember(id: String) -> Option { } } -pub fn session_get_rgba(id: String) -> Option>> { - if let Some(session) = SESSIONS.read().unwrap().get(&id) { - return match session.get_rgba() { - Some(buf) => Some(ZeroCopyBuffer(buf)), - _ => None - }; - } - None -} - pub fn session_get_toggle_option(id: String, arg: String) -> Option { if let Some(session) = SESSIONS.read().unwrap().get(&id) { Some(session.get_toggle_option(arg)) diff --git a/src/ui/remote.rs b/src/ui/remote.rs index b6663ad7e..ecf96ab32 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -3,6 +3,7 @@ use std::{ ops::{Deref, DerefMut}, sync::{Arc, Mutex}, }; +use std::sync::RwLock; use sciter::{ dom::{ @@ -17,6 +18,7 @@ use sciter::{ use hbb_common::{ allow_err, fs::TransferJobMeta, log, message_proto::*, rendezvous_proto::ConnType, }; +use hbb_common::tokio::io::AsyncReadExt; use crate::{ client::*, @@ -201,12 +203,12 @@ impl InvokeUiSession for SciterHandler { self.call("adaptSize", &make_args!()); } - fn on_rgba(&self, data: Vec) { + fn on_rgba(&self, data: Arc>>) { VIDEO .lock() .unwrap() .as_mut() - .map(|v| v.render_frame(&data).ok()); + .map(|v| v.render_frame(data.read().unwrap().as_ref()).ok()); } fn set_peer_info(&self, pi: &PeerInfo) { @@ -284,9 +286,7 @@ impl InvokeUiSession for SciterHandler { } /// RGBA is directly rendered by [on_rgba]. No need to store the rgba for the sciter ui. - fn get_rgba(&self) -> Option> { - None - } + fn get_rgba(&mut self, _buffer: *mut u8) {} } pub struct SciterSession(Session); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index cbf6d0171..85deb68c2 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -712,7 +712,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn update_block_input_state(&self, on: bool); fn job_progress(&self, id: i32, file_num: i32, speed: f64, finished_size: f64); fn adapt_size(&self); - fn on_rgba(&self, data: Vec); + fn on_rgba(&self, data: Arc>>); fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str, retry: bool); #[cfg(any(target_os = "android", target_os = "ios"))] fn clipboard(&self, content: String); @@ -722,7 +722,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn on_voice_call_closed(&self, reason: &str); fn on_voice_call_waiting(&self); fn on_voice_call_incoming(&self); - fn get_rgba(&self) -> Option>; + fn get_rgba(&mut self, buffer: *mut u8); } impl Deref for Session { @@ -957,7 +957,7 @@ pub async fn io_loop(handler: Session) { let frame_count = Arc::new(AtomicUsize::new(0)); let frame_count_cl = frame_count.clone(); let ui_handler = handler.ui_handler.clone(); - let (video_sender, audio_sender) = start_video_audio_threads(move |data: Vec| { + let (video_sender, audio_sender) = start_video_audio_threads(move |data: Arc>> | { frame_count_cl.fetch_add(1, Ordering::Relaxed); ui_handler.on_rgba(data); }); From e0007788b1bec4af91bc286d46f3725c25614a65 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 12 Feb 2023 08:25:48 +0800 Subject: [PATCH 109/199] no blank issue, and make logo.svg compatible with flutter without inline style --- .github/ISSUE_TEMPLATE/config.yml | 1 + flutter/assets/logo.svg | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 7b43e397b..2da6bbaf1 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,3 +1,4 @@ +blank_issues_enabled: false contact_links: - name: Ask a question url: https://github.com/rustdesk/rustdesk/discussions/category_choices diff --git a/flutter/assets/logo.svg b/flutter/assets/logo.svg index 965218c95..d3a3f7b37 100644 --- a/flutter/assets/logo.svg +++ b/flutter/assets/logo.svg @@ -1 +1 @@ - \ No newline at end of file + From fbbb2cd4ff9ac856e5511b3b6de796197caafdfe Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 12 Feb 2023 08:49:09 +0800 Subject: [PATCH 110/199] fix another svg compatibility, move def back, to make href can find --- flutter/assets/logo.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/assets/logo.svg b/flutter/assets/logo.svg index d3a3f7b37..13eb73f22 100644 --- a/flutter/assets/logo.svg +++ b/flutter/assets/logo.svg @@ -1 +1 @@ - + From 3d40569dee56c903b481f3ab27108f524bb74e6c Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 12 Feb 2023 09:03:13 +0800 Subject: [PATCH 111/199] change all ocusNode: FocusNode()..requestFocus(), to autofocus: true`` --- flutter/lib/common/widgets/address_book.dart | 2 +- flutter/lib/common/widgets/dialog.dart | 6 +++--- flutter/lib/common/widgets/peer_card.dart | 4 ++-- flutter/lib/desktop/pages/desktop_home_page.dart | 2 +- flutter/lib/desktop/pages/file_manager_page.dart | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index 5cd2af2be..bd2a01296 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -386,7 +386,7 @@ class _AddressBookState extends State { errorText: msg.isEmpty ? null : translate(msg), ), controller: controller, - focusNode: FocusNode()..requestFocus(), + autofocus: true, ), ), ], diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart index 837a197dc..e96a2b406 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -54,7 +54,7 @@ void changeIdDialog() { ], maxLength: 16, controller: controller, - focusNode: FocusNode()..requestFocus(), + autofocus: true, ), const SizedBox( height: 4.0, @@ -99,7 +99,7 @@ void changeWhiteList({Function()? callback}) async { errorText: msg.isEmpty ? null : translate(msg), ), controller: controller, - focusNode: FocusNode()..requestFocus()), + autofocus: true), ), ], ), @@ -186,7 +186,7 @@ Future changeDirectAccessPort( r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')), ], controller: controller, - focusNode: FocusNode()..requestFocus()), + autofocus: true), ), ], ), diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index c9af6328c..3c9a438a0 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -641,7 +641,7 @@ abstract class BasePeerCard extends StatelessWidget { child: Form( child: TextFormField( controller: controller, - focusNode: FocusNode()..requestFocus(), + autofocus: true, decoration: const InputDecoration(border: OutlineInputBorder()), ), @@ -1013,7 +1013,7 @@ void _rdpDialog(String id) async { decoration: const InputDecoration( border: OutlineInputBorder(), hintText: '3389'), controller: portController, - focusNode: FocusNode()..requestFocus(), + autofocus: true, ), ), ], diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 2986adc7a..cde1e6d74 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -634,7 +634,7 @@ void setPasswordDialog() async { border: const OutlineInputBorder(), errorText: errMsg0.isNotEmpty ? errMsg0 : null), controller: p0, - focusNode: FocusNode()..requestFocus(), + autofocus: true, onChanged: (value) { rxPass.value = value.trim(); }, diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 9955c2768..27bb0377d 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -798,7 +798,7 @@ class _FileManagerPageState extends State "Please enter the folder name"), ), controller: name, - focusNode: FocusNode()..requestFocus(), + autofocus: true, ), ], ), From d2e24173d0d87e840e41e22dc1a74b588322979e Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 12 Feb 2023 10:28:04 +0800 Subject: [PATCH 112/199] opt: read uint8list directly from rust codes --- flutter/lib/models/model.dart | 30 +++--------------- flutter/lib/models/native_model.dart | 39 +++++++++++++++++------ src/client.rs | 8 ++--- src/flutter.rs | 47 +++++++++++++++++++--------- src/ui/remote.rs | 8 +++-- src/ui_session_interface.rs | 7 +++-- 6 files changed, 78 insertions(+), 61 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index e09a99875..8cf90eba9 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -417,8 +417,6 @@ class ImageModel with ChangeNotifier { String id = ''; - int decodeCount = 0; - WeakReference parent; final List _callbacksOnFirstImage = []; @@ -439,20 +437,16 @@ class ImageModel with ChangeNotifier { } } - if (decodeCount >= 1) { - return; - } - final pid = parent.target?.id; - decodeCount += 1; ui.decodeImageFromPixels( rgba, parent.target?.ffiModel.display.width ?? 0, parent.target?.ffiModel.display.height ?? 0, isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888, (image) { - decodeCount -= 1; if (parent.target?.id != pid) return; try { + // Unlock the rgba memory from rust codes. + platformFFI.nextRgba(id); // my throw exception, because the listener maybe already dispose update(image); } catch (e) { @@ -1370,8 +1364,6 @@ class FFI { final cb = ffiModel.startEventListener(id); () async { // Preserved for the rgba data. - Pointer? buffer; - int? bufferSize; await for (final message in stream) { if (message is EventToUI_Event) { try { @@ -1383,29 +1375,15 @@ class FFI { } else if (message is EventToUI_Rgba) { // Fetch the image buffer from rust codes. final sz = platformFFI.getRgbaSize(id); - if (sz == null) { + if (sz == null || sz == 0) { return; } - // The buffer does not exists or the bufferSize is not - // equal to the required size. - if (buffer == null || bufferSize != sz) { - // reallocate buffer - if (buffer != null) { - malloc.free(buffer); - } - buffer = malloc.allocate(sz); - bufferSize = sz; - } - final rgba = platformFFI.getRgba(id, buffer, bufferSize!); + final rgba = platformFFI.getRgba(id, sz); if (rgba != null) { imageModel.onRgba(rgba); } } } - // Free the buffer allocated on the heap. - if (buffer != null) { - malloc.free(buffer); - } }(); // every instance will bind a stream this.id = id; diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 588c3646f..ba62b775e 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -9,6 +9,7 @@ import 'package:ffi/ffi.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/consts.dart'; +import 'package:get/get.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:path_provider/path_provider.dart'; import 'package:win32/win32.dart' as win32; @@ -23,10 +24,11 @@ class RgbaFrame extends Struct { } typedef F2 = Pointer Function(Pointer, Pointer); -typedef F3 = Void Function(Pointer, Pointer); -typedef F3Dart = void Function(Pointer, Pointer); +typedef F3 = Pointer Function(Pointer); typedef F4 = Uint64 Function(Pointer); typedef F4Dart = int Function(Pointer); +typedef F5 = Void Function(Pointer); +typedef F5Dart = void Function(Pointer); typedef HandleEvent = Future Function(Map evt); /// FFI wrapper around the native Rust core. @@ -47,8 +49,9 @@ class PlatformFFI { final _toAndroidChannel = const MethodChannel('mChannel'); RustdeskImpl get ffiBind => _ffiBind; - F3Dart? _session_get_rgba; + F3? _session_get_rgba; F4Dart? _session_get_rgba_size; + F5Dart? _session_next_rgba; static get localeName => Platform.localeName; @@ -97,13 +100,19 @@ class PlatformFFI { return res; } - Uint8List? getRgba(String id, Pointer buffer, int bufSize) { + Uint8List? getRgba(String id, int bufSize) { if (_session_get_rgba == null) return null; var a = id.toNativeUtf8(); - _session_get_rgba!(a, buffer); - final data = buffer.asTypedList(bufSize); - malloc.free(a); - return data; + try { + final buffer = _session_get_rgba!(a); + if (buffer == nullptr) { + return null; + } + final data = buffer.asTypedList(bufSize); + return data; + } finally { + malloc.free(a); + } } int? getRgbaSize(String id) { @@ -114,6 +123,13 @@ class PlatformFFI { return bufferSize; } + void nextRgba(String id) { + if (_session_next_rgba == null) return; + final a = id.toNativeUtf8(); + _session_next_rgba!(a); + malloc.free(a); + } + /// Init the FFI class, loads the native Rust core library. Future init(String appType) async { _appType = appType; @@ -129,8 +145,11 @@ class PlatformFFI { debugPrint('initializing FFI $_appType'); try { _translate = dylib.lookupFunction('translate'); - _session_get_rgba = dylib.lookupFunction("session_get_rgba"); - _session_get_rgba_size = dylib.lookupFunction("session_get_rgba_size"); + _session_get_rgba = dylib.lookupFunction("session_get_rgba"); + _session_get_rgba_size = + dylib.lookupFunction("session_get_rgba_size"); + _session_next_rgba = + dylib.lookupFunction("session_next_rgba"); try { // SYSTEM user failed _dir = (await getApplicationDocumentsDirectory()).path; diff --git a/src/client.rs b/src/client.rs index c6e0a759f..a21592578 100644 --- a/src/client.rs +++ b/src/client.rs @@ -817,7 +817,7 @@ impl AudioHandler { pub struct VideoHandler { decoder: Decoder, latency_controller: Arc>, - pub rgb: Arc>>, + pub rgb: Vec, recorder: Arc>>, record: bool, } @@ -850,7 +850,7 @@ impl VideoHandler { } match &vf.union { Some(frame) => { - let res = self.decoder.handle_video_frame(frame, &mut self.rgb.write().unwrap()); + let res = self.decoder.handle_video_frame(frame, &mut self.rgb); if self.record { self.recorder .lock() @@ -1545,7 +1545,7 @@ pub type MediaSender = mpsc::Sender; /// * `video_callback` - The callback for video frame. Being called when a video frame is ready. pub fn start_video_audio_threads(video_callback: F) -> (MediaSender, MediaSender) where - F: 'static + FnMut(Arc>>) + Send, + F: 'static + FnMut(&mut Vec) + Send, { let (video_sender, video_receiver) = mpsc::channel::(); let mut video_callback = video_callback; @@ -1560,7 +1560,7 @@ where match data { MediaData::VideoFrame(vf) => { if let Ok(true) = video_handler.handle_frame(vf) { - video_callback(video_handler.rgb.clone()); + video_callback(&mut video_handler.rgb); } } MediaData::Reset => { diff --git a/src/flutter.rs b/src/flutter.rs index bb6f85bb9..a60e379f9 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -15,7 +15,7 @@ use std::{ os::raw::{c_char, c_int}, sync::{Arc, RwLock}, }; -use libc::memcpy; +use std::sync::atomic::{AtomicBool, Ordering}; pub(super) const APP_TYPE_MAIN: &str = "main"; pub(super) const APP_TYPE_CM: &str = "cm"; @@ -111,8 +111,10 @@ pub unsafe extern "C" fn free_c_args(ptr: *mut *mut c_char, len: c_int) { #[derive(Default, Clone)] pub struct FlutterHandler { pub event_stream: Arc>>>, + // SAFETY: [rgba] is guarded by [rgba_valid], and it's safe to reach [rgba] with `rgba_valid == true`. + // We must check the `rgba_valid` before reading [rgba]. pub rgba: Arc>>, - pub rgba_valid: Arc> + pub rgba_valid: Arc } impl FlutterHandler { @@ -291,18 +293,18 @@ impl InvokeUiSession for FlutterHandler { // unused in flutter fn adapt_size(&self) {} - fn on_rgba(&self, data: Arc>>) { + fn on_rgba(&self, data: &mut Vec) { // If the current rgba is not fetched by flutter, i.e., is valid. // We give up sending a new event to flutter. - if *self.rgba_valid.read().unwrap() { + if self.rgba_valid.load(Ordering::Relaxed) { return; } + self.rgba_valid.store(true, Ordering::Relaxed); // Return the rgba buffer to the video handler for reusing allocated rgba buffer. - std::mem::swap::>(data.write().unwrap().as_mut(), self.rgba.write().unwrap().as_mut()); + std::mem::swap::>(data, &mut *self.rgba.write().unwrap()); if let Some(stream) = &*self.event_stream.read().unwrap() { stream.add(EventToUI::Rgba); } - let _ = std::mem::replace(&mut *self.rgba_valid.write().unwrap(), true); } fn set_peer_info(&self, pi: &PeerInfo) { @@ -421,13 +423,17 @@ impl InvokeUiSession for FlutterHandler { self.push_event("on_voice_call_incoming", [].into()); } - fn get_rgba(&mut self, buffer: *mut u8) { - // [Safety] - // * It must be ensures the buffer has enough space to place the whole rgba. - let max_len = self.rgba.read().unwrap().len(); - unsafe { std::ptr::copy_nonoverlapping(self.rgba.read().unwrap().as_ptr(), buffer, max_len)}; - // mark the rgba has been taken from flutter. - let _ = std::mem::replace(&mut *self.rgba_valid.write().unwrap(), false); + #[inline] + fn get_rgba(&self) -> *const u8 { + if self.rgba_valid.load(Ordering::Relaxed) { + return self.rgba.read().unwrap().as_ptr(); + } + std::ptr::null_mut() + } + + #[inline] + fn next_rgba(&mut self) { + self.rgba_valid.store(false, Ordering::Relaxed); } } @@ -668,11 +674,22 @@ pub fn session_get_rgba_size(id: *const char) -> usize { } #[no_mangle] -pub fn session_get_rgba(id: *const char, buffer: *mut u8) { +pub fn session_get_rgba(id: *const char) -> *const u8 { let id = unsafe { std::ffi::CStr::from_ptr(id as _) }; if let Ok(id) = id.to_str() { if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { - return session.get_rgba(buffer); + return session.get_rgba(); + } + } + std::ptr::null() +} + +#[no_mangle] +pub fn session_next_rgba(id: *const char) { + let id = unsafe { std::ffi::CStr::from_ptr(id as _) }; + if let Ok(id) = id.to_str() { + if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { + return session.next_rgba(); } } } \ No newline at end of file diff --git a/src/ui/remote.rs b/src/ui/remote.rs index ecf96ab32..e44e31401 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -203,12 +203,12 @@ impl InvokeUiSession for SciterHandler { self.call("adaptSize", &make_args!()); } - fn on_rgba(&self, data: Arc>>) { + fn on_rgba(&self, data: &mut Vec) { VIDEO .lock() .unwrap() .as_mut() - .map(|v| v.render_frame(data.read().unwrap().as_ref()).ok()); + .map(|v| v.render_frame(data).ok()); } fn set_peer_info(&self, pi: &PeerInfo) { @@ -286,7 +286,9 @@ impl InvokeUiSession for SciterHandler { } /// RGBA is directly rendered by [on_rgba]. No need to store the rgba for the sciter ui. - fn get_rgba(&mut self, _buffer: *mut u8) {} + fn get_rgba(&self) -> *const u8 { std::ptr::null() } + + fn next_rgba(&mut self) {} } pub struct SciterSession(Session); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 85deb68c2..25c15f52f 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -712,7 +712,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn update_block_input_state(&self, on: bool); fn job_progress(&self, id: i32, file_num: i32, speed: f64, finished_size: f64); fn adapt_size(&self); - fn on_rgba(&self, data: Arc>>); + fn on_rgba(&self, data: &mut Vec); fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str, retry: bool); #[cfg(any(target_os = "android", target_os = "ios"))] fn clipboard(&self, content: String); @@ -722,7 +722,8 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn on_voice_call_closed(&self, reason: &str); fn on_voice_call_waiting(&self); fn on_voice_call_incoming(&self); - fn get_rgba(&mut self, buffer: *mut u8); + fn get_rgba(&self) -> *const u8; + fn next_rgba(&mut self); } impl Deref for Session { @@ -957,7 +958,7 @@ pub async fn io_loop(handler: Session) { let frame_count = Arc::new(AtomicUsize::new(0)); let frame_count_cl = frame_count.clone(); let ui_handler = handler.ui_handler.clone(); - let (video_sender, audio_sender) = start_video_audio_threads(move |data: Arc>> | { + let (video_sender, audio_sender) = start_video_audio_threads(move |data: &mut Vec | { frame_count_cl.fetch_add(1, Ordering::Relaxed); ui_handler.on_rgba(data); }); From 9fb5b2cb5f9511c2b4754fa1a18cacd1cce1922d Mon Sep 17 00:00:00 2001 From: csf Date: Sun, 12 Feb 2023 21:26:04 +0900 Subject: [PATCH 113/199] use flutter_keyboard_visibility --- flutter/lib/mobile/pages/remote_page.dart | 71 +++++++++-------------- flutter/pubspec.lock | 48 +++++++++++++++ flutter/pubspec.yaml | 1 + 3 files changed, 75 insertions(+), 45 deletions(-) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 54b6f1d47..d1faa5494 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -7,6 +7,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/mobile/widgets/gesture_help.dart'; import 'package:flutter_hbb/models/chat_model.dart'; +import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart'; import 'package:provider/provider.dart'; import 'package:wakelock/wakelock.dart'; @@ -33,10 +34,8 @@ class RemotePage extends StatefulWidget { } class _RemotePageState extends State { - Timer? _interval; Timer? _timer; bool _showBar = !isWebDesktop; - double _bottom = 0; String _value = ''; double _scale = 1; double _mouseScrollIntegral = 0; // mouse scroll speed controller @@ -44,6 +43,8 @@ class _RemotePageState extends State { var _more = true; var _fn = false; + late final keyboardVisibilityController = KeyboardVisibilityController(); + late final StreamSubscription keyboardSubscription; final FocusNode _mobileFocusNode = FocusNode(); final FocusNode _physicalFocusNode = FocusNode(); var _showEdit = false; // use soft keyboard @@ -58,14 +59,14 @@ class _RemotePageState extends State { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); gFFI.dialogManager .showLoading(translate('Connecting...'), onCancel: closeConnection); - _interval = - Timer.periodic(Duration(milliseconds: 30), (timer) => interval()); }); Wakelock.enable(); _physicalFocusNode.requestFocus(); gFFI.ffiModel.updateEventListener(widget.id); gFFI.inputModel.listenToMouse(true); gFFI.qualityMonitorModel.checkShowQualityMonitor(widget.id); + keyboardSubscription = + keyboardVisibilityController.onChange.listen(onSoftKeyboardChanged); } @override @@ -76,49 +77,27 @@ class _RemotePageState extends State { _mobileFocusNode.dispose(); _physicalFocusNode.dispose(); gFFI.close(); - _interval?.cancel(); _timer?.cancel(); gFFI.dialogManager.dismissAll(); SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); Wakelock.disable(); + keyboardSubscription.cancel(); super.dispose(); } - void resetTool() { + void onSoftKeyboardChanged(bool visible) { inputModel.resetModifiers(); - } - - bool isKeyboardShown() { - return _bottom >= 100; - } - - // crash on web before widget initiated. - void intervalUnsafe() { - var v = MediaQuery.of(context).viewInsets.bottom; - if (v != _bottom) { - resetTool(); - setState(() { - _bottom = v; - if (v < 100) { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, - overlays: []); - // [pi.version.isNotEmpty] -> check ready or not, avoid login without soft-keyboard - if (gFFI.chatModel.chatWindowOverlayEntry == null && - gFFI.ffiModel.pi.version.isNotEmpty) { - gFFI.invokeMethod("enable_soft_keyboard", false); - } - } - }); + if (!visible) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); + // [pi.version.isNotEmpty] -> check ready or not, avoid login without soft-keyboard + if (gFFI.chatModel.chatWindowOverlayEntry == null && + gFFI.ffiModel.pi.version.isNotEmpty) { + gFFI.invokeMethod("enable_soft_keyboard", false); + } } } - void interval() { - try { - intervalUnsafe(); - } catch (e) {} - } - // handle mobile virtual keyboard void handleSoftKeyboardInput(String newValue) { var oldValue = _value; @@ -219,8 +198,9 @@ class _RemotePageState extends State { @override Widget build(BuildContext context) { final pi = Provider.of(context).pi; - final hideKeyboard = isKeyboardShown() && _showEdit; - final showActionButton = !_showBar || hideKeyboard; + final isHideKeyboardFAB = + keyboardVisibilityController.isVisible && _showEdit; + final showActionButton = !_showBar || isHideKeyboardFAB; final keyboard = gFFI.ffiModel.permissions['keyboard'] != false; return WillPopScope( @@ -230,21 +210,21 @@ class _RemotePageState extends State { }, child: getRawPointerAndKeyBody(Scaffold( // workaround for https://github.com/rustdesk/rustdesk/issues/3131 - floatingActionButtonLocation: hideKeyboard + floatingActionButtonLocation: isHideKeyboardFAB ? FABLocation(FloatingActionButtonLocation.endFloat, 0, -35) : null, floatingActionButton: !showActionButton ? null : FloatingActionButton( - mini: !hideKeyboard, + mini: !isHideKeyboardFAB, child: Icon( - hideKeyboard ? Icons.expand_more : Icons.expand_less, + isHideKeyboardFAB ? Icons.expand_more : Icons.expand_less, color: Colors.white, ), backgroundColor: MyTheme.accent, onPressed: () { setState(() { - if (hideKeyboard) { + if (isHideKeyboardFAB) { _showEdit = false; gFFI.invokeMethod("enable_soft_keyboard", false); _mobileFocusNode.unfocus(); @@ -725,7 +705,7 @@ class _RemotePageState extends State { // } Widget getHelpTools() { - final keyboard = isKeyboardShown(); + final keyboard = keyboardVisibilityController.isVisible; if (!keyboard) { return SizedBox(); } @@ -858,9 +838,10 @@ class _RemotePageState extends State { spacing: space, runSpacing: space, children: [SizedBox(width: 9999)] + - (keyboard - ? modifiers + keys + (_fn ? fn : []) + (_more ? more : []) - : modifiers), + modifiers + + keys + + (_fn ? fn : []) + + (_more ? more : []), )); } } diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index cd618dfc4..91a061fb9 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -488,6 +488,54 @@ packages: url: "https://github.com/Kingtous/flutter_improved_scrolling" source: git version: "0.0.3" + flutter_keyboard_visibility: + dependency: "direct main" + description: + name: flutter_keyboard_visibility + sha256: "86b71bbaffa38e885f5c21b1182408b9be6951fd125432cf6652c636254cef2d" + url: "https://pub.dev" + source: hosted + version: "5.4.0" + flutter_keyboard_visibility_linux: + dependency: transitive + description: + name: flutter_keyboard_visibility_linux + sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_keyboard_visibility_macos: + dependency: transitive + description: + name: flutter_keyboard_visibility_macos + sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086 + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_keyboard_visibility_platform_interface: + dependency: transitive + description: + name: flutter_keyboard_visibility_platform_interface + sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + flutter_keyboard_visibility_web: + dependency: transitive + description: + name: flutter_keyboard_visibility_web + sha256: d3771a2e752880c79203f8d80658401d0c998e4183edca05a149f5098ce6e3d1 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + flutter_keyboard_visibility_windows: + dependency: transitive + description: + name: flutter_keyboard_visibility_windows + sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73 + url: "https://pub.dev" + source: hosted + version: "1.0.0" flutter_launcher_icons: dependency: "direct main" description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 8701d9f5b..df29252c9 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -91,6 +91,7 @@ dependencies: win32: any password_strength: ^0.2.0 flutter_launcher_icons: ^0.11.0 + flutter_keyboard_visibility: ^5.4.0 dev_dependencies: From 6e4e463f5f28e5c819e46570e12bb2e2a867ccc1 Mon Sep 17 00:00:00 2001 From: csf Date: Sun, 12 Feb 2023 22:03:43 +0900 Subject: [PATCH 114/199] update HelpTools, use StatefulWidget --- flutter/lib/mobile/pages/remote_page.dart | 74 ++++++++++++++--------- 1 file changed, 46 insertions(+), 28 deletions(-) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index d1faa5494..1ec57b46e 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -41,8 +41,6 @@ class _RemotePageState extends State { double _mouseScrollIntegral = 0; // mouse scroll speed controller Orientation? _currentOrientation; - var _more = true; - var _fn = false; late final keyboardVisibilityController = KeyboardVisibilityController(); late final StreamSubscription keyboardSubscription; final FocusNode _mobileFocusNode = FocusNode(); @@ -96,6 +94,8 @@ class _RemotePageState extends State { gFFI.invokeMethod("enable_soft_keyboard", false); } } + // update for Scaffold + setState(() {}); } // handle mobile virtual keyboard @@ -478,6 +478,7 @@ class _RemotePageState extends State { } Widget getBodyForMobile() { + final keyboardIsVisible = keyboardVisibilityController.isVisible; return Container( color: MyTheme.canvasColor, child: Stack(children: () { @@ -488,7 +489,7 @@ class _RemotePageState extends State { right: 10, child: QualityMonitor(gFFI.qualityMonitorModel), ), - getHelpTools(), + KeyHelpTools(requestShow: keyboardIsVisible), SizedBox( width: 0, height: 0, @@ -703,33 +704,51 @@ class _RemotePageState extends State { // ])); // }, clickMaskDismiss: true); // } +} - Widget getHelpTools() { - final keyboard = keyboardVisibilityController.isVisible; - if (!keyboard) { +class KeyHelpTools extends StatefulWidget { + /// need to show by external request, etc [keyboardIsVisible] or [changeTouchMode] + final bool requestShow; + + KeyHelpTools({required this.requestShow}); + + @override + State createState() => _KeyHelpToolsState(); +} + +class _KeyHelpToolsState extends State { + var _more = true; + var _fn = false; + + InputModel get inputModel => gFFI.inputModel; + + Widget wrap(String text, void Function() onPressed, + [bool? active, IconData? icon]) { + return TextButton( + style: TextButton.styleFrom( + minimumSize: Size(0, 0), + padding: EdgeInsets.symmetric(vertical: 10, horizontal: 9.75), + //adds padding inside the button + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + //limits the touch area to the button area + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5.0), + ), + backgroundColor: active == true ? MyTheme.accent80 : null, + ), + child: icon != null + ? Icon(icon, size: 17, color: Colors.white) + : Text(translate(text), + style: TextStyle(color: Colors.white, fontSize: 11)), + onPressed: onPressed); + } + + @override + Widget build(BuildContext context) { + if (!widget.requestShow) { return SizedBox(); } final size = MediaQuery.of(context).size; - wrap(String text, void Function() onPressed, - [bool? active, IconData? icon]) { - return TextButton( - style: TextButton.styleFrom( - minimumSize: Size(0, 0), - padding: EdgeInsets.symmetric(vertical: 10, horizontal: 9.75), - //adds padding inside the button - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - //limits the touch area to the button area - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5.0), - ), - backgroundColor: active == true ? MyTheme.accent80 : null, - ), - child: icon != null - ? Icon(icon, size: 17, color: Colors.white) - : Text(translate(text), - style: TextStyle(color: Colors.white, fontSize: 11)), - onPressed: onPressed); - } final pi = gFFI.ffiModel.pi; final isMac = pi.platform == kPeerPlatformMacOS; @@ -832,8 +851,7 @@ class _RemotePageState extends State { final space = size.width > 320 ? 4.0 : 2.0; return Container( color: Color(0xAA000000), - padding: EdgeInsets.only( - top: keyboard ? 24 : 4, left: 0, right: 0, bottom: 8), + padding: EdgeInsets.only(top: widget.requestShow ? 24 : 4, bottom: 8), child: Wrap( spacing: space, runSpacing: space, From 4b52431dbf295b1d71361335ddcb6838a48c2c2e Mon Sep 17 00:00:00 2001 From: csf Date: Sun, 12 Feb 2023 22:20:51 +0900 Subject: [PATCH 115/199] KeyHelpTools add pin , and keep enable when hasModifierOn --- flutter/lib/mobile/pages/remote_page.dart | 42 +++++++++++++++-------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 1ec57b46e..63a289c95 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -85,7 +85,6 @@ class _RemotePageState extends State { } void onSoftKeyboardChanged(bool visible) { - inputModel.resetModifiers(); if (!visible) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); // [pi.version.isNotEmpty] -> check ready or not, avoid login without soft-keyboard @@ -719,11 +718,12 @@ class KeyHelpTools extends StatefulWidget { class _KeyHelpToolsState extends State { var _more = true; var _fn = false; + var _pin = false; InputModel get inputModel => gFFI.inputModel; Widget wrap(String text, void Function() onPressed, - [bool? active, IconData? icon]) { + {bool? active, IconData? icon}) { return TextButton( style: TextButton.styleFrom( minimumSize: Size(0, 0), @@ -737,7 +737,7 @@ class _KeyHelpToolsState extends State { backgroundColor: active == true ? MyTheme.accent80 : null, ), child: icon != null - ? Icon(icon, size: 17, color: Colors.white) + ? Icon(icon, size: 14, color: Colors.white) : Text(translate(text), style: TextStyle(color: Colors.white, fontSize: 11)), onPressed: onPressed); @@ -745,8 +745,13 @@ class _KeyHelpToolsState extends State { @override Widget build(BuildContext context) { - if (!widget.requestShow) { - return SizedBox(); + final hasModifierOn = inputModel.ctrl || + inputModel.alt || + inputModel.shift || + inputModel.command; + + if (!_pin && !hasModifierOn && !widget.requestShow) { + return Offstage(); } final size = MediaQuery.of(context).size; @@ -755,16 +760,16 @@ class _KeyHelpToolsState extends State { final modifiers = [ wrap('Ctrl ', () { setState(() => inputModel.ctrl = !inputModel.ctrl); - }, inputModel.ctrl), + }, active: inputModel.ctrl), wrap(' Alt ', () { setState(() => inputModel.alt = !inputModel.alt); - }, inputModel.alt), + }, active: inputModel.alt), wrap('Shift', () { setState(() => inputModel.shift = !inputModel.shift); - }, inputModel.shift), + }, active: inputModel.shift), wrap(isMac ? ' Cmd ' : ' Win ', () { setState(() => inputModel.command = !inputModel.command); - }, inputModel.command), + }, active: inputModel.command), ]; final keys = [ wrap( @@ -777,7 +782,14 @@ class _KeyHelpToolsState extends State { } }, ), - _fn), + active: _fn), + wrap( + '', + () => setState( + () => _pin = !_pin, + ), + active: _pin, + icon: Icons.push_pin), wrap( ' ... ', () => setState( @@ -788,7 +800,7 @@ class _KeyHelpToolsState extends State { } }, ), - _more), + active: _more), ]; final fn = [ SizedBox(width: 9999), @@ -828,16 +840,16 @@ class _KeyHelpToolsState extends State { SizedBox(width: 9999), wrap('', () { inputModel.inputKey('VK_LEFT'); - }, false, Icons.keyboard_arrow_left), + }, icon: Icons.keyboard_arrow_left), wrap('', () { inputModel.inputKey('VK_UP'); - }, false, Icons.keyboard_arrow_up), + }, icon: Icons.keyboard_arrow_up), wrap('', () { inputModel.inputKey('VK_DOWN'); - }, false, Icons.keyboard_arrow_down), + }, icon: Icons.keyboard_arrow_down), wrap('', () { inputModel.inputKey('VK_RIGHT'); - }, false, Icons.keyboard_arrow_right), + }, icon: Icons.keyboard_arrow_right), wrap(isMac ? 'Cmd+C' : 'Ctrl+C', () { sendPrompt(isMac, 'VK_C'); }), From 14a187f47105ae2d60ec6b91ae36a65894732be8 Mon Sep 17 00:00:00 2001 From: csf Date: Sun, 12 Feb 2023 22:44:53 +0900 Subject: [PATCH 116/199] change GestureHelp from ModalBottomSheet to bottomNavigationBar, add show KeyTools when GestureHelp showed --- flutter/lib/mobile/pages/remote_page.dart | 73 ++++++++++++----------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 63a289c95..951d63faf 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -36,12 +36,13 @@ class RemotePage extends StatefulWidget { class _RemotePageState extends State { Timer? _timer; bool _showBar = !isWebDesktop; + bool _showGestureHelp = false; String _value = ''; double _scale = 1; double _mouseScrollIntegral = 0; // mouse scroll speed controller Orientation? _currentOrientation; - late final keyboardVisibilityController = KeyboardVisibilityController(); + final keyboardVisibilityController = KeyboardVisibilityController(); late final StreamSubscription keyboardSubscription; final FocusNode _mobileFocusNode = FocusNode(); final FocusNode _physicalFocusNode = FocusNode(); @@ -197,9 +198,9 @@ class _RemotePageState extends State { @override Widget build(BuildContext context) { final pi = Provider.of(context).pi; - final isHideKeyboardFAB = + final keyboardIsVisible = keyboardVisibilityController.isVisible && _showEdit; - final showActionButton = !_showBar || isHideKeyboardFAB; + final showActionButton = !_showBar || keyboardIsVisible || _showGestureHelp; final keyboard = gFFI.ffiModel.permissions['keyboard'] != false; return WillPopScope( @@ -209,33 +210,39 @@ class _RemotePageState extends State { }, child: getRawPointerAndKeyBody(Scaffold( // workaround for https://github.com/rustdesk/rustdesk/issues/3131 - floatingActionButtonLocation: isHideKeyboardFAB + floatingActionButtonLocation: keyboardIsVisible ? FABLocation(FloatingActionButtonLocation.endFloat, 0, -35) : null, floatingActionButton: !showActionButton ? null : FloatingActionButton( - mini: !isHideKeyboardFAB, + mini: !keyboardIsVisible, child: Icon( - isHideKeyboardFAB ? Icons.expand_more : Icons.expand_less, + (keyboardIsVisible || _showGestureHelp) + ? Icons.expand_more + : Icons.expand_less, color: Colors.white, ), backgroundColor: MyTheme.accent, onPressed: () { setState(() { - if (isHideKeyboardFAB) { + if (keyboardIsVisible) { _showEdit = false; gFFI.invokeMethod("enable_soft_keyboard", false); _mobileFocusNode.unfocus(); _physicalFocusNode.requestFocus(); + } else if (_showGestureHelp) { + _showGestureHelp = false; } else { _showBar = !_showBar; } }); }), - bottomNavigationBar: _showBar && pi.displays.isNotEmpty - ? getBottomAppBar(keyboard) - : null, + bottomNavigationBar: _showGestureHelp + ? getGestureHelp() + : (_showBar && pi.displays.isNotEmpty + ? getBottomAppBar(keyboard) + : null), body: Overlay( initialEntries: [ OverlayEntry(builder: (context) { @@ -325,7 +332,8 @@ class _RemotePageState extends State { icon: Icon(gFFI.ffiModel.touchMode ? Icons.touch_app : Icons.mouse), - onPressed: changeTouchMode, + onPressed: () => setState( + () => _showGestureHelp = !_showGestureHelp), ), ]) + (isWeb @@ -488,7 +496,7 @@ class _RemotePageState extends State { right: 10, child: QualityMonitor(gFFI.qualityMonitorModel), ), - KeyHelpTools(requestShow: keyboardIsVisible), + KeyHelpTools(requestShow: (keyboardIsVisible || _showGestureHelp)), SizedBox( width: 0, height: 0, @@ -658,29 +666,20 @@ class _RemotePageState extends State { }(); } - void changeTouchMode() { - setState(() => _showEdit = false); - showModalBottomSheet( - // backgroundColor: MyTheme.grayBg, - isScrollControlled: true, - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(5))), - builder: (context) => DraggableScrollableSheet( - expand: false, - builder: (context, scrollController) { - return SingleChildScrollView( - controller: ScrollController(), - padding: EdgeInsets.symmetric(vertical: 10), - child: GestureHelp( - touchMode: gFFI.ffiModel.touchMode, - onTouchModeChange: (t) { - gFFI.ffiModel.toggleTouchMode(); - final v = gFFI.ffiModel.touchMode ? 'Y' : ''; - bind.sessionPeerOption( - id: widget.id, name: "touch", value: v); - })); - })); + /// aka changeTouchMode + BottomAppBar getGestureHelp() { + return BottomAppBar( + child: SingleChildScrollView( + controller: ScrollController(), + padding: EdgeInsets.symmetric(vertical: 10), + child: GestureHelp( + touchMode: gFFI.ffiModel.touchMode, + onTouchModeChange: (t) { + gFFI.ffiModel.toggleTouchMode(); + final v = gFFI.ffiModel.touchMode ? 'Y' : ''; + bind.sessionPeerOption( + id: widget.id, name: "touch", value: v); + }))); } // * Currently mobile does not enable map mode @@ -719,6 +718,7 @@ class _KeyHelpToolsState extends State { var _more = true; var _fn = false; var _pin = false; + final _keyboardVisibilityController = KeyboardVisibilityController(); InputModel get inputModel => gFFI.inputModel; @@ -863,7 +863,8 @@ class _KeyHelpToolsState extends State { final space = size.width > 320 ? 4.0 : 2.0; return Container( color: Color(0xAA000000), - padding: EdgeInsets.only(top: widget.requestShow ? 24 : 4, bottom: 8), + padding: EdgeInsets.only( + top: _keyboardVisibilityController.isVisible ? 24 : 4, bottom: 8), child: Wrap( spacing: space, runSpacing: space, From 0ecc35dcb3e4e0e89f8d0405ef8dc230180cace8 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 13 Feb 2023 15:12:05 +0800 Subject: [PATCH 117/199] opt: fix codesign with strict and verbose mode --- .github/workflows/flutter-nightly.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index f03cd0be8..1ab21dbff 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -242,9 +242,9 @@ jobs: security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain # start sign the rustdesk.app and dmg rm rustdesk-${{ env.VERSION }}.dmg || true - codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep ./flutter/build/macos/Build/Products/Release/RustDesk.app -v + codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict ./flutter/build/macos/Build/Products/Release/RustDesk.app -vvv create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app - codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep rustdesk-${{ env.VERSION }}.dmg -v + codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict rustdesk-${{ env.VERSION }}.dmg -vvv # notarize the rustdesk-${{ env.VERSION }}.dmg rcodesign notary-submit --api-key-path ${{ github.workspace }}/rustdesk.json --staple rustdesk-${{ env.VERSION }}.dmg From d45224dfd8ee487263532e33c0cc707361306f45 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 13 Feb 2023 16:04:47 +0800 Subject: [PATCH 118/199] refactor login error message Signed-off-by: fufesou --- flutter/lib/common/widgets/login.dart | 33 ++++++++++++++------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/flutter/lib/common/widgets/login.dart b/flutter/lib/common/widgets/login.dart index 05fc1fc5c..14a2c38bc 100644 --- a/flutter/lib/common/widgets/login.dart +++ b/flutter/lib/common/widgets/login.dart @@ -197,24 +197,25 @@ class _WidgetOPState extends State { _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, - ), + offstage: + _failedMsg.isEmpty && widget.curOP.value != widget.config.op, + child: RichText( + text: TextSpan( + text: '$_stateMsg ', + style: + DefaultTextStyle.of(context).style.copyWith(fontSize: 12), + children: [ + TextSpan( + text: _failedMsg, + style: DefaultTextStyle.of(context).style.copyWith( + fontSize: 14, + color: Colors.red, + ), ), ], - )); + ), + ), + ); }), Obx( () => Offstage( From 9492f401f4e147b0c28f46392e075c78d1da7644 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 13 Feb 2023 16:18:46 +0800 Subject: [PATCH 119/199] fix: allowing idle scroll events --- flutter/lib/common.dart | 25 +++++++++++++++++++ .../lib/desktop/pages/connection_page.dart | 2 +- .../lib/desktop/pages/desktop_home_page.dart | 1 + .../desktop/pages/desktop_setting_page.dart | 16 ++++++------ 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 6c1245a7d..ba7e3d762 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1735,6 +1735,7 @@ Future updateSystemWindowTheme() async { } } } + /// macOS only /// /// Note: not found a general solution for rust based AVFoundation bingding. @@ -1762,3 +1763,27 @@ Future osxCanRecordAudio() async { Future osxRequestAudio() async { return await kMacOSPermChannel.invokeMethod("requestRecordAudio"); } + +class DraggableNeverScrollableScrollPhysics extends ScrollPhysics { + /// Creates scroll physics that does not let the user scroll. + const DraggableNeverScrollableScrollPhysics({super.parent}); + + @override + DraggableNeverScrollableScrollPhysics applyTo(ScrollPhysics? ancestor) { + return DraggableNeverScrollableScrollPhysics(parent: buildParent(ancestor)); + } + + @override + bool shouldAcceptUserOffset(ScrollMetrics position) { + // TODO: find a better solution to check if the offset change is caused by the scrollbar. + // Workaround: when dragging with the scrollbar, it always triggers an [IdleScrollActivity]. + if (position is ScrollPositionWithSingleContext) { + // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member + return position.activity is IdleScrollActivity; + } + return false; + } + + @override + bool get allowImplicitScrolling => false; +} diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index eee4c6a20..f352c313e 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -120,7 +120,7 @@ class _ConnectionPageState extends State scrollController: _scrollController, child: CustomScrollView( controller: _scrollController, - physics: NeverScrollableScrollPhysics(), + physics: DraggableNeverScrollableScrollPhysics(), slivers: [ SliverList( delegate: SliverChildListDelegate([ diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index cde1e6d74..af7f14815 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -75,6 +75,7 @@ class _DesktopHomePageState extends State scrollController: _leftPaneScrollController, child: SingleChildScrollView( controller: _leftPaneScrollController, + physics: DraggableNeverScrollableScrollPhysics(), child: Column( children: [ buildTip(context), diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 80dcd80b1..378ddbd1b 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -128,7 +128,7 @@ class _DesktopSettingPageState extends State scrollController: controller, child: PageView( controller: controller, - physics: NeverScrollableScrollPhysics(), + physics: DraggableNeverScrollableScrollPhysics(), children: const [ _General(), _Safety(), @@ -170,7 +170,7 @@ class _DesktopSettingPageState extends State return DesktopScrollWrapper( scrollController: scrollController, child: ListView( - physics: NeverScrollableScrollPhysics(), + physics: DraggableNeverScrollableScrollPhysics(), controller: scrollController, children: tabs .asMap() @@ -234,7 +234,7 @@ class _GeneralState extends State<_General> { return DesktopScrollWrapper( scrollController: scrollController, child: ListView( - physics: NeverScrollableScrollPhysics(), + physics: DraggableNeverScrollableScrollPhysics(), controller: scrollController, children: [ theme(), @@ -456,7 +456,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { return DesktopScrollWrapper( scrollController: scrollController, child: SingleChildScrollView( - physics: NeverScrollableScrollPhysics(), + physics: DraggableNeverScrollableScrollPhysics(), controller: scrollController, child: Column( children: [ @@ -908,7 +908,7 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { scrollController: scrollController, child: ListView( controller: scrollController, - physics: NeverScrollableScrollPhysics(), + physics: DraggableNeverScrollableScrollPhysics(), children: [ _lock(locked, 'Unlock Network Settings', () { locked = false; @@ -1094,7 +1094,7 @@ class _DisplayState extends State<_Display> { scrollController: scrollController, child: ListView( controller: scrollController, - physics: NeverScrollableScrollPhysics(), + physics: DraggableNeverScrollableScrollPhysics(), children: [ viewStyle(context), scrollStyle(context), @@ -1334,7 +1334,7 @@ class _AccountState extends State<_Account> { return DesktopScrollWrapper( scrollController: scrollController, child: ListView( - physics: NeverScrollableScrollPhysics(), + physics: DraggableNeverScrollableScrollPhysics(), controller: scrollController, children: [ _Card(title: 'Account', children: [accountAction()]), @@ -1378,7 +1378,7 @@ class _AboutState extends State<_About> { scrollController: scrollController, child: SingleChildScrollView( controller: scrollController, - physics: NeverScrollableScrollPhysics(), + physics: DraggableNeverScrollableScrollPhysics(), child: _Card(title: '${translate('About')} RustDesk', children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, From 6f106251f923d215f4b76e93143b1bf50838b141 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 13 Feb 2023 16:40:24 +0800 Subject: [PATCH 120/199] force relay when id is suffixed with "/r" Signed-off-by: 21pages --- flutter/lib/common.dart | 25 ++++++++------- .../lib/desktop/pages/connection_page.dart | 10 ++++-- .../lib/desktop/pages/desktop_home_page.dart | 1 + .../lib/desktop/pages/file_manager_page.dart | 6 ++-- .../desktop/pages/file_manager_tab_page.dart | 12 +++++-- .../lib/desktop/pages/port_forward_page.dart | 6 ++-- .../desktop/pages/port_forward_tab_page.dart | 8 ++++- flutter/lib/desktop/pages/remote_page.dart | 3 ++ .../lib/desktop/pages/remote_tab_page.dart | 2 ++ flutter/lib/models/model.dart | 13 ++++---- flutter/lib/utils/multi_window_manager.dart | 28 +++++++++++----- src/client.rs | 32 +++++++++++-------- src/flutter.rs | 13 ++++---- src/flutter_ffi.rs | 11 +++++-- src/ui/remote.rs | 20 ++++++++---- 15 files changed, 127 insertions(+), 63 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 6c1245a7d..ca34eace4 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1405,13 +1405,14 @@ bool callUniLinksUriHandler(Uri uri) { connectMainDesktop(String id, {required bool isFileTransfer, required bool isTcpTunneling, - required bool isRDP}) async { + required bool isRDP, + bool? forceRelay}) async { if (isFileTransfer) { - await rustDeskWinManager.newFileTransfer(id); + await rustDeskWinManager.newFileTransfer(id, forceRelay: forceRelay); } else if (isTcpTunneling || isRDP) { - await rustDeskWinManager.newPortForward(id, isRDP); + await rustDeskWinManager.newPortForward(id, isRDP, forceRelay: forceRelay); } else { - await rustDeskWinManager.newRemoteDesktop(id); + await rustDeskWinManager.newRemoteDesktop(id, forceRelay: forceRelay); } } @@ -1422,7 +1423,8 @@ connectMainDesktop(String id, connect(BuildContext context, String id, {bool isFileTransfer = false, bool isTcpTunneling = false, - bool isRDP = false}) async { + bool isRDP = false, + bool forceRelay = false}) async { if (id == '') return; id = id.replaceAll(' ', ''); assert(!(isFileTransfer && isTcpTunneling && isRDP), @@ -1430,18 +1432,18 @@ connect(BuildContext context, String id, if (isDesktop) { if (desktopType == DesktopType.main) { - await connectMainDesktop( - id, - isFileTransfer: isFileTransfer, - isTcpTunneling: isTcpTunneling, - isRDP: isRDP, - ); + await connectMainDesktop(id, + isFileTransfer: isFileTransfer, + isTcpTunneling: isTcpTunneling, + isRDP: isRDP, + forceRelay: forceRelay); } else { await rustDeskWinManager.call(WindowType.Main, kWindowConnect, { 'id': id, 'isFileTransfer': isFileTransfer, 'isTcpTunneling': isTcpTunneling, 'isRDP': isRDP, + "forceRelay": forceRelay, }); } } else { @@ -1735,6 +1737,7 @@ Future updateSystemWindowTheme() async { } } } + /// macOS only /// /// Note: not found a general solution for rust based AVFoundation bingding. diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index eee4c6a20..71660cfa7 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -66,7 +66,8 @@ class _ConnectionPageState extends State _idFocusNode.addListener(() { _idInputFocused.value = _idFocusNode.hasFocus; // select all to faciliate removing text, just following the behavior of address input of chrome - _idController.selection = TextSelection(baseOffset: 0, extentOffset: _idController.value.text.length); + _idController.selection = TextSelection( + baseOffset: 0, extentOffset: _idController.value.text.length); }); windowManager.addListener(this); } @@ -149,8 +150,11 @@ class _ConnectionPageState extends State /// Callback for the connect button. /// Connects to the selected peer. void onConnect({bool isFileTransfer = false}) { - final id = _idController.id; - connect(context, id, isFileTransfer: isFileTransfer); + var id = _idController.id; + var forceRelay = id.endsWith(r'/r'); + if (forceRelay) id = id.substring(0, id.length - 2); + connect(context, id, + isFileTransfer: isFileTransfer, forceRelay: forceRelay); } /// UI for the remote ID TextField. diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index cde1e6d74..ced8e33eb 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -556,6 +556,7 @@ class _DesktopHomePageState extends State isFileTransfer: call.arguments['isFileTransfer'], isTcpTunneling: call.arguments['isTcpTunneling'], isRDP: call.arguments['isRDP'], + forceRelay: call.arguments['forceRelay'], ); } }); diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 27bb0377d..988baca57 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -46,8 +46,10 @@ enum MouseFocusScope { } class FileManagerPage extends StatefulWidget { - const FileManagerPage({Key? key, required this.id}) : super(key: key); + const FileManagerPage({Key? key, required this.id, this.forceRelay}) + : super(key: key); final String id; + final bool? forceRelay; @override State createState() => _FileManagerPageState(); @@ -102,7 +104,7 @@ class _FileManagerPageState extends State void initState() { super.initState(); _ffi = FFI(); - _ffi.start(widget.id, isFileTransfer: true); + _ffi.start(widget.id, isFileTransfer: true, forceRelay: widget.forceRelay); WidgetsBinding.instance.addPostFrameCallback((_) { _ffi.dialogManager .showLoading(translate('Connecting...'), onCancel: closeConnection); diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index b2566e267..7540f7662 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -41,7 +41,11 @@ class _FileManagerTabPageState extends State { selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, onTabCloseButton: () => () => tabController.closeBy(params['id']), - page: FileManagerPage(key: ValueKey(params['id']), id: params['id']))); + page: FileManagerPage( + key: ValueKey(params['id']), + id: params['id'], + forceRelay: params['forceRelay'], + ))); } @override @@ -64,7 +68,11 @@ class _FileManagerTabPageState extends State { selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, onTabCloseButton: () => tabController.closeBy(id), - page: FileManagerPage(key: ValueKey(id), id: id))); + page: FileManagerPage( + key: ValueKey(id), + id: id, + forceRelay: args['forceRelay'], + ))); } else if (call.method == "onDestroy") { tabController.clear(); } else if (call.method == kWindowActionRebuild) { diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart index 2385813eb..2ac6bf23a 100644 --- a/flutter/lib/desktop/pages/port_forward_page.dart +++ b/flutter/lib/desktop/pages/port_forward_page.dart @@ -26,10 +26,12 @@ class _PortForward { } class PortForwardPage extends StatefulWidget { - const PortForwardPage({Key? key, required this.id, required this.isRDP}) + const PortForwardPage( + {Key? key, required this.id, required this.isRDP, this.forceRelay}) : super(key: key); final String id; final bool isRDP; + final bool? forceRelay; @override State createState() => _PortForwardPageState(); @@ -47,7 +49,7 @@ class _PortForwardPageState extends State void initState() { super.initState(); _ffi = FFI(); - _ffi.start(widget.id, isPortForward: true); + _ffi.start(widget.id, isPortForward: true, forceRelay: widget.forceRelay); Get.put(_ffi, tag: 'pf_${widget.id}'); if (!Platform.isLinux) { Wakelock.enable(); diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index ca354f297..ee5dd9b53 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -44,6 +44,7 @@ class _PortForwardTabPageState extends State { key: ValueKey(params['id']), id: params['id'], isRDP: isRDP, + forceRelay: params['forceRelay'], ))); } @@ -72,7 +73,12 @@ class _PortForwardTabPageState extends State { label: id, selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, - page: PortForwardPage(id: id, isRDP: isRDP))); + page: PortForwardPage( + key: ValueKey(args['id']), + id: id, + isRDP: isRDP, + forceRelay: args['forceRelay'], + ))); } else if (call.method == "onDestroy") { tabController.clear(); } else if (call.method == kWindowActionRebuild) { diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 211d36c39..f9db985d9 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -34,11 +34,13 @@ class RemotePage extends StatefulWidget { required this.id, required this.menubarState, this.switchUuid, + this.forceRelay, }) : super(key: key); final String id; final MenubarState menubarState; final String? switchUuid; + final bool? forceRelay; final SimpleWrapper?> _lastState = SimpleWrapper(null); FFI get ffi => (_lastState.value! as _RemotePageState)._ffi; @@ -107,6 +109,7 @@ class _RemotePageState extends State _ffi.start( widget.id, switchUuid: widget.switchUuid, + forceRelay: widget.forceRelay, ); WidgetsBinding.instance.addPostFrameCallback((_) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 9b00b481f..c251aadc1 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -70,6 +70,7 @@ class _ConnectionTabPageState extends State { id: peerId, menubarState: _menubarState, switchUuid: params['switch_uuid'], + forceRelay: params['forceRelay'], ), )); _update_remote_count(); @@ -104,6 +105,7 @@ class _ConnectionTabPageState extends State { id: id, menubarState: _menubarState, switchUuid: switchUuid, + forceRelay: args['forceRelay'], ), )); } else if (call.method == "onDestroy") { diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 8cf90eba9..d0a2ea601 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1339,7 +1339,8 @@ class FFI { void start(String id, {bool isFileTransfer = false, bool isPortForward = false, - String? switchUuid}) { + String? switchUuid, + bool? forceRelay}) { assert(!(isFileTransfer && isPortForward), 'more than one connect type'); if (isFileTransfer) { connType = ConnType.fileTransfer; @@ -1355,11 +1356,11 @@ class FFI { } // ignore: unused_local_variable final addRes = bind.sessionAddSync( - id: id, - isFileTransfer: isFileTransfer, - isPortForward: isPortForward, - switchUuid: switchUuid ?? "", - ); + id: id, + isFileTransfer: isFileTransfer, + isPortForward: isPortForward, + switchUuid: switchUuid ?? "", + forceRelay: forceRelay ?? false); final stream = bind.sessionStart(id: id); final cb = ffiModel.startEventListener(id); () async { diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 3af189ef6..864659a66 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -41,11 +41,15 @@ class RustDeskMultiWindowManager { int? _fileTransferWindowId; int? _portForwardWindowId; - Future newRemoteDesktop(String remoteId, - {String? switch_uuid}) async { + Future newRemoteDesktop( + String remoteId, { + String? switch_uuid, + bool? forceRelay, + }) async { var params = { "type": WindowType.RemoteDesktop.index, "id": remoteId, + "forceRelay": forceRelay }; if (switch_uuid != null) { params['switch_uuid'] = switch_uuid; @@ -78,9 +82,12 @@ class RustDeskMultiWindowManager { } } - Future newFileTransfer(String remoteId) async { - final msg = - jsonEncode({"type": WindowType.FileTransfer.index, "id": remoteId}); + Future newFileTransfer(String remoteId, {bool? forceRelay}) async { + var msg = jsonEncode({ + "type": WindowType.FileTransfer.index, + "id": remoteId, + "forceRelay": forceRelay, + }); try { final ids = await DesktopMultiWindow.getAllSubWindowIds(); @@ -107,9 +114,14 @@ class RustDeskMultiWindowManager { } } - Future newPortForward(String remoteId, bool isRDP) async { - final msg = jsonEncode( - {"type": WindowType.PortForward.index, "id": remoteId, "isRDP": isRDP}); + Future newPortForward(String remoteId, bool isRDP, + {bool? forceRelay}) async { + final msg = jsonEncode({ + "type": WindowType.PortForward.index, + "id": remoteId, + "isRDP": isRDP, + "forceRelay": forceRelay, + }); try { final ids = await DesktopMultiWindow.getAllSubWindowIds(); diff --git a/src/client.rs b/src/client.rs index a21592578..05b34d781 100644 --- a/src/client.rs +++ b/src/client.rs @@ -3,15 +3,15 @@ use std::{ net::SocketAddr, ops::{Deref, Not}, str::FromStr, - sync::{Arc, atomic::AtomicBool, mpsc, Mutex, RwLock}, + sync::{atomic::AtomicBool, mpsc, Arc, Mutex, RwLock}, }; pub use async_trait::async_trait; use bytes::Bytes; #[cfg(not(any(target_os = "android", target_os = "linux")))] use cpal::{ - Device, - Host, StreamConfig, traits::{DeviceTrait, HostTrait, StreamTrait}, + traits::{DeviceTrait, HostTrait, StreamTrait}, + Device, Host, StreamConfig, }; use magnum_opus::{Channels::*, Decoder as AudioDecoder}; use sha2::{Digest, Sha256}; @@ -19,26 +19,26 @@ use uuid::Uuid; pub use file_trait::FileManager; use hbb_common::{ - AddrMangle, allow_err, anyhow::{anyhow, Context}, bail, config::{ - Config, CONNECT_TIMEOUT, PeerConfig, PeerInfoSerde, READ_TIMEOUT, RELAY_PORT, + Config, PeerConfig, PeerInfoSerde, CONNECT_TIMEOUT, READ_TIMEOUT, RELAY_PORT, RENDEZVOUS_TIMEOUT, - }, get_version_number, - log, - message_proto::{*, option_message::BoolOption}, + }, + get_version_number, log, + message_proto::{option_message::BoolOption, *}, protobuf::Message as _, rand, rendezvous_proto::*, - ResultType, socket_client, sodiumoxide::crypto::{box_, secretbox, sign}, - Stream, timeout, tokio::time::Duration, + timeout, + tokio::time::Duration, + AddrMangle, ResultType, Stream, }; -pub use helper::*; pub use helper::LatencyController; +pub use helper::*; use scrap::{ codec::{Decoder, DecoderCfg}, record::{Recorder, RecorderContext}, @@ -943,7 +943,13 @@ impl LoginConfigHandler { /// /// * `id` - id of peer /// * `conn_type` - Connection type enum. - pub fn initialize(&mut self, id: String, conn_type: ConnType, switch_uuid: Option) { + pub fn initialize( + &mut self, + id: String, + conn_type: ConnType, + switch_uuid: Option, + force_relay: bool, + ) { self.id = id; self.conn_type = conn_type; let config = self.load_config(); @@ -952,7 +958,7 @@ impl LoginConfigHandler { self.session_id = rand::random(); self.supported_encoding = None; self.restarting_remote_device = false; - self.force_relay = !self.get_option("force-always-relay").is_empty(); + self.force_relay = !self.get_option("force-always-relay").is_empty() || force_relay; self.direct = None; self.received = false; self.switch_uuid = switch_uuid; diff --git a/src/flutter.rs b/src/flutter.rs index a60e379f9..0161e644a 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -3,19 +3,19 @@ use crate::{ flutter_ffi::EventToUI, ui_session_interface::{io_loop, InvokeUiSession, Session}, }; -use flutter_rust_bridge::{StreamSink}; +use flutter_rust_bridge::StreamSink; use hbb_common::{ bail, config::LocalConfig, get_version_number, message_proto::*, rendezvous_proto::ConnType, ResultType, }; use serde_json::json; +use std::sync::atomic::{AtomicBool, Ordering}; use std::{ collections::HashMap, ffi::CString, os::raw::{c_char, c_int}, sync::{Arc, RwLock}, }; -use std::sync::atomic::{AtomicBool, Ordering}; pub(super) const APP_TYPE_MAIN: &str = "main"; pub(super) const APP_TYPE_CM: &str = "cm"; @@ -114,7 +114,7 @@ pub struct FlutterHandler { // SAFETY: [rgba] is guarded by [rgba_valid], and it's safe to reach [rgba] with `rgba_valid == true`. // We must check the `rgba_valid` before reading [rgba]. pub rgba: Arc>>, - pub rgba_valid: Arc + pub rgba_valid: Arc, } impl FlutterHandler { @@ -449,6 +449,7 @@ pub fn session_add( is_file_transfer: bool, is_port_forward: bool, switch_uuid: &str, + force_relay: bool, ) -> ResultType<()> { let session_id = get_session_id(id.to_owned()); LocalConfig::set_remote_id(&session_id); @@ -477,7 +478,7 @@ pub fn session_add( .lc .write() .unwrap() - .initialize(session_id, conn_type, switch_uuid); + .initialize(session_id, conn_type, switch_uuid, force_relay); if let Some(same_id_session) = SESSIONS.write().unwrap().insert(id.to_owned(), session) { same_id_session.close(); @@ -667,7 +668,7 @@ pub fn session_get_rgba_size(id: *const char) -> usize { let id = unsafe { std::ffi::CStr::from_ptr(id as _) }; if let Ok(id) = id.to_str() { if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { - return session.rgba.read().unwrap().len(); + return session.rgba.read().unwrap().len(); } } 0 @@ -692,4 +693,4 @@ pub fn session_next_rgba(id: *const char) { return session.next_rgba(); } } -} \ No newline at end of file +} diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index b4e79b361..3025d722c 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,3 +1,4 @@ +use crate::ui_session_interface::InvokeUiSession; use crate::{ client::file_trait::FileManager, common::make_fd_to_json, @@ -20,7 +21,6 @@ use std::{ os::raw::c_char, str::FromStr, }; -use crate::ui_session_interface::InvokeUiSession; // use crate::hbbs_http::account::AuthResult; @@ -84,8 +84,15 @@ pub fn session_add_sync( is_file_transfer: bool, is_port_forward: bool, switch_uuid: String, + force_relay: bool, ) -> SyncReturn { - if let Err(e) = session_add(&id, is_file_transfer, is_port_forward, &switch_uuid) { + if let Err(e) = session_add( + &id, + is_file_transfer, + is_port_forward, + &switch_uuid, + force_relay, + ) { SyncReturn(format!("Failed to add session with id {}, {}", &id, e)) } else { SyncReturn("".to_owned()) diff --git a/src/ui/remote.rs b/src/ui/remote.rs index e44e31401..447c2e31d 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1,24 +1,24 @@ +use std::sync::RwLock; use std::{ collections::HashMap, ops::{Deref, DerefMut}, sync::{Arc, Mutex}, }; -use std::sync::RwLock; use sciter::{ dom::{ - Element, - event::{BEHAVIOR_EVENTS, EVENT_GROUPS, EventReason, PHASE_MASK}, HELEMENT, + event::{EventReason, BEHAVIOR_EVENTS, EVENT_GROUPS, PHASE_MASK}, + Element, HELEMENT, }, make_args, + video::{video_destination, AssetPtr, COLOR_SPACE}, Value, - video::{AssetPtr, COLOR_SPACE, video_destination}, }; +use hbb_common::tokio::io::AsyncReadExt; use hbb_common::{ allow_err, fs::TransferJobMeta, log, message_proto::*, rendezvous_proto::ConnType, }; -use hbb_common::tokio::io::AsyncReadExt; use crate::{ client::*, @@ -286,7 +286,9 @@ impl InvokeUiSession for SciterHandler { } /// RGBA is directly rendered by [on_rgba]. No need to store the rgba for the sciter ui. - fn get_rgba(&self) -> *const u8 { std::ptr::null() } + fn get_rgba(&self) -> *const u8 { + std::ptr::null() + } fn next_rgba(&mut self) {} } @@ -467,7 +469,11 @@ impl SciterSession { ConnType::DEFAULT_CONN }; - session.lc.write().unwrap().initialize(id, conn_type, None); + session + .lc + .write() + .unwrap() + .initialize(id, conn_type, None, false); Self(session) } From 201646da4c0248bdc64dffac22c243489abfaa51 Mon Sep 17 00:00:00 2001 From: Ikko Eltociear Ashimine Date: Mon, 13 Feb 2023 18:20:40 +0900 Subject: [PATCH 121/199] add translate ja readme --- docs/README-JP.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/README-JP.md b/docs/README-JP.md index 6d3b6d380..36c74dfed 100644 --- a/docs/README-JP.md +++ b/docs/README-JP.md @@ -14,7 +14,7 @@ Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitt [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) -Rustで書かれた、設定不要ですぐに使えるリモートデスクトップソフトウェアです。自分のデータを完全にコントロールでき、セキュリティの心配もありません。私たちのランデブー/リレーサーバを使うことも、[自分で設定する](https://rustdesk.com/server) ことも、 [自分でランデブー/リレーサーバを書くこともできます。](https://github.com/rustdesk/rustdesk-server-demo). +Rustで書かれた、設定不要ですぐに使えるリモートデスクトップソフトウェアです。自分のデータを完全にコントロールでき、セキュリティの心配もありません。私たちのランデブー/リレーサーバを使うことも、[自分で設定する](https://rustdesk.com/server) ことも、 [自分でランデブー/リレーサーバを書くこともできます](https://github.com/rustdesk/rustdesk-server-demo)。 ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) @@ -58,7 +58,7 @@ RustDeskは誰からの貢献も歓迎します。 貢献するには [`docs/CON -## [Build](https://rustdesk.com/docs/en/dev/build/) +## [ビルド](https://rustdesk.com/docs/en/dev/build/) ## Linuxでのビルド手順 @@ -105,7 +105,7 @@ cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ cd ``` -### Build +### ビルド ```sh curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh @@ -154,7 +154,7 @@ target/release/rustdesk これらのコマンドをRustDeskリポジトリのルートから実行していることを確認してください。そうしないと、アプリケーションが必要なリソースを見つけられない可能性があります。また、 `install` や `run` などの他の cargo サブコマンドは、ホストではなくコンテナ内にプログラムをインストールまたは実行するため、現在この方法ではサポートされていないことに注意してください。 -## File Structure +## ファイル構造 - **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: ビデオコーデック、コンフィグ、tcp/udpラッパー、protobuf、ファイル転送用のfs関数、その他のユーティリティ関数 - **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: スクリーンキャプチャ @@ -165,7 +165,7 @@ target/release/rustdesk - **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server), と通信し、リモートダイレクト (TCP hole punching) または中継接続を待つ。 - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: プラットフォーム固有のコード -## Snapshot +## スナップショット ![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) From 65e1b7d74e653319fc453f24836c14b88e824a60 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Mon, 13 Feb 2023 10:53:05 +0100 Subject: [PATCH 122/199] edited icon #2722 --- .../AppIcon.appiconset/app_icon_1024.png | Bin 53345 -> 37517 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 5475 -> 3032 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 978 -> 448 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 10828 -> 6198 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 1555 -> 875 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 23370 -> 13870 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 2851 -> 1583 bytes res/mac-icon.png | Bin 51695 -> 37517 bytes 8 files changed, 0 insertions(+), 0 deletions(-) diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png index 9af6f2121eb5a5394d671f8e0ab600cef240b3cb..fc39cb2ff2713d9b7e7c93bc6091721947d5c893 100644 GIT binary patch literal 37517 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 a2it#tsAIaS9p3lv(b>V(zQoQq_WuBm=|rLc literal 53345 zcmeEu`9IX}*Z*q7S8?)!V+|H1u}@tD`Oo%1~BJkPn#YhJ3WD$*Z1c?g0adZk-8G$4o? ze58gD2f!aRyWmp@f(2M#zpk!y{W{kpCr3+bI|~T%Nb(JrzEzJp@tlx(Tj3o04A)55 z8%aInvBzCH{PNG#1xuz(ohCW2Sl`kxJVn#v|5P?eo0i4Oh~1DzZl3llzCr1S!tHHg zfsz*sA)_2aTSKu`Ba<1BAj##|IpvR)4<@}^$* zV#v=gEuL82 zY_&V9bc*-*9ix1gi-L(K7p^>3#=EqzIC119m0z{ROWG15u@?+GbYm)2ZK(y$KR9^w z27h+C|2ey_);&re!rm+egra|~+!@no(uSRGgxhp*Rr$@!t`8dIT@{_=ea3_$1&#Uy z!`~RC;U6WD%o?qeq0d&sw@K@^DyFNE%QqE+*kR%J!y}*41-$FVk6c(bZ_1pxEvGO4 z0a2a$Lwx+nXM;3!>$JhvsVoLmj8}9m?8uQPAC6iSZBt>NDdps8>Xpa8TTIk6yl#!V4SOQ`uzOG1G%VG z-B&N*=ip+m45_K_zE`KtxOE2+77%gUt9`P{s((ARSDH`4-pZ{#-YPXdxqWNJYwis3 z+SW|{N^{%N*4&I;G(Z9bVTyGvyx@BuQL_7gAs)=${}2Yjwf}(&j@bY30{Xv4fXx0+ zE)+QY9~dZ7*aJhDMgJzbHRw6ps+?{+ z2R8`f3ccBTys*5&)g^ruCO!53b}8>p-$}gA+OB7gh2H9)%tKs%5D?~4y6QX={yrMN zs%@`BN?nT@kXi+U6KpID=4F#qO9MSL*-dn3Uk|N*ve09@b43>7ML|$Noa9}pXFJ>x zg3D{u(yEsw1t17{vyF|0G-+>;Q}f(88pCv;y4%~Qxj4W4j;u7~#|1(1^2wOw_I@da z-SAxYH+{6cYrQ@|9!8lfWeh7js9B{x{BK@-isq~y@u8m1_wUOPGl)6}epe^bI>JUO zE}0R6>aw+O&$TFI8RhmJC?$H{c6w4*A7f!Jc2%6~ClK6GO+FOSc3q~hk55}l*@;gH zXx3?b;Nzd=p$S9w`D)3bygp7LoW7ZhCF&~@f_T$ZU6{Iln>Qz~t0H%2VG!n_)6lwy ziDQ-AY&ba0KvrK(01V^D%#Ar)6T2prO3eC03qdDx<(&w2VX-Ji!hl-j$rpwjZPZpWB!_bQp2k?b3vN5if0`P?Fmx%| zc)PCW+Yoca250Pm1m#;d;!uR1!-YCq`BM0W?C>HiL{z} zcHldZUY%oGYb5qvpMni(MXlNB*nF6U-ocNZ@GmbRh(AJdL-}r?NRC^fO(eY<*fZCm zq)^1N(XsccvyDky!^9Yig3D4uT!uhu@EoRAhpBlBeB0fEp?MdukGCzJmnXb)eecY+ z@1}+H+=~$<%qL;gfl{tzGvUG`s+PZRoRr1wJsYN(-WQ+it25h^qTa}+?ft9xeE&=d zW|#+pp1)D$X&c;ummzDH|L@9bn$o%`FT3Jqczqb;-Ncz`ROyDZI(Vu3?ZyelKpn(Tf#eHSx{AhcuDAFYP2<@%19 zO|8@5ijOCzAcw`lW5-tfXY;Lzmv?2u3=`9SmxD z?iKD<+eQ+@51H+W1ySnN?0bGV-6qsC#6nMmG94`(3P|y~>t>t=0N9YL&9*1zmcXS0 zol7>@4LVnsIff1kFZsHMNQkSeD`;U0|62#YdSPFFyjhl#CZ(EPLraFZVh(X>Zqp0| zguFe~P_D`@tU1fm?E8$ey|m85cn<9?b>itPlaJ@~`hlDuFhX1|T|sj)Q;*7Rw&GJL zkMT5U%%ZiuQ~M%Q?(20Mb~3?V$w8P7b&=vWxH=oEB zpBFUql1Fiabq`H7xW1Cy)+uFDX{Q16g$=N2=;pa4_2s2l=yjh1^HqaG%y-dESw~d; zAW(NY1FMD(NZc&-v}xZ20-n1l&00qfgWz{+UUX;IujKkR?`aES8P(d7Ek#Q!vt#e; z>HGxy{|#me>k1;eO+8Ar2~6DEzd9GuPn$NcW@ZxDB$&$d0vw_a@%nsCgMCihK4G#F ztOhAa%9r+ZrL1bkSV&uYT|%Kj7QnnBKj~A@(yo;!wkKE9fU`NP*?s-S5A?c4Gx?~N z0m8YmSsEo>EA&b~(yLJpH~&=A)~H~$aw~6CBF4fsJ_U(_pwX_N5f|6>k2W60McqobVaTqT z?>UeA)Vyqtl%|IG1y&vqnYVlH%t>hgQDd?nuhxs_`YtHDy7*&)Cl7KzN2Kv*_c_d`I#QY-$YuwjYekZV8D9pA z07GBDU86pmbwn*!Y-oQYVNMQjcLMqhH~~=ED7%$|AVM}v_F&ggHz< z=lk>1DnKZnTo6X2i>^~OqN<;)wGZT$uV;XjdC#sH#FTac1%P04LUhHyq-IQbi_p8j z&ZW@-t*sA3NYm2EX08DQbm838GycTexnlKyzc0Ey3u+hsT3f8xefLdDAaz93FpJ)Lr}iVx87 zl@))@H%Hua+_LwLfYCY52CeSB7^)u=Ehfzx0G!p$5x!jCuH`e}sF)X$3Z=H%Tc0Lh zQo@=#bAOqLdf<{uJ8-ekiwZjYkWajYo_Kr;;tQZKG@2q8`10yqGl>*y=9=cS39TI{ z&JR>pqyCM8F#g#rF;DTo8FIA~_TY{=Z{_G$-j~N|uh$&}Bs~mFf^WtDk`X(HPQj0P zEieakxGSyisZG0IoP{0BTs2 zgh?9OdvQ>{e&tlthr?Rxbzjcp^B?Ah>X1`3^&8b>-(38@tzzD4<+&9t2^j;t0=ZEG zxgAg!VeVx)j4!*qzq8X3zSq@eXG4Ksyzg}haRd$>US+}$S$Q3$tZ=ho;}%IY*S8fc ze~KFV)lxPano}REyeC?Wh_Jf%cGh{ER}=X9Fg4VqA#%9?&YbOa%Fw7#lbo8}JA1B; zF-{SJvet{#1N*fT_l*GZ5xra5k53_c;EL@!BcJF|LyruzK_KI!s09@?swHJo$_A8T z$q3y@vhUx@tp|={@4Gx(FB0v1!sN@kQ*aDIQv`CQ1O=c_IP~jm%7=HNlO@WlzhYYK zcAWDzmtt-Z+MRqgdg&yc(QUgwB4$W40|YN}(&#neP7=Ih^Y)!7t!dx5;JmW3-9ovY z>N1`+cjCtfW_f~NH%gsP8EA*kN(paS3HErQQ?{xkb%-Gl29>Qn>!3}w9cZC<5$v6n z8v5auZ1YiB4Gz6U;U|g<`Gf>Vto7L1n{K-r7KG$={jj#}N)^vjwa>j5YV~~MMW*Po z;`J48L%AJ=2i8*ko12cR$5$(UH*)T#zZ1eAyY#M1KC@e=<%nzarFWS;-Y&bfHIk~D z*nZ=k%G%;OB`WA9U@V0iv+zzP%Ib_3{bft>WRZjvS|}=rUYb>}D>>#_vVdM@h^<%B z7vp2=9|lsB-=c4M8Yic3Z3R zs|!eV>9!okTZ(B}Br=@@NSO7}rn_dY*BuG4_VYCKf@_7L)|lN-puv6x;#y4NaZ}RO zkI9Ck`!Dh?^=THD@kLITtVEY$*o15aem-S|GhgzXGdsen*U>A&|EC9=`6YtgM$qEs z^>;IaT0&b_w3~xBb7u%yJ=#h%R-zDpz>7;AU zxjAAPuZ5rD?V&kk9FO*xh?Z=BQWLWnstoBW5D++P@|7BQmHN?iYSXXcGche?jp8eZ z|4jJ!)K%U#700_d=0{O|)57hV*%YIRku=bSuMWAsYNfzkQ@>MykQY^d^7%~ALd8k# zE z0{z!Bo%e)jb7j@CCnf?Q9uki>+km59_o+@Mxk)5+)7N1KAPPbbtO9m>n z&~CPc!zLZ!RW4Yo3br|VtFH=z+?d*h!A*l+B$su6aoXj0d-uY*p-o#N2bIC~Cz&?A zRUgi6eSixehmhR8eJDJF^x-^i=d2Jv8-U}YEB=U>^g*bjbwj_pGPeos=j_-Y6?N=G z$zg$;K?SP_*v$@t58}cp;fmJYULYjn1q|a6-$9d4Twe!GVhH2*$?m1}tTfoA*` z$8`jk96juSLF_GDYInn!+O+f9*IW$-*SXWf)bO@wIHz4sbn^OHI5bXd8FHk#XXnM! z5ejihI%pRB0D7)t$Qxz#R6VxQ?x37&reSPbuDf1q$G95^Ml3M$6 z65v<$nSAh?t9PO$Rv0R8vKNX;GeG<|-TT`79LSHgm!<66V$2KemP=7*sJW<5?5r2$ z1YfI=iLno1rPp?vS=Yx;6*nHQwTDn1+gkV|p?Wd+(ZOT)6(IFp;i!Q%A2=$l)lsdv z-qx#_hJ2YVvYwDkSc8WnF~~Z?Laa;HSa+OF-lq%=={d|rQ3I+cYoU4JCuKw+t_nBp z**jn8VYrhp6zfNO?^lvOjmy^{)2p^4tQ%W>0~R}>j(@^m8^^A!aZ}Id$WpN7ywkZe z$Ni|a_=t5OII@8gC(G5x^2sw_8fhTv_IJ6@EBcG`OTl@iuP#NEbLMv2fuPV~Wv#2f zTm4CK&Y<@+dC2hl>#YO-nlW(IQO#mZjqO42#ILFkw^ft1M6#6gBTqNtk6*kBJsj;i z5}V_e^4k-|1jk$gMVBX2kIE)JA&j#%R&!n|cCCFVqJ{7SB`1dDhPW|=(W*6*j%bYn zqe8uI;m+y2v3jQI@nwM(r}ba@wR#XniI}ZFB8DrrOiV#+D?;kEez_;zIpFF}ZB?6X z^xH}PmDi>g+i=4qMW4^I*3DW|U4B<}XdHfT7Fz6!BRyr;>{r`q%k))-neS_=c# zn8WRt(f0!!n#>4Aarc+WzC8l6KL+RrqgsD&;@Xb2Hjnp9^7GHBM27ovVj~NRqI~j@ z?`YKuLw7=dTvJoBZB=o>yIgtI_5BHAtw#D95; zGxy`=itj*2H=a5xk=aS##^%O3#24s1<;311)9wX8@`wvAt@cXxbW~I4&P%vHs7aD2 z;P!roI;ICfHHGS>4JJZ)aV(0|>@d{EMt}`%TnaU2aYaW_e@>4hwrc1#Zlv&mFe$c} zzv&_B->cNHN8_uTmsKo7(^p=OO)6$ zA`95k_H{`8rd_|AOp)_Tpe26UL?+fsv_XU**N@NIVz-&r z=s-noD|huV9;CvI>rbIH>bg`2A!lDR5uW?kle;n0TMf~}jmS2-r zPyE#sA;J05C!)T0@hlsi!FY)Pz0*Y30SOyJb`wE_>6P8oTv3kq=ks2)?{H@$pq9q? z+*xaY{4dP{%Ot&_)3OF9BE|bO`u*hOx*7Iy>b=6bkuZI$-)U<~xYAc8c1GWXmuT0+ zPyT2WshmH-<_}}GcW*Yb-uCWf`uqm~3)Wwj_spFP3&uFJkPTQG$JR)zw{&gd%t1%IdA`X#vV#{!WeBC&*Ll1vrNRi+JBouZ>yLEvUSFAB)#Ar?p zjek}ev!p=a(Ic6DaxDH}8fNqK88rk7F}${Jyt9%oL2M)s9BtnNc~HN2 zvuCc#aOM-}fRhaa9D_U!AujfpC0FgHnu6HiZ)SAuw75uc^XpuDCFXk>>S3>oW&hD_ zPt}CZTE=(;uf5ZFvD?NNIBi>-nYoAz2gF5W)_ytzlCVU;io&@eM0rrgy8UU)rIPH^ zLgF<{?X}aSfiaOF|L=`JY)o(@d|kR$7rkTKR%ty9T*cVh^rx|mgDZ=8_Jnm%>TMHj zq{&UT1$tnGW5SBP#H(dbv_f1-sFsmwUVtl(c{s_a6Zo>Cx3X{hXGksW(`*Sdv>; zbZ&5dMcn(5uGj&Rr zw5q<(U%LROhPcfE?Ubu-PfVf3{B#m-tnpO}HC(e)KJJz&?WGwu$wtTnLbPq8Kql6& zwRKv#MbFV8MlhxnC+>dow3Ui|RNtWph)cdrQ?!}kJnrONGJ^U940YA_MrjU61k$b~ z6)kSfMpSH@`jww2O}Q(g4_%7g{UO+dfE|#=#sGGt=-et4@(v)eylEWQH%pYq0g#WLYBz;L6dbzP9aI7!sni8FvaI{^;(8vvMi3V%*T z4UQoveIYZDR-2AFy5J0qpP|vr<{UfeMq!NbKPL$VvbKdHhze_qKp!ijrG%A!WEW+w zO!?A}+EriXuc=c59DlIy7uasU1pMBv(`sv5&$=(U)FHl#w{(_;*<7YA(Es}y(X`cb z*{W4SXC-4CN3sjIjdgEwcXx;($9JEJ9)nO48$P5~D9D2dKMNp&dzm|H4&2@L(t2Np z&oo{*{-cEGax9pv{XZsK(c^G;I9{RWaIcp;R)teAHEuwCxdVddkVc!y7o-`PpF>=R z6&sxDXHy7a5p+bCmLRr3fjvQa$}pSP%F^1lrGxWffs5Z5+kWk|-lAV=^X;l9xuTDU zmKKRpeJu|mj{=O{?_V`{z1j;T9^Ce7F4)K2p?sjUBr8ngJlGC#Lp^L`_f)<_=x%=H z=M=~3aE9tnsv-6{k*PD?JX>NqEF07ieC%|tPQ0mJ_gRoFiNi7T>lO1@7NmZ%fI*=F zGSYUUkvC;-@UUjsL7Mo&g+Y$`gia99(A@)%I0#2$sZ5Mdlcfuk3V&JTE9d-{X+$9; z4h}VHZAF6wl#Ry1!{_HRHN@Or`4PM1Ko%#u;68a+4vPph1^A!$py|y{c`8c$kt!;l z6t%ffkS8@h=W4XL!k##8ASJ$pe}Y=ST6zeELLi|4jD5c*{-&zcrZp#+hQ zN=6!;Zip?u>5w2{3<#mXNHcU|r0ZN;Bjf;T6BWQ;26n2bL74KDg~2|q8dh7?d(NM0j@E^@y%?ze zq&cTY5QqQOW$8BojP1O_t(0|3M7-ML{K_;lYlzQ$-*+rs;OK zSadeFC`gIXnH&d&-K7a$7z)D(QODW$3r#(W1C;8-9}z*O#pu{iV}rs)Eof$b02+!9 zArjYN)3T_?w!$eN#{2K+k2kCa43zdzTW6o40dT&hT_0S2-u5Gih91<=XSeD{8?wj~ zd7myjea0C%k*jLl*Rr?-Kl{8j1de3Srx%bfkOz5p$mj*zxzDc$otA9{HkN85&7J7` zdAYcf(7}p%ZrFdO^PfBjh0U<`t)sWiW4u& zQj4ssJT$!H+=Ig&iBm>E)Pb7yNQjylhC=n0eWVIwh)vTMKmdK$?hb~cQQhS0~>xML7p!3h5BKNc7OiSV|CRQpDC+7|$ z;wHK-0G{|99E~5286H_g*M$*#vc_hJpUm$o_Pt}FVWo+ugix#250uzffL@o!@RfZX zk9cSAG+Q|O%0mN6NaCl4p4@V2a%y~*zkWVFBWJC$}aCqSRwti=1ojSn0dB597$?BfvYZ!h{YqQ&JUS)TN zY}l5;OnMv?c&<%3nzoL6J7D+S1~RRq@oFI1)BEH3tL6zMk7S{`GsJ1A@m>u6J|GD% zAg;KiqPA?1>2n>5U=5#U`8w`hR3=4h3MRAJc z7tbJv_Sqii_Y>CuUtr#hOD&p;60sA%>E>vfM|Z*{0|4Mi(W}oc6#zuz=c5@b-Herm zQ`sVKFnUj-{@K&lrfBH&`KIk;rhQwiXM&%>lQ-sm>Qp+WzErrz3Z=5&_ky2z1#y{z z1Y}O%ISg3KxXok5n{9q&nkhy-BqQ<#7IP|&>vEZtoF)MBN=N5PC?OQ4Yg_T`InmgKuUlk&(Ck5>lBOZl$*0s z=e`?nk_2XGdNGaa6y{U_QuHBWQ=n+pUe3k<6XU(S=wc9hsz*cjYa}!u&IB2PBJD2_ zXHj5E0G~(EI>x0xy}RWD8rA5l&ZNkZH^LPNq6B;1}h=ElAyFg;462 zZtS+1_RlKkuKz@GfrLEfO?#2W9(7u=C*!7aAOJqt5z)KV&bOm5UtfEXBSou2SBsd zaH#IRueNvJ@);C_JY#s0ig|dpti)&LrLgBmQE8t*b^8zn1kB|%2``&V6aUXVuB|ku zssG$!Ss*Z=Xo=}d>*~59Z0z^UXrYIwa4hzuV20hV)wB+~|9S0rdRT&>;42RB)hhr% zFGj2lWXQ_p+kTHp#2uWQ^Lcqhnk|SPcZHg6{Bs7|HbcaPvR9x-$4;b^33UYQ`&XH? z5_S(GkKBej~ zXTckbO25noeCfFnaHWW&L??2)EECRnH&s{9@t^aj`YX4CZTuacELy&oJA>h$kkAQ0 zVh&Td76NW15?j+tNb;P=vj8y}q$XxcMY&9p92fplEGUATF@sNnmer zCGQ$zIyp!YS=|{g-AN$}Y6u-$V(jd)3C8&5sk?r?B!nJ6{jFDjbHR(kZLKF=@k2CvN-484HvVoYo||a)Z_7CS&#ES)sTXQh%ZdulKY*ZGASx?L=hnk3(hO-`9465% z#;b(zDmv33A|B3JH{k)QFcf;>KCSjw)RjMiWk0^T9m3fT$Yq_f)@!Ja3l1%v zjZ1;lZ;OAQ2GsS1((~2@{w=2cSOOdaC=61l#cP*sLCfIQAu0%!cD1x!zBBU0CCbnQ zF!WGc?W}>ww=u<{wZq%oiKBOy`KZ$<9ElRtD14wjLC|gcdV=j(r{>95aR|)OH>($e zp}K2@5UST%ctv(=(xKA(?t?!fy{AGcHH-hME5kRVQrJ&^<&PDxB{2SjHk|bc==&%q ztVLfMziINm^Mu3$LwiQGBWVHIzK2gk7edk0{02q> z1~RJ;W3QE+8y}4SACFwQl8V<7k{xihV^lko_Vw@B_RneO?dLv>f5p1C++q9qO!ft3 zFdG5}n7B#1oZQ3@*>wG=d6u`06Fo~ z{}CUa&N&yMFdJ_#YwkR=1nh#|@ZkHEf8t9u;Noe_OZQtPv}Q%tW-iNxfabfeOwa}M z>dAvQ*a2`>0D{-Y2Zu(`&D|rZtsnlQit-QW6}?*}bhpS}q=m%~&30Lsuwl-;`_$0H z<3#Edw65&mZ@d&exMR49)IYp@;9E2^)D9TWBF@{-E^%A+irV&TX6lK1M z#6ZKXYJV{xKbxAT;WQC>QExE0Xq{9)agX8}?wkVfrT-_$|HB%v1z62(^79c+gy3Hc z9fAV7Ni1i_v8hurH^`yeRS%r=DL#pkTmFaLjMq$@6Xf505|+LX+g(Pks5zsgsE^tU zsZ-={vWaG)|AoEnn=9HA`)L`QKAsKKtVhh>M~~#5!Jr!tiC`9Z2h1JFk90e z&ah7+!J$D=!&Y}Eft^}%zCx!Ro}b8~$a*0ZdgFvWx7+T)qqaYgjCc;faz`%n@qds< z&QZ;6DvM|0vh)<3L(rRrj^d2%EZx6D{wwwU;~V00&pDknb%c{&iW+*(2*v4Id%;oM z947G*YO*?}{~pC`em&I$2U5K6UFXZS?jbA0W?EC5_LDzSKYOZbvf(G^3pD31)Lj{Qbmbt`-@Ow`zc%D ztu|Sc6(&qL|I_xKR!$_1klnXJV%rznWTA(k0l@SH{Y3-S zP22MPNlWK^P!rqDybeWzG?KzV{~^fJMWkf(D>T#rdWh;Eb|nK(&KurYH`|I1yQ<*sbGyfxG@l)LVnFmxnhTL8kq~=k2En#6J8=Y}*8ONpW}{tNC~| z=x73Mc}}f)(oRd;H(=7e6jLq0P9p2f_x44JPU|6}<=S%&WM(o%X*xtcZmZg5!TuT@ z%5jthX_CBLrrBPkr_@!fS~n#9K8FgBM0<3%(J{DPSRDcmaeYZKii~NOX2kjJV!6O+ z_W7}zT;`g@;(dzL3eX6^S5bE<0LOTOH1>v&1Fp#$O^WgG$~gTu-=K;AcfP@W&p801 zd2OO62bziGP!M#Z<6DIgpUi*|+k=`!sYAt#%UN8Vm-pBJCFlA$;lqv{Zj5S77)?e) zlr9pk2S-`M)wXL4j+a@K?S%Q;1yGb341l_y(d0{*G>WWW{B7*qJAytA6hln|4u3Hf zeF@wTa7=tcgEdzFr!@kcTLCsv^FaTMi}k(IPQ?D^ZT6eRccW_m$(;Vbljs5AQrc%w zH?CEj;U#!Ck52ENI^A3Cps=4lGMoQL`e@p!aE&@`op!I2mB)1MTS=0{+oCNtB~W~( zC;@EhU*;)GZ7cki8@0NpKIA>HLcBr0JuWBoYlh%`T3hF9Q^Y2|Ux{XiE*XJ1vX|D` zw)HH~`6*xMF^t|^NZxuKtEQGGGI9`t-rWFAGat-vj&+j%#6&08xNkG`P2B5G@~@N- zZuJ3~<-aU};d4UJEksz&3>teJSP$YV97+rJU5T%_RwH?NK2Wazlx?Esd$Gn{3gP&jKb(lu-(R^u>E1rs_ZMX;M%Do|&aeR%;?9fXj7sEah8RXxC%7q#%n{k@o4U2;y7a&4>2M$f zj8VN7h#I*$L?^wto!|!s`2M#6olV__uFsmTuAud8gj`E9-et;v!)Keh&i&c`e(@%I z-g&`(Y|gp0kCrQ!v+8Ws`XV?cWTi6weii5^Zu%;=#1v}HkqnNqPEPG?JWmPEe`!&S z6`8}9gAx&Tlx6$Gny79;H*Sy?)Xg9D0*&wjjhJ(ogoFC-ndiW{*JbUoZ;;vTDgHoV zrKNUVyIpP+`^j_WTbsAzbxgu{mbxTewa_WKeHnePc+S32yJnC379cb|5^v}1{{(c} zRQD%`TNt%nTqE8%OGvQX_uZ_!t|22pWS|2T$-`z=<}>#pZmjBmg|PGbzb^ zbj_|XD&Fke$A((x0efYI)%M6~|GNV$Zwr4$uum1(T?h|dN+lTzy)ThUrl%R2xrH;B z70^oy36j)|`2K$1As_Zh&MSOBMm}N_9;#L#xY!Iq_d!`CpvhD1n}tejdtw7>Z7;2( zm^wB);kIS=TR+gqaw+efhUM#mh#man*l#T}q_=e&?E{6iJpCbY>e9pR&B0A0g(c@7 zsx=$13wYKD@qRRjEUvgbyNi|Y>Q0(K)_D@i+FAwN=B9=X`qkn=@S(cU@V zUX{XeDWVmPwc1W*6CA5y${Vj?${ka89dI;!cST1b<0mJKI7IColJi|GystpkQKj#a z_nk{@2fhzQMCf*<#=5+tm5CE2;*yFRuuE;^E5HlBfCC*;>$vqTMrS?W7{Ia-#2bB+ zT__QUlJip7*Fcx{J*OAvmI+chw~uHx?7o?5?=%#CruaK4@dU@^%Cr5OcB#e0M0dpSfM5hr+}mfsM6Ozq+}fBPThC4oHn0jd-FR-N{1l9dCYvmn6)DQyAT zy+JA%%->6w-_t&+#Z~(^kS&%LC^HT z@za_xW9N4WSdxWL`)47ilN3kM#0^1kyT=AMYj*}7nQXA^U1TEb$P496toooZ}*nZRA?cB&|q$@fq2h z#(hcOpTijPLvU}~Tq;b6hfZOO)@5I}Z(nzEyi`3DA68zaxfo(YXQpJW(tEW1*HN5# zv5BT+&L@q`Zmp)Vgxm3W(CinQ6P`TH&yIONYH|^YauzoqTZ}zaJ&lfMHb1@3uEEWK zp>r!f#>ZSyds(e0d^8h(i(Y&t^o&)`Dal&N{=(jJC1b}s&78W8<7P(2&z7dPjSG&*tTpWHLcdbl9Sj}PbE)6*?wBhi*>-9yI2W>904{Gpbsn~=?e(OcZ`UN5etTTNzHP!oy@WD`VGA+VdZbEIZA^~dKjS0>&o#@3QqE6kx`Vx<~Z07nx zxmVc?-swjgmgz=b!wLwv7rtn&5%fMVc$tg3$l3E-QO^6Zl&6h%w-uw7RLQ`$#&q+l z#=>7eR_YU;&c=wAEA3QOIQqJmNvsWC1^ocZe?z;(Oid{mQOYi*dW34{auNuiO%* zLwuedNLw(-6&!oAR?z&Wz42vsNj=S&$d7D15ZQGc5z18E6+hP0k z5mwhAC5{tdno^XphIr+`KALdTmz)M?{9<=w@5*JOW&Zl2B+(vqi{?qGo+k2)j-aAy zq41P7W;e-QOt#j@zRX_Pd@N-AaixWIdG+Xk2-lw?=b9a>D_Sk514Ck^6Ms!9wBlwD zN|2L<7Ud1FI{_1-pr40BV&yfx)6W2}%KMf<-}9{oVR8*vRdCxDI+DC~YNU$LfgoT+1ziz&6`{2?r z6CCE9EcN!?O^$0B3TGdu2i^N3NLrcxoU6vOi77I~6BP%MGs_MrN zeBnI=OA0+;Se>DrVm%u!wU!W8^Oy@W459(t)H5)7X9k;x0rxkc&w-NDc?Lo%_?V

    ^0>;>{-A-^Ls1s>XX&4yG45lUQ|yu+oo`W-dQ3 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 diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png index 4bed6f3fa9928528fa6e0046af8bc011861b76f3..3bd2b7ede030927dcf652ee5db6131bc94df7c69 100644 GIT binary patch delta 433 zcmV;i0Z#tX2fzc68Gi!+001a04^sdD0J>02R7C&)000000RaI300960|NsC0`T6649sARW5($XWmZ0HjGoK~xyi zZO%mk!cYuF(SITpoYdW2acBSAB27EwjdIVQeCE&CIgFdL)Od-Bs%9P@TLgTUpGIqT z9}!?VVHoVPel?X@07?(iP01y|D!w9tlPcIK?=Bz)G(go3a15^0l3^qS z+QkM7JFg!e0cc?BVWcRf3_Wlt=Rl9sJ2r=~27trK^(q|;j}3l(#;;)9f}b(H?1d>! b*kS(wqNfsT9EuRY00000NkvXXu0mjf_j}yV delta 967 zcmV;&133J^1JVbO8Gi-<001BJ|6u?C0fcEoLr_UWLm+T+Z)Rz1WdHzpoPCi!NW(xF zhTo=2MJgTaAmWgrI$01Eanx2QLWNK(wCZ4T=^r#{NK#xJ1=oUuKZ{id7iV1^Tm?b! z2gKFINzp}0{4Oc9i1Ci&9^U)jm%Hx(p;={`)iVKTx@~4s34bw{Ull{I5D-F`W<+L| zF)vAJ_>Ql81o(az=UM)Be~tmQU@;&d63;Qiw23!}XEtqv^FDEum1ULqoOs-%3lcwa zUGeyhbIE0aXGYCzCP^G67E4{MbTKQN8u2u7Ox1MC7jhn}oVPe@)jDh6lfN)r&{vkZ zPHO}yEMf@~M1Lr#qk<}I#A(+_v5=wjgpYsN^-JVZ$W;L&#{z25AiI9>Klt5St2j03 zC500}?~CJni~^xupw)1k?_D?aC4=ki2wis2}wjjRCocUlh11tK@`WoGy9{dra_X{Lam@_E@_D%cnOLuJ=PvA zIf{7l*rWafDn0aWDHHqU&M1E|H;@*PI0A zCM~`BxfvjzH1PQAAr687EBSPs6Jvk0B`TjJU~sJ6PGJ6Wx8gJbw`d+uzO-PHoM$;P z_!qYJB|4`ZsU$FWr8}@_%@AZn8aN_3dbs@nNb1PrVE#aExUC+aQ{yrW`T^I*DF`?Y zkAFyWo!TDz6YzS^NAsAG1OtVX1-BoNKF4tXI>*EXhWx0KBrtQ4K~l9>yB$1u*9HVf z>8eGZ-~%1#rk>wbgJpR1M&Rj(0Li3)GzD59KiCYpQ47mA&iASc0m`1e?S5}CEvMO} zz@|e(WVR_2%Z`n)L|8q_(E#ObWzWcst3V43OLsUn_rswT#u+lh-Rn`UR^O|f7@$0@ pynWVXa=Wj8zf2KCc^m(@egUUy%9+Dj8;bw{002ovPDHLkV1fvh%1i(N diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png index 22893b8ea78c8c2d4dc1835a5e19b3c056e11d28..88f2eee49a9a8162eeedf5df764e8842ac2d0912 100644 GIT binary patch literal 6198 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 literal 10828 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 WX8Oy%I)Y}20+i&`WNY5Q!u}tVQGC(> diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png index 583a485712bcc8691458c7abb38b37906ae6649a..18151e82b11bfd52df745f84cdc3fb0fddc45f30 100644 GIT binary patch delta 863 zcmV-l1EBnq4C@Av8Gi!+002a!ipBr{0b)>0R7C&)000002L}fN0s;U400000_xJbf z>+Amh{{R2~;o;%_{{HXp@B91vM@L5j0s;a80ssI21Ox;C00960|NHy<|NsB7v9Y3} zqR!6F{QUg?|NpPAuk`fv|NsC0|Nj7g`v8*j0J7%*cm4o+{eJ*``~ZUb0I%o(wC3;k z|KRZd0EPJghxh=D_5g|Y0G9CpneYIh?f|9i0I2H#s_Fo(=>WIo0GsfDz5d|!`~Z&h z5Tx(`p6>vo?Etyt0KDVB-2d%LqNkiOPc@y0hvieK~y-)t&w$e!axwkX_{cgp)P18DOLqaixhV$ zQg?U#{=K~r?#T4dzM0J2?fXq`_pRceG^bJ9Wm0RV`?hV&{-;K4JwQWhxkxcd5u!gO z-OH`)qJL{?bN@3VRc0}u7ccp@)wT5vAY$>a-f#5av!nQ2i>~YtBGc=XMF5A7r@_#6 zpE!7y-e3^Gz(HsSfQYv4Zo666BdVNXU`PV4oX$WndSS#~dh^WxO6C{=+O%@Q*pn(sRX_lJe*o_FD_}7s7D+MInZcmu_DZH059rckLd@)M? z67S`Oj#~7>j(W9JYCgbTLecyn06yvi62p55*2p%MaKe2MqJ55QN&q;W2=Ir5xDmvf zj#xneJ0rc3I4Jp!6AW(6rEEwPn6hTX6;1pPld0eZMyGreZ30lPB ptY+h|Oh%K1Q4q1rkW36KI=}E|c%z@Oh>`#R002ovPDHLkV1jA>&Itej delta 1549 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 diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png index f98ccf1f3eca7f764f5a066ae22f6b88fcf6653f..d2bd35cd1da49a6401a7cac00f58241c4cccfe1b 100644 GIT binary patch delta 1577 zcmV+^2G;qb7OxDD8Gi!+005o0f$RVP0rgN!R7C&)00II65)u*u0s;X60RjR71_lNR z2?_T0_Wu6<3JMAe3JUS@@c{t=`1tq%0RaL60@T#h0|NsJ3JU)I{{R2~{QUg?|NoAT zjsO4v0RaI3000LE2Lb{D`uh6({QUX(`QYH-?(XjT`ueuEwtxEi`u+X=udlBJ1O&#$ z#^mJW-rnB-|Nj7k`2c|W0IcZ%ujl}_Hx9l0D1iYl=A?7`v92n z0G#gty5s1H0Jr3Ty#7F*{lDz_0J-Jw_y1t9{1J}$VAJma zp6@`%>|m?@0C@f&wd|kE|B%H0zu^AQ?ET>P{GZhPpxF9Aruv}W_;APfK(FOn@nFvIaNF-7uJ2IH?jW-6 zK+Wm^wdSwX|8TJXA%^~Nx%_~|`mf~qFrE1zm-vv__pj>qfYS95p7If)@nFsHF}m*& zr|&_Jh)@5W(c_%RYJl001m>QchC<7X=hZG!hBu;SLbnMM6I>t|kWX(6W$o zIx8O>DD?8w$Gww>gJE4drJ<8s6XK8n00X^AL_t(o!|jxJQxicL#$!(JQcXytgT1>z zYzX3kQNW-m3PK>kh|-Hx5u`~Iq=UV8{hVIeO@H3I+hxdqj^i`T5Xd~gdG>9l|N85+ zGdU^Ix(hM2%bJ*UXeZllMO%{fFx{Vwwyb2DKM}>DM5F%zf{H)GVPsGsIK8|UiA3s` zf&pZZzLZwI`)yBej?b5S>eR`Tw=Z1ScxtGmt^qpxr`|JX&*tR#DhZ*yZ*kKIhY-~0 z>VN6eXJi7&P%9D2`&tmx0qFN8A)wBpo3{WV7iTD6(EyUX03hs*eNom`Py`8;(G1!; z>=gjOW{vr~uM`yAfdo>?C0!M50AP>J|NQy$5~0XQc%a`49sqbmG?7;U3G3f_?x}Ua zejqBK&7X$}cZ$9*j0V(!`s&Li{<)PF8h?O8SW|_VaHU|T3d{v@)l1zZW61%)HvGTV ztFwNU@TCn=R4;dPkpRp_z~r^7g-V9c%~ZeA&ENR1Z83lu16oT;Fky`5e}*UOBLW zfZ;4mcuWTb$#%~%0HKSRP(}v?q|K_7ce9=WsL56dlZc^v5<%YXHQWUwKq8cu7Pqqj zDj~v3ke{z)c*g|b-Hb32j9gL)<19e0y4{sO7=XK%FXd;;1V$07$bDF5JfZO%CN$9j z9pu7HGXMkUR6^EkI$(j^s37ABb$^c)44|B}YEcv$sQxj0X(hr3_C-Q<4@Hsqo2x+my7DRr zVHF`-4Y2yF@8eIQ?2DnHGR586kV{)3U;9oJ1Al-T?15%D z)y3q>;SAk~Xb&tRXVLlPZsOh45V~U%V&Ndbq4783c`#&{2NULi0Div)@KPiMn8EMw zL_qs_0I(m|0Q#kFHW&y~+FN_V4gj_zNUQs=q8YMOLimktL5|n}K=4?Q9x*6JD_I(D zAJXra#UlWKIU@xbR5#Fgw|}OlW+XJ+MA^=R8D0|*?@mB*DZ$N|AwWn!>WIhhIGQe) zG_q&9;>eKT^4LsTnYlFALCeKPUrSb<#f7I-RU;$*Gb?qJZbJ7o64Q$$>0PNOJ2cd=Xp+%{30v*8UOIS-6nWV b|Cs&&CV3;(y9-hE00000NkvXXu0mjfVM!{+ literal 2851 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 a2it#tsAIaS9p3lv(b>V(zQoQq_WuBm=|rLc literal 51695 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%0D}t}!A64# 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 Te#RJt2VrvD;u!IW+pYft1QtW! From c8bee2792926b461cea0cc6c435e5ef230bcd053 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 13 Feb 2023 18:39:14 +0800 Subject: [PATCH 123/199] remove RustDesk renamed to rustdesk which is for ps convience but cause code sign failure --- build.py | 1 - 1 file changed, 1 deletion(-) diff --git a/build.py b/build.py index dce434720..9e490166f 100755 --- a/build.py +++ b/build.py @@ -322,7 +322,6 @@ def build_flutter_dmg(version, features): os.system('sed -i "" "s/char \*\*rustdesk_core_main(int \*args_len);//" flutter/macos/Runner/bridge_generated.h') os.chdir('flutter') os.system('flutter build macos --release') - os.system('mv ./build/macos/Build/Products/Release/RustDesk.app/Contents/MacOS/RustDesk ./build/macos/Build/Products/Release/RustDesk.app/Contents/MacOS/rustdesk') os.system( "create-dmg rustdesk.dmg ./build/macos/Build/Products/Release/RustDesk.app") os.rename("rustdesk.dmg", f"../rustdesk-{version}.dmg") From 8a68974f4f1fa17073a82c331075ed5dd2ca4a0a Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 14 Feb 2023 14:30:42 +0800 Subject: [PATCH 124/199] Try out change CFBundleExecutable to rustdesk from EXECUTABLE_NAME, so that it is not "RustDesk" --- flutter/macos/Runner/Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/macos/Runner/Info.plist b/flutter/macos/Runner/Info.plist index 96616e8c4..0438f9d85 100644 --- a/flutter/macos/Runner/Info.plist +++ b/flutter/macos/Runner/Info.plist @@ -5,7 +5,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable - $(EXECUTABLE_NAME) + rustdesk CFBundleIconFile CFBundleIdentifier From b65f940a25ebb3414e493908ee456742ecf230eb Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Tue, 14 Feb 2023 14:45:31 +0800 Subject: [PATCH 125/199] fix: issue #3204 --- src/lang.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang.rs b/src/lang.rs index f24d015e2..3dc81c8aa 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -81,7 +81,7 @@ pub fn translate_locale(name: String, locale: &str) -> String { if lang.is_empty() { // zh_CN on Linux, zh-Hans-CN on mac, zh_CN_#Hans on Android if locale.starts_with("zh") { - lang = (if locale.contains("TW") { "tw" } else { "cn" }).to_owned(); + lang = (if locale.contains("tw") { "tw" } else { "cn" }).to_owned(); } } if lang.is_empty() { From 60fa453495152f21be622de154efdd28d79efd39 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 14 Feb 2023 14:50:01 +0800 Subject: [PATCH 126/199] revert back, https://stackoverflow.com/questions/3654931/application-failed-codesign-verification, codesign fail after change executable_name --- flutter/macos/Runner/Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/macos/Runner/Info.plist b/flutter/macos/Runner/Info.plist index 0438f9d85..96616e8c4 100644 --- a/flutter/macos/Runner/Info.plist +++ b/flutter/macos/Runner/Info.plist @@ -5,7 +5,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable - rustdesk + $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier From 1adfc2c7b00cea74ce299676c9b1c5828291fb7d Mon Sep 17 00:00:00 2001 From: FastAct <93490087+FastAct@users.noreply.github.com> Date: Tue, 14 Feb 2023 10:05:47 +0100 Subject: [PATCH 127/199] changed language files added dutch translatoinn --- src/lang.rs | 3 + src/lang/nl.rs | 453 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 456 insertions(+) create mode 100644 src/lang/nl.rs diff --git a/src/lang.rs b/src/lang.rs index f24d015e2..a50d2b5b9 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -14,6 +14,7 @@ mod id; mod it; mod ja; mod ko; +mod nl; mod pl; mod ptbr; mod ro; @@ -40,6 +41,7 @@ lazy_static::lazy_static! { ("it", "Italiano"), ("fr", "Français"), ("de", "Deutsch"), + ("nl", "Nederlands"), ("cn", "简体中文"), ("tw", "繁體中文"), ("pt", "Português"), @@ -99,6 +101,7 @@ pub fn translate_locale(name: String, locale: &str) -> String { "it" => it::T.deref(), "tw" => tw::T.deref(), "de" => de::T.deref(), + "nl" => nl::T.deref(), "es" => es::T.deref(), "hu" => hu::T.deref(), "ru" => ru::T.deref(), diff --git a/src/lang/nl.rs b/src/lang/nl.rs new file mode 100644 index 000000000..3b01492d3 --- /dev/null +++ b/src/lang/nl.rs @@ -0,0 +1,453 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Status"), + ("Your Desktop", "Uw Bureaublad"), + ("desk_tip", "Uw bureaublad is toegankelijk via de ID en het wachtwoord hieronder."), + ("Password", "Wachtwoord"), + ("Ready", "Klaar"), + ("Established", "Opgezet"), + ("connecting_status", "Verbinding maken met het RustDesk netwerk..."), + ("Enable Service", "Service Inschakelen"), + ("Start Service", "Start Service"), + ("Service is running", "De service loopt."), + ("Service is not running", "De service loopt niet"), + ("not_ready_status", "Niet klaar, controleer de netwerkverbinding"), + ("Control Remote Desktop", "Beheer Extern Bureaublad"), + ("Transfer File", "Bestand Overzetten"), + ("Connect", "Verbinden"), + ("Recent Sessions", "Recente Behandelingen"), + ("Address Book", "Adresboek"), + ("Confirmation", "Bevestiging"), + ("TCP Tunneling", "TCP Tunneling"), + ("Remove", "Verwijder"), + ("Refresh random password", "Vernieuw willekeurig wachtwoord"), + ("Set your own password", "Stel je eigen wachtwoord in"), + ("Enable Keyboard/Mouse", "Toetsenbord/Muis Inschakelen"), + ("Enable Clipboard", "Klembord Inschakelen"), + ("Enable File Transfer", "Bestandsoverdracht Inschakelen"), + ("Enable TCP Tunneling", "TCP Tunneling Inschakelen"), + ("IP Whitelisting", "IP Witte Lijst"), + ("ID/Relay Server", "ID/Relay Server"), + ("Import Server Config", "Importeer Serverconfiguratie"), + ("Export Server Config", "Exporteer Serverconfiguratie"), + ("Import server configuration successfully", "Importeren serverconfiguratie succesvol"), + ("Export server configuration successfully", "Exporteren serverconfiguratie succesvol"), + ("Invalid server configuration", "Ongeldige Serverconfiguratie"), + ("Clipboard is empty", "Klembord is leeg"), + ("Stop service", "Stop service"), + ("Change ID", "Wijzig ID"), + ("Website", "Website"), + ("About", "Over"), + ("Slogan_tip", "Gedaan met het hart in deze chaotische wereld!"), + ("Privacy Statement", "Privacyverklaring"), + ("Mute", "Geluid uit"), + ("Build Date", "Versie datum"), + ("Version", "Versie"), + ("Home", "Startpagina"), + ("Audio Input", "Audio Ingang"), + ("Enhancements", "Verbeteringen"), + ("Hardware Codec", "Hardware Codec"), + ("Adaptive Bitrate", "Aangepaste Bitsnelheid"), + ("ID Server", "Server ID"), + ("Relay Server", "Relay Server"), + ("API Server", "API Server"), + ("invalid_http", "Moet beginnen met http:// of https://"), + ("Invalid IP", "Ongeldig IP"), + ("id_change_tip", "Alleen de letters a-z, A-Z, 0-9, _ (underscore) kunnen worden gebruikt. De eerste letter moet a-z, A-Z zijn. De lengte moet tussen 6 en 16 liggen."), + ("Invalid format", "Ongeldig formaat"), + ("server_not_support", "Nog niet ondersteund door de server"), + ("Not available", "Niet beschikbaar"), + ("Too frequent", "Te vaak"), + ("Cancel", "Annuleer"), + ("Skip", "Overslaan"), + ("Close", "Sluit"), + ("Retry", "Probeer opnieuw"), + ("OK", "OK"), + ("Password Required", "Wachtwoord vereist"), + ("Please enter your password", "Geef uw wachtwoord in"), + ("Remember password", "Wachtwoord onthouden"), + ("Wrong Password", "Verkeerd wachtwoord"), + ("Do you want to enter again?", "Wil je opnieuw ingeven?"), + ("Connection Error", "Fout bij verbinding"), + ("Error", "Fout"), + ("Reset by the peer", "Reset door de peer"), + ("Connecting...", "Verbinding maken..."), + ("Connection in progress. Please wait.", "Verbinding in uitvoering. Even geduld a.u.b."), + ("Please try 1 minute later", "Probeer 1 minuut later"), + ("Login Error", "Login Fout"), + ("Successful", "Succesvol"), + ("Connected, waiting for image...", "Verbonden, wacht op beeld..."), + ("Name", "Naam"), + ("Type", "Type"), + ("Modified", "Gewijzigd"), + ("Size", "Grootte"), + ("Show Hidden Files", "Toon verborgen bestanden"), + ("Receive", "Ontvangen"), + ("Send", "Verzenden"), + ("Refresh File", "Bestand Verversen"), + ("Local", "Lokaal"), + ("Remote", "Op afstand"), + ("Remote Computer", "Externe Computer"), + ("Local Computer", "Lokale Computer"), + ("Confirm Delete", "Bevestig Verwijderen"), + ("Delete", "Verwijder"), + ("Properties", "Eigenschappen"), + ("Multi Select", "Meervoudig selecteren"), + ("Select All", "Selecteer Alle"), + ("Unselect All", "Deselecteer alles"), + ("Empty Directory", "Lege Map"), + ("Not an empty directory", "Geen Lege Map"), + ("Are you sure you want to delete this file?", "Weet je zeker dat je dit bestand wilt verwijderen?"), + ("Are you sure you want to delete this empty directory?", "Weet je zeker dat je deze lege map wilt verwijderen?"), + ("Are you sure you want to delete the file of this directory?", "Weet je zeker dat je het bestand uit deze map wilt verwijderen?"), + ("Do this for all conflicts", "Doe dit voor alle conflicten"), + ("This is irreversible!", "Dit is onomkeerbaar!"), + ("Deleting", "Verwijderen"), + ("files", "bestanden"), + ("Waiting", "Wachten"), + ("Finished", "Voltooid"), + ("Speed", "Snelheid"), + ("Custom Image Quality", "Aangepaste beeldkwaliteit"), + ("Privacy mode", "Privacymodus"), + ("Block user input", "Gebruikersinvoer blokkeren"), + ("Unblock user input", "Gebruikersinvoer opheffen"), + ("Adjust Window", "Venster Aanpassen"), + ("Original", "Origineel"), + ("Shrink", "Verkleinen"), + ("Stretch", "Uitrekken"), + ("Scrollbar", "Schuifbalk"), + ("ScrollAuto", "Auto Schuiven"), + ("Good image quality", "Goede beeldkwaliteit"), + ("Balanced", "Gebalanceerd"), + ("Optimize reaction time", "Optimaliseer reactietijd"), + ("Custom", "Aangepast"), + ("Show remote cursor", "Toon cursor van extern bureaublad"), + ("Show quality monitor", "Kwaliteitsmonitor tonen"), + ("Disable clipboard", "Klembord uitschakelen"), + ("Lock after session end", "Vergrendelen na einde sessie"), + ("Insert", "Invoegen"), + ("Insert Lock", "Vergrendeling Invoegen"), + ("Refresh", "Vernieuwen"), + ("ID does not exist", "ID bestaat niet"), + ("Failed to connect to rendezvous server", "Verbinding met rendez-vous-server mislukt"), + ("Please try later", "Probeer later opnieuw"), + ("Remote desktop is offline", "Extern bureaublad is offline"), + ("Key mismatch", "Code onjuist"), + ("Timeout", "Time-out"), + ("Failed to connect to relay server", "Verbinding met relayserver mislukt"), + ("Failed to connect via rendezvous server", "Verbinding via rendez-vous-server mislukt"), + ("Failed to connect via relay server", "Verbinding via relaisserver mislukt"), + ("Failed to make direct connection to remote desktop", "Onmogelijk direct verbinding te maken met extern bureaublad"), + ("Set Password", "Wachtwoord Instellen"), + ("OS Password", "OS Wachtwoord"), + ("install_tip", "Je gebruikt een niet geinstalleerde versie. Als gevolg van UAC-beperkingen is het in sommige gevallen niet mogelijk om als controleterminal de muis en het toetsenbord te bedienen of het scherm over te nemen. Klik op de knop hieronder om RustDesk op het systeem te installeren om het bovenstaande probleem te voorkomen."), + ("Click to upgrade", "Klik voor upgrade"), + ("Click to download", "Klik om te downloaden"), + ("Click to update", "Klik om bij te werken"), + ("Configure", "Configureren"), + ("config_acc", "Om je bureaublad op afstand te kunnen bedienen, moet je RustDesk \"toegankelijkheid\" toestemming geven."), + ("config_screen", "Om toegang te krijgen tot het externe bureaublad, moet je RustDesk de toestemming \"schermregistratie\" geven."), + ("Installing ...", "Installeren ..."), + ("Install", "Installeer"), + ("Installation", "Installatie"), + ("Installation Path", "Installatie Pad"), + ("Create start menu shortcuts", "Startmenu snelkoppelingen maken"), + ("Create desktop icon", "Bureaubladpictogram maken"), + ("agreement_tip", "Het starten van de installatie betekent het accepteren van de licentieovereenkomst."), + ("Accept and Install", "Accepteren en installeren"), + ("End-user license agreement", "Licentieovereenkomst eindgebruiker"), + ("Generating ...", "Genereert ..."), + ("Your installation is lower version.", "Uw installatie is een lagere versie."), + ("not_close_tcp_tip", "Gelieve dit venster niet te sluiten wanneer u de tunnel gebruikt"), + ("Listening ...", "Luisteren ..."), + ("Remote Host", "Externe Host"), + ("Remote Port", "Externe Poort"), + ("Action", "Actie"), + ("Add", "Toevoegen"), + ("Local Port", "Lokale Poort"), + ("Local Address", "Lokaal Adres"), + ("Change Local Port", "Wijzig Lokale Poort"), + ("setup_server_tip", "Als u een snellere verbindingssnelheid nodig heeft, kunt u ervoor kiezen om uw eigen server aan te maken"), + ("Too short, at least 6 characters.", "e kort, minstens 6 tekens."), + ("The confirmation is not identical.", "De bevestiging is niet identiek."), + ("Permissions", "Machtigingen"), + ("Accept", "Accepteren"), + ("Dismiss", "Afwijzen"), + ("Disconnect", "Verbinding verbreken"), + ("Allow using keyboard and mouse", "Gebruik toetsenbord en muis toestaan"), + ("Allow using clipboard", "Gebruik klembord toestaan"), + ("Allow hearing sound", "Geluidsweergave toestaan"), + ("Allow file copy and paste", "Kopieren en plakken van bestanden toestaan"), + ("Connected", "Verbonden"), + ("Direct and encrypted connection", "Directe en versleutelde verbinding"), + ("Relayed and encrypted connection", "Doorgeschakelde en versleutelde verbinding"), + ("Direct and unencrypted connection", "Directe en niet-versleutelde verbinding"), + ("Relayed and unencrypted connection", "Doorgeschakelde en niet-versleutelde verbinding"), + ("Enter Remote ID", "Voer Extern ID in"), + ("Enter your password", "Voer uw wachtwoord in"), + ("Logging in...", "Aanmelden..."), + ("Enable RDP session sharing", "Delen van RDP-sessie inschakelen"), + ("Auto Login", "Automatisch Aanmelden"), + ("Enable Direct IP Access", "Directe IP-toegang inschakelen"), + ("Rename", "Naam wijzigen"), + ("Space", "Spatie"), + ("Create Desktop Shortcut", "Snelkoppeling op bureaublad maken"), + ("Change Path", "Pad wijzigen"), + ("Create Folder", "Map Maken"), + ("Please enter the folder name", "Geef de mapnaam op"), + ("Fix it", "Repareer het"), + ("Warning", "Waarschuwing"), + ("Login screen using Wayland is not supported", "Aanmeldingsscherm via Wayland wordt niet ondersteund"), + ("Reboot required", "Opnieuw opstarten vereist"), + ("Unsupported display server ", "Niet-ondersteunde weergaveserver"), + ("x11 expected", "x11 verwacht"), + ("Port", "Poort"), + ("Settings", "Instellingen"), + ("Username", "Gebruikersnaam"), + ("Invalid port", "Ongeldige poort"), + ("Closed manually by the peer", "Handmatig gesloten door de peer"), + ("Enable remote configuration modification", "Wijziging configuratie op afstand inschakelen"), + ("Run without install", "Uitvoeren zonder installatie"), + ("Always connected via relay", "Altijd verbonden via relay"), + ("Always connect via relay", "Altijd verbinden via relay"), + ("whitelist_tip", "Alleen een IP-adres op de witte lijst krijgt toegang tot mijn toestel"), + ("Login", "Log In"), + ("Verify", "Controleer"), + ("Remember me", "Herinner mij"), + ("Trust this device", "Vertrouw dit apparaat"), + ("Verification code", "Verificatie code"), + ("verification_tip", "Er is een nieuw apparaat gedetecteerd en er is een verificatiecode naar het geregistreerde e-mailadres gestuurd, voer de verificatiecode in om de verbinding voort te zetten."), + ("Logout", "Log Uit"), + ("Tags", "Labels"), + ("Search ID", "Zoek ID"), + ("whitelist_sep", "Gescheiden door komma, puntkomma, spatie of nieuwe regel"), + ("Add ID", "ID Toevoegen"), + ("Add Tag", "Label Toevoegen"), + ("Unselect all tags", "Alle labels verwijderen"), + ("Network error", "Netwerkfout"), + ("Username missed", "Gebruikersnaam gemist"), + ("Password missed", "Wachtwoord vergeten"), + ("Wrong credentials", "Verkeerde inloggegevens"), + ("Edit Tag", "Label Bewerken"), + ("Unremember Password", "Wachtwoord vergeten"), + ("Favorites", "Favorieten"), + ("Add to Favorites", "Toevoegen aan Favorieten"), + ("Remove from Favorites", "Verwijderen uit Favorieten"), + ("Empty", "Leeg"), + ("Invalid folder name", "Ongeldige mapnaam"), + ("Socks5 Proxy", "Socks5 Proxy"), + ("Hostname", "Hostnaam"), + ("Discovered", "Ontdekt"), + ("install_daemon_tip", "Om bij het opstarten van de computer te kunnen beginnen, moet je de systeemdienst installeren."), + ("Remote ID", "Externe ID"), + ("Paste", "Plakken"), + ("Paste here?", "Hier plakken"), + ("Are you sure to close the connection?", "Weet je zeker dat je de verbinding wilt sluiten?"), + ("Download new version", "Download nieuwe versie"), + ("Touch mode", "Aanraak modus"), + ("Mouse mode", "Muismodus"), + ("One-Finger Tap", "Een-Vinger Tik"), + ("Left Mouse", "Linkermuis"), + ("One-Long Tap", "Een-Vinger-Lange-Tik"), + ("Two-Finger Tap", "Twee-Vingers-Tik"), + ("Right Mouse", "Rechter muis"), + ("One-Finger Move", "Een-Vinger-Verplaatsing"), + ("Double Tap & Move", "Dubbel Tik en Verplaatsen"), + ("Mouse Drag", "Muis Slepen"), + ("Three-Finger vertically", "Drie-Vinger verticaal"), + ("Mouse Wheel", "Muiswiel"), + ("Two-Finger Move", "Twee-Vingers Verplaatsen"), + ("Canvas Move", "Canvas Verplaatsen"), + ("Pinch to Zoom", "Knijp om te Zoomen"), + ("Canvas Zoom", "Canvas Zoom"), + ("Reset canvas", "Reset canvas"), + ("No permission of file transfer", "Geen toestemming voor bestandsoverdracht"), + ("Note", "Opmerking"), + ("Connection", "Verbinding"), + ("Share Screen", "Scherm Delen"), + ("CLOSE", "SLUITEN"), + ("OPEN", "OPEN"), + ("Chat", "Chat"), + ("Total", "Totaal"), + ("items", "items"), + ("Selected", "Geselecteerd"), + ("Screen Capture", "Schermopname"), + ("Input Control", "Invoercontrole"), + ("Audio Capture", "Audio Opnemen"), + ("File Connection", "Bestandsverbinding"), + ("Screen Connection", "Schermverbinding"), + ("Do you accept?", "Sta je toe?"), + ("Open System Setting", "Systeeminstelling Openen"), + ("How to get Android input permission?", "Hoe krijg ik Android invoer toestemming?"), + ("android_input_permission_tip1", "Om ervoor te zorgen dat een extern apparaat uw Android-apparaat kan besturen via muis of aanraking, moet u RustDesk toestaan om de \"Toegankelijkheid\" service te gebruiken."), + ("android_input_permission_tip2", "Ga naar de volgende pagina met systeeminstellingen, zoek en ga naar [Geinstalleerde Services], schakel de service [RustDesk Input] in."), + ("android_new_connection_tip", "Er is een nieuw controleverzoek binnengekomen, dat uw huidige apparaat wil controleren."), + ("android_service_will_start_tip", "Als u \"Schermopname\" inschakelt, wordt de service automatisch gestart, zodat andere apparaten een verbinding met uw apparaat kunnen aanvragen."), + ("android_stop_service_tip", "Het sluiten van de service zal automatisch alle gemaakte verbindingen sluiten."), + ("android_version_audio_tip", "De huidige versie van Android ondersteunt geen audio-opname, upgrade naar Android 10 of hoger."), + ("android_start_service_tip", "Druk op [Start Service] of op de permissie OPEN [Screenshot] om de service voor het overnemen van het scherm te starten."), + ("Account", "Account"), + ("Overwrite", "Overschrijven"), + ("This file exists, skip or overwrite this file?", "Dit bestand bestaat reeds, overslaan of overschrijven?"), + ("Quit", "Afsluiten"), + ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), + ("Help", "https://rustdesk.com/docs/en/manual/linux/#x11-required"), + ("Failed", "Mislukt"), + ("Succeeded", "Geslaagd"), + ("Someone turns on privacy mode, exit", "Iemand schakelt privacymodus in, afsluiten"), + ("Unsupported", "Niet Ondersteund"), + ("Peer denied", "Peer geweigerd"), + ("Please install plugins", "Installeer plugins"), + ("Peer exit", "Peer afgesloten"), + ("Failed to turn off", "Uitschakelen mislukt"), + ("Turned off", "Uitgeschakeld"), + ("In privacy mode", "In privacymodus"), + ("Out privacy mode", "Uit privacymodus"), + ("Language", "Taal"), + ("Keep RustDesk background service", "RustDesk achtergronddienst behouden"), + ("Ignore Battery Optimizations", "Negeer Batterij Optimalisaties"), + ("android_open_battery_optimizations_tip", "Ga naar de volgende pagina met instellingen"), + ("Connection not allowed", "Verbinding niet toegestaan"), + ("Legacy mode", "Verouderde modus"), + ("Map mode", "Map mode"), + ("Translate mode", "Vertaalmodus"), + ("Use permanent password", "Gebruik permanent wachtwoord"), + ("Use both passwords", "Gebruik beide wachtwoorden"), + ("Set permanent password", "Stel permanent wachtwoord in"), + ("Enable Remote Restart", "Schakel Herstart op afstand in"), + ("Allow remote restart", "Opnieuw Opstarten op afstand toestaan"), + ("Restart Remote Device", "Apparaat op afstand herstarten"), + ("Are you sure you want to restart", "Weet je zeker dat je wilt herstarten"), + ("Restarting Remote Device", "Apparaat op afstand herstarten"), + ("remote_restarting_tip", "Apparaat op afstand wordt opnieuw opgestart, sluit dit bericht en maak na een ogenblik opnieuw verbinding met het permanente wachtwoord."), + ("Copied", "Gekopieerd"), + ("Exit Fullscreen", "Volledig Scherm sluiten"), + ("Fullscreen", "Volledig Scherm"), + ("Mobile Actions", "Mobiele Acties"), + ("Select Monitor", "Selecteer Monitor"), + ("Control Actions", "Controleacties"), + ("Display Settings", "Beeldscherminstellingen"), + ("Ratio", "Verhouding"), + ("Image Quality", "Beeldkwaliteit"), + ("Scroll Style", "Scroll Stijl"), + ("Show Menubar", "Toon Menubalk"), + ("Hide Menubar", "Verberg Menubalk"), + ("Direct Connection", "Directe Verbinding"), + ("Relay Connection", "Relaisverbinding"), + ("Secure Connection", "Beveiligde Verbinding"), + ("Insecure Connection", "Onveilige Verbinding"), + ("Scale original", "Oorspronkelijke schaal"), + ("Scale adaptive", "Schaalaanpassing"), + ("General", "Algemeen"), + ("Security", "Beveiliging"), + ("Theme", "Thema"), + ("Dark Theme", "Donker Thema"), + ("Dark", "Donker"), + ("Light", "Licht"), + ("Follow System", "Volg Systeem"), + ("Enable hardware codec", "Hardware codec inschakelen"), + ("Unlock Security Settings", "Beveiligingsinstellingen vrijgeven"), + ("Enable Audio", "Audio Inschakelen"), + ("Unlock Network Settings", "Netwerkinstellingen Vrijgeven"), + ("Server", "Server"), + ("Direct IP Access", "Directe IP toegang"), + ("Proxy", "Proxy"), + ("Apply", "Toepassen"), + ("Disconnect all devices?", "Alle apparaten uitschakelen?"), + ("Clear", "Wis"), + ("Audio Input Device", "Audio-invoerapparaat"), + ("Deny remote access", "Toegang op afstand weigeren"), + ("Use IP Whitelisting", "Gebruik een witte lijst van IP-adressen"), + ("Network", "Netwerk"), + ("Enable RDP", "Zet RDP aan"), + ("Pin menubar", "Menubalk Vastzetten"), + ("Unpin menubar", "Menubalk vrijmaken"), + ("Recording", "Opnemen"), + ("Directory", "Map"), + ("Automatically record incoming sessions", "Automatisch inkomende sessies opnemen"), + ("Change", "Wissel"), + ("Start session recording", "Start de sessieopname"), + ("Stop session recording", "Stop de sessieopname"), + ("Enable Recording Session", "Opnamesessie Activeren"), + ("Allow recording session", "Opnamesessie toestaan"), + ("Enable LAN Discovery", "LAN-detectie inschakelen"), + ("Deny LAN Discovery", "LAN-detectie Weigeren"), + ("Write a message", "Schrijf een bericht"), + ("Prompt", "Verzoek"), + ("Please wait for confirmation of UAC...", "Wacht op bevestiging van UAC..."), + ("elevated_foreground_window_tip", "Het momenteel geopende venster van de op afstand bediende computer vereist hogere rechten. Daarom is het momenteel niet mogelijk de muis en het toetsenbord te gebruiken. Vraag de gebruiker wiens computer u op afstand bedient om het venster te minimaliseren of de rechten te verhogen. Om dit probleem in de toekomst te voorkomen, wordt aanbevolen de software te installeren op de op afstand bediende computer."), + ("Disconnected", "Afgesloten"), + ("Other", "Andere"), + ("Confirm before closing multiple tabs", "Bevestig voordat u meerdere tabbladen sluit"), + ("Keyboard Settings", "Toetsenbord instellingen"), + ("Full Access", "Volledige Toegang"), + ("Screen Share", "Scherm Delen"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland vereist Ubuntu 21.04 of een hogere versie."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland vereist een hogere versie van Linux distro. Probeer X11 desktop of verander je OS."), + ("JumpLink", "JumpLink"), + ("Please Select the screen to be shared(Operate on the peer side).", "Selecteer het scherm dat moet worden gedeeld (Bediening aan de kant van de peer)."), + ("Show RustDesk", "Toon RustDesk"), + ("This PC", "Deze PC"), + ("or", "of"), + ("Continue with", "Ga verder met"), + ("Elevate", "Verhoog"), + ("Zoom cursor", "Cursor Zoomen"), + ("Accept sessions via password", "Sessies accepteren via wachtwoord"), + ("Accept sessions via click", "Sessies accepteren via klik"), + ("Accept sessions via both", "Accepteer sessies via beide"), + ("Please wait for the remote side to accept your session request...", "Wacht tot de andere kant uw sessieverzoek accepteert..."), + ("One-time Password", "Eenmalig Wachtwoord"), + ("Use one-time password", "Gebruik een eenmalig Wachtwoord"), + ("One-time password length", "Eenmalig Wachtwoord lengre"), + ("Request access to your device", "Toegang tot uw toestel aanvragen"), + ("Hide connection management window", "Verberg het venster voor verbindingsbeheer"), + ("hide_cm_tip", "Dit kan alleen als de toegang via een permanent wachtwoord verloopt."), + ("wayland_experiment_tip", "Wayland ondersteuning is slechts experimenteel. Gebruik alsjeblieft X11 als je onbeheerde toegang nodig hebt."), + ("Right click to select tabs", "Rechts klikken om tabbladen te selecteren"), + ("Skipped", "Overgeslagen"), + ("Add to Address Book", "Toevoegen aan Adresboek"), + ("Group", "Groep"), + ("Search", "Zoek"), + ("Closed manually by web console", "Handmatig gesloten door webconsole"), + ("Local keyboard type", "Lokaal toetsenbord"), + ("Select local keyboard type", "Selecteer lokaal toetsenbord"), + ("software_render_tip", "Als u een NVIDIA grafische kaart hebt en het externe venster sluit onmiddellijk na verbinding, kan het helpen om het nieuwe stuurprogramma te installeren en te kiezen voor software rendering. Een software herstart is vereist."), + ("Always use software rendering", "Gebruik altijd software rendering"), + ("config_input", "config_invoer"), + ("config_microphone", "config_microfoon"), + ("request_elevation_tip", "U kunt ook meer rechten vragen als iemand aan de andere kant aanwezig is."), + ("Wait", "Wacht"), + ("Elevation Error", "Verhogingsfout"), + ("Ask the remote user for authentication", "Vraag de gebruiker op afstand om bevestiging"), + ("Choose this if the remote account is administrator", ""), + ("Transmit the username and password of administrator", ""), + ("still_click_uac_tip", "De gebruiker op afstand moet altijd bevestigen via het UAC-venster van de werkende RustDesk."), + ("Request Elevation", "Verzoek om meer rechten"), + ("wait_accept_uac_tip", "Wacht tot de gebruiker op afstand het UAC-dialoogvenster accepteert."), + ("Elevate successfully", "Succesvolle verhoging van privileges"), + ("uppercase", "Hoofdletter"), + ("lowercase", "kleine letter"), + ("digit", "cijfer"), + ("special character", "speciaal teken"), + ("length>=8", "lengte>=8"), + ("Weak", "Zwak"), + ("Medium", "Midelmatig"), + ("Strong", "Sterk"), + ("Switch Sides", "Wissel van kant"), + ("Please confirm if you want to share your desktop?", "bevestig als je je bureaublad wilt delen?"), + ("Closed as expected", "Gesloten zoals verwacht"), + ("Display", "Weergave"), + ("Default View Style", "Standaard Weergave Stijl"), + ("Default Scroll Style", "Standaard Scroll Stijl"), + ("Default Image Quality", "Standaard Beeldkwaliteit"), + ("Default Codec", "tandaard Codec"), + ("Bitrate", "Bitrate"), + ("FPS", "FPS"), + ("Auto", "Auto"), + ("Other Default Options", "Andere Standaardopties"), + ("Voice call", "Spraakoproep"), + ("Text chat", "Tekst chat"), + ("Stop voice call", "Stop spraakoproep"), + ].iter().cloned().collect(); +} From cea123c79f5f0e39ed394df0f60f0d404949ee27 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 14 Feb 2023 19:20:22 +0800 Subject: [PATCH 128/199] more lang in setup.nsi --- res/setup.nsi | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/res/setup.nsi b/res/setup.nsi index 5410e0ff5..635851d0a 100644 --- a/res/setup.nsi +++ b/res/setup.nsi @@ -56,8 +56,74 @@ InstallDir "$PROGRAMFILES64\${PRODUCT_NAME}" #################################################################### # Language -!insertmacro MUI_LANGUAGE "English" +!insertmacro MUI_LANGUAGE "English" ; The first language is the default language +!insertmacro MUI_LANGUAGE "French" +!insertmacro MUI_LANGUAGE "German" +!insertmacro MUI_LANGUAGE "Spanish" +!insertmacro MUI_LANGUAGE "SpanishInternational" !insertmacro MUI_LANGUAGE "SimpChinese" +!insertmacro MUI_LANGUAGE "TradChinese" +!insertmacro MUI_LANGUAGE "Japanese" +!insertmacro MUI_LANGUAGE "Korean" +!insertmacro MUI_LANGUAGE "Italian" +!insertmacro MUI_LANGUAGE "Dutch" +!insertmacro MUI_LANGUAGE "Danish" +!insertmacro MUI_LANGUAGE "Swedish" +!insertmacro MUI_LANGUAGE "Norwegian" +!insertmacro MUI_LANGUAGE "NorwegianNynorsk" +!insertmacro MUI_LANGUAGE "Finnish" +!insertmacro MUI_LANGUAGE "Greek" +!insertmacro MUI_LANGUAGE "Russian" +!insertmacro MUI_LANGUAGE "Portuguese" +!insertmacro MUI_LANGUAGE "PortugueseBR" +!insertmacro MUI_LANGUAGE "Polish" +!insertmacro MUI_LANGUAGE "Ukrainian" +!insertmacro MUI_LANGUAGE "Czech" +!insertmacro MUI_LANGUAGE "Slovak" +!insertmacro MUI_LANGUAGE "Croatian" +!insertmacro MUI_LANGUAGE "Bulgarian" +!insertmacro MUI_LANGUAGE "Hungarian" +!insertmacro MUI_LANGUAGE "Thai" +!insertmacro MUI_LANGUAGE "Romanian" +!insertmacro MUI_LANGUAGE "Latvian" +!insertmacro MUI_LANGUAGE "Macedonian" +!insertmacro MUI_LANGUAGE "Estonian" +!insertmacro MUI_LANGUAGE "Turkish" +!insertmacro MUI_LANGUAGE "Lithuanian" +!insertmacro MUI_LANGUAGE "Slovenian" +!insertmacro MUI_LANGUAGE "Serbian" +!insertmacro MUI_LANGUAGE "SerbianLatin" +!insertmacro MUI_LANGUAGE "Arabic" +!insertmacro MUI_LANGUAGE "Farsi" +!insertmacro MUI_LANGUAGE "Hebrew" +!insertmacro MUI_LANGUAGE "Indonesian" +!insertmacro MUI_LANGUAGE "Mongolian" +!insertmacro MUI_LANGUAGE "Luxembourgish" +!insertmacro MUI_LANGUAGE "Albanian" +!insertmacro MUI_LANGUAGE "Breton" +!insertmacro MUI_LANGUAGE "Belarusian" +!insertmacro MUI_LANGUAGE "Icelandic" +!insertmacro MUI_LANGUAGE "Malay" +!insertmacro MUI_LANGUAGE "Bosnian" +!insertmacro MUI_LANGUAGE "Kurdish" +!insertmacro MUI_LANGUAGE "Irish" +!insertmacro MUI_LANGUAGE "Uzbek" +!insertmacro MUI_LANGUAGE "Galician" +!insertmacro MUI_LANGUAGE "Afrikaans" +!insertmacro MUI_LANGUAGE "Catalan" +!insertmacro MUI_LANGUAGE "Esperanto" +!insertmacro MUI_LANGUAGE "Asturian" +!insertmacro MUI_LANGUAGE "Basque" +!insertmacro MUI_LANGUAGE "Pashto" +!insertmacro MUI_LANGUAGE "ScotsGaelic" +!insertmacro MUI_LANGUAGE "Georgian" +!insertmacro MUI_LANGUAGE "Vietnamese" +!insertmacro MUI_LANGUAGE "Welsh" +!insertmacro MUI_LANGUAGE "Armenian" +!insertmacro MUI_LANGUAGE "Corsican" +!insertmacro MUI_LANGUAGE "Tatar" +!insertmacro MUI_LANGUAGE "Hindi" + #################################################################### # Sections From d2e0cb396f90cc24ef126da7c0d3766b26ee07f1 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 14 Feb 2023 19:44:14 +0800 Subject: [PATCH 129/199] relay hint msgbox Signed-off-by: 21pages --- flutter/lib/models/model.dart | 42 +++++++++++++++++++++++- src/client.rs | 4 +++ src/client/io_loop.rs | 18 +++++++--- src/flutter_ffi.rs | 7 ++-- src/lang/ca.rs | 3 +- src/lang/cn.rs | 3 +- src/lang/cs.rs | 3 +- src/lang/da.rs | 3 +- src/lang/de.rs | 3 +- src/lang/en.rs | 3 +- src/lang/eo.rs | 3 +- src/lang/es.rs | 3 +- src/lang/fa.rs | 3 +- src/lang/fr.rs | 3 +- src/lang/gr.rs | 3 +- src/lang/hu.rs | 3 +- src/lang/id.rs | 3 +- src/lang/it.rs | 3 +- src/lang/ja.rs | 3 +- src/lang/ko.rs | 3 +- src/lang/kz.rs | 3 +- src/lang/pl.rs | 3 +- src/lang/pt_PT.rs | 3 +- src/lang/ptbr.rs | 3 +- src/lang/ro.rs | 3 +- src/lang/ru.rs | 3 +- src/lang/sk.rs | 3 +- src/lang/sl.rs | 3 +- src/lang/sq.rs | 3 +- src/lang/sr.rs | 3 +- src/lang/sv.rs | 3 +- src/lang/template.rs | 3 +- src/lang/th.rs | 3 +- src/lang/tr.rs | 3 +- src/lang/tw.rs | 3 +- src/lang/ua.rs | 3 +- src/lang/vn.rs | 3 +- src/ui/remote.rs | 6 ++-- src/ui_session_interface.rs | 62 ++++++++++++++++++++++++++++------- 39 files changed, 179 insertions(+), 59 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index d0a2ea601..0bd6934a8 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -298,6 +298,8 @@ class FfiModel with ChangeNotifier { showWaitUacDialog(id, dialogManager, type); } else if (type == 'elevation-error') { showElevationError(id, type, title, text, dialogManager); + } else if (type == "relay-hint") { + showRelayHintDialog(id, type, title, text, dialogManager); } else { var hasRetry = evt['hasRetry'] == 'true'; showMsgBox(id, type, title, text, link, hasRetry, dialogManager); @@ -312,7 +314,7 @@ class FfiModel with ChangeNotifier { _timer?.cancel(); if (hasRetry) { _timer = Timer(Duration(seconds: _reconnects), () { - bind.sessionReconnect(id: id); + bind.sessionReconnect(id: id, forceRelay: false); clearPermissions(); dialogManager.showLoading(translate('Connecting...'), onCancel: closeConnection); @@ -323,6 +325,44 @@ class FfiModel with ChangeNotifier { } } + void showRelayHintDialog(String id, String type, String title, String text, + OverlayDialogManager dialogManager) { + dialogManager.show(tag: '$id-$type', (setState, close) { + onClose() { + closeConnection(); + close(); + } + + reconnect(bool forceRelay) { + bind.sessionReconnect(id: id, forceRelay: forceRelay); + clearPermissions(); + dialogManager.showLoading(translate('Connecting...'), + onCancel: closeConnection); + } + + final style = + ElevatedButton.styleFrom(backgroundColor: Colors.green[700]); + return CustomAlertDialog( + title: null, + content: msgboxContent(type, title, + "${translate(text)}\n\n${translate('relay_hint_tip')}"), + actions: [ + dialogButton('Close', onPressed: onClose, isOutline: true), + dialogButton('Retry', onPressed: () => reconnect(false)), + dialogButton('Connect via relay', + onPressed: () => reconnect(true), buttonStyle: style), + dialogButton('Always connect via relay', onPressed: () { + const option = 'force-always-relay'; + bind.sessionPeerOption( + id: id, name: option, value: bool2option(option, true)); + reconnect(true); + }, buttonStyle: style), + ], + onCancel: onClose, + ); + }); + } + /// Handle the peer info event based on [evt]. handlePeerInfo(Map evt, String peerId) async { // recent peer updated by handle_peer_info(ui_session_interface.rs) --> handle_peer_info(client.rs) --> save_config(client.rs) diff --git a/src/client.rs b/src/client.rs index 05b34d781..77221bdb2 100644 --- a/src/client.rs +++ b/src/client.rs @@ -916,6 +916,8 @@ pub struct LoginConfigHandler { pub direct: Option, pub received: bool, switch_uuid: Option, + pub success_time: Option, + pub direct_error_counter: usize, } impl Deref for LoginConfigHandler { @@ -962,6 +964,8 @@ impl LoginConfigHandler { self.direct = None; self.received = false; self.switch_uuid = switch_uuid; + self.success_time = None; + self.direct_error_counter = 0; } /// Check if the client should auto login. diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 5186aff4d..de91b091d 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -25,9 +25,8 @@ use hbb_common::{allow_err, get_time, message_proto::*, sleep}; use hbb_common::{fs, log, Stream}; use crate::client::{ - new_voice_call_request, Client, CodecFormat, MediaData, MediaSender, - QualityStatus, MILLI1, SEC30, SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, - SERVER_KEYBOARD_ENABLED, + new_voice_call_request, Client, CodecFormat, MediaData, MediaSender, QualityStatus, MILLI1, + SEC30, SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::common::{check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}; @@ -148,7 +147,15 @@ impl Remote { Err(err) => { log::error!("Connection closed: {}", err); self.handler.set_force_relay(direct, received); - self.handler.msgbox("error", "Connection Error", &err.to_string(), ""); + let msgtype = "error"; + let title = "Connection Error"; + let text = err.to_string(); + let show_relay_hint = self.handler.show_relay_hint(last_recv_time, msgtype, title, &text); + if show_relay_hint{ + self.handler.msgbox("relay-hint", title, &text, ""); + } else { + self.handler.msgbox(msgtype, title, &text, ""); + } break; } Ok(ref bytes) => { @@ -754,7 +761,8 @@ impl Remote { Data::CloseVoiceCall => { self.stop_voice_call(); let msg = new_voice_call_request(false); - self.handler.on_voice_call_closed("Closed manually by the peer"); + self.handler + .on_voice_call_closed("Closed manually by the peer"); allow_err!(peer.send(&msg).await); } _ => {} diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 3025d722c..f8ee512d8 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,4 +1,3 @@ -use crate::ui_session_interface::InvokeUiSession; use crate::{ client::file_trait::FileManager, common::make_fd_to_json, @@ -7,7 +6,7 @@ use crate::{ flutter::{session_add, session_start_}, ui_interface::{self, *}, }; -use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; +use flutter_rust_bridge::{StreamSink, SyncReturn}; use hbb_common::{ config::{self, LocalConfig, PeerConfig, ONLINE}, fs, log, @@ -157,9 +156,9 @@ pub fn session_record_screen(id: String, start: bool, width: usize, height: usiz } } -pub fn session_reconnect(id: String) { +pub fn session_reconnect(id: String, force_relay: bool) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.reconnect(); + session.reconnect(force_relay); } } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index e98c6636a..d483a185d 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Tancat manualment pel peer"), ("Enable remote configuration modification", "Habilitar modificació remota de configuració"), ("Run without install", "Executar sense instal·lar"), - ("Always connected via relay", "Connectat sempre a través de relay"), + ("Connect via relay", ""), ("Always connect via relay", "Connecta sempre a través de relay"), ("whitelist_tip", ""), ("Login", "Inicia sessió"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 64c37709a..7dea516ba 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "被对方手动关闭"), ("Enable remote configuration modification", "允许远程修改配置"), ("Run without install", "无安装运行"), - ("Always connected via relay", "强制走中继连接"), + ("Connect via relay", "中继连接"), ("Always connect via relay", "强制走中继连接"), ("whitelist_tip", "只有白名单里的ip才能访问我"), ("Login", "登录"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "语音通话"), ("Text chat", "文字聊天"), ("Stop voice call", "停止语音聊天"), + ("relay_hint_tip", "可能无法直连,可以尝试中继连接。\n另外,如果想直接使用中继连接,可以在ID后面添加/r,或者在卡片选项里选择强制走中继连接。"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 70a3eb6c7..97a3ebc48 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Ručně ukončeno protějškem"), ("Enable remote configuration modification", "Umožnit upravování nastavení vzdáleného"), ("Run without install", "Spustit bez instalování"), - ("Always connected via relay", "Vždy spojováno prostřednictvím brány pro předávání (relay)"), + ("Connect via relay", ""), ("Always connect via relay", "Vždy se spojovat prostřednictvím brány pro předávání (relay)"), ("whitelist_tip", "Přístup je umožněn pouze z IP adres, nacházejících se na seznamu povolených"), ("Login", "Přihlásit se"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index ae943e1e8..bab81914e 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Manuelt lukket af peer"), ("Enable remote configuration modification", "Tillad at ændre afstandskonfigurationen"), ("Run without install", "Kør uden installation"), - ("Always connected via relay", "Tilslut altid via relæ-server"), + ("Connect via relay", ""), ("Always connect via relay", "Forbindelse via relæ-server"), ("whitelist_tip", "Kun IP'er på udgivelseslisten kan få adgang til mig"), ("Login", "Login"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 1743505cc..05d02dd58 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Von der Gegenstelle manuell geschlossen"), ("Enable remote configuration modification", "Änderung der Konfiguration aus der Ferne zulassen"), ("Run without install", "Ohne Installation ausführen"), - ("Always connected via relay", "Immer über Relay-Server verbunden"), + ("Connect via relay", ""), ("Always connect via relay", "Immer über Relay-Server verbinden"), ("whitelist_tip", "Nur IPs auf der Whitelist können zugreifen."), ("Login", "Anmelden"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Sprachanruf"), ("Text chat", "Text-Chat"), ("Stop voice call", "Sprachanruf beenden"), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 37c08a974..4bfa86349 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -42,6 +42,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("request_elevation_tip","You can also request elevation if there is someone on the remote side."), ("wait_accept_uac_tip","Please wait for the remote user to accept the UAC dialog."), ("still_click_uac_tip", "Still requires the remote user to click OK on the UAC window of running RustDesk."), - ("config_microphone", "In order to speak remotely, you need to grant RustDesk \"Record Audio\" permissions.") + ("config_microphone", "In order to speak remotely, you need to grant RustDesk \"Record Audio\" permissions."), + ("relay_hint_tip", "It may not be possible to connect directly, you can try to connect via relay. \nIn addition, if you want to use relay on your first try, you can add the \"/r\" suffix to the ID, or select the option \"Always connect via relay\" in the peer card."), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index f457833f8..47eeb3367 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Manuale fermita de la samtavolano"), ("Enable remote configuration modification", "Permesi foran redaktadon de la konfiguracio"), ("Run without install", "Plenumi sen instali"), - ("Always connected via relay", "Ĉiam konektata per relajso"), + ("Connect via relay", ""), ("Always connect via relay", "Ĉiam konekti per relajso"), ("whitelist_tip", "Nur la IP en la blanka listo povas kontroli mian komputilon"), ("Login", "Konekti"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 939a4831f..4634cea81 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Cerrado manualmente por el par"), ("Enable remote configuration modification", "Habilitar modificación remota de configuración"), ("Run without install", "Ejecutar sin instalar"), - ("Always connected via relay", "Siempre conectado a través de relay"), + ("Connect via relay", ""), ("Always connect via relay", "Conéctese siempre a través de relay"), ("whitelist_tip", "Solo las direcciones IP autorizadas pueden conectarse a este escritorio"), ("Login", "Iniciar sesión"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Llamada de voz"), ("Text chat", "Chat de texto"), ("Stop voice call", "Detener llamada de voz"), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 8413673a1..2d0f29a5b 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "به صورت دستی توسط میزبان بسته شد"), ("Enable remote configuration modification", "فعال بودن اعمال تغییرات پیکربندی از راه دور"), ("Run without install", "بدون نصب اجرا شود"), - ("Always connected via relay", "متصل است Relay همیشه با"), + ("Connect via relay", ""), ("Always connect via relay", "برای اتصال استفاده شود Relay از"), ("whitelist_tip", "های مجاز می توانند به این دسکتاپ متصل شوند IP فقط"), ("Login", "ورود"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "تماس صوتی"), ("Text chat", "گفتگو متنی (چت متنی)"), ("Stop voice call", "توقف تماس صوتی"), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 39ee3bc7f..4e0e79aa0 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Fermé manuellement par le pair"), ("Enable remote configuration modification", "Autoriser la modification de la configuration à distance"), ("Run without install", "Exécuter sans installer"), - ("Always connected via relay", "Forcer la connexion relais"), + ("Connect via relay", ""), ("Always connect via relay", "Forcer la connexion relais"), ("whitelist_tip", "Seule une IP de la liste blanche peut accéder à mon appareil"), ("Login", "Connexion"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 7cb678ecc..09284738a 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Έκλεισε από τον απομακρυσμένο σταθμό"), ("Enable remote configuration modification", "Ενεργοποίηση απομακρυσμένης τροποποίησης ρυθμίσεων"), ("Run without install", "Εκτέλεση χωρίς εγκατάσταση"), - ("Always connected via relay", "Πάντα συνδεδεμένο μέσω αναμετάδοσης"), + ("Connect via relay", ""), ("Always connect via relay", "Σύνδεση πάντα μέσω αναμετάδοσης"), ("whitelist_tip", "Μόνο οι IP της λίστας επιτρεπόμενων έχουν πρόσβαση"), ("Login", "Σύνδεση"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 25562f556..16c99d207 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "A kapcsolatot a másik fél manuálisan bezárta"), ("Enable remote configuration modification", "Távoli konfiguráció módosítás engedélyezése"), ("Run without install", "Futtatás feltelepítés nélkül"), - ("Always connected via relay", "Mindig közvetítőn keresztül csatlakozik"), + ("Connect via relay", ""), ("Always connect via relay", "Mindig közvetítőn keresztüli csatlakozás"), ("whitelist_tip", "Csak az engedélyezési listán szereplő címek csatlakozhatnak"), ("Login", "Belépés"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 68a80e540..f4be0396f 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Ditutup secara manual oleh peer"), ("Enable remote configuration modification", "Aktifkan modifikasi konfigurasi jarak jauh"), ("Run without install", "Jalankan tanpa menginstal"), - ("Always connected via relay", "Selalu terhubung melalui relai"), + ("Connect via relay", ""), ("Always connect via relay", "Selalu terhubung melalui relai"), ("whitelist_tip", "Hanya whitelisted IP yang dapat mengakses saya"), ("Login", "Masuk"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index a4ea58304..15f7b977f 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Chiuso manualmente dal peer"), ("Enable remote configuration modification", "Abilita la modifica remota della configurazione"), ("Run without install", "Esegui senza installare"), - ("Always connected via relay", "Connesso sempre tramite relay"), + ("Connect via relay", ""), ("Always connect via relay", "Collegati sempre tramite relay"), ("whitelist_tip", "Solo gli indirizzi IP autorizzati possono connettersi a questo desktop"), ("Login", "Accedi"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Chiamata vocale"), ("Text chat", "Chat testuale"), ("Stop voice call", "Interrompi la chiamata vocale"), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 7069c0daf..acf1c9b96 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "相手が手動で切断しました"), ("Enable remote configuration modification", "リモート設定変更を有効化"), ("Run without install", "インストールせずに実行"), - ("Always connected via relay", "常に中継サーバー経由で接続"), + ("Connect via relay", ""), ("Always connect via relay", "常に中継サーバー経由で接続"), ("whitelist_tip", "ホワイトリストに登録されたIPからのみ接続を許可します"), ("Login", "ログイン"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 43eb552d3..e1bc43182 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "다른 사용자에 의해 종료됨"), ("Enable remote configuration modification", "원격 구성 변경 활성화"), ("Run without install", "설치 없이 실행"), - ("Always connected via relay", "항상 relay를 통해 접속됨"), + ("Connect via relay", ""), ("Always connect via relay", "항상 relay를 통해 접속하기"), ("whitelist_tip", "화이트리스트에 있는 IP만 현 데스크탑에 접속 가능합니다"), ("Login", "로그인"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 49c7b9916..488290537 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Пир қолымен жабылған"), ("Enable remote configuration modification", "Қашықтан қалыптарды өзгертуді іске қосу"), ("Run without install", "Орнатпай-ақ Іске қосу"), - ("Always connected via relay", "Әрқашан да релай сербері арқылы қосулы"), + ("Connect via relay", ""), ("Always connect via relay", "Әрқашан да релай сербері арқылы қосылу"), ("whitelist_tip", "Маған тек ақ-тізімделген IP қол жеткізе алады"), ("Login", "Кіру"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 41239961a..e6ba5b171 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Połączenie zakończone ręcznie przez peer"), ("Enable remote configuration modification", "Włącz zdalną modyfikację konfiguracji"), ("Run without install", "Uruchom bez instalacji"), - ("Always connected via relay", "Zawsze połączony pośrednio"), + ("Connect via relay", ""), ("Always connect via relay", "Zawsze łącz pośrednio"), ("whitelist_tip", "Zezwalaj na łączenie z tym komputerem tylko z adresów IP znajdujących się na białej liście"), ("Login", "Zaloguj"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index e69a140c9..a1ad932b1 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Fechada manualmente pelo destino"), ("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"), + ("Connect via relay", ""), ("Always connect via relay", "Sempre conectar via relay"), ("whitelist_tip", "Somente IPs na whitelist podem me acessar"), ("Login", "Login"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 0887a5915..5ece46006 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), + ("Connect via relay", ""), ("Always connect via relay", "Sempre conectar via relay"), ("whitelist_tip", "Somente IPs confiáveis podem me acessar"), ("Login", "Login"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 304353d42..e9b83e298 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Închis manual de dispozitivul pereche"), ("Enable remote configuration modification", "Activează modificarea configurației de la distanță"), ("Run without install", "Rulează fără instalare"), - ("Always connected via relay", "Se conectează mereu prin retransmisie"), + ("Connect via relay", ""), ("Always connect via relay", "Se conectează mereu prin retransmisie"), ("whitelist_tip", "Doar adresele IP autorizate pot accesa acest dispozitiv"), ("Login", "Conectare"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 1792eccce..a8ef18d8a 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Закрыто удалённым узлом вручную"), ("Enable remote configuration modification", "Разрешить удалённое изменение конфигурации"), ("Run without install", "Запустить без установки"), - ("Always connected via relay", "Всегда подключается через ретрансляционный сервер"), + ("Connect via relay", ""), ("Always connect via relay", "Всегда подключаться через ретрансляционный сервер"), ("whitelist_tip", "Только IP-адреса из белого списка могут получить доступ ко мне"), ("Login", "Войти"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Голосовой вызов"), ("Text chat", "Текстовый чат"), ("Stop voice call", "Завершить голосовой вызов"), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 6f6f7a18e..47a795342 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Manuálne ukončené opačnou stranou pripojenia"), ("Enable remote configuration modification", "Povoliť zmeny konfigurácie zo vzdialeného PC"), ("Run without install", "Spustiť bez inštalácie"), - ("Always connected via relay", "Vždy pripojené cez prepájací server"), + ("Connect via relay", ""), ("Always connect via relay", "Vždy pripájať cez prepájací server"), ("whitelist_tip", "Len vymenované IP adresy majú oprávnenie sa pripojiť k vzdialenej správe"), ("Login", "Prihlásenie"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 2fb74fa5d..1eb33b970 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Povezavo ročno prekinil odjemalec"), ("Enable remote configuration modification", "Omogoči oddaljeno spreminjanje nastavitev"), ("Run without install", "Zaženi brez namestitve"), - ("Always connected via relay", "Vedno povezan preko posrednika"), + ("Connect via relay", ""), ("Always connect via relay", "Vedno poveži preko posrednika"), ("whitelist_tip", "Dostop je možen samo iz dovoljenih IPjev"), ("Login", "Prijavi"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 5d4a6e1ad..1ade9757a 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "E mbyllur manualisht nga peer"), ("Enable remote configuration modification", "Aktivizoni modifikimin e konfigurimit në distancë"), ("Run without install", "Ekzekuto pa instaluar"), - ("Always connected via relay", "Gjithmonë i ldihur me transmetues"), + ("Connect via relay", ""), ("Always connect via relay", "Gjithmonë lidheni me transmetues"), ("whitelist_tip", "Vetëm IP e listës së bardhë mund të më aksesoj."), ("Login", "Hyrje"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 31a3ade8f..e5704093d 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Klijent ručno raskinuo konekciju"), ("Enable remote configuration modification", "Dozvoli modifikaciju udaljene konfiguracije"), ("Run without install", "Pokreni bez instalacije"), - ("Always connected via relay", "Uvek spojne preko posrednika"), + ("Connect via relay", ""), ("Always connect via relay", "Uvek se spoj preko posrednika"), ("whitelist_tip", "Samo dozvoljene IP mi mogu pristupiti"), ("Login", "Prijava"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index e30c09e44..063892074 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Stängd manuellt av klienten"), ("Enable remote configuration modification", "Tillåt fjärrkonfigurering"), ("Run without install", "Kör utan installation"), - ("Always connected via relay", "Anslut alltid via relay"), + ("Connect via relay", ""), ("Always connect via relay", "Anslut alltid via relay"), ("whitelist_tip", "Bara vitlistade IPs kan koppla upp till mig"), ("Login", "Logga in"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index b88618074..4190ba399 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", ""), ("Enable remote configuration modification", ""), ("Run without install", ""), - ("Always connected via relay", ""), + ("Connect via relay", ""), ("Always connect via relay", ""), ("whitelist_tip", ""), ("Login", ""), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 1c75aaae7..629c5ac77 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "ถูกปิดโดยอีกฝั่งการการเชื่อมต่อ"), ("Enable remote configuration modification", "เปิดการใช้งานการแก้ไขการตั้งค่าปลายทาง"), ("Run without install", "ใช้งานโดยไม่ต้องติดตั้ง"), - ("Always connected via relay", "เชื่อมต่อผ่านรีเลย์เสมอ"), + ("Connect via relay", ""), ("Always connect via relay", "เชื่อมต่อผ่านรีเลย์เสมอ"), ("whitelist_tip", "อนุญาตเฉพาะการเชื่อมต่อจาก IP ที่ไวท์ลิสต์"), ("Login", "เข้าสู่ระบบ"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index a9e2c1715..b683fb78a 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Eş tarafından manuel olarak kapatıldı"), ("Enable remote configuration modification", "Uzaktan yapılandırma değişikliğini etkinleştir"), ("Run without install", "Yüklemeden çalıştır"), - ("Always connected via relay", "Her zaman röle ile bağlı"), + ("Connect via relay", ""), ("Always connect via relay", "Always connect via relay"), ("whitelist_tip", "Bu masaüstüne yalnızca yetkili IP adresleri bağlanabilir"), ("Login", "Giriş yap"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 7c49a29a2..e4957e3d7 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "由對方手動關閉"), ("Enable remote configuration modification", "啟用遠端更改設定"), ("Run without install", "跳過安裝直接執行"), - ("Always connected via relay", "一律透過轉送連線"), + ("Connect via relay", ""), ("Always connect via relay", "一律透過轉送連線"), ("whitelist_tip", "只有白名單中的 IP 可以存取"), ("Login", "登入"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 92c99d90c..3c1d7776a 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Закрито вузлом вручну"), ("Enable remote configuration modification", "Дозволити віддалену зміну конфігурації"), ("Run without install", "Запустити без установки"), - ("Always connected via relay", "Завжди підключений через ретрансляційний сервер"), + ("Connect via relay", ""), ("Always connect via relay", "Завжди підключатися через ретрансляційний сервер"), ("whitelist_tip", "Тільки IP-адреси з білого списку можуть отримати доступ до мене"), ("Login", "Увійти"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 8bb1d45e9..76f611429 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Đóng thủ công bởi peer"), ("Enable remote configuration modification", "Cho phép thay đổi cấu hình bên từ xa"), ("Run without install", "Chạy mà không cần cài"), - ("Always connected via relay", "Luôn đuợc kết nối qua relay"), + ("Connect via relay", ""), ("Always connect via relay", "Luôn kết nối qua relay"), ("whitelist_tip", "Chỉ có những IP đựoc cho phép mới có thể truy cập"), ("Login", "Đăng nhập"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 447c2e31d..1725a8f41 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1,4 +1,3 @@ -use std::sync::RwLock; use std::{ collections::HashMap, ops::{Deref, DerefMut}, @@ -15,7 +14,6 @@ use sciter::{ Value, }; -use hbb_common::tokio::io::AsyncReadExt; use hbb_common::{ allow_err, fs::TransferJobMeta, log, message_proto::*, rendezvous_proto::ConnType, }; @@ -348,7 +346,7 @@ impl sciter::EventHandler for SciterSession { let site = AssetPtr::adopt(ptr as *mut video_destination); log::debug!("[video] start video"); *VIDEO.lock().unwrap() = Some(site); - self.reconnect(); + self.reconnect(false); } } BEHAVIOR_EVENTS::VIDEO_INITIALIZED => { @@ -397,7 +395,7 @@ impl sciter::EventHandler for SciterSession { fn transfer_file(); fn tunnel(); fn lock_screen(); - fn reconnect(); + fn reconnect(bool); fn get_chatbox(); fn get_icon(); fn get_home_dir(); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 25c15f52f..97db904d4 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1,29 +1,30 @@ use std::collections::HashMap; use std::ops::{Deref, DerefMut}; use std::str::FromStr; -use std::sync::{Arc, Mutex, RwLock}; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex, RwLock}; +use std::time::Duration; use async_trait::async_trait; use bytes::Bytes; use rdev::{Event, EventType::*}; use uuid::Uuid; -use hbb_common::{allow_err, message_proto::*}; -use hbb_common::{fs, get_version_number, log, Stream}; use hbb_common::config::{Config, LocalConfig, PeerConfig, RS_PUB_KEY}; use hbb_common::rendezvous_proto::ConnType; use hbb_common::tokio::{self, sync::mpsc}; +use hbb_common::{allow_err, message_proto::*}; +use hbb_common::{fs, get_version_number, log, Stream}; -use crate::{client::Data, client::Interface}; -use crate::client::{ - check_if_retry, FileManager, handle_hash, handle_login_error, handle_login_from_ui, - handle_test_delay, input_os_password, Key, KEY_MAP, load_config, LoginConfigHandler, - QualityStatus, send_mouse, start_video_audio_threads, -}; use crate::client::io_loop::Remote; +use crate::client::{ + check_if_retry, handle_hash, handle_login_error, handle_login_from_ui, handle_test_delay, + input_os_password, load_config, send_mouse, start_video_audio_threads, FileManager, Key, + LoginConfigHandler, QualityStatus, KEY_MAP, +}; use crate::common::{self, GrabState}; use crate::keyboard; +use crate::{client::Data, client::Interface}; pub static IS_IN: AtomicBool = AtomicBool::new(false); @@ -531,9 +532,13 @@ impl Session { } } - pub fn reconnect(&self) { + pub fn reconnect(&self, force_relay: bool) { self.send(Data::Close); let cloned = self.clone(); + // override only if true + if true == force_relay { + cloned.lc.write().unwrap().force_relay = true; + } let mut lock = self.thread.lock().unwrap(); lock.take().map(|t| t.join()); *lock = Some(std::thread::spawn(move || { @@ -674,10 +679,42 @@ impl Session { pub fn request_voice_call(&self) { self.send(Data::NewVoiceCall); } - + pub fn close_voice_call(&self) { self.send(Data::CloseVoiceCall); } + + pub fn show_relay_hint( + &mut self, + last_recv_time: tokio::time::Instant, + msgtype: &str, + title: &str, + text: &str, + ) -> bool { + let duration = Duration::from_secs(3); + let counter_interval = 3; + let lock = self.lc.read().unwrap(); + let success_time = lock.success_time; + let direct = lock.direct.unwrap_or(false); + let received = lock.received; + drop(lock); + if let Some(success_time) = success_time { + if direct && last_recv_time.duration_since(success_time) < duration { + let retry_for_relay = direct && !received; + let retry = check_if_retry(msgtype, title, text, retry_for_relay); + if retry && !retry_for_relay { + self.lc.write().unwrap().direct_error_counter += 1; + if self.lc.read().unwrap().direct_error_counter % counter_interval == 0 { + #[cfg(feature = "flutter")] + return true; + } + } + } else { + self.lc.write().unwrap().direct_error_counter = 0; + } + } + false + } } pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { @@ -813,6 +850,7 @@ impl Interface for Session { "Connected, waiting for image...", "", ); + self.lc.write().unwrap().success_time = Some(tokio::time::Instant::now()); } self.on_connected(self.lc.read().unwrap().conn_type); #[cfg(windows)] @@ -958,7 +996,7 @@ pub async fn io_loop(handler: Session) { let frame_count = Arc::new(AtomicUsize::new(0)); let frame_count_cl = frame_count.clone(); let ui_handler = handler.ui_handler.clone(); - let (video_sender, audio_sender) = start_video_audio_threads(move |data: &mut Vec | { + let (video_sender, audio_sender) = start_video_audio_threads(move |data: &mut Vec| { frame_count_cl.fetch_add(1, Ordering::Relaxed); ui_handler.on_rgba(data); }); From 491317bd6fe5cd931e61215a0c40e101a705054b Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Tue, 14 Feb 2023 13:57:33 +0100 Subject: [PATCH 130/199] modernized menu bar --- flutter/assets/actions.svg | 3 + flutter/assets/chat.svg | 3 +- flutter/assets/close.svg | 2 + flutter/assets/display.svg | 2 + flutter/assets/fullscreen.svg | 2 + flutter/assets/fullscreen_exit.svg | 2 + flutter/assets/keyboard.svg | 2 + flutter/assets/pinned.svg | 2 + flutter/assets/rec.svg | 2 + flutter/assets/unpinned.svg | 2 + .../lib/desktop/widgets/remote_menubar.dart | 227 +++++++++--------- 11 files changed, 138 insertions(+), 111 deletions(-) create mode 100644 flutter/assets/actions.svg create mode 100644 flutter/assets/close.svg create mode 100644 flutter/assets/display.svg create mode 100644 flutter/assets/fullscreen.svg create mode 100644 flutter/assets/fullscreen_exit.svg create mode 100644 flutter/assets/keyboard.svg create mode 100644 flutter/assets/pinned.svg create mode 100644 flutter/assets/rec.svg create mode 100644 flutter/assets/unpinned.svg diff --git a/flutter/assets/actions.svg b/flutter/assets/actions.svg new file mode 100644 index 000000000..feaf416cd --- /dev/null +++ b/flutter/assets/actions.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/flutter/assets/chat.svg b/flutter/assets/chat.svg index 03491be6e..830ef0d33 100644 --- a/flutter/assets/chat.svg +++ b/flutter/assets/chat.svg @@ -1 +1,2 @@ - \ No newline at end of file + + \ No newline at end of file diff --git a/flutter/assets/close.svg b/flutter/assets/close.svg new file mode 100644 index 000000000..1e9a30711 --- /dev/null +++ b/flutter/assets/close.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/display.svg b/flutter/assets/display.svg new file mode 100644 index 000000000..8a87116ff --- /dev/null +++ b/flutter/assets/display.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/fullscreen.svg b/flutter/assets/fullscreen.svg new file mode 100644 index 000000000..73d79cf0e --- /dev/null +++ b/flutter/assets/fullscreen.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/fullscreen_exit.svg b/flutter/assets/fullscreen_exit.svg new file mode 100644 index 000000000..f2b3ae27b --- /dev/null +++ b/flutter/assets/fullscreen_exit.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/keyboard.svg b/flutter/assets/keyboard.svg new file mode 100644 index 000000000..569c68727 --- /dev/null +++ b/flutter/assets/keyboard.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/pinned.svg b/flutter/assets/pinned.svg new file mode 100644 index 000000000..2563015f7 --- /dev/null +++ b/flutter/assets/pinned.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/rec.svg b/flutter/assets/rec.svg new file mode 100644 index 000000000..14546b971 --- /dev/null +++ b/flutter/assets/rec.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/unpinned.svg b/flutter/assets/unpinned.svg new file mode 100644 index 000000000..ba4ab5328 --- /dev/null +++ b/flutter/assets/unpinned.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 6bb49000b..77d687d93 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -405,9 +405,10 @@ class _RemoteMenubarState extends State { Widget _buildMenubar(BuildContext context) { final List menubarItems = []; + final double iconSize = Theme.of(context).iconTheme.size ?? 30.0; if (!isWebDesktop) { - menubarItems.add(_buildPinMenubar(context)); - menubarItems.add(_buildFullscreen(context)); + menubarItems.add(_buildPinMenubar(context, iconSize)); + menubarItems.add(_buildFullscreen(context, iconSize)); if (widget.ffi.ffiModel.isPeerAndroid) { menubarItems.add(IconButton( tooltip: translate('Mobile Actions'), @@ -420,77 +421,84 @@ class _RemoteMenubarState extends State { )); } } - menubarItems.add(_buildMonitor(context)); - menubarItems.add(_buildControl(context)); - menubarItems.add(_buildDisplay(context)); - menubarItems.add(_buildKeyboard(context)); + menubarItems.add(_buildMonitor(context, iconSize)); + menubarItems.add(_buildControl(context, iconSize)); + menubarItems.add(_buildDisplay(context, iconSize)); + menubarItems.add(_buildKeyboard(context, iconSize)); if (!isWeb) { - menubarItems.add(_buildChat(context)); - menubarItems.add(_buildVoiceCall(context)); + menubarItems.add(_buildChat(context, iconSize)); + menubarItems.add(_buildVoiceCall(context, iconSize)); } - menubarItems.add(_buildRecording(context)); - menubarItems.add(_buildClose(context)); + menubarItems.add(_buildRecording(context, iconSize)); + menubarItems.add(_buildClose(context, iconSize)); return PopupMenuTheme( - data: const PopupMenuThemeData( - textStyle: TextStyle(color: _MenubarTheme.commonColor)), - child: Column(mainAxisSize: MainAxisSize.min, children: [ + data: const PopupMenuThemeData( + textStyle: TextStyle(color: _MenubarTheme.commonColor)), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ Container( - decoration: BoxDecoration( - color: Colors.white, - border: Border.all(color: MyTheme.border), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical( + bottom: Radius.circular(10), ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: menubarItems, - )), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: menubarItems, + ), + ), _buildDraggableShowHide(context), - ])); + ], + ), + ); } - Widget _buildPinMenubar(BuildContext context) { - return Obx(() => IconButton( - tooltip: translate(pin ? 'Unpin menubar' : 'Pin menubar'), - onPressed: () { - widget.state.switchPin(); - }, - icon: Obx(() => Transform.rotate( - angle: pin ? math.pi / 4 : 0, - child: Icon( - Icons.push_pin, - color: pin ? _MenubarTheme.commonColor : Colors.grey, - ))), - )); + Widget _buildPinMenubar(BuildContext context, double iconSize) { + return Obx( + () => IconButton( + padding: EdgeInsets.zero, + iconSize: iconSize, + tooltip: translate(pin ? 'Unpin menubar' : 'Pin menubar'), + onPressed: () { + widget.state.switchPin(); + }, + icon: SvgPicture.asset( + pin ? "assets/pinned.svg" : "assets/unpinned.svg", + color: pin ? _MenubarTheme.commonColor : Colors.grey[800], + ), + ), + ); } - Widget _buildFullscreen(BuildContext context) { + Widget _buildFullscreen(BuildContext context, double iconSize) { return IconButton( + padding: EdgeInsets.zero, + iconSize: iconSize, tooltip: translate(isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'), onPressed: () { _setFullscreen(!isFullscreen); }, - icon: isFullscreen - ? const Icon( - Icons.fullscreen_exit, - color: _MenubarTheme.commonColor, - ) - : const Icon( - Icons.fullscreen, - color: _MenubarTheme.commonColor, - ), + icon: SvgPicture.asset( + isFullscreen ? "assets/fullscreen_exit.svg" : "assets/fullscreen.svg", + color: _MenubarTheme.commonColor, + ), ); } - Widget _buildMonitor(BuildContext context) { + Widget _buildMonitor(BuildContext context, double iconSize) { final pi = widget.ffi.ffiModel.pi; return mod_menu.PopupMenuButton( + iconSize: iconSize, tooltip: translate('Select Monitor'), padding: EdgeInsets.zero, position: mod_menu.PopupMenuPosition.under, icon: Stack( alignment: Alignment.center, children: [ - const Icon( - Icons.personal_video, + SvgPicture.asset( + "assets/display.svg", color: _MenubarTheme.commonColor, ), Padding( @@ -499,8 +507,7 @@ class _RemoteMenubarState extends State { RxInt display = CurrentDisplayState.find(widget.id); return Text( '${display.value + 1}/${pi.displays.length}', - style: const TextStyle( - color: _MenubarTheme.commonColor, fontSize: 8), + style: const TextStyle(color: Colors.white, fontSize: 8), ); }), ) @@ -513,23 +520,22 @@ class _RemoteMenubarState extends State { Stack( alignment: Alignment.center, children: [ - const Icon( - Icons.personal_video, - color: _MenubarTheme.commonColor, - ), + SvgPicture.asset("assets/display.svg"), TextButton( child: Container( - alignment: AlignmentDirectional.center, - constraints: - const BoxConstraints(minHeight: _MenubarTheme.height), - child: Padding( - padding: const EdgeInsets.only(bottom: 2.5), - child: Text( - (i + 1).toString(), - style: - const TextStyle(color: _MenubarTheme.commonColor), + alignment: AlignmentDirectional.center, + constraints: + const BoxConstraints(minHeight: _MenubarTheme.height), + child: Padding( + padding: const EdgeInsets.only(bottom: 2.5), + child: Text( + (i + 1).toString(), + style: TextStyle( + color: Theme.of(context).scaffoldBackgroundColor, ), - )), + ), + ), + ), onPressed: () { if (Navigator.canPop(context)) { Navigator.pop(context); @@ -561,11 +567,12 @@ class _RemoteMenubarState extends State { ); } - Widget _buildControl(BuildContext context) { + Widget _buildControl(BuildContext context, double iconSize) { return mod_menu.PopupMenuButton( + iconSize: iconSize, padding: EdgeInsets.zero, - icon: const Icon( - Icons.bolt, + icon: SvgPicture.asset( + "assets/actions.svg", color: _MenubarTheme.commonColor, ), tooltip: translate('Control Actions'), @@ -583,7 +590,7 @@ class _RemoteMenubarState extends State { ); } - Widget _buildDisplay(BuildContext context) { + Widget _buildDisplay(BuildContext context, double iconSize) { return FutureBuilder(future: () async { widget.state.viewStyle.value = await bind.sessionGetViewStyle(id: widget.id) ?? ''; @@ -595,9 +602,10 @@ class _RemoteMenubarState extends State { return Obx(() { final remoteCount = RemoteCountState.find().value; return mod_menu.PopupMenuButton( + iconSize: iconSize, padding: EdgeInsets.zero, - icon: const Icon( - Icons.tv, + icon: SvgPicture.asset( + "assets/display.svg", color: _MenubarTheme.commonColor, ), tooltip: translate('Display Settings'), @@ -622,15 +630,16 @@ class _RemoteMenubarState extends State { }); } - Widget _buildKeyboard(BuildContext context) { + Widget _buildKeyboard(BuildContext context, double iconSize) { FfiModel ffiModel = Provider.of(context); if (ffiModel.permissions['keyboard'] == false) { return Offstage(); } return mod_menu.PopupMenuButton( + iconSize: iconSize, padding: EdgeInsets.zero, - icon: const Icon( - Icons.keyboard, + icon: SvgPicture.asset( + "assets/keyboard.svg", color: _MenubarTheme.commonColor, ), tooltip: translate('Keyboard Settings'), @@ -648,57 +657,54 @@ class _RemoteMenubarState extends State { ); } - Widget _buildRecording(BuildContext context) { + Widget _buildRecording(BuildContext context, double iconSize) { return Consumer(builder: ((context, value, child) { if (value.permissions['recording'] != false) { return Consumer( - builder: (context, value, child) => IconButton( - tooltip: value.start - ? translate('Stop session recording') - : translate('Start session recording'), - onPressed: () => value.toggle(), - icon: value.start - ? Icon( - Icons.pause_circle_filled, - color: _MenubarTheme.commonColor, - ) - : SvgPicture.asset( - "assets/record_screen.svg", - color: _MenubarTheme.commonColor, - width: Theme.of(context).iconTheme.size ?? 22.0, - height: Theme.of(context).iconTheme.size ?? 22.0, - ), - )); + builder: (context, value, child) => IconButton( + padding: EdgeInsets.zero, + iconSize: iconSize, + tooltip: value.start + ? translate('Stop session recording') + : translate('Start session recording'), + onPressed: () => value.toggle(), + icon: SvgPicture.asset( + "assets/rec.svg", + color: value.start ? Colors.red : _MenubarTheme.commonColor, + ), + ), + ); } else { return Offstage(); } })); } - Widget _buildClose(BuildContext context) { + Widget _buildClose(BuildContext context, double iconSize) { return IconButton( + iconSize: iconSize, + padding: EdgeInsets.zero, tooltip: translate('Close'), onPressed: () { clientClose(widget.id, widget.ffi.dialogManager); }, - icon: const Icon( - Icons.close, - color: _MenubarTheme.commonColor, + icon: SvgPicture.asset( + "assets/close.svg", + color: Colors.red, ), ); } final _chatButtonKey = GlobalKey(); - Widget _buildChat(BuildContext context) { + Widget _buildChat(BuildContext context, double iconSize) { FfiModel ffiModel = Provider.of(context); return mod_menu.PopupMenuButton( + iconSize: iconSize, key: _chatButtonKey, padding: EdgeInsets.zero, icon: SvgPicture.asset( "assets/chat.svg", color: _MenubarTheme.commonColor, - width: Theme.of(context).iconTheme.size ?? 24.0, - height: Theme.of(context).iconTheme.size ?? 24.0, ), tooltip: translate('Chat'), position: mod_menu.PopupMenuPosition.under, @@ -719,15 +725,14 @@ class _RemoteMenubarState extends State { switch (widget.ffi.chatModel.voiceCallStatus.value) { case VoiceCallStatus.waitingForResponse: return IconButton( - onPressed: () { - widget.ffi.chatModel.closeVoiceCall(widget.id); - }, - icon: SvgPicture.asset( - "assets/voice_call_waiting.svg", - color: Colors.red, - width: Theme.of(context).iconTheme.size ?? 20.0, - height: Theme.of(context).iconTheme.size ?? 20.0, - )); + onPressed: () { + widget.ffi.chatModel.closeVoiceCall(widget.id); + }, + icon: SvgPicture.asset( + "assets/voice_call_waiting.svg", + color: Colors.red, + ), + ); case VoiceCallStatus.connected: return IconButton( onPressed: () { @@ -736,7 +741,6 @@ class _RemoteMenubarState extends State { icon: Icon( Icons.phone_disabled_rounded, color: Colors.red, - size: Theme.of(context).iconTheme.size ?? 22.0, ), ); default: @@ -755,13 +759,14 @@ class _RemoteMenubarState extends State { } } - Widget _buildVoiceCall(BuildContext context) { + Widget _buildVoiceCall(BuildContext context, double iconSize) { return Obx( () { final tooltipText = _getVoiceCallTooltip(); return tooltipText == null ? const Offstage() : IconButton( + iconSize: iconSize, padding: EdgeInsets.zero, icon: _getVoiceCallIcon(), tooltip: translate(tooltipText), @@ -1748,7 +1753,7 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { child: Icon( Icons.drag_indicator, size: 20, - color: Colors.grey, + color: Colors.grey[800], ), feedback: widget, onDragStarted: (() { @@ -1801,7 +1806,9 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { child: Container( decoration: BoxDecoration( color: Colors.white, - border: Border.all(color: MyTheme.border), + borderRadius: BorderRadius.vertical( + bottom: Radius.circular(5), + ), ), child: SizedBox( height: 20, From 50f751c21521fd63985a9123f05bb706c048ba37 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 10 Feb 2023 09:03:19 +0800 Subject: [PATCH 131/199] temp commit Signed-off-by: fufesou --- src/keyboard.rs | 10 ++++++---- src/server/input_service.rs | 3 +++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index 105b84400..9ca5a16f0 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -759,10 +759,12 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option) { match &event.unicode { Some(unicode_info) => { - for code in &unicode_info.unicode { - let mut evt = key_event.clone(); - evt.set_unicode(*code as _); - events.push(evt); + if let Some(name) = unicode_info.name { + if name.len() > 0 { + let mut evt = key_event.clone(); + evt.set_seq(name); + events.push(evt); + } } } None => {} diff --git a/src/server/input_service.rs b/src/server/input_service.rs index edf0ef497..2b19bbaff 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -1093,6 +1093,9 @@ fn translate_keyboard_mode(evt: &KeyEvent) { #[cfg(target_os = "windows")] allow_err!(rdev::simulate_unicode(_unicode as _)); } + Some(key_event::Union::Seq(seq)) => { + ENIGO.lock().unwrap().key_sequence(&seq); + } Some(key_event::Union::Chr(..)) => { #[cfg(target_os = "windows")] From e24f5e7eed10b321500fb6fdfe64d7e8bb766d87 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 13 Feb 2023 14:55:57 +0800 Subject: [PATCH 132/199] mid commit Signed-off-by: fufesou --- libs/hbb_common/protos/message.proto | 2 ++ src/keyboard.rs | 33 ++++------------------------ src/server/input_service.rs | 7 +++--- 3 files changed, 9 insertions(+), 33 deletions(-) diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index ed2706382..7e3d0b0a4 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -201,6 +201,8 @@ message KeyEvent { bool press = 2; oneof union { ControlKey control_key = 3; + // high word, sym key code. win: virtual-key code, linux: keysym ?, macos: + // low word, position key code. win: scancode, linux: key code, macos: key code uint32 chr = 4; uint32 unicode = 5; string seq = 6; diff --git a/src/keyboard.rs b/src/keyboard.rs index 9ca5a16f0..02f34132f 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -785,34 +785,9 @@ fn is_hot_key_modifiers_down() -> bool { return false; } -pub fn translate_virtual_keycode(event: &Event, mut key_event: KeyEvent) -> Option { - match event.event_type { - EventType::KeyPress(..) => { - key_event.down = true; - } - EventType::KeyRelease(..) => { - key_event.down = false; - } - _ => return None, - }; - - let mut peer = get_peer_platform().to_lowercase(); - peer.retain(|c| !c.is_whitespace()); - - // #[cfg(target_os = "windows")] - // let keycode = match peer.as_str() { - // "windows" => event.code, - // "macos" => { - // if hbb_common::config::LocalConfig::get_kb_layout_type() == "ISO" { - // rdev::win_scancode_to_macos_iso_code(event.scan_code)? - // } else { - // rdev::win_scancode_to_macos_code(event.scan_code)? - // } - // } - // _ => rdev::win_scancode_to_linux_code(event.scan_code)?, - // }; - - key_event.set_chr(event.code as _); +pub fn translate_vk_scan_code(event: &Event, mut key_event: KeyEvent) -> Option { + let mut key_event = map_keyboard_mode(event, key_event)?; + key_event.set_chr((key_event.chr() & 0x0000FFFF) | ((event.code as u32) << 16)); Some(key_event) } @@ -853,7 +828,7 @@ pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec { - #[cfg(target_os = "windows")] - allow_err!(rdev::simulate_unicode(_unicode as _)); - } Some(key_event::Union::Seq(seq)) => { ENIGO.lock().unwrap().key_sequence(&seq); } @@ -1101,6 +1097,9 @@ fn translate_keyboard_mode(evt: &KeyEvent) { #[cfg(target_os = "windows")] translate_process_virtual_keycode(evt.chr(), evt.down) } + Some(key_event::Union::Unicode(..)) => { + // Do not handle unicode for now. + } _ => { log::debug!("Unreachable. Unexpected key event {:?}", &evt); } From 50ce57024c74ed9faab2099ed5c544be4e51e3a4 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 13 Feb 2023 16:26:14 +0800 Subject: [PATCH 133/199] macos, win, translate mode, Signed-off-by: fufesou --- src/keyboard.rs | 1 + src/server/input_service.rs | 13 ++++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index 02f34132f..8aa5f72d2 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -787,6 +787,7 @@ fn is_hot_key_modifiers_down() -> bool { pub fn translate_vk_scan_code(event: &Event, mut key_event: KeyEvent) -> Option { let mut key_event = map_keyboard_mode(event, key_event)?; + #[cfg(target_os = "windows")] key_event.set_chr((key_event.chr() & 0x0000FFFF) | ((event.code as u32) << 16)); Some(key_event) } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 0f40cb7d6..18ff433a3 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -1082,9 +1082,14 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { } #[cfg(target_os = "windows")] -fn translate_process_virtual_keycode(vk: u32, down: bool) { +fn translate_process_code(code: u32, down: bool) { crate::platform::windows::try_change_desktop(); - sim_rdev_rawkey_virtual(vk, down); + let vk_code = + + match code >> 16 { + 0 => sim_rdev_rawkey_position(code, down), + vk_code => sim_rdev_rawkey_virtual(vk_code, down), + }; } fn translate_keyboard_mode(evt: &KeyEvent) { @@ -1095,7 +1100,9 @@ fn translate_keyboard_mode(evt: &KeyEvent) { Some(key_event::Union::Chr(..)) => { #[cfg(target_os = "windows")] - translate_process_virtual_keycode(evt.chr(), evt.down) + translate_process_code(evt.chr(), evt.down); + #[cfg(not(target_os = "windows"))] + sim_rdev_rawkey_position(code, down); } Some(key_event::Union::Unicode(..)) => { // Do not handle unicode for now. From 7dfcc401e5d59b53e6243211639ef990cc4a2384 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 14 Feb 2023 15:42:02 +0800 Subject: [PATCH 134/199] translate mode, mac --> win, init debug Signed-off-by: fufesou --- Cargo.lock | 3 +- .../lib/desktop/widgets/remote_menubar.dart | 4 +- src/flutter_ffi.rs | 1 - src/keyboard.rs | 95 ++++++++++++++----- src/server/input_service.rs | 6 +- 5 files changed, 77 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f0f66e287..2fcdef290 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4554,12 +4554,13 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/fufesou/rdev#cedc4e62744566775026af4b434ef799804c1130" +source = "git+https://github.com/fufesou/rdev#593f0ba37139ed6f4f88a4120e972612ec4b1c6f" dependencies = [ "cocoa", "core-foundation 0.9.3", "core-foundation-sys 0.8.3", "core-graphics 0.22.3", + "dispatch", "enum-map", "epoll", "inotify", diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 6bb49000b..9f8265fec 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1510,8 +1510,8 @@ class _RemoteMenubarState extends State { if (bind.sessionIsKeyboardModeSupported( id: widget.id, mode: mode.key)) { if (mode.key == 'translate') { - if (!Platform.isWindows || - widget.ffi.ffiModel.pi.platform != kPeerPlatformWindows) { + if (Platform.isLinux || + widget.ffi.ffiModel.pi.platform == kPeerPlatformLinux) { continue; } } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index b4e79b361..0e307abe3 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -20,7 +20,6 @@ use std::{ os::raw::c_char, str::FromStr, }; -use crate::ui_session_interface::InvokeUiSession; // use crate::hbbs_http::account::AuthResult; diff --git a/src/keyboard.rs b/src/keyboard.rs index 8aa5f72d2..7e4ba2b39 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -18,6 +18,13 @@ use std::{ #[cfg(windows)] static mut IS_ALT_GR: bool = false; +#[allow(dead_code)] +const OS_LOWER_WINDOWS: &str = "windows"; +#[allow(dead_code)] +const OS_LOWER_LINUX: &str = "linux"; +#[allow(dead_code)] +const OS_LOWER_MACOS: &str = "macos"; + #[cfg(any(target_os = "windows", target_os = "macos"))] static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); @@ -202,6 +209,9 @@ pub fn update_grab_get_key_name() { #[cfg(target_os = "windows")] static mut IS_0X021D_DOWN: bool = false; +#[cfg(target_os = "macos")] +static mut IS_LEFT_OPTION_DOWN: bool = false; + pub fn start_grab_loop() { #[cfg(any(target_os = "windows", target_os = "macos"))] std::thread::spawn(move || { @@ -213,6 +223,7 @@ pub fn start_grab_loop() { let mut _keyboard_mode = KeyboardMode::Map; let _scan_code = event.scan_code; + let _code = event.code; let res = if KEYBOARD_HOOKED.load(Ordering::SeqCst) { _keyboard_mode = client::process_event(&event, None); if is_press { @@ -246,6 +257,13 @@ pub fn start_grab_loop() { } } + #[cfg(target_os = "macos")] + unsafe { + if _code as u32 == rdev::kVK_Option { + IS_LEFT_OPTION_DOWN = is_press; + } + } + return res; }; let func = move |event: Event| match event.event_type { @@ -253,11 +271,13 @@ pub fn start_grab_loop() { EventType::KeyRelease(key) => try_handle_keyboard(event, key, false), _ => Some(event), }; + #[cfg(target_os = "macos")] + rdev::set_is_main_thread(false); + #[cfg(target_os = "windows")] + rdev::set_event_popup(false); if let Err(error) = rdev::grab(func) { log::error!("rdev Error: {:?}", error) } - #[cfg(target_os = "windows")] - rdev::set_event_popup(false); }); #[cfg(target_os = "linux")] @@ -395,13 +415,16 @@ pub fn event_to_key_events( _ => {} } + let mut peer = get_peer_platform().to_lowercase(); + peer.retain(|c| !c.is_whitespace()); + key_event.mode = keyboard_mode.into(); let mut key_events = match keyboard_mode { - KeyboardMode::Map => match map_keyboard_mode(event, key_event) { + KeyboardMode::Map => match map_keyboard_mode(peer.as_str(), event, key_event) { Some(event) => [event].to_vec(), None => Vec::new(), }, - KeyboardMode::Translate => translate_keyboard_mode(event, key_event), + KeyboardMode::Translate => translate_keyboard_mode(peer.as_str(), event, key_event), _ => { #[cfg(not(any(target_os = "android", target_os = "ios")))] { @@ -424,7 +447,6 @@ pub fn event_to_key_events( } } } - key_events } @@ -698,7 +720,7 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Vec Option { +pub fn map_keyboard_mode(peer: &str, event: &Event, mut key_event: KeyEvent) -> Option { match event.event_type { EventType::KeyPress(..) => { key_event.down = true; @@ -709,12 +731,9 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option return None, }; - let mut peer = get_peer_platform().to_lowercase(); - peer.retain(|c| !c.is_whitespace()); - #[cfg(target_os = "windows")] - let keycode = match peer.as_str() { - "windows" => { + let keycode = match peer { + OS_LOWER_WINDOWS => { // https://github.com/rustdesk/rustdesk/issues/1371 // Filter scancodes that are greater than 255 and the hight word is not 0xE0. if event.scan_code > 255 && (event.scan_code >> 8) != 0xE0 { @@ -722,7 +741,7 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option { + OS_LOWER_MACOS => { if hbb_common::config::LocalConfig::get_kb_layout_type() == "ISO" { rdev::win_scancode_to_macos_iso_code(event.scan_code)? } else { @@ -732,15 +751,15 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option rdev::win_scancode_to_linux_code(event.scan_code)?, }; #[cfg(target_os = "macos")] - let keycode = match peer.as_str() { - "windows" => rdev::macos_code_to_win_scancode(event.code as _)?, - "macos" => event.code as _, + let keycode = match peer { + OS_LOWER_WINDOWS => rdev::macos_code_to_win_scancode(event.code as _)?, + OS_LOWER_MACOS => event.code as _, _ => rdev::macos_code_to_linux_code(event.code as _)?, }; #[cfg(target_os = "linux")] - let keycode = match peer.as_str() { - "windows" => rdev::linux_code_to_win_scancode(event.code as _)?, - "macos" => { + let keycode = match peer { + OS_LOWER_WINDOWS => rdev::linux_code_to_win_scancode(event.code as _)?, + OS_LOWER_MACOS => { if hbb_common::config::LocalConfig::get_kb_layout_type() == "ISO" { rdev::linux_code_to_macos_iso_code(event.code as _)? } else { @@ -759,10 +778,10 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option) { match &event.unicode { Some(unicode_info) => { - if let Some(name) = unicode_info.name { + if let Some(name) = &unicode_info.name { if name.len() > 0 { let mut evt = key_event.clone(); - evt.set_seq(name); + evt.set_seq(name.to_string()); events.push(evt); } } @@ -785,21 +804,42 @@ fn is_hot_key_modifiers_down() -> bool { return false; } -pub fn translate_vk_scan_code(event: &Event, mut key_event: KeyEvent) -> Option { +#[inline] +#[cfg(target_os = "windows")] +pub fn translate_key_code(event: &Event, mut key_event: KeyEvent) -> Option { let mut key_event = map_keyboard_mode(event, key_event)?; - #[cfg(target_os = "windows")] key_event.set_chr((key_event.chr() & 0x0000FFFF) | ((event.code as u32) << 16)); Some(key_event) } -pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec { +#[inline] +#[cfg(not(target_os = "windows"))] +pub fn translate_key_code(peer: &str, event: &Event, key_event: KeyEvent) -> Option { + map_keyboard_mode(peer, event, key_event) +} + +pub fn translate_keyboard_mode(peer: &str, event: &Event, key_event: KeyEvent) -> Vec { let mut events: Vec = Vec::new(); if let Some(unicode_info) = &event.unicode { if unicode_info.is_dead { + #[cfg(target_os = "macos")] + if peer != OS_LOWER_MACOS && unsafe { IS_LEFT_OPTION_DOWN } { + // try clear dead key state + // rdev::clear_dead_key_state(); + } else { + return events; + } + #[cfg(not(target_os = "macos"))] return events; } } + #[cfg(target_os = "macos")] + // ignore right option key + if event.code as u32 == rdev::kVK_RightOption { + return events; + } + #[cfg(target_os = "windows")] unsafe { if event.scan_code == 0x021D { @@ -825,11 +865,16 @@ pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec { - ENIGO.lock().unwrap().key_sequence(&seq); + ENIGO.lock().unwrap().key_sequence(seq); } Some(key_event::Union::Chr(..)) => { #[cfg(target_os = "windows")] translate_process_code(evt.chr(), evt.down); #[cfg(not(target_os = "windows"))] - sim_rdev_rawkey_position(code, down); + sim_rdev_rawkey_position(evt.chr(), evt.down); } Some(key_event::Union::Unicode(..)) => { // Do not handle unicode for now. From b2d13647be0a84be2047194f7786346bbbd049f2 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 14 Feb 2023 15:58:36 +0800 Subject: [PATCH 135/199] translate mode, mac --> win, debug 2 Signed-off-by: fufesou --- libs/enigo/src/win/win_impl.rs | 42 ++++++++++++++++------------------ src/keyboard.rs | 4 ++-- src/server/input_service.rs | 2 -- 3 files changed, 22 insertions(+), 26 deletions(-) diff --git a/libs/enigo/src/win/win_impl.rs b/libs/enigo/src/win/win_impl.rs index 2e1108b9e..115cb9789 100644 --- a/libs/enigo/src/win/win_impl.rs +++ b/libs/enigo/src/win/win_impl.rs @@ -39,7 +39,7 @@ fn mouse_event(flags: u32, data: u32, dx: i32, dy: i32) -> DWORD { unsafe { SendInput(1, &mut input as LPINPUT, size_of::() as c_int) } } -fn keybd_event(flags: u32, vk: u16, scan: u16) -> DWORD { +fn keybd_event(mut flags: u32, vk: u16, scan: u16) -> DWORD { let mut scan = scan; unsafe { // https://github.com/rustdesk/rustdesk/issues/366 @@ -52,35 +52,33 @@ fn keybd_event(flags: u32, vk: u16, scan: u16) -> DWORD { scan = MapVirtualKeyExW(vk as _, 0, LAYOUT) as _; } } - let mut input: INPUT = unsafe { std::mem::MaybeUninit::zeroed().assume_init() }; - input.type_ = INPUT_KEYBOARD; + + if flags & KEYEVENTF_UNICODE == 0 { + if scan >> 8 == 0xE0 || scan >> 8 == 0xE1 { + flags |= winapi::um::winuser::KEYEVENTF_EXTENDEDKEY; + } + } + let mut union: INPUT_u = unsafe { std::mem::zeroed() }; unsafe { - let dst_ptr = (&mut input.u as *mut _) as *mut u8; - let flags = match vk as _ { - winapi::um::winuser::VK_HOME | - winapi::um::winuser::VK_UP | - winapi::um::winuser::VK_PRIOR | - winapi::um::winuser::VK_LEFT | - winapi::um::winuser::VK_RIGHT | - winapi::um::winuser::VK_END | - winapi::um::winuser::VK_DOWN | - winapi::um::winuser::VK_NEXT | - winapi::um::winuser::VK_INSERT | - winapi::um::winuser::VK_DELETE => flags | winapi::um::winuser::KEYEVENTF_EXTENDEDKEY, - _ => flags, - }; - - let k = KEYBDINPUT { + *union.ki_mut() = KEYBDINPUT { wVk: vk, wScan: scan, dwFlags: flags, time: 0, dwExtraInfo: ENIGO_INPUT_EXTRA_VALUE, }; - let src_ptr = (&k as *const _) as *const u8; - std::ptr::copy_nonoverlapping(src_ptr, dst_ptr, size_of::()); } - unsafe { SendInput(1, &mut input as LPINPUT, size_of::() as c_int) } + let mut inputs = [INPUT { + type_: INPUT_KEYBOARD, + u: union, + }; 1]; + unsafe { + SendInput( + inputs.len() as UINT, + inputs.as_mut_ptr(), + size_of::() as c_int, + ) + } } fn get_error() -> String { diff --git a/src/keyboard.rs b/src/keyboard.rs index 7e4ba2b39..4dcbe5c97 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -806,8 +806,8 @@ fn is_hot_key_modifiers_down() -> bool { #[inline] #[cfg(target_os = "windows")] -pub fn translate_key_code(event: &Event, mut key_event: KeyEvent) -> Option { - let mut key_event = map_keyboard_mode(event, key_event)?; +pub fn translate_key_code(peer: &str, event: &Event, key_event: KeyEvent) -> Option { + let mut key_event = map_keyboard_mode(peer, event, key_event)?; key_event.set_chr((key_event.chr() & 0x0000FFFF) | ((event.code as u32) << 16)); Some(key_event) } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 59f503a14..67267bd94 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -1084,8 +1084,6 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { #[cfg(target_os = "windows")] fn translate_process_code(code: u32, down: bool) { crate::platform::windows::try_change_desktop(); - let vk_code = - match code >> 16 { 0 => sim_rdev_rawkey_position(code, down), vk_code => sim_rdev_rawkey_virtual(vk_code, down), From a20f6b7d5e442f305d4058818d80243093c9a2eb Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 14 Feb 2023 17:11:27 +0800 Subject: [PATCH 136/199] translate mode, fix win dead key Signed-off-by: fufesou --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 2fcdef290..b308de149 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4554,7 +4554,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/fufesou/rdev#593f0ba37139ed6f4f88a4120e972612ec4b1c6f" +source = "git+https://github.com/fufesou/rdev#5b9fb5e42117f44e0ce0fe7cf2bddf270c75f1dc" dependencies = [ "cocoa", "core-foundation 0.9.3", From e24f72040e5577d6ed44c73f4b45635b712a19f7 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 14 Feb 2023 22:09:25 +0800 Subject: [PATCH 137/199] translate mode, trivial changes Signed-off-by: fufesou --- flutter/lib/desktop/widgets/remote_menubar.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 9f8265fec..1a1a558fa 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1515,8 +1515,11 @@ class _RemoteMenubarState extends State { continue; } } - list.add(MenuEntryRadioOption( - text: translate(mode.menu), value: mode.key)); + var text = translate(mode.menu); + if (mode.key == 'translate') { + text = '$text beta legacy 2'; + } + list.add(MenuEntryRadioOption(text: text, value: mode.key)); } } return list; From 16dd1f3c797c7a6015d9cfadef4ea33e1f8d6d67 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 14 Feb 2023 22:20:12 +0800 Subject: [PATCH 138/199] translate mode, trivial changes Signed-off-by: fufesou --- flutter/lib/desktop/widgets/remote_menubar.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 1a1a558fa..66a13f606 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1517,7 +1517,7 @@ class _RemoteMenubarState extends State { } var text = translate(mode.menu); if (mode.key == 'translate') { - text = '$text beta legacy 2'; + text = '$text beta'; } list.add(MenuEntryRadioOption(text: text, value: mode.key)); } From 20be9e10b11e02b6e463ce3390abe0ab2670cfc7 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 14 Feb 2023 11:50:04 +0800 Subject: [PATCH 139/199] opt: scrollable on menubar, avoid overflow --- flutter/lib/desktop/widgets/remote_menubar.dart | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 66a13f606..c68b394e4 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -439,9 +439,12 @@ class _RemoteMenubarState extends State { color: Colors.white, border: Border.all(color: MyTheme.border), ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: menubarItems, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisSize: MainAxisSize.min, + children: menubarItems, + ), )), _buildDraggableShowHide(context), ])); From 8df357c9411faa4a23908a3aeb6fd72414634f60 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Wed, 15 Feb 2023 15:03:19 +0800 Subject: [PATCH 140/199] refactor: use listview for file lists --- flutter/lib/consts.dart | 12 + .../lib/desktop/pages/file_manager_page.dart | 298 ++++++++++-------- flutter/lib/models/file_model.dart | 4 + 3 files changed, 186 insertions(+), 128 deletions(-) diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 26e25a209..2b4bc7f32 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -50,6 +50,18 @@ const int kMobileMaxDisplayHeight = 1280; const int kDesktopMaxDisplayWidth = 1920; const int kDesktopMaxDisplayHeight = 1080; +const double kDesktopFileTransferNameColWidth = 200; +const double kDesktopFileTransferModifiedColWidth = 120; +const double kDesktopFileTransferRowHeight = 25.0; +const double kDesktopFileTransferHeaderHeight = 25.0; + +// https://en.wikipedia.org/wiki/Non-breaking_space +const int $nbsp = 0x00A0; + +extension StringExtension on String { + String get nonBreaking => replaceAll(' ', String.fromCharCode($nbsp)); +} + const Size kConnectionManagerWindowSize = Size(300, 400); // Tabbar transition duration, now we remove the duration const Duration kTabTransitionDuration = Duration.zero; diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 27bb0377d..fef0dd3d3 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -236,10 +236,7 @@ class _FileManagerPageState extends State crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( - child: SingleChildScrollView( - controller: scrollController, - child: _buildDataTable(context, isLocal, scrollController), - ), + child: _buildFileList(context, isLocal, scrollController), ) ], )), @@ -248,25 +245,11 @@ class _FileManagerPageState extends State ); } - Widget _buildDataTable( + Widget _buildFileList( BuildContext context, bool isLocal, ScrollController scrollController) { - const rowHeight = 25.0; final fd = model.getCurrentDir(isLocal); final entries = fd.entries; - final sortIndex = (SortBy style) { - switch (style) { - case SortBy.name: - return 0; - case SortBy.type: - return 0; - case SortBy.modified: - return 1; - case SortBy.size: - return 2; - } - }(model.getSortStyle(isLocal)); - final sortAscending = - isLocal ? model.localSortAscending : model.remoteSortAscending; + final selectedEntries = getSelectedItems(isLocal); return MouseRegion( onEnter: (evt) { @@ -287,7 +270,6 @@ class _FileManagerPageState extends State onNext: (buffer) { debugPrint("searching next for $buffer"); assert(buffer.length == 1); - final selectedEntries = getSelectedItems(isLocal); assert(selectedEntries.length <= 1); var skipCount = 0; if (selectedEntries.items.isNotEmpty) { @@ -312,7 +294,8 @@ class _FileManagerPageState extends State return; } _jumpToEntry( - isLocal, searchResult.first, scrollController, rowHeight, buffer); + isLocal, searchResult.first, scrollController, + kDesktopFileTransferRowHeight, buffer); }, onSearch: (buffer) { debugPrint("searching for $buffer"); @@ -327,7 +310,8 @@ class _FileManagerPageState extends State return; } _jumpToEntry( - isLocal, searchResult.first, scrollController, rowHeight, buffer); + isLocal, searchResult.first, scrollController, + kDesktopFileTransferRowHeight, buffer); }, child: ObxValue( (searchText) { @@ -336,118 +320,120 @@ class _FileManagerPageState extends State return element.name.contains(searchText.value); }).toList(growable: false) : entries; - return DataTable( - key: ValueKey(isLocal ? 0 : 1), - showCheckboxColumn: false, - dataRowHeight: rowHeight, - headingRowHeight: 30, - horizontalMargin: 8, - columnSpacing: 8, - showBottomBorder: true, - sortColumnIndex: sortIndex, - sortAscending: sortAscending, - columns: [ - DataColumn( - label: Text( - translate("Name"), - ).marginSymmetric(horizontal: 4), - onSort: (columnIndex, ascending) { - model.changeSortStyle(SortBy.name, - isLocal: isLocal, ascending: ascending); - }), - DataColumn( - label: Text( - translate("Modified"), - ), - onSort: (columnIndex, ascending) { - model.changeSortStyle(SortBy.modified, - isLocal: isLocal, ascending: ascending); - }), - DataColumn( - label: Text(translate("Size")), - onSort: (columnIndex, ascending) { - model.changeSortStyle(SortBy.size, - isLocal: isLocal, ascending: ascending); - }), - ], - rows: filteredEntries.map((entry) { + final rows = filteredEntries.map((entry) { final sizeStr = entry.isFile ? readableFileSize(entry.size.toDouble()) : ""; final lastModifiedStr = entry.isDrive ? " " : "${entry.lastModified().toString().replaceAll(".000", "")} "; - return DataRow( - key: ValueKey(entry.name), - onSelectChanged: (s) { - _onSelectedChanged(getSelectedItems(isLocal), - filteredEntries, entry, isLocal); - }, - selected: getSelectedItems(isLocal).contains(entry), - cells: [ - DataCell( - Container( - width: 200, - child: Tooltip( - waitDuration: Duration(milliseconds: 500), - message: entry.name, - child: Row(children: [ - entry.isDrive - ? Image( - image: iconHardDrive, - fit: BoxFit.scaleDown, - color: Theme.of(context) - .iconTheme - .color - ?.withOpacity(0.7)) - .paddingAll(4) - : Icon( - entry.isFile - ? Icons.feed_outlined - : Icons.folder, - size: 20, - color: Theme.of(context) - .iconTheme - .color - ?.withOpacity(0.7), - ).marginSymmetric(horizontal: 2), - Expanded( - child: Text(entry.name, - overflow: TextOverflow.ellipsis)) - ]), + final isSelected = selectedEntries.contains(entry); + return SizedBox( + key: ValueKey(entry.name), + height: kDesktopFileTransferRowHeight, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + const Divider( + height: 1, + ), + Expanded( + child: Ink( + decoration: isSelected + ? BoxDecoration(color: Theme.of(context).hoverColor) + : null, + child: InkWell( + child: Row(children: [ + GestureDetector( + child: Container( + width: kDesktopFileTransferNameColWidth, + child: Tooltip( + waitDuration: Duration(milliseconds: 500), + message: entry.name, + child: Row(children: [ + entry.isDrive + ? Image( + image: iconHardDrive, + fit: BoxFit.scaleDown, + color: Theme.of(context) + .iconTheme + .color + ?.withOpacity(0.7)) + .paddingAll(4) + : Icon( + entry.isFile + ? Icons.feed_outlined + : Icons.folder, + size: 20, + color: Theme.of(context) + .iconTheme + .color + ?.withOpacity(0.7), + ).marginSymmetric(horizontal: 2), + Expanded( + child: Text(entry.name.nonBreaking, + overflow: TextOverflow.ellipsis)) + ]), + )), + onTap: () { + final items = getSelectedItems(isLocal); + // handle double click + if (_checkDoubleClick(entry)) { + openDirectory(entry.path, isLocal: isLocal); + items.clear(); + return; + } + _onSelectedChanged( + items, filteredEntries, entry, isLocal); + }, + ), + GestureDetector( + child: SizedBox( + width: kDesktopFileTransferModifiedColWidth, + child: Tooltip( + waitDuration: Duration(milliseconds: 500), + message: lastModifiedStr, + child: Text( + lastModifiedStr, + style: TextStyle( + fontSize: 12, color: MyTheme.darkGray), + )), )), - onTap: () { - final items = getSelectedItems(isLocal); - - // handle double click - if (_checkDoubleClick(entry)) { - openDirectory(entry.path, isLocal: isLocal); - items.clear(); - return; - } - _onSelectedChanged( - items, filteredEntries, entry, isLocal); - }, + GestureDetector( + child: Tooltip( + waitDuration: Duration(milliseconds: 500), + message: sizeStr, + child: Text( + sizeStr, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 10, + color: MyTheme.darkGray), + ))), + ]), + ), ), - DataCell(FittedBox( - child: Tooltip( - waitDuration: Duration(milliseconds: 500), - message: lastModifiedStr, - child: Text( - lastModifiedStr, - style: TextStyle( - fontSize: 12, color: MyTheme.darkGray), - )))), - DataCell(Tooltip( - waitDuration: Duration(milliseconds: 500), - message: sizeStr, - child: Text( - sizeStr, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 10, color: MyTheme.darkGray), - ))), - ]); - }).toList(growable: false), + ), + ], + ), + ); + }).toList(growable: false); + + return Column( + children: [ + // Header + _buildFileBrowserHeader(context, isLocal), + // Body + Expanded( + child: ListView.builder( + controller: scrollController, + itemExtent: kDesktopFileTransferRowHeight, + itemBuilder: (context, index) { + return rows[index]; + }, + itemCount: rows.length, + ), + ), + ], ); }, isLocal ? _searchTextLocal : _searchTextRemote, @@ -1133,4 +1119,60 @@ class _FileManagerPageState extends State } }); } + + Widget headerItemFunc( + double? width, SortBy sortBy, String name, bool isLocal) { + final headerTextStyle = + Theme.of(context).dataTableTheme.headingTextStyle ?? TextStyle(); + return ObxValue>( + (ascending) => InkWell( + onTap: () { + if (ascending.value == null) { + ascending.value = true; + } else { + ascending.value = !ascending.value!; + } + model.changeSortStyle(sortBy, + isLocal: isLocal, ascending: ascending.value!); + }, + child: SizedBox( + width: width, + height: kDesktopFileTransferHeaderHeight, + child: Row( + children: [ + Text( + name, + style: headerTextStyle, + ).marginSymmetric( + horizontal: sortBy == SortBy.name ? 4 : 0.0), + ascending.value != null + ? Icon(ascending.value! + ? Icons.arrow_upward + : Icons.arrow_downward) + : const Offstage() + ], + ), + ), + ), () { + if (model.getSortStyle(isLocal) == sortBy) { + return model.getSortAscending(isLocal).obs; + } else { + return Rx(null); + } + }()); + } + + Widget _buildFileBrowserHeader(BuildContext context, bool isLocal) { + return Row( + children: [ + headerItemFunc(kDesktopFileTransferNameColWidth, SortBy.name, + translate("Name"), isLocal), + headerItemFunc(kDesktopFileTransferModifiedColWidth, SortBy.modified, + translate("Modified"), isLocal), + Expanded( + child: + headerItemFunc(null, SortBy.size, translate("Size"), isLocal)) + ], + ); + } } diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 18d42d143..5817e54fe 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -75,6 +75,10 @@ class FileModel extends ChangeNotifier { return isLocal ? _localSortStyle : _remoteSortStyle; } + bool getSortAscending(bool isLocal) { + return isLocal ? _localSortAscending : _remoteSortAscending; + } + FileDirectory _currentLocalDir = FileDirectory(); FileDirectory get currentLocalDir => _currentLocalDir; From 66378f63d9bb329bfa659380fc6b6de17f17b37d Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 15 Feb 2023 15:25:28 +0800 Subject: [PATCH 141/199] fix macos command-tab Signed-off-by: fufesou --- src/server/input_service.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 67267bd94..917a815bb 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -719,7 +719,7 @@ fn reset_input() { let _lock = VIRTUAL_INPUT_MTX.lock(); VIRTUAL_INPUT = VirtualInput::new( CGEventSourceStateID::Private, - CGEventTapLocation::AnnotatedSession, + CGEventTapLocation::Session, ) .ok(); } From 2047fd822b97659f291d02c6f573503a03eb2b8e Mon Sep 17 00:00:00 2001 From: Kingtous Date: Wed, 15 Feb 2023 16:44:40 +0800 Subject: [PATCH 142/199] opt: early unlock frame --- flutter/lib/models/model.dart | 10 ++++++---- flutter/lib/utils/image.dart | 2 ++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 8cf90eba9..a1d9ff0df 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -438,15 +438,17 @@ class ImageModel with ChangeNotifier { } final pid = parent.target?.id; - ui.decodeImageFromPixels( + img.decodeImageFromPixels( rgba, parent.target?.ffiModel.display.width ?? 0, parent.target?.ffiModel.display.height ?? 0, - isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888, (image) { + isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888, + onPixelsCopied: () { + // Unlock the rgba memory from rust codes. + platformFFI.nextRgba(id); + }).then((image) { if (parent.target?.id != pid) return; try { - // Unlock the rgba memory from rust codes. - platformFFI.nextRgba(id); // my throw exception, because the listener maybe already dispose update(image); } catch (e) { diff --git a/flutter/lib/utils/image.dart b/flutter/lib/utils/image.dart index 7a6bcbc15..a153dbc63 100644 --- a/flutter/lib/utils/image.dart +++ b/flutter/lib/utils/image.dart @@ -11,6 +11,7 @@ Future decodeImageFromPixels( int? rowBytes, int? targetWidth, int? targetHeight, + VoidCallback? onPixelsCopied, bool allowUpscaling = true, }) async { if (targetWidth != null) { @@ -22,6 +23,7 @@ Future decodeImageFromPixels( final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(pixels); + onPixelsCopied?.call(); final ui.ImageDescriptor descriptor = ui.ImageDescriptor.raw( buffer, width: width, From c5d39b0c105cf95f987be60bd6d573a7ba89aa03 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Wed, 15 Feb 2023 11:40:17 +0100 Subject: [PATCH 143/199] reworked --- flutter/assets/actions.svg | 3 +- flutter/assets/chat.svg | 2 +- flutter/assets/close.svg | 2 +- flutter/assets/display.svg | 2 +- flutter/assets/fullscreen.svg | 2 +- flutter/assets/fullscreen_exit.svg | 2 +- flutter/assets/keyboard.svg | 2 +- flutter/assets/pinned.svg | 2 +- flutter/assets/rec.svg | 2 +- flutter/assets/unpinned.svg | 2 +- flutter/lib/desktop/pages/remote_page.dart | 2 +- .../lib/desktop/pages/remote_tab_page.dart | 9 ++- .../widgets/material_mod_popup_menu.dart | 9 +-- flutter/lib/desktop/widgets/menu_button.dart | 63 +++++++++++++++++ .../lib/desktop/widgets/remote_menubar.dart | 67 +++++++++++-------- 15 files changed, 125 insertions(+), 46 deletions(-) create mode 100644 flutter/lib/desktop/widgets/menu_button.dart diff --git a/flutter/assets/actions.svg b/flutter/assets/actions.svg index feaf416cd..5403853db 100644 --- a/flutter/assets/actions.svg +++ b/flutter/assets/actions.svg @@ -1,3 +1,2 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/chat.svg b/flutter/assets/chat.svg index 830ef0d33..7088107b0 100644 --- a/flutter/assets/chat.svg +++ b/flutter/assets/chat.svg @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/close.svg b/flutter/assets/close.svg index 1e9a30711..7488acc9f 100644 --- a/flutter/assets/close.svg +++ b/flutter/assets/close.svg @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/display.svg b/flutter/assets/display.svg index 8a87116ff..b5a88106e 100644 --- a/flutter/assets/display.svg +++ b/flutter/assets/display.svg @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/fullscreen.svg b/flutter/assets/fullscreen.svg index 73d79cf0e..cd01f93f9 100644 --- a/flutter/assets/fullscreen.svg +++ b/flutter/assets/fullscreen.svg @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/fullscreen_exit.svg b/flutter/assets/fullscreen_exit.svg index f2b3ae27b..8d4414897 100644 --- a/flutter/assets/fullscreen_exit.svg +++ b/flutter/assets/fullscreen_exit.svg @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/keyboard.svg b/flutter/assets/keyboard.svg index 569c68727..d5481d7a1 100644 --- a/flutter/assets/keyboard.svg +++ b/flutter/assets/keyboard.svg @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/pinned.svg b/flutter/assets/pinned.svg index 2563015f7..dd718b96a 100644 --- a/flutter/assets/pinned.svg +++ b/flutter/assets/pinned.svg @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/rec.svg b/flutter/assets/rec.svg index 14546b971..33a57e9d0 100644 --- a/flutter/assets/rec.svg +++ b/flutter/assets/rec.svg @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/unpinned.svg b/flutter/assets/unpinned.svg index ba4ab5328..9e9e3de8b 100644 --- a/flutter/assets/unpinned.svg +++ b/flutter/assets/unpinned.svg @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 211d36c39..dac62032f 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -201,7 +201,7 @@ class _RemotePageState extends State Widget buildBody(BuildContext context) { return Scaffold( - backgroundColor: Theme.of(context).backgroundColor, + backgroundColor: Theme.of(context).colorScheme.background, /// the Overlay key will be set with _blockableOverlayState in BlockableOverlay /// see override build() in [BlockableOverlay] diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 9b00b481f..610a7d1a5 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -22,7 +22,10 @@ import 'package:bot_toast/bot_toast.dart'; import '../../models/platform_model.dart'; class _MenuTheme { - static const Color commonColor = MyTheme.accent; + static const Color blueColor = MyTheme.button; + static const Color hoverBlueColor = MyTheme.accent; + static const Color redColor = Colors.redAccent; + static const Color hoverRedColor = Colors.red; // kMinInteractiveDimension static const double height = 20.0; static const double dividerHeight = 12.0; @@ -134,7 +137,7 @@ class _ConnectionTabPageState extends State { width: stateGlobal.windowBorderWidth.value), ), child: Scaffold( - backgroundColor: Theme.of(context).backgroundColor, + backgroundColor: Theme.of(context).colorScheme.background, body: DesktopTab( controller: tabController, onWindowCloseButton: handleWindowCloseButton, @@ -280,7 +283,7 @@ class _ConnectionTabPageState extends State { .map((entry) => entry.build( context, const MenuConfig( - commonColor: _MenuTheme.commonColor, + commonColor: _MenuTheme.blueColor, height: _MenuTheme.height, dividerHeight: _MenuTheme.dividerHeight, ))) diff --git a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart index 666c9a6e2..05c3059d4 100644 --- a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart +++ b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart @@ -5,6 +5,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/desktop/widgets/menu_button.dart'; // Examples can assume: // enum Commands { heroAndScholar, hurricaneCame } @@ -1391,22 +1393,21 @@ class PopupMenuButtonState extends State> { onTap: widget.enabled ? showButtonMenu : null, onHover: widget.onHover, canRequestFocus: _canRequestFocus, - radius: widget.splashRadius, enableFeedback: enableFeedback, child: widget.child, ), ); } - return IconButton( + return MenuButton( icon: widget.icon ?? Icon(Icons.adaptive.more), - padding: widget.padding, - splashRadius: widget.splashRadius, iconSize: widget.iconSize ?? iconTheme.size ?? _kDefaultIconSize, tooltip: widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip, onPressed: widget.enabled ? showButtonMenu : null, enableFeedback: enableFeedback, + color: MyTheme.button, + hoverColor: MyTheme.accent, ); } } diff --git a/flutter/lib/desktop/widgets/menu_button.dart b/flutter/lib/desktop/widgets/menu_button.dart new file mode 100644 index 000000000..ce63dcab1 --- /dev/null +++ b/flutter/lib/desktop/widgets/menu_button.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; + +class MenuButton extends StatefulWidget { + final GestureTapCallback? onPressed; + final Color color; + final Color hoverColor; + final Color? splashColor; + final Widget icon; + final double iconSize; + final String tooltip; + final EdgeInsetsGeometry padding; + final bool enableFeedback; + const MenuButton({ + super.key, + required this.onPressed, + required this.color, + required this.hoverColor, + required this.icon, + required this.iconSize, + required this.tooltip, + this.splashColor, + this.padding = const EdgeInsets.all(5), + this.enableFeedback = true, + }); + + @override + State createState() => _MenuButtonState(); +} + +class _MenuButtonState extends State { + bool _isHover = false; + + @override + Widget build(BuildContext context) { + return Padding( + padding: widget.padding, + child: Tooltip( + message: widget.tooltip, + child: Material( + type: MaterialType.transparency, + child: Ink( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: _isHover ? widget.hoverColor : widget.color, + ), + child: InkWell( + onHover: (val) { + setState(() { + _isHover = val; + }); + }, + borderRadius: BorderRadius.circular(5), + splashColor: widget.splashColor, + enableFeedback: widget.enableFeedback, + onTap: widget.onPressed, + child: widget.icon, + ), + ), + ), + ), + ); + } +} diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 77d687d93..ff586a1f1 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -5,6 +5,7 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_hbb/desktop/widgets/menu_button.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/consts.dart'; @@ -94,7 +95,10 @@ class MenubarState { } class _MenubarTheme { - static const Color commonColor = MyTheme.accent; + static const Color blueColor = MyTheme.button; + static const Color hoverBlueColor = MyTheme.accent; + static const Color redColor = Colors.redAccent; + static const Color hoverRedColor = Colors.red; // kMinInteractiveDimension static const double height = 20.0; static const double dividerHeight = 12.0; @@ -412,7 +416,7 @@ class _RemoteMenubarState extends State { if (widget.ffi.ffiModel.isPeerAndroid) { menubarItems.add(IconButton( tooltip: translate('Mobile Actions'), - color: _MenubarTheme.commonColor, + color: _MenubarTheme.blueColor, icon: const Icon(Icons.build), onPressed: () { widget.ffi.dialogManager @@ -433,7 +437,7 @@ class _RemoteMenubarState extends State { menubarItems.add(_buildClose(context, iconSize)); return PopupMenuTheme( data: const PopupMenuThemeData( - textStyle: TextStyle(color: _MenubarTheme.commonColor)), + textStyle: TextStyle(color: _MenubarTheme.blueColor)), child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -457,8 +461,7 @@ class _RemoteMenubarState extends State { Widget _buildPinMenubar(BuildContext context, double iconSize) { return Obx( - () => IconButton( - padding: EdgeInsets.zero, + () => MenuButton( iconSize: iconSize, tooltip: translate(pin ? 'Unpin menubar' : 'Pin menubar'), onPressed: () { @@ -466,15 +469,16 @@ class _RemoteMenubarState extends State { }, icon: SvgPicture.asset( pin ? "assets/pinned.svg" : "assets/unpinned.svg", - color: pin ? _MenubarTheme.commonColor : Colors.grey[800], + color: Colors.white, ), + color: pin ? _MenubarTheme.blueColor : Colors.grey[800]!, + hoverColor: pin ? _MenubarTheme.hoverBlueColor : Colors.grey[850]!, ), ); } Widget _buildFullscreen(BuildContext context, double iconSize) { - return IconButton( - padding: EdgeInsets.zero, + return MenuButton( iconSize: iconSize, tooltip: translate(isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'), onPressed: () { @@ -482,8 +486,10 @@ class _RemoteMenubarState extends State { }, icon: SvgPicture.asset( isFullscreen ? "assets/fullscreen_exit.svg" : "assets/fullscreen.svg", - color: _MenubarTheme.commonColor, + color: Colors.white, ), + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, ); } @@ -492,14 +498,13 @@ class _RemoteMenubarState extends State { return mod_menu.PopupMenuButton( iconSize: iconSize, tooltip: translate('Select Monitor'), - padding: EdgeInsets.zero, position: mod_menu.PopupMenuPosition.under, icon: Stack( alignment: Alignment.center, children: [ SvgPicture.asset( "assets/display.svg", - color: _MenubarTheme.commonColor, + color: Colors.white, ), Padding( padding: const EdgeInsets.only(bottom: 3.9), @@ -520,7 +525,10 @@ class _RemoteMenubarState extends State { Stack( alignment: Alignment.center, children: [ - SvgPicture.asset("assets/display.svg"), + SvgPicture.asset( + "assets/display.svg", + color: Colors.white, + ), TextButton( child: Container( alignment: AlignmentDirectional.center, @@ -531,7 +539,7 @@ class _RemoteMenubarState extends State { child: Text( (i + 1).toString(), style: TextStyle( - color: Theme.of(context).scaffoldBackgroundColor, + color: Colors.white, ), ), ), @@ -573,7 +581,7 @@ class _RemoteMenubarState extends State { padding: EdgeInsets.zero, icon: SvgPicture.asset( "assets/actions.svg", - color: _MenubarTheme.commonColor, + color: Colors.white, ), tooltip: translate('Control Actions'), position: mod_menu.PopupMenuPosition.under, @@ -581,7 +589,7 @@ class _RemoteMenubarState extends State { .map((entry) => entry.build( context, const MenuConfig( - commonColor: _MenubarTheme.commonColor, + commonColor: _MenubarTheme.blueColor, height: _MenubarTheme.height, dividerHeight: _MenubarTheme.dividerHeight, ))) @@ -606,7 +614,7 @@ class _RemoteMenubarState extends State { padding: EdgeInsets.zero, icon: SvgPicture.asset( "assets/display.svg", - color: _MenubarTheme.commonColor, + color: Colors.white, ), tooltip: translate('Display Settings'), position: mod_menu.PopupMenuPosition.under, @@ -616,7 +624,7 @@ class _RemoteMenubarState extends State { .map((entry) => entry.build( context, const MenuConfig( - commonColor: _MenubarTheme.commonColor, + commonColor: _MenubarTheme.blueColor, height: _MenubarTheme.height, dividerHeight: _MenubarTheme.dividerHeight, ))) @@ -640,7 +648,7 @@ class _RemoteMenubarState extends State { padding: EdgeInsets.zero, icon: SvgPicture.asset( "assets/keyboard.svg", - color: _MenubarTheme.commonColor, + color: Colors.white, ), tooltip: translate('Keyboard Settings'), position: mod_menu.PopupMenuPosition.under, @@ -648,7 +656,7 @@ class _RemoteMenubarState extends State { .map((entry) => entry.build( context, const MenuConfig( - commonColor: _MenubarTheme.commonColor, + commonColor: _MenubarTheme.blueColor, height: _MenubarTheme.height, dividerHeight: _MenubarTheme.dividerHeight, ))) @@ -661,8 +669,7 @@ class _RemoteMenubarState extends State { return Consumer(builder: ((context, value, child) { if (value.permissions['recording'] != false) { return Consumer( - builder: (context, value, child) => IconButton( - padding: EdgeInsets.zero, + builder: (context, value, child) => MenuButton( iconSize: iconSize, tooltip: value.start ? translate('Stop session recording') @@ -670,8 +677,13 @@ class _RemoteMenubarState extends State { onPressed: () => value.toggle(), icon: SvgPicture.asset( "assets/rec.svg", - color: value.start ? Colors.red : _MenubarTheme.commonColor, + color: Colors.white, ), + color: + value.start ? _MenubarTheme.redColor : _MenubarTheme.blueColor, + hoverColor: value.start + ? _MenubarTheme.hoverRedColor + : _MenubarTheme.hoverBlueColor, ), ); } else { @@ -681,17 +693,18 @@ class _RemoteMenubarState extends State { } Widget _buildClose(BuildContext context, double iconSize) { - return IconButton( + return MenuButton( iconSize: iconSize, - padding: EdgeInsets.zero, tooltip: translate('Close'), onPressed: () { clientClose(widget.id, widget.ffi.dialogManager); }, icon: SvgPicture.asset( "assets/close.svg", - color: Colors.red, + color: Colors.white, ), + color: _MenubarTheme.redColor, + hoverColor: _MenubarTheme.hoverRedColor, ); } @@ -704,7 +717,7 @@ class _RemoteMenubarState extends State { padding: EdgeInsets.zero, icon: SvgPicture.asset( "assets/chat.svg", - color: _MenubarTheme.commonColor, + color: Colors.white, ), tooltip: translate('Chat'), position: mod_menu.PopupMenuPosition.under, @@ -712,7 +725,7 @@ class _RemoteMenubarState extends State { .map((entry) => entry.build( context, const MenuConfig( - commonColor: _MenubarTheme.commonColor, + commonColor: _MenubarTheme.blueColor, height: _MenubarTheme.height, dividerHeight: _MenubarTheme.dividerHeight, ))) From 952596080279c8778cd3cd7edd8af6da80fa9089 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Wed, 15 Feb 2023 13:19:15 +0100 Subject: [PATCH 144/199] added new call end/wait icons --- flutter/assets/call_end.svg | 2 + flutter/assets/call_wait.svg | 2 + flutter/lib/desktop/pages/remote_page.dart | 2 +- .../lib/desktop/pages/remote_tab_page.dart | 2 +- .../widgets/material_mod_popup_menu.dart | 1 - flutter/lib/desktop/widgets/menu_button.dart | 6 +- .../lib/desktop/widgets/remote_menubar.dart | 79 +++++++------------ 7 files changed, 38 insertions(+), 56 deletions(-) create mode 100644 flutter/assets/call_end.svg create mode 100644 flutter/assets/call_wait.svg diff --git a/flutter/assets/call_end.svg b/flutter/assets/call_end.svg new file mode 100644 index 000000000..39367c3c5 --- /dev/null +++ b/flutter/assets/call_end.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/call_wait.svg b/flutter/assets/call_wait.svg new file mode 100644 index 000000000..42a11fe56 --- /dev/null +++ b/flutter/assets/call_wait.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index dac62032f..211d36c39 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -201,7 +201,7 @@ class _RemotePageState extends State Widget buildBody(BuildContext context) { return Scaffold( - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: Theme.of(context).backgroundColor, /// the Overlay key will be set with _blockableOverlayState in BlockableOverlay /// see override build() in [BlockableOverlay] diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 610a7d1a5..7bd2a4126 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -137,7 +137,7 @@ class _ConnectionTabPageState extends State { width: stateGlobal.windowBorderWidth.value), ), child: Scaffold( - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: Theme.of(context).backgroundColor, body: DesktopTab( controller: tabController, onWindowCloseButton: handleWindowCloseButton, diff --git a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart index 05c3059d4..47de1be20 100644 --- a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart +++ b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart @@ -1401,7 +1401,6 @@ class PopupMenuButtonState extends State> { return MenuButton( icon: widget.icon ?? Icon(Icons.adaptive.more), - iconSize: widget.iconSize ?? iconTheme.size ?? _kDefaultIconSize, tooltip: widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip, onPressed: widget.enabled ? showButtonMenu : null, diff --git a/flutter/lib/desktop/widgets/menu_button.dart b/flutter/lib/desktop/widgets/menu_button.dart index ce63dcab1..b2871e0cd 100644 --- a/flutter/lib/desktop/widgets/menu_button.dart +++ b/flutter/lib/desktop/widgets/menu_button.dart @@ -6,8 +6,7 @@ class MenuButton extends StatefulWidget { final Color hoverColor; final Color? splashColor; final Widget icon; - final double iconSize; - final String tooltip; + final String? tooltip; final EdgeInsetsGeometry padding; final bool enableFeedback; const MenuButton({ @@ -16,9 +15,8 @@ class MenuButton extends StatefulWidget { required this.color, required this.hoverColor, required this.icon, - required this.iconSize, - required this.tooltip, this.splashColor, + this.tooltip = "", this.padding = const EdgeInsets.all(5), this.enableFeedback = true, }); diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index ff586a1f1..5029560b0 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -409,10 +409,9 @@ class _RemoteMenubarState extends State { Widget _buildMenubar(BuildContext context) { final List menubarItems = []; - final double iconSize = Theme.of(context).iconTheme.size ?? 30.0; if (!isWebDesktop) { - menubarItems.add(_buildPinMenubar(context, iconSize)); - menubarItems.add(_buildFullscreen(context, iconSize)); + menubarItems.add(_buildPinMenubar(context)); + menubarItems.add(_buildFullscreen(context)); if (widget.ffi.ffiModel.isPeerAndroid) { menubarItems.add(IconButton( tooltip: translate('Mobile Actions'), @@ -425,16 +424,16 @@ class _RemoteMenubarState extends State { )); } } - menubarItems.add(_buildMonitor(context, iconSize)); - menubarItems.add(_buildControl(context, iconSize)); - menubarItems.add(_buildDisplay(context, iconSize)); - menubarItems.add(_buildKeyboard(context, iconSize)); + menubarItems.add(_buildMonitor(context)); + menubarItems.add(_buildControl(context)); + menubarItems.add(_buildDisplay(context)); + menubarItems.add(_buildKeyboard(context)); if (!isWeb) { - menubarItems.add(_buildChat(context, iconSize)); - menubarItems.add(_buildVoiceCall(context, iconSize)); + menubarItems.add(_buildChat(context)); + menubarItems.add(_buildVoiceCall(context)); } - menubarItems.add(_buildRecording(context, iconSize)); - menubarItems.add(_buildClose(context, iconSize)); + menubarItems.add(_buildRecording(context)); + menubarItems.add(_buildClose(context)); return PopupMenuTheme( data: const PopupMenuThemeData( textStyle: TextStyle(color: _MenubarTheme.blueColor)), @@ -459,10 +458,9 @@ class _RemoteMenubarState extends State { ); } - Widget _buildPinMenubar(BuildContext context, double iconSize) { + Widget _buildPinMenubar(BuildContext context) { return Obx( () => MenuButton( - iconSize: iconSize, tooltip: translate(pin ? 'Unpin menubar' : 'Pin menubar'), onPressed: () { widget.state.switchPin(); @@ -477,9 +475,8 @@ class _RemoteMenubarState extends State { ); } - Widget _buildFullscreen(BuildContext context, double iconSize) { + Widget _buildFullscreen(BuildContext context) { return MenuButton( - iconSize: iconSize, tooltip: translate(isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'), onPressed: () { _setFullscreen(!isFullscreen); @@ -493,10 +490,9 @@ class _RemoteMenubarState extends State { ); } - Widget _buildMonitor(BuildContext context, double iconSize) { + Widget _buildMonitor(BuildContext context) { final pi = widget.ffi.ffiModel.pi; return mod_menu.PopupMenuButton( - iconSize: iconSize, tooltip: translate('Select Monitor'), position: mod_menu.PopupMenuPosition.under, icon: Stack( @@ -575,9 +571,8 @@ class _RemoteMenubarState extends State { ); } - Widget _buildControl(BuildContext context, double iconSize) { + Widget _buildControl(BuildContext context) { return mod_menu.PopupMenuButton( - iconSize: iconSize, padding: EdgeInsets.zero, icon: SvgPicture.asset( "assets/actions.svg", @@ -598,7 +593,7 @@ class _RemoteMenubarState extends State { ); } - Widget _buildDisplay(BuildContext context, double iconSize) { + Widget _buildDisplay(BuildContext context) { return FutureBuilder(future: () async { widget.state.viewStyle.value = await bind.sessionGetViewStyle(id: widget.id) ?? ''; @@ -610,7 +605,6 @@ class _RemoteMenubarState extends State { return Obx(() { final remoteCount = RemoteCountState.find().value; return mod_menu.PopupMenuButton( - iconSize: iconSize, padding: EdgeInsets.zero, icon: SvgPicture.asset( "assets/display.svg", @@ -638,13 +632,12 @@ class _RemoteMenubarState extends State { }); } - Widget _buildKeyboard(BuildContext context, double iconSize) { + Widget _buildKeyboard(BuildContext context) { FfiModel ffiModel = Provider.of(context); if (ffiModel.permissions['keyboard'] == false) { return Offstage(); } return mod_menu.PopupMenuButton( - iconSize: iconSize, padding: EdgeInsets.zero, icon: SvgPicture.asset( "assets/keyboard.svg", @@ -665,12 +658,11 @@ class _RemoteMenubarState extends State { ); } - Widget _buildRecording(BuildContext context, double iconSize) { + Widget _buildRecording(BuildContext context) { return Consumer(builder: ((context, value, child) { if (value.permissions['recording'] != false) { return Consumer( builder: (context, value, child) => MenuButton( - iconSize: iconSize, tooltip: value.start ? translate('Stop session recording') : translate('Start session recording'), @@ -692,9 +684,8 @@ class _RemoteMenubarState extends State { })); } - Widget _buildClose(BuildContext context, double iconSize) { + Widget _buildClose(BuildContext context) { return MenuButton( - iconSize: iconSize, tooltip: translate('Close'), onPressed: () { clientClose(widget.id, widget.ffi.dialogManager); @@ -709,10 +700,9 @@ class _RemoteMenubarState extends State { } final _chatButtonKey = GlobalKey(); - Widget _buildChat(BuildContext context, double iconSize) { + Widget _buildChat(BuildContext context) { FfiModel ffiModel = Provider.of(context); return mod_menu.PopupMenuButton( - iconSize: iconSize, key: _chatButtonKey, padding: EdgeInsets.zero, icon: SvgPicture.asset( @@ -737,24 +727,15 @@ class _RemoteMenubarState extends State { Widget _getVoiceCallIcon() { switch (widget.ffi.chatModel.voiceCallStatus.value) { case VoiceCallStatus.waitingForResponse: - return IconButton( - onPressed: () { - widget.ffi.chatModel.closeVoiceCall(widget.id); - }, - icon: SvgPicture.asset( - "assets/voice_call_waiting.svg", - color: Colors.red, - ), + return SvgPicture.asset( + "assets/call_wait.svg", + color: Colors.white, ); + case VoiceCallStatus.connected: - return IconButton( - onPressed: () { - widget.ffi.chatModel.closeVoiceCall(widget.id); - }, - icon: Icon( - Icons.phone_disabled_rounded, - color: Colors.red, - ), + return SvgPicture.asset( + "assets/call_end.svg", + color: Colors.white, ); default: return const Offstage(); @@ -772,18 +753,18 @@ class _RemoteMenubarState extends State { } } - Widget _buildVoiceCall(BuildContext context, double iconSize) { + Widget _buildVoiceCall(BuildContext context) { return Obx( () { final tooltipText = _getVoiceCallTooltip(); return tooltipText == null ? const Offstage() - : IconButton( - iconSize: iconSize, - padding: EdgeInsets.zero, + : MenuButton( icon: _getVoiceCallIcon(), tooltip: translate(tooltipText), onPressed: () => bind.sessionRequestVoiceCall(id: widget.id), + color: _MenubarTheme.redColor, + hoverColor: _MenubarTheme.hoverRedColor, ); }, ); From 957bb65b9f624d6b00377787033e545cf6423562 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Wed, 15 Feb 2023 13:27:21 +0100 Subject: [PATCH 145/199] adjusted spacing --- flutter/lib/desktop/widgets/menu_button.dart | 2 +- flutter/lib/desktop/widgets/remote_menubar.dart | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/flutter/lib/desktop/widgets/menu_button.dart b/flutter/lib/desktop/widgets/menu_button.dart index b2871e0cd..904195f71 100644 --- a/flutter/lib/desktop/widgets/menu_button.dart +++ b/flutter/lib/desktop/widgets/menu_button.dart @@ -17,7 +17,7 @@ class MenuButton extends StatefulWidget { required this.icon, this.splashColor, this.tooltip = "", - this.padding = const EdgeInsets.all(5), + this.padding = const EdgeInsets.symmetric(horizontal: 2.5, vertical: 5), this.enableFeedback = true, }); diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 5029560b0..afc5b2d9f 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -449,7 +449,11 @@ class _RemoteMenubarState extends State { ), child: Row( mainAxisSize: MainAxisSize.min, - children: menubarItems, + children: [ + SizedBox(width: 2.5), + ...menubarItems, + SizedBox(width: 2.5) + ], ), ), _buildDraggableShowHide(context), From d5502f58ef5c1c95554ad7917f9aa1eeab21d004 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 15 Feb 2023 20:39:30 +0800 Subject: [PATCH 146/199] release session stream after close Signed-off-by: fufesou --- flutter/lib/models/model.dart | 3 +++ src/flutter_ffi.rs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index a1d9ff0df..865a8bea6 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1368,6 +1368,9 @@ class FFI { // Preserved for the rgba data. await for (final message in stream) { if (message is EventToUI_Event) { + if (message.field0 == "close") { + break; + } try { Map event = json.decode(message.field0); await cb(event); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 0e307abe3..3f9940854 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -132,6 +132,9 @@ pub fn session_login(id: String, password: String, remember: bool) { pub fn session_close(id: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { + if let Some(stream) = &*session.event_stream.read().unwrap() { + stream.add(EventToUI::Event("close".to_owned())); + } session.close(); } let _ = SESSIONS.write().unwrap().remove(&id); From eac6dae3a7aed17b81916b9369c2ed94914f054f Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Wed, 15 Feb 2023 14:14:21 +0100 Subject: [PATCH 147/199] increased margin --- flutter/lib/desktop/widgets/menu_button.dart | 2 +- flutter/lib/desktop/widgets/remote_menubar.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/lib/desktop/widgets/menu_button.dart b/flutter/lib/desktop/widgets/menu_button.dart index 904195f71..7c9fe67eb 100644 --- a/flutter/lib/desktop/widgets/menu_button.dart +++ b/flutter/lib/desktop/widgets/menu_button.dart @@ -17,7 +17,7 @@ class MenuButton extends StatefulWidget { required this.icon, this.splashColor, this.tooltip = "", - this.padding = const EdgeInsets.symmetric(horizontal: 2.5, vertical: 5), + this.padding = const EdgeInsets.symmetric(horizontal: 3, vertical: 6), this.enableFeedback = true, }); diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 189f58f4b..933850c99 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -452,9 +452,9 @@ class _RemoteMenubarState extends State { child: Row( mainAxisSize: MainAxisSize.min, children: [ - SizedBox(width: 2.5), + SizedBox(width: 3), ...menubarItems, - SizedBox(width: 2.5) + SizedBox(width: 3) ], ), ), From d8fe75860465a09fc5b80143069a7cd719cafb2f Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 15 Feb 2023 21:27:50 +0800 Subject: [PATCH 148/199] set event stream to None in rust side Signed-off-by: fufesou --- flutter/lib/models/model.dart | 1 + src/flutter.rs | 8 ++++++++ src/flutter_ffi.rs | 9 +++------ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 865a8bea6..39b1cdd03 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1389,6 +1389,7 @@ class FFI { } } } + debugPrint('Exit session event loop'); }(); // every instance will bind a stream this.id = id; diff --git a/src/flutter.rs b/src/flutter.rs index a60e379f9..d0f397d3f 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -134,6 +134,14 @@ impl FlutterHandler { stream.add(EventToUI::Event(out)); } } + + pub fn close_event_stream(&mut self) { + let mut stream_lock = self.event_stream.write().unwrap(); + if let Some(stream) = &*stream_lock { + stream.add(EventToUI::Event("close".to_owned())); + } + *stream_lock = None; + } } impl InvokeUiSession for FlutterHandler { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 3f9940854..53ddb724a 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -6,7 +6,7 @@ use crate::{ flutter::{session_add, session_start_}, ui_interface::{self, *}, }; -use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; +use flutter_rust_bridge::{StreamSink, SyncReturn}; use hbb_common::{ config::{self, LocalConfig, PeerConfig, ONLINE}, fs, log, @@ -131,13 +131,10 @@ pub fn session_login(id: String, password: String, remember: bool) { } pub fn session_close(id: String) { - if let Some(session) = SESSIONS.read().unwrap().get(&id) { - if let Some(stream) = &*session.event_stream.read().unwrap() { - stream.add(EventToUI::Event("close".to_owned())); - } + if let Some(mut session) = SESSIONS.write().unwrap().remove(&id) { + session.close_event_stream(); session.close(); } - let _ = SESSIONS.write().unwrap().remove(&id); } pub fn session_refresh(id: String) { From 432f0b7e3e3924c8f90704f76f07ba4b38f7bd4a Mon Sep 17 00:00:00 2001 From: grummbeer Date: Wed, 15 Feb 2023 15:20:09 +0100 Subject: [PATCH 149/199] CustomDialog. Add left padding to actions --- flutter/lib/common.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index ba7e3d762..bdef5f638 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -665,7 +665,7 @@ class CustomAlertDialog extends StatelessWidget { child: content), ), actions: actions, - actionsPadding: EdgeInsets.fromLTRB(0, 0, padding, padding), + actionsPadding: EdgeInsets.fromLTRB(padding, 0, padding, padding), ), ); } From 8f64940147214b266cdae0f82dcb16660bcc5f08 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Wed, 15 Feb 2023 20:17:36 +0100 Subject: [PATCH 150/199] changed linux icon --- flutter/assets/linux.svg | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/flutter/assets/linux.svg b/flutter/assets/linux.svg index 74248b5f0..5427305ba 100644 --- a/flutter/assets/linux.svg +++ b/flutter/assets/linux.svg @@ -1,6 +1,2 @@ - - - - - - + + \ No newline at end of file From 97ad7a42bdeb7ba6460aa24e562ae4bbe2dbbd4d Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 16 Feb 2023 10:58:27 +0800 Subject: [PATCH 151/199] fix: window manager called on Android & bugfix etc. --- flutter/lib/common.dart | 3 +++ flutter/lib/desktop/widgets/remote_menubar.dart | 2 +- src/ui/header.tis | 3 --- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index ba7e3d762..8a33f214c 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -336,6 +336,9 @@ closeConnection({String? id}) { } void window_on_top(int? id) { + if (!isDesktop) { + return; + } if (id == null) { // main window windowManager.restore(); diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 933850c99..0fa12cd6f 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -769,7 +769,7 @@ class _RemoteMenubarState extends State { : MenuButton( icon: _getVoiceCallIcon(), tooltip: translate(tooltipText), - onPressed: () => bind.sessionRequestVoiceCall(id: widget.id), + onPressed: () => bind.sessionCloseVoiceCall(id: widget.id), color: _MenubarTheme.redColor, hoverColor: _MenubarTheme.hoverRedColor, ); diff --git a/src/ui/header.tis b/src/ui/header.tis index 009995f4f..1fb694397 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -434,9 +434,6 @@ function toggleMenuState() { var c = handler.get_option("codec-preference"); if (!c) c = "auto"; values.push(c); - var a = handler.get_audio_mode(); - if (!a) a = "guest-to-host"; - values.push(a); for (var el in $$(menu#display-options li)) { el.attributes.toggleClass("selected", values.indexOf(el.id) >= 0); } From ed441242bf290b4df7fba366ce29d692baa84994 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 16 Feb 2023 14:54:13 +0800 Subject: [PATCH 152/199] add reconnect button on Connection Error Signed-off-by: 21pages --- flutter/lib/common.dart | 9 ++++++++- flutter/lib/models/model.dart | 32 +++++++++++++++++--------------- src/lang/ca.rs | 3 ++- src/lang/cn.rs | 5 +++-- src/lang/cs.rs | 3 ++- src/lang/da.rs | 3 ++- src/lang/de.rs | 3 ++- src/lang/eo.rs | 3 ++- src/lang/es.rs | 3 ++- src/lang/fa.rs | 3 ++- src/lang/fr.rs | 3 ++- src/lang/gr.rs | 3 ++- src/lang/hu.rs | 3 ++- src/lang/id.rs | 3 ++- src/lang/it.rs | 3 ++- src/lang/ja.rs | 3 ++- src/lang/ko.rs | 3 ++- src/lang/kz.rs | 3 ++- src/lang/nl.rs | 6 ++++-- src/lang/pl.rs | 3 ++- src/lang/pt_PT.rs | 3 ++- src/lang/ptbr.rs | 3 ++- src/lang/ro.rs | 3 ++- src/lang/ru.rs | 3 ++- src/lang/sk.rs | 3 ++- src/lang/sl.rs | 3 ++- src/lang/sq.rs | 3 ++- src/lang/sr.rs | 3 ++- src/lang/sv.rs | 3 ++- src/lang/template.rs | 3 ++- src/lang/th.rs | 3 ++- src/lang/tr.rs | 3 ++- src/lang/tw.rs | 11 ++++++----- src/lang/ua.rs | 3 ++- src/lang/vn.rs | 3 ++- 35 files changed, 98 insertions(+), 55 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 9f375860d..c01fe8910 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -676,7 +676,7 @@ class CustomAlertDialog extends StatelessWidget { void msgBox(String id, String type, String title, String text, String link, OverlayDialogManager dialogManager, - {bool? hasCancel}) { + {bool? hasCancel, ReconnectHandle? reconnect}) { dialogManager.dismissAll(); List buttons = []; bool hasOk = false; @@ -716,6 +716,13 @@ void msgBox(String id, String type, String title, String text, String link, dialogManager.dismissAll(); })); } + if (reconnect != null && title == "Connection Error") { + buttons.insert( + 0, + dialogButton('Reconnect', isOutline: true, onPressed: () { + reconnect(dialogManager, id, false); + })); + } if (link.isNotEmpty) { buttons.insert(0, dialogButton('JumpLink', onPressed: jumplink)); } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 28d3ae622..458ca29f4 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -33,6 +33,7 @@ import 'input_model.dart'; import 'platform_model.dart'; typedef HandleMsgBox = Function(Map evt, String id); +typedef ReconnectHandle = Function(OverlayDialogManager, String, bool); final _waitForImage = {}; class FfiModel with ChangeNotifier { @@ -310,14 +311,12 @@ class FfiModel with ChangeNotifier { showMsgBox(String id, String type, String title, String text, String link, bool hasRetry, OverlayDialogManager dialogManager, {bool? hasCancel}) { - msgBox(id, type, title, text, link, dialogManager, hasCancel: hasCancel); + msgBox(id, type, title, text, link, dialogManager, + hasCancel: hasCancel, reconnect: reconnect); _timer?.cancel(); if (hasRetry) { _timer = Timer(Duration(seconds: _reconnects), () { - bind.sessionReconnect(id: id, forceRelay: false); - clearPermissions(); - dialogManager.showLoading(translate('Connecting...'), - onCancel: closeConnection); + reconnect(dialogManager, id, false); }); _reconnects *= 2; } else { @@ -325,6 +324,14 @@ class FfiModel with ChangeNotifier { } } + void reconnect( + OverlayDialogManager dialogManager, String id, bool forceRelay) { + bind.sessionReconnect(id: id, forceRelay: forceRelay); + clearPermissions(); + dialogManager.showLoading(translate('Connecting...'), + onCancel: closeConnection); + } + void showRelayHintDialog(String id, String type, String title, String text, OverlayDialogManager dialogManager) { dialogManager.show(tag: '$id-$type', (setState, close) { @@ -333,13 +340,6 @@ class FfiModel with ChangeNotifier { close(); } - reconnect(bool forceRelay) { - bind.sessionReconnect(id: id, forceRelay: forceRelay); - clearPermissions(); - dialogManager.showLoading(translate('Connecting...'), - onCancel: closeConnection); - } - final style = ElevatedButton.styleFrom(backgroundColor: Colors.green[700]); return CustomAlertDialog( @@ -348,14 +348,16 @@ class FfiModel with ChangeNotifier { "${translate(text)}\n\n${translate('relay_hint_tip')}"), actions: [ dialogButton('Close', onPressed: onClose, isOutline: true), - dialogButton('Retry', onPressed: () => reconnect(false)), + dialogButton('Retry', + onPressed: () => reconnect(dialogManager, id, false)), dialogButton('Connect via relay', - onPressed: () => reconnect(true), buttonStyle: style), + onPressed: () => reconnect(dialogManager, id, true), + buttonStyle: style), dialogButton('Always connect via relay', onPressed: () { const option = 'force-always-relay'; bind.sessionPeerOption( id: id, name: option, value: bool2option(option, true)); - reconnect(true); + reconnect(dialogManager, id, true); }, buttonStyle: style), ], onCancel: onClose, diff --git a/src/lang/ca.rs b/src/lang/ca.rs index d483a185d..3220c824a 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 7dea516ba..d0fdcb3fd 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -422,7 +422,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ask the remote user for authentication", "请求远端用户授权"), ("Choose this if the remote account is administrator", "当对面电脑是管理员账号时选择该选项"), ("Transmit the username and password of administrator", "发送管理员账号的用户名密码"), - ("still_click_uac_tip", "依然需要被控端用戶在運行 RustDesk 的 UAC 窗口點擊確認。"), + ("still_click_uac_tip", "依然需要被控端用户在运行 RustDesk 的 UAC 窗口点击确认。"), ("Request Elevation", "请求提权"), ("wait_accept_uac_tip", "请等待远端用户确认 UAC 对话框。"), ("Elevate successfully", "提权成功"), @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", "文字聊天"), ("Stop voice call", "停止语音聊天"), ("relay_hint_tip", "可能无法直连,可以尝试中继连接。\n另外,如果想直接使用中继连接,可以在ID后面添加/r,或者在卡片选项里选择强制走中继连接。"), - ].iter().cloned().collect(); + ("Reconnect", "重连"), + ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 97a3ebc48..aca4778e6 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index bab81914e..7b959a778 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 05d02dd58..1672af2b9 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", "Text-Chat"), ("Stop voice call", "Sprachanruf beenden"), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 47eeb3367..9c9097f6e 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 4634cea81..dd1322873 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", "Chat de texto"), ("Stop voice call", "Detener llamada de voz"), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 2d0f29a5b..db565fe28 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", "گفتگو متنی (چت متنی)"), ("Stop voice call", "توقف تماس صوتی"), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 4e0e79aa0..fd46b4cf2 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 09284738a..90c8e105a 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 16c99d207..78648a034 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index f4be0396f..d06cc649a 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 15f7b977f..57215e2e5 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", "Chat testuale"), ("Stop voice call", "Interrompi la chiamata vocale"), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index acf1c9b96..6e72d4b04 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index e1bc43182..b7b59ed9c 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 488290537..9fdc29260 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 3b01492d3..2502cb34c 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Handmatig gesloten door de peer"), ("Enable remote configuration modification", "Wijziging configuratie op afstand inschakelen"), ("Run without install", "Uitvoeren zonder installatie"), - ("Always connected via relay", "Altijd verbonden via relay"), + ("Connect via relay", ""), ("Always connect via relay", "Altijd verbinden via relay"), ("whitelist_tip", "Alleen een IP-adres op de witte lijst krijgt toegang tot mijn toestel"), ("Login", "Log In"), @@ -449,5 +449,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Spraakoproep"), ("Text chat", "Tekst chat"), ("Stop voice call", "Stop spraakoproep"), - ].iter().cloned().collect(); + ("relay_hint_tip", ""), + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index e6ba5b171..24563d21f 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index a1ad932b1..078bf3761 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 5ece46006..e08700d44 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index e9b83e298..5be2a914a 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index a8ef18d8a..4af362953 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", "Текстовый чат"), ("Stop voice call", "Завершить голосовой вызов"), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 47a795342..bf4b85b1b 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 1eb33b970..f464cb8fc 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 1ade9757a..a6b83d9f3 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index e5704093d..09c34b4fc 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 063892074..2154b2729 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 4190ba399..f46a301f6 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 629c5ac77..93e984be3 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index b683fb78a..214ee83df 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index e4957e3d7..db26e5387 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -446,9 +446,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "幀率"), ("Auto", "自動"), ("Other Default Options", "其它默認選項"), - ("Voice call", ""), - ("Text chat", ""), - ("Stop voice call", ""), - ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Voice call", "語音通話"), + ("Text chat", "文字聊天"), + ("Stop voice call", "停止語音聊天"), + ("relay_hint_tip", "可能無法直連,可以嘗試中繼連接。 \n另外,如果想直接使用中繼連接,可以在ID後面添加/r,或者在卡片選項裡選擇強制走中繼連接。"), + ("Reconnect", "重連"), + ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 3c1d7776a..c3894726a 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 76f611429..45c2cc519 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } From 24473ebd7bb8353c21b32d76b95ed8e11ee13050 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 16 Feb 2023 15:01:15 +0800 Subject: [PATCH 153/199] fix: issue #3231 --- flutter/lib/desktop/widgets/remote_menubar.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 0fa12cd6f..2b7f8c00a 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1459,8 +1459,6 @@ class _RemoteMenubarState extends State { if (perms['audio'] != false) { displayMenu .add(_createSwitchMenuEntry('Mute', 'disable-audio', padding, true)); - displayMenu - .add(_createSwitchMenuEntry('Mute', 'disable-audio', padding, true)); } if (Platform.isWindows && From 9d4f899dfd6df25f6bef117d74593a90f796ba53 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 16 Feb 2023 15:16:54 +0800 Subject: [PATCH 154/199] fix using default onSubmit after tab tapped Signed-off-by: 21pages --- flutter/lib/common.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index c01fe8910..0880fdb91 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -632,6 +632,7 @@ class CustomAlertDialog extends StatelessWidget { if (!scopeNode.hasFocus) scopeNode.requestFocus(); }); const double padding = 16; + bool tabTapped = false; return FocusScope( node: scopeNode, autofocus: true, @@ -641,13 +642,15 @@ class CustomAlertDialog extends StatelessWidget { onCancel?.call(); } return KeyEventResult.handled; // avoid TextField exception on escape - } else if (onSubmit != null && + } else if (!tabTapped && + onSubmit != null && key.logicalKey == LogicalKeyboardKey.enter) { if (key is RawKeyDownEvent) onSubmit?.call(); return KeyEventResult.handled; } else if (key.logicalKey == LogicalKeyboardKey.tab) { if (key is RawKeyDownEvent) { scopeNode.nextFocus(); + tabTapped = true; } return KeyEventResult.handled; } From 4cddaa4f0c97906593ce7301f725efb6fd0d86ce Mon Sep 17 00:00:00 2001 From: grummbeer Date: Wed, 15 Feb 2023 16:41:34 +0100 Subject: [PATCH 155/199] Unify button style for desktop --- flutter/lib/common.dart | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 0880fdb91..c2f8f9a34 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -500,12 +500,14 @@ class OverlayDialogManager { Offstage( offstage: !showCancel, child: Center( - child: TextButton( - style: flatButtonStyle, - onPressed: cancel, - child: Text(translate('Cancel'), - style: - const TextStyle(color: MyTheme.accent))))) + child: isDesktop + ? dialogButton('Cancel', onPressed: cancel) + : TextButton( + style: flatButtonStyle, + onPressed: cancel, + child: Text(translate('Cancel'), + style: const TextStyle( + color: MyTheme.accent))))) ])), onCancel: showCancel ? cancel : null, ); From b62a05e15f7e3ad3fc2803dcc60db91cc08467b4 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Thu, 16 Feb 2023 07:45:31 +0100 Subject: [PATCH 156/199] CustomDialog. Set padding bottom to default if no actions set --- flutter/lib/common.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index c2f8f9a34..85aae4c80 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -662,8 +662,8 @@ class CustomAlertDialog extends StatelessWidget { scrollable: true, title: title, titlePadding: EdgeInsets.fromLTRB(padding, 24, padding, 0), - contentPadding: EdgeInsets.fromLTRB( - contentPadding ?? padding, 25, contentPadding ?? padding, 10), + contentPadding: EdgeInsets.fromLTRB(contentPadding ?? padding, 25, + contentPadding ?? padding, actions is List ? 10 : padding), content: ConstrainedBox( constraints: contentBoxConstraints, child: Theme( From 891121c64d179db48e117b8c010a0a301f6462aa Mon Sep 17 00:00:00 2001 From: grummbeer Date: Thu, 16 Feb 2023 11:05:07 +0100 Subject: [PATCH 157/199] Unify input labels. Remove colon from login labels --- flutter/lib/common/widgets/login.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/lib/common/widgets/login.dart b/flutter/lib/common/widgets/login.dart index 14a2c38bc..43dc3a658 100644 --- a/flutter/lib/common/widgets/login.dart +++ b/flutter/lib/common/widgets/login.dart @@ -324,13 +324,13 @@ class LoginWidgetUserPass extends StatelessWidget { children: [ const SizedBox(height: 8.0), DialogTextField( - title: '${translate("Username")}:', + title: translate("Username"), controller: username, focusNode: userFocusNode, prefixIcon: Icon(Icons.account_circle_outlined), errorText: usernameMsg), DialogTextField( - title: '${translate("Password")}:', + title: translate("Password"), obscureText: true, controller: pass, prefixIcon: Icon(Icons.lock_outline), From 10305ab54809e720eb07a580b03a452346ad1bea Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 16 Feb 2023 20:01:06 +0800 Subject: [PATCH 158/199] refact text clipboard Signed-off-by: fufesou --- src/client.rs | 105 +++++++++++++++++++++++++++++++++-- src/client/io_loop.rs | 106 ++++++++++++------------------------ src/flutter.rs | 28 ++++++++++ src/ui/remote.rs | 5 +- src/ui_session_interface.rs | 45 +++++++++++++-- 5 files changed, 207 insertions(+), 82 deletions(-) diff --git a/src/client.rs b/src/client.rs index 77221bdb2..97012e516 100644 --- a/src/client.rs +++ b/src/client.rs @@ -3,7 +3,7 @@ use std::{ net::SocketAddr, ops::{Deref, Not}, str::FromStr, - sync::{atomic::AtomicBool, mpsc, Arc, Mutex, RwLock}, + sync::{mpsc, Arc, Mutex, RwLock}, }; pub use async_trait::async_trait; @@ -34,7 +34,7 @@ use hbb_common::{ socket_client, sodiumoxide::crypto::{box_, secretbox, sign}, timeout, - tokio::time::Duration, + tokio::{sync::mpsc::UnboundedSender, time::Duration}, AddrMangle, ResultType, Stream, }; pub use helper::LatencyController; @@ -50,21 +50,30 @@ use crate::{ server::video_service::{SCRAP_X11_REF_URL, SCRAP_X11_REQUIRED}, }; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::{ + common::{check_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}, + ui_session_interface::SessionPermissionConfig, +}; + pub use super::lang::*; pub mod file_trait; pub mod helper; pub mod io_loop; -pub static SERVER_KEYBOARD_ENABLED: AtomicBool = AtomicBool::new(true); -pub static SERVER_FILE_TRANSFER_ENABLED: AtomicBool = AtomicBool::new(true); -pub static SERVER_CLIPBOARD_ENABLED: AtomicBool = AtomicBool::new(true); pub const MILLI1: Duration = Duration::from_millis(1); pub const SEC30: Duration = Duration::from_secs(30); /// Client of the remote desktop. pub struct Client; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +struct TextClipboardState { + is_required: bool, + running: bool, +} + #[cfg(not(any(target_os = "android", target_os = "linux")))] lazy_static::lazy_static! { static ref AUDIO_HOST: Host = cpal::default_host(); @@ -73,6 +82,8 @@ lazy_static::lazy_static! { #[cfg(not(any(target_os = "android", target_os = "ios")))] lazy_static::lazy_static! { static ref ENIGO: Arc> = Arc::new(Mutex::new(enigo::Enigo::new())); + static ref OLD_CLIPBOARD_TEXT: Arc> = Default::default(); + static ref TEXT_CLIPBOARD_STATE: Arc> = Arc::new(Mutex::new(TextClipboardState::new())); } #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -598,6 +609,86 @@ impl Client { conn.send(&msg_out).await?; Ok(conn) } + + #[inline] + #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + pub fn set_is_text_clipboard_required(b: bool) { + TEXT_CLIPBOARD_STATE.lock().unwrap().is_required = b; + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn try_stop_clipboard(_self_id: &str) { + #[cfg(feature = "flutter")] + if crate::flutter::other_sessions_running(_self_id) { + return; + } + TEXT_CLIPBOARD_STATE.lock().unwrap().running = false; + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn try_start_clipboard(_conf_tx: Option<(SessionPermissionConfig, UnboundedSender)>) { + let mut clipboard_lock = TEXT_CLIPBOARD_STATE.lock().unwrap(); + if clipboard_lock.running { + return; + } + + match ClipboardContext::new() { + Ok(mut ctx) => { + clipboard_lock.running = true; + // ignore clipboard update before service start + check_clipboard(&mut ctx, Some(&OLD_CLIPBOARD_TEXT)); + std::thread::spawn(move || { + log::info!("Start text clipboard loop"); + loop { + std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL)); + if !TEXT_CLIPBOARD_STATE.lock().unwrap().running { + break; + } + + if !TEXT_CLIPBOARD_STATE.lock().unwrap().is_required { + continue; + } + + if let Some(msg) = check_clipboard(&mut ctx, Some(&OLD_CLIPBOARD_TEXT)) { + #[cfg(feature = "flutter")] + crate::flutter::send_text_clipboard_msg(msg); + #[cfg(not(feature = "flutter"))] + if let Some((cfg, tx)) = &_conf_tx { + if cfg.is_text_clipboard_required() { + let _ = tx.send(Data::Message(msg)); + } + } + } + } + log::info!("Stop text clipboard loop"); + }); + } + Err(err) => { + log::error!("Failed to start clipboard service of client: {}", err); + } + } + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn get_current_text_clipboard_msg() -> Option { + let txt = &*OLD_CLIPBOARD_TEXT.lock().unwrap(); + if txt.is_empty() { + None + } else { + Some(crate::create_clipboard_msg(txt.clone())) + } + } +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +impl TextClipboardState { + fn new() -> Self { + Self { + is_required: true, + running: false, + } + } } /// Audio handler for the [`Client`]. @@ -1148,6 +1239,10 @@ impl LoginConfigHandler { if !name.contains("block-input") { self.save_config(config); } + #[cfg(feature = "flutter")] + if name == "disable-clipboard" { + crate::flutter::update_text_clipboard_required(); + } let mut misc = Misc::new(); misc.set_option(option); let mut msg_out = Message::new(); diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index de91b091d..427d0a72a 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -26,10 +26,10 @@ use hbb_common::{fs, log, Stream}; use crate::client::{ new_voice_call_request, Client, CodecFormat, MediaData, MediaSender, QualityStatus, MILLI1, - SEC30, SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED, + SEC30, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::common::{check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}; +use crate::common::update_clipboard; use crate::common::{get_default_sound_input, set_sound_input}; use crate::ui_session_interface::{InvokeUiSession, Session}; use crate::{audio_service, common, ConnInner, CLIENT_SERVER}; @@ -91,7 +91,6 @@ impl Remote { } pub async fn io_loop(&mut self, key: &str, token: &str) { - let stop_clipboard = self.start_clipboard(); let mut last_recv_time = Instant::now(); let mut received = false; let conn_type = if self.handler.is_file_transfer() { @@ -110,9 +109,6 @@ impl Remote { .await { Ok((mut peer, direct)) => { - SERVER_KEYBOARD_ENABLED.store(true, Ordering::SeqCst); - SERVER_CLIPBOARD_ENABLED.store(true, Ordering::SeqCst); - SERVER_FILE_TRANSFER_ENABLED.store(true, Ordering::SeqCst); self.handler.set_connection_type(peer.is_secured(), direct); // flutter -> connection_ready self.handler.set_connection_info(direct, false); @@ -237,12 +233,7 @@ impl Remote { .msgbox("error", "Connection Error", &err.to_string(), ""); } } - if let Some(stop) = stop_clipboard { - stop.send(()).ok(); - } - SERVER_KEYBOARD_ENABLED.store(false, Ordering::SeqCst); - SERVER_CLIPBOARD_ENABLED.store(false, Ordering::SeqCst); - SERVER_FILE_TRANSFER_ENABLED.store(false, Ordering::SeqCst); + Client::try_stop_clipboard(&self.handler.id); } fn handle_job_status(&mut self, id: i32, file_num: i32, err: Option) { @@ -347,46 +338,6 @@ impl Remote { Some(tx) } - fn start_clipboard(&mut self) -> Option> { - if self.handler.is_file_transfer() || self.handler.is_port_forward() { - return None; - } - let (tx, rx) = std::sync::mpsc::channel(); - let old_clipboard = self.old_clipboard.clone(); - let tx_protobuf = self.sender.clone(); - let lc = self.handler.lc.clone(); - #[cfg(not(any(target_os = "android", target_os = "ios")))] - match ClipboardContext::new() { - Ok(mut ctx) => { - // ignore clipboard update before service start - check_clipboard(&mut ctx, Some(&old_clipboard)); - std::thread::spawn(move || loop { - std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL)); - match rx.try_recv() { - Ok(_) | Err(std::sync::mpsc::TryRecvError::Disconnected) => { - log::debug!("Exit clipboard service of client"); - break; - } - _ => {} - } - if !SERVER_CLIPBOARD_ENABLED.load(Ordering::SeqCst) - || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) - || lc.read().unwrap().disable_clipboard.v - { - continue; - } - if let Some(msg) = check_clipboard(&mut ctx, Some(&old_clipboard)) { - tx_protobuf.send(Data::Message(msg)).ok(); - } - }); - } - Err(err) => { - log::error!("Failed to start clipboard service of client: {}", err); - } - } - Some(tx) - } - async fn handle_msg_from_ui(&mut self, data: Data, peer: &mut Stream) -> bool { match data { Data::Close => { @@ -885,22 +836,28 @@ impl Remote { Some(login_response::Union::PeerInfo(pi)) => { self.handler.handle_peer_info(pi); self.check_clipboard_file_context(); - if !(self.handler.is_file_transfer() - || self.handler.is_port_forward() - || !SERVER_CLIPBOARD_ENABLED.load(Ordering::SeqCst) - || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) - || self.handler.lc.read().unwrap().disable_clipboard.v) - { - let txt = self.old_clipboard.lock().unwrap().clone(); - if !txt.is_empty() { - let msg_out = crate::create_clipboard_msg(txt); - let sender = self.sender.clone(); - tokio::spawn(async move { - // due to clipboard service interval time - sleep(common::CLIPBOARD_INTERVAL as f32 / 1_000.).await; - sender.send(Data::Message(msg_out)).ok(); - }); - } + if !(self.handler.is_file_transfer() || self.handler.is_port_forward()) { + let sender = self.sender.clone(); + let permission_config = self.handler.get_permission_config(); + + #[cfg(feature = "flutter")] + Client::try_start_clipboard(None); + #[cfg(not(feature = "flutter"))] + Client::try_start_clipboard(Some(( + permission_config.clone(), + sender.clone(), + ))); + + tokio::spawn(async move { + // due to clipboard service interval time + sleep(common::CLIPBOARD_INTERVAL as f32 / 1_000.).await; + if permission_config.is_text_clipboard_required() { + if let Some(msg_out) = Client::get_current_text_clipboard_msg() + { + sender.send(Data::Message(msg_out)).ok(); + } + } + }); } if self.handler.is_file_transfer() { @@ -1092,18 +1049,23 @@ impl Remote { log::info!("Change permission {:?} -> {}", p.permission, p.enabled); match p.permission.enum_value_or_default() { Permission::Keyboard => { - SERVER_KEYBOARD_ENABLED.store(p.enabled, Ordering::SeqCst); + #[cfg(feature = "flutter")] + crate::flutter::update_text_clipboard_required(); + *self.handler.server_keyboard_enabled.write().unwrap() = p.enabled; self.handler.set_permission("keyboard", p.enabled); } Permission::Clipboard => { - SERVER_CLIPBOARD_ENABLED.store(p.enabled, Ordering::SeqCst); + #[cfg(feature = "flutter")] + crate::flutter::update_text_clipboard_required(); + *self.handler.server_clipboard_enabled.write().unwrap() = p.enabled; self.handler.set_permission("clipboard", p.enabled); } Permission::Audio => { self.handler.set_permission("audio", p.enabled); } Permission::File => { - SERVER_FILE_TRANSFER_ENABLED.store(p.enabled, Ordering::SeqCst); + *self.handler.server_file_transfer_enabled.write().unwrap() = + p.enabled; if !p.enabled && self.handler.is_file_transfer() { return true; } @@ -1416,7 +1378,7 @@ impl Remote { fn check_clipboard_file_context(&self) { #[cfg(windows)] { - let enabled = SERVER_FILE_TRANSFER_ENABLED.load(Ordering::SeqCst) + let enabled = *self.handler.server_file_transfer_enabled.read().unwrap() && self.handler.lc.read().unwrap().enable_file_transfer.v; ContextSend::enable(enabled); } diff --git a/src/flutter.rs b/src/flutter.rs index bd1f4f1af..c8f875da5 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -464,6 +464,9 @@ pub fn session_add( let session: Session = Session { id: session_id.clone(), + server_keyboard_enabled: Arc::new(RwLock::new(true)), + server_file_transfer_enabled: Arc::new(RwLock::new(true)), + server_clipboard_enabled: Arc::new(RwLock::new(true)), ..Default::default() }; @@ -514,6 +517,31 @@ pub fn session_start_(id: &str, event_stream: StreamSink) -> ResultTy } } +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn update_text_clipboard_required() { + let is_required = SESSIONS + .read() + .unwrap() + .iter() + .any(|(_id, session)| session.is_text_clipboard_required()); + Client::set_is_text_clipboard_required(is_required); +} + +#[inline] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn other_sessions_running(id: &str) -> bool { + SESSIONS.read().unwrap().keys().filter(|k| *k != id).count() != 0 +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn send_text_clipboard_msg(msg: Message) { + for (_id, session) in SESSIONS.read().unwrap().iter() { + if session.is_text_clipboard_required() { + session.send(Data::Message(msg.clone())); + } + } +} + // Server Side #[cfg(not(any(target_os = "ios")))] pub mod connection_manager { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 1725a8f41..a86f07d0f 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1,7 +1,7 @@ use std::{ collections::HashMap, ops::{Deref, DerefMut}, - sync::{Arc, Mutex}, + sync::{Arc, Mutex, RwLock}, }; use sciter::{ @@ -454,6 +454,9 @@ impl SciterSession { id: id.clone(), password: password.clone(), args, + server_keyboard_enabled: Arc::new(RwLock::new(true)), + server_file_transfer_enabled: Arc::new(RwLock::new(true)), + server_clipboard_enabled: Arc::new(RwLock::new(true)), ..Default::default() }; diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 97db904d4..947f8fb6f 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1,9 +1,11 @@ use std::collections::HashMap; use std::ops::{Deref, DerefMut}; use std::str::FromStr; -use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; -use std::sync::{Arc, Mutex, RwLock}; -use std::time::Duration; +use std::sync::{ + atomic::{AtomicBool, AtomicUsize, Ordering}, + Arc, Mutex, RwLock, +}; +use std::time::{Duration, SystemTime}; use async_trait::async_trait; use bytes::Bytes; @@ -37,9 +39,38 @@ pub struct Session { pub sender: Arc>>>, pub thread: Arc>>>, pub ui_handler: T, + pub server_keyboard_enabled: Arc>, + pub server_file_transfer_enabled: Arc>, + pub server_clipboard_enabled: Arc>, +} + +#[derive(Clone)] +pub struct SessionPermissionConfig { + pub lc: Arc>, + pub server_keyboard_enabled: Arc>, + pub server_file_transfer_enabled: Arc>, + pub server_clipboard_enabled: Arc>, +} + +impl SessionPermissionConfig { + pub fn is_text_clipboard_required(&self) -> bool { + println!("REMOVE ME ==================== is_text_clipboard_required {} -{}-{}", *self.server_clipboard_enabled.read().unwrap(), *self.server_keyboard_enabled.read().unwrap(), !self.lc.read().unwrap().disable_clipboard.v); + *self.server_clipboard_enabled.read().unwrap() + && *self.server_keyboard_enabled.read().unwrap() + && !self.lc.read().unwrap().disable_clipboard.v + } } impl Session { + pub fn get_permission_config(&self) -> SessionPermissionConfig { + SessionPermissionConfig { + lc: self.lc.clone(), + server_keyboard_enabled: self.server_keyboard_enabled.clone(), + server_file_transfer_enabled: self.server_file_transfer_enabled.clone(), + server_clipboard_enabled: self.server_clipboard_enabled.clone(), + } + } + pub fn is_file_transfer(&self) -> bool { self.lc .read() @@ -128,6 +159,12 @@ impl Session { self.lc.read().unwrap().is_privacy_mode_supported() } + pub fn is_text_clipboard_required(&self) -> bool { + *self.server_clipboard_enabled.read().unwrap() + && *self.server_keyboard_enabled.read().unwrap() + && !self.lc.read().unwrap().disable_clipboard.v + } + pub fn refresh_video(&self) { self.send(Data::Message(LoginConfigHandler::refresh())); } @@ -445,7 +482,7 @@ impl Session { KeyRelease(key) }; let event = Event { - time: std::time::SystemTime::now(), + time: SystemTime::now(), unicode: None, code: keycode as _, scan_code: scancode as _, From 241925dc83c7b656e92171977eb1676c2c0e1908 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 16 Feb 2023 20:28:06 +0800 Subject: [PATCH 159/199] remove debug print Signed-off-by: fufesou --- src/ui_session_interface.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 947f8fb6f..2344f84a1 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -54,7 +54,6 @@ pub struct SessionPermissionConfig { impl SessionPermissionConfig { pub fn is_text_clipboard_required(&self) -> bool { - println!("REMOVE ME ==================== is_text_clipboard_required {} -{}-{}", *self.server_clipboard_enabled.read().unwrap(), *self.server_keyboard_enabled.read().unwrap(), !self.lc.read().unwrap().disable_clipboard.v); *self.server_clipboard_enabled.read().unwrap() && *self.server_keyboard_enabled.read().unwrap() && !self.lc.read().unwrap().disable_clipboard.v From 0d2113cd293446317ec1f64a263347615070ae0c Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 16 Feb 2023 20:48:42 +0800 Subject: [PATCH 160/199] build android Signed-off-by: fufesou --- src/client.rs | 5 ++++- src/client/io_loop.rs | 6 ++++++ src/flutter_ffi.rs | 4 +++- src/keyboard.rs | 4 +++- src/ui_session_interface.rs | 1 + 5 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/client.rs b/src/client.rs index 97012e516..51e7f9a29 100644 --- a/src/client.rs +++ b/src/client.rs @@ -18,6 +18,8 @@ use sha2::{Digest, Sha256}; use uuid::Uuid; pub use file_trait::FileManager; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use hbb_common::tokio::sync::mpsc::UnboundedSender; use hbb_common::{ allow_err, anyhow::{anyhow, Context}, @@ -34,7 +36,7 @@ use hbb_common::{ socket_client, sodiumoxide::crypto::{box_, secretbox, sign}, timeout, - tokio::{sync::mpsc::UnboundedSender, time::Duration}, + tokio::time::Duration, AddrMangle, ResultType, Stream, }; pub use helper::LatencyController; @@ -1240,6 +1242,7 @@ impl LoginConfigHandler { self.save_config(config); } #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] if name == "disable-clipboard" { crate::flutter::update_text_clipboard_required(); } diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 427d0a72a..c673531ec 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -233,6 +233,7 @@ impl Remote { .msgbox("error", "Connection Error", &err.to_string(), ""); } } + #[cfg(not(any(target_os = "android", target_os = "ios")))] Client::try_stop_clipboard(&self.handler.id); } @@ -841,13 +842,16 @@ impl Remote { let permission_config = self.handler.get_permission_config(); #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] Client::try_start_clipboard(None); #[cfg(not(feature = "flutter"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] Client::try_start_clipboard(Some(( permission_config.clone(), sender.clone(), ))); + #[cfg(not(any(target_os = "android", target_os = "ios")))] tokio::spawn(async move { // due to clipboard service interval time sleep(common::CLIPBOARD_INTERVAL as f32 / 1_000.).await; @@ -1050,12 +1054,14 @@ impl Remote { match p.permission.enum_value_or_default() { Permission::Keyboard => { #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] crate::flutter::update_text_clipboard_required(); *self.handler.server_keyboard_enabled.write().unwrap() = p.enabled; self.handler.set_permission("keyboard", p.enabled); } Permission::Clipboard => { #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] crate::flutter::update_text_clipboard_required(); *self.handler.server_clipboard_enabled.write().unwrap() = p.enabled; self.handler.set_permission("clipboard", p.enabled); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 0aa7de07f..f3bc45856 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,11 +1,13 @@ use crate::{ client::file_trait::FileManager, common::make_fd_to_json, - common::{get_default_sound_input, is_keyboard_mode_supported}, + common::is_keyboard_mode_supported, flutter::{self, SESSIONS}, flutter::{session_add, session_start_}, ui_interface::{self, *}, }; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::common::get_default_sound_input; use flutter_rust_bridge::{StreamSink, SyncReturn}; use hbb_common::{ config::{self, LocalConfig, PeerConfig, ONLINE}, diff --git a/src/keyboard.rs b/src/keyboard.rs index 4dcbe5c97..3f7ed6779 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -5,7 +5,9 @@ use crate::common::GrabState; use crate::flutter::{CUR_SESSION_ID, SESSIONS}; #[cfg(not(any(feature = "flutter", feature = "cli")))] use crate::ui::CUR_SESSION; -use hbb_common::{log, message_proto::*}; +use hbb_common::message_proto::*; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use hbb_common::log; use rdev::{Event, EventType, Key}; #[cfg(any(target_os = "windows", target_os = "macos"))] use std::sync::atomic::{AtomicBool, Ordering}; diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 2344f84a1..b225151ff 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1,3 +1,4 @@ +#[cfg(not(any(target_os = "android", target_os = "ios")))] use std::collections::HashMap; use std::ops::{Deref, DerefMut}; use std::str::FromStr; From 4cd36e9bd0b3405313f20d67e7da8d71366cc370 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Thu, 16 Feb 2023 16:23:46 +0100 Subject: [PATCH 161/199] Unify password field behavior --- .../desktop/pages/desktop_setting_page.dart | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 378ddbd1b..25c485a2a 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1832,6 +1832,7 @@ void changeSocks5Proxy() async { var proxyController = TextEditingController(text: proxy); var userController = TextEditingController(text: username); var pwdController = TextEditingController(text: password); + RxBool obscure = true.obs; var isInProgress = false; gFFI.dialogManager.show((setState, close) { @@ -1929,12 +1930,17 @@ void changeSocks5Proxy() async { width: 24.0, ), Expanded( - child: TextField( - decoration: const InputDecoration( - border: OutlineInputBorder(), - ), - controller: pwdController, - ), + child: Obx(() => TextField( + obscureText: obscure.value, + decoration: InputDecoration( + border: const OutlineInputBorder(), + suffixIcon: IconButton( + onPressed: () => obscure.value = !obscure.value, + icon: Icon(obscure.value + ? Icons.visibility_off + : Icons.visibility))), + controller: pwdController, + )), ), ], ), From 6432183bb4ff58776ad8682c9e8e55200609d1cf Mon Sep 17 00:00:00 2001 From: "Miguel F. G" <116861809+flusheDData@users.noreply.github.com> Date: Thu, 16 Feb 2023 16:44:39 +0100 Subject: [PATCH 162/199] Update es.rs New terms added --- 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 dd1322873..63c1d26fc 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -449,7 +449,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Llamada de voz"), ("Text chat", "Chat de texto"), ("Stop voice call", "Detener llamada de voz"), - ("relay_hint_tip", ""), - ("Reconnect", ""), + ("relay_hint_tip", "Puede que no sea posible conectar directamente. Puedes tratar de conectar a través de relay. \nAdicionalmente, si quieres usar relay en el primer intento, puedes añadir el sufijo \"/r\" a la ID o seleccionar la opción \"Conectar siempre a través de relay\" en la tarjeta del par."), + ("Reconnect", "Reconectar"), ].iter().cloned().collect(); } From a0caf8f257d43bc83df8edb6c17d5d2a3ec3ae1d Mon Sep 17 00:00:00 2001 From: ilGigioVr88 Date: Thu, 16 Feb 2023 17:15:37 +0100 Subject: [PATCH 163/199] Update it.rs --- src/lang/it.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 57215e2e5..ab0c8064c 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -450,6 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", "Chat testuale"), ("Stop voice call", "Interrompi la chiamata vocale"), ("relay_hint_tip", ""), - ("Reconnect", ""), + ("Reconnect", "Riconnetti"), ].iter().cloned().collect(); } From 897f694ad4a76d35cac57891eddb5204eebcd1ce Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Thu, 16 Feb 2023 18:17:42 +0100 Subject: [PATCH 164/199] fix for #3240 --- flutter/assets/actions_mobile.svg | 2 ++ flutter/lib/desktop/widgets/remote_menubar.dart | 10 +++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 flutter/assets/actions_mobile.svg diff --git a/flutter/assets/actions_mobile.svg b/flutter/assets/actions_mobile.svg new file mode 100644 index 000000000..6aed6053e --- /dev/null +++ b/flutter/assets/actions_mobile.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 2b7f8c00a..3bec6862a 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -413,14 +413,18 @@ class _RemoteMenubarState extends State { menubarItems.add(_buildPinMenubar(context)); menubarItems.add(_buildFullscreen(context)); if (widget.ffi.ffiModel.isPeerAndroid) { - menubarItems.add(IconButton( + menubarItems.add(MenuButton( tooltip: translate('Mobile Actions'), - color: _MenubarTheme.blueColor, - icon: const Icon(Icons.build), + icon: SvgPicture.asset( + "assets/actions_mobile.svg", + color: Colors.white, + ), onPressed: () { widget.ffi.dialogManager .toggleMobileActionsOverlay(ffi: widget.ffi); }, + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, )); } } From 285b5033165f48b802852d1eba16f97c7b0fd377 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Thu, 16 Feb 2023 19:20:26 +0100 Subject: [PATCH 165/199] improve input of permanent password --- .../lib/desktop/pages/desktop_home_page.dart | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index d9afbea55..b5cadbcdf 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -596,13 +596,13 @@ void setPasswordDialog() async { }); final pass = p0.text.trim(); if (pass.isNotEmpty) { - for (var r in rules) { - if (!r.validate(pass)) { - setState(() { - errMsg0 = '${translate('Prompt')}: ${r.name}'; - }); - return; - } + final Iterable violations = rules.where((r) => !r.validate(pass)); + if (violations.isNotEmpty) { + setState(() { + errMsg0 = + '${translate('Prompt')}: ${violations.map((r) => r.name).join(', ')}'; + }); + return; } } if (p1.text.trim() != pass) { @@ -639,6 +639,9 @@ void setPasswordDialog() async { autofocus: true, onChanged: (value) { rxPass.value = value.trim(); + setState(() { + errMsg0 = ''; + }); }, ), ), @@ -662,6 +665,11 @@ void setPasswordDialog() async { labelText: translate('Confirmation'), errorText: errMsg1.isNotEmpty ? errMsg1 : null), controller: p1, + onChanged: (value) { + setState(() { + errMsg1 = ''; + }); + }, ), ), ], From 512563f7967182918f67fc1d8c435c7e2959f987 Mon Sep 17 00:00:00 2001 From: solokot Date: Fri, 17 Feb 2023 02:08:02 +0300 Subject: [PATCH 166/199] update ru.rs --- src/lang/ru.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 4af362953..c389d6821 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -209,8 +209,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Закрыто удалённым узлом вручную"), ("Enable remote configuration modification", "Разрешить удалённое изменение конфигурации"), ("Run without install", "Запустить без установки"), - ("Connect via relay", ""), - ("Always connect via relay", "Всегда подключаться через ретрансляционный сервер"), + ("Connect via relay", "Подключится через ретранслятор"), + ("Always connect via relay", "Всегда подключаться через ретранслятор"), ("whitelist_tip", "Только IP-адреса из белого списка могут получить доступ ко мне"), ("Login", "Войти"), ("Verify", "Проверить"), @@ -449,7 +449,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Голосовой вызов"), ("Text chat", "Текстовый чат"), ("Stop voice call", "Завершить голосовой вызов"), - ("relay_hint_tip", ""), - ("Reconnect", ""), + ("relay_hint_tip", "Прямое подключение может оказаться невозможным. В этом случае можно попытаться подключиться через сервер ретрансляции. \nКроме того, если вы хотите сразу использовать сервер ретрансляции, можно добавить к ID суффикс \"/r\" или включить \"Всегда подключаться через ретранслятор\" в настройках удалённого узла."), + ("Reconnect", "Переподключить"), ].iter().cloned().collect(); } From 000799d1814e7d854f53ce5c51aad0829c7aebbf Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 17 Feb 2023 11:59:03 +0800 Subject: [PATCH 167/199] fix CI --- .github/workflows/flutter-ci.yml | 2 +- .github/workflows/flutter-nightly.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index 5d4cf39c9..78c60df37 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -105,7 +105,7 @@ jobs: - name: Install build runtime run: | - brew install llvm create-dmg nasm yasm cmake gcc wget ninja + brew install llvm create-dmg nasm yasm cmake gcc wget ninja pkg-config - name: Install flutter uses: subosito/flutter-action@v2 diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 1ab21dbff..ffcadd18b 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -183,7 +183,7 @@ jobs: - name: Install build runtime run: | - brew install llvm create-dmg nasm yasm cmake gcc wget ninja + brew install llvm create-dmg nasm yasm cmake gcc wget ninja pkg-config - name: Install flutter uses: subosito/flutter-action@v2 From 302499d1e01babe5d7eb147b07407e1bbfd3d4d5 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 17 Feb 2023 13:32:17 +0800 Subject: [PATCH 168/199] fix sync displays info && select monitor menu Signed-off-by: fufesou --- .../widgets/material_mod_popup_menu.dart | 2 +- flutter/lib/desktop/widgets/menu_button.dart | 6 +- .../lib/desktop/widgets/remote_menubar.dart | 86 ++++++++++--------- flutter/lib/models/model.dart | 24 ++++++ flutter/lib/models/state_model.dart | 1 + libs/hbb_common/protos/message.proto | 1 + src/client/io_loop.rs | 8 ++ src/common.rs | 2 + src/flutter.rs | 33 ++++--- src/server/connection.rs | 84 ++++++++++-------- src/server/video_service.rs | 52 +++++++++-- src/ui/header.tis | 8 ++ src/ui/remote.rs | 34 +++++--- src/ui_session_interface.rs | 1 + 14 files changed, 234 insertions(+), 108 deletions(-) diff --git a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart index 47de1be20..3e85cb296 100644 --- a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart +++ b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart @@ -1400,7 +1400,7 @@ class PopupMenuButtonState extends State> { } return MenuButton( - icon: widget.icon ?? Icon(Icons.adaptive.more), + child: widget.icon ?? Icon(Icons.adaptive.more), tooltip: widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip, onPressed: widget.enabled ? showButtonMenu : null, diff --git a/flutter/lib/desktop/widgets/menu_button.dart b/flutter/lib/desktop/widgets/menu_button.dart index 7c9fe67eb..96cc9fa9b 100644 --- a/flutter/lib/desktop/widgets/menu_button.dart +++ b/flutter/lib/desktop/widgets/menu_button.dart @@ -5,7 +5,7 @@ class MenuButton extends StatefulWidget { final Color color; final Color hoverColor; final Color? splashColor; - final Widget icon; + final Widget child; final String? tooltip; final EdgeInsetsGeometry padding; final bool enableFeedback; @@ -14,7 +14,7 @@ class MenuButton extends StatefulWidget { required this.onPressed, required this.color, required this.hoverColor, - required this.icon, + required this.child, this.splashColor, this.tooltip = "", this.padding = const EdgeInsets.symmetric(horizontal: 3, vertical: 6), @@ -51,7 +51,7 @@ class _MenuButtonState extends State { splashColor: widget.splashColor, enableFeedback: widget.enableFeedback, onTap: widget.onPressed, - child: widget.icon, + child: widget.child, ), ), ), diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 2b7f8c00a..c97ef9d32 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -472,7 +472,7 @@ class _RemoteMenubarState extends State { onPressed: () { widget.state.switchPin(); }, - icon: SvgPicture.asset( + child: SvgPicture.asset( pin ? "assets/pinned.svg" : "assets/unpinned.svg", color: Colors.white, ), @@ -488,7 +488,7 @@ class _RemoteMenubarState extends State { onPressed: () { _setFullscreen(!isFullscreen); }, - icon: SvgPicture.asset( + child: SvgPicture.asset( isFullscreen ? "assets/fullscreen_exit.svg" : "assets/fullscreen.svg", color: Colors.white, ), @@ -499,7 +499,7 @@ class _RemoteMenubarState extends State { Widget _buildMonitor(BuildContext context) { final pi = widget.ffi.ffiModel.pi; - return mod_menu.PopupMenuButton( + final monitor = mod_menu.PopupMenuButton( tooltip: translate('Select Monitor'), position: mod_menu.PopupMenuPosition.under, icon: Stack( @@ -524,43 +524,44 @@ class _RemoteMenubarState extends State { itemBuilder: (BuildContext context) { final List rowChildren = []; for (int i = 0; i < pi.displays.length; i++) { - rowChildren.add( - Stack( - alignment: Alignment.center, - children: [ - SvgPicture.asset( - "assets/display.svg", - color: Colors.white, - ), - TextButton( - child: Container( - alignment: AlignmentDirectional.center, - constraints: - const BoxConstraints(minHeight: _MenubarTheme.height), - child: Padding( - padding: const EdgeInsets.only(bottom: 2.5), - child: Text( - (i + 1).toString(), - style: TextStyle( - color: Colors.white, - ), + rowChildren.add(MenuButton( + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + child: Container( + alignment: AlignmentDirectional.center, + constraints: + const BoxConstraints(minHeight: _MenubarTheme.height), + child: Stack( + alignment: Alignment.center, + children: [ + SvgPicture.asset( + "assets/display.svg", + color: Colors.white, + ), + Padding( + padding: const EdgeInsets.only(bottom: 2.5), + child: Text( + (i + 1).toString(), + style: TextStyle( + color: Colors.white, + fontSize: 12, ), ), - ), - onPressed: () { - if (Navigator.canPop(context)) { - Navigator.pop(context); - _menuDismissCallback(); - } - RxInt display = CurrentDisplayState.find(widget.id); - if (display.value != i) { - bind.sessionSwitchDisplay(id: widget.id, value: i); - } - }, - ) - ], + ) + ], + ), ), - ); + onPressed: () { + if (Navigator.canPop(context)) { + Navigator.pop(context); + _menuDismissCallback(); + } + RxInt display = CurrentDisplayState.find(widget.id); + if (display.value != i) { + bind.sessionSwitchDisplay(id: widget.id, value: i); + } + }, + )); } return >[ mod_menu.PopupMenuItem( @@ -576,6 +577,11 @@ class _RemoteMenubarState extends State { ]; }, ); + + return Obx(() => Offstage( + offstage: stateGlobal.displaysCount.value < 2, + child: monitor, + )); } Widget _buildControl(BuildContext context) { @@ -674,7 +680,7 @@ class _RemoteMenubarState extends State { ? translate('Stop session recording') : translate('Start session recording'), onPressed: () => value.toggle(), - icon: SvgPicture.asset( + child: SvgPicture.asset( "assets/rec.svg", color: Colors.white, ), @@ -697,7 +703,7 @@ class _RemoteMenubarState extends State { onPressed: () { clientClose(widget.id, widget.ffi.dialogManager); }, - icon: SvgPicture.asset( + child: SvgPicture.asset( "assets/close.svg", color: Colors.white, ), @@ -767,7 +773,7 @@ class _RemoteMenubarState extends State { return tooltipText == null ? const Offstage() : MenuButton( - icon: _getVoiceCallIcon(), + child: _getVoiceCallIcon(), tooltip: translate(tooltipText), onPressed: () => bind.sessionCloseVoiceCall(id: widget.id), color: _MenubarTheme.redColor, diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 458ca29f4..1afb5b147 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -140,6 +140,8 @@ class FfiModel with ChangeNotifier { handleMsgBox(evt, peerId); } else if (name == 'peer_info') { handlePeerInfo(evt, peerId); + } else if (name == 'sync_peer_info') { + handleSyncPeerInfo(evt, peerId); } else if (name == 'connection_ready') { setConnectionType( peerId, evt['secure'] == 'true', evt['direct'] == 'true'); @@ -415,6 +417,7 @@ class FfiModel with ChangeNotifier { d.cursorEmbedded = d0['cursor_embedded'] == 1; _pi.displays.add(d); } + stateGlobal.displaysCount.value = _pi.displays.length; if (_pi.currentDisplay < _pi.displays.length) { _display = _pi.displays[_pi.currentDisplay]; } @@ -431,6 +434,27 @@ class FfiModel with ChangeNotifier { notifyListeners(); } + /// Handle the peer info synchronization event based on [evt]. + handleSyncPeerInfo(Map evt, String peerId) async { + if (evt['displays'] != null) { + List displays = json.decode(evt['displays']); + List newDisplays = []; + for (int i = 0; i < displays.length; ++i) { + Map d0 = displays[i]; + var d = Display(); + d.x = d0['x'].toDouble(); + d.y = d0['y'].toDouble(); + d.width = d0['width']; + d.height = d0['height']; + d.cursorEmbedded = d0['cursor_embedded'] == 1; + newDisplays.add(d); + } + _pi.displays = newDisplays; + stateGlobal.displaysCount.value = _pi.displays.length; + } + notifyListeners(); + } + updateBlockInputState(Map evt, String peerId) { _inputBlocked = evt['input_state'] == 'on'; notifyListeners(); diff --git a/flutter/lib/models/state_model.dart b/flutter/lib/models/state_model.dart index e4c9fa03f..761c95ded 100644 --- a/flutter/lib/models/state_model.dart +++ b/flutter/lib/models/state_model.dart @@ -14,6 +14,7 @@ class StateGlobal { final RxDouble _resizeEdgeSize = RxDouble(kWindowEdgeSize); final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth); final RxBool showRemoteMenuBar = false.obs; + final RxInt displaysCount = 0.obs; int get windowId => _windowId; bool get fullscreen => _fullscreen; diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 7e3d0b0a4..2a3fd05b4 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -636,5 +636,6 @@ message Message { SwitchSidesResponse switch_sides_response = 22; VoiceCallRequest voice_call_request = 23; VoiceCallResponse voice_call_response = 24; + PeerInfo peer_info = 25; } } diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index c673531ec..b51c481a5 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1253,6 +1253,14 @@ impl Remote { } } } + Some(message::Union::PeerInfo(pi)) => { + match pi.conn_id { + crate::SYNC_PEER_INFO_DISPLAYS => { + self.handler.set_displays(&pi.displays); + } + _ => {} + } + } _ => {} } } diff --git a/src/common.rs b/src/common.rs index ee44cf4f2..02d367b5e 100644 --- a/src/common.rs +++ b/src/common.rs @@ -37,6 +37,8 @@ pub type NotifyMessageBox = fn(String, String, String, String) -> dyn Future) -> String { + let mut msg_vec = Vec::new(); + for ref d in displays.iter() { + let mut h: HashMap<&str, i32> = Default::default(); + h.insert("x", d.x); + h.insert("y", d.y); + h.insert("width", d.width); + h.insert("height", d.height); + h.insert("cursor_embedded", if d.cursor_embedded { 1 } else { 0 }); + msg_vec.push(h); + } + serde_json::ser::to_string(&msg_vec).unwrap_or("".to_owned()) + } } impl InvokeUiSession for FlutterHandler { @@ -316,17 +330,7 @@ impl InvokeUiSession for FlutterHandler { } fn set_peer_info(&self, pi: &PeerInfo) { - let mut displays = Vec::new(); - for ref d in pi.displays.iter() { - let mut h: HashMap<&str, i32> = Default::default(); - h.insert("x", d.x); - h.insert("y", d.y); - h.insert("width", d.width); - h.insert("height", d.height); - h.insert("cursor_embedded", if d.cursor_embedded { 1 } else { 0 }); - displays.push(h); - } - let displays = serde_json::ser::to_string(&displays).unwrap_or("".to_owned()); + let displays = Self::make_displays_msg(&pi.displays); let mut features: HashMap<&str, i32> = Default::default(); for ref f in pi.features.iter() { features.insert("privacy_mode", if f.privacy_mode { 1 } else { 0 }); @@ -351,6 +355,13 @@ impl InvokeUiSession for FlutterHandler { ); } + fn set_displays(&self, displays: &Vec) { + self.push_event( + "sync_peer_info", + vec![("displays", &Self::make_displays_msg(displays))], + ); + } + fn on_connected(&self, _conn_type: ConnType) {} fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str, retry: bool) { diff --git a/src/server/connection.rs b/src/server/connection.rs index 53ccd7008..1a974c51d 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -6,7 +6,10 @@ use crate::common::update_clipboard; #[cfg(windows)] use crate::portable_service::client as portable_client; use crate::{ - client::{start_audio_thread, LatencyController, MediaData, MediaSender, new_voice_call_request, new_voice_call_response}, + client::{ + new_voice_call_request, new_voice_call_response, start_audio_thread, LatencyController, + MediaData, MediaSender, + }, common::{get_default_sound_input, set_sound_input}, video_service, }; @@ -672,15 +675,15 @@ impl Connection { .collect(); if !whitelist.is_empty() && whitelist - .iter() - .filter(|x| x == &"0.0.0.0") - .next() - .is_none() + .iter() + .filter(|x| x == &"0.0.0.0") + .next() + .is_none() && whitelist - .iter() - .filter(|x| IpCidr::from_str(x).map_or(false, |y| y.contains(addr.ip()))) - .next() - .is_none() + .iter() + .filter(|x| IpCidr::from_str(x).map_or(false, |y| y.contains(addr.ip()))) + .next() + .is_none() { self.send_login_error("Your ip is blocked by the peer") .await; @@ -806,7 +809,7 @@ impl Connection { }; self.post_conn_audit(json!({"peer": self.peer_info, "type": conn_type})); #[allow(unused_mut)] - let mut username = crate::platform::get_active_username(); + let mut username = crate::platform::get_active_username(); let mut res = LoginResponse::new(); let mut pi = PeerInfo { username: username.clone(), @@ -833,7 +836,7 @@ impl Connection { h265, ..Default::default() }) - .into(); + .into(); } if self.port_forward_socket.is_some() { @@ -877,7 +880,7 @@ impl Connection { privacy_mode: video_service::is_privacy_mode_supported(), ..Default::default() }) - .into(); + .into(); let mut sub_service = false; if self.file_transfer.is_some() { @@ -893,10 +896,11 @@ impl Connection { res.set_error(format!("{}", err)); } Ok((current, displays)) => { - pi.displays = displays.into(); + pi.displays = displays.clone(); pi.current_display = current as _; res.set_peer_info(pi); sub_service = true; + *super::video_service::LAST_SYNC_DISPLAYS.write().unwrap() = displays; } } } @@ -1160,7 +1164,7 @@ impl Connection { "Failed to access remote {}, please make sure if it is open", addr )) - .await; + .await; return false; } } @@ -1324,12 +1328,12 @@ impl Connection { } } Some(message::Union::Clipboard(cb)) => - { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if self.clipboard { - update_clipboard(cb, None); - } + { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if self.clipboard { + update_clipboard(cb, None); } + } Some(message::Union::Cliprdr(_clip)) => { if self.file_transfer_enabled() { #[cfg(windows)] @@ -1512,15 +1516,15 @@ impl Connection { } Some(misc::Union::RestartRemoteDevice(_)) => - { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if self.restart { - match system_shutdown::reboot() { - Ok(_) => log::info!("Restart by the peer"), - Err(e) => log::error!("Failed to restart:{}", e), - } + { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if self.restart { + match system_shutdown::reboot() { + Ok(_) => log::info!("Restart by the peer"), + Err(e) => log::error!("Failed to restart:{}", e), } } + } Some(misc::Union::ElevationRequest(r)) => match r.union { Some(elevation_request::Union::Direct(_)) => { #[cfg(windows)] @@ -1530,8 +1534,8 @@ impl Connection { err = portable_client::start_portable_service( portable_client::StartPara::Direct, ) - .err() - .map_or("".to_string(), |e| e.to_string()); + .err() + .map_or("".to_string(), |e| e.to_string()); } self.portable.elevation_requested = err.is_empty(); let mut misc = Misc::new(); @@ -1549,8 +1553,8 @@ impl Connection { err = portable_client::start_portable_service( portable_client::StartPara::Logon(_r.username, _r.password), ) - .err() - .map_or("".to_string(), |e| e.to_string()); + .err() + .map_or("".to_string(), |e| e.to_string()); } self.portable.elevation_requested = err.is_empty(); let mut misc = Misc::new(); @@ -1571,7 +1575,11 @@ impl Connection { // No video frame will be sent here, so we need to disable latency controller, or audio check may fail. latency_controller.lock().unwrap().set_audio_only(true); self.audio_sender = Some(start_audio_thread(Some(latency_controller))); - allow_err!(self.audio_sender.as_ref().unwrap().send(MediaData::AudioFormat(format))); + allow_err!(self + .audio_sender + .as_ref() + .unwrap() + .send(MediaData::AudioFormat(format))); } } #[cfg(feature = "flutter")] @@ -1583,7 +1591,7 @@ impl Connection { "--switch_uuid", uuid.to_string().as_ref(), ]) - .ok(); + .ok(); self.send_close_reason_no_retry("Closed as expected").await; self.on_close("switch sides", false).await; return false; @@ -1596,7 +1604,9 @@ impl Connection { if let Some(sender) = &self.audio_sender { allow_err!(sender.send(MediaData::AudioFrame(frame))); } else { - log::warn!("Processing audio frame without the voice call audio sender."); + log::warn!( + "Processing audio frame without the voice call audio sender." + ); } } } @@ -1646,7 +1656,9 @@ impl Connection { pub async fn close_voice_call(&mut self) { // Restore to the prior audio device. - if let Some(sound_input) = std::mem::replace(&mut self.audio_input_device_before_voice_call, None) { + if let Some(sound_input) = + std::mem::replace(&mut self.audio_input_device_before_voice_call, None) + { set_sound_input(sound_input); } // Notify the connection manager that the voice call has been closed. @@ -1821,13 +1833,13 @@ impl Connection { lock_screen().await; } #[cfg(not(any(target_os = "android", target_os = "ios")))] - let data = if self.chat_unanswered { + let data = if self.chat_unanswered { ipc::Data::Disconnected } else { ipc::Data::Close }; #[cfg(any(target_os = "android", target_os = "ios"))] - let data = ipc::Data::Close; + let data = ipc::Data::Close; self.tx_to_cm.send(data).ok(); self.port_forward_socket.take(); } diff --git a/src/server/video_service.rs b/src/server/video_service.rs index bc9c5ff6f..52b1717c4 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -65,6 +65,7 @@ lazy_static::lazy_static! { pub static ref VIDEO_QOS: Arc> = Default::default(); pub static ref IS_UAC_RUNNING: Arc> = Default::default(); pub static ref IS_FOREGROUND_WINDOW_ELEVATED: Arc> = Default::default(); + pub static ref LAST_SYNC_DISPLAYS: Arc>> = Default::default(); } fn is_capturer_mag_supported() -> bool { @@ -407,6 +408,43 @@ fn get_capturer(use_yuv: bool, portable_service_running: bool) -> ResultType Option> { + let displays = try_get_displays().ok()?; + let last_sync_displays = &*LAST_SYNC_DISPLAYS.read().unwrap(); + + if displays.len() != last_sync_displays.len() { + Some(displays) + } else { + for i in 0..displays.len() { + if displays[i].height() != (last_sync_displays[i].height as usize) { + return Some(displays); + } + if displays[i].width() != (last_sync_displays[i].width as usize) { + return Some(displays); + } + if displays[i].origin() != (last_sync_displays[i].x, last_sync_displays[i].y) { + return Some(displays); + } + } + None + } +} + +fn check_displays_changed() -> Option { + let displays = check_displays_new()?; + let (current, displays) = get_displays_2(&displays); + let mut pi = PeerInfo { + conn_id: crate::SYNC_PEER_INFO_DISPLAYS, + ..Default::default() + }; + pi.displays = displays.clone(); + pi.current_display = current as _; + let mut msg_out = Message::new(); + msg_out.set_peer_info(pi); + *LAST_SYNC_DISPLAYS.write().unwrap() = displays; + Some(msg_out) +} + fn run(sp: GenericService) -> ResultType<()> { #[cfg(windows)] ensure_close_virtual_device()?; @@ -529,6 +567,11 @@ fn run(sp: GenericService) -> ResultType<()> { let now = time::Instant::now(); if last_check_displays.elapsed().as_millis() > 1000 { last_check_displays = now; + + if let Some(msg_out) = check_displays_changed() { + sp.send(msg_out); + } + if c.ndisplay != get_display_num() { log::info!("Displays changed"); *SWITCH.lock().unwrap() = true; @@ -798,11 +841,7 @@ fn get_display_num() -> usize { } } - if let Ok(d) = try_get_displays() { - d.len() - } else { - 0 - } + LAST_SYNC_DISPLAYS.read().unwrap().len() } pub(super) fn get_displays_2(all: &Vec) -> (usize, Vec) { @@ -861,6 +900,7 @@ pub async fn switch_display(i: i32) { } } +#[inline] pub fn refresh() { #[cfg(target_os = "android")] Display::refresh_size(); @@ -888,10 +928,12 @@ fn get_primary() -> usize { 0 } +#[inline] pub async fn switch_to_primary() { switch_display(get_primary() as _).await; } +#[inline] #[cfg(not(windows))] fn try_get_displays() -> ResultType> { Ok(Display::all()?) diff --git a/src/ui/header.tis b/src/ui/header.tis index 1fb694397..e25c0d544 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -480,6 +480,14 @@ handler.updatePi = function(v) { } } +handler.updateDisplays = function(v) { + pi.displays = v; + header.update(); + if (is_port_forward) { + view.windowState = View.WINDOW_MINIMIZED; + } +} + function updatePrivacyMode() { var el = $(li#privacy-mode); if (el) { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index a86f07d0f..4794efb65 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -53,6 +53,20 @@ impl SciterHandler { allow_err!(e.call_method(func, &super::value_crash_workaround(args)[..])); } } + + fn make_displays_array(displays: &Vec) -> Value { + let mut displays_value = Value::array(0); + for d in displays.iter() { + let mut display = Value::map(); + display.set_item("x", d.x); + display.set_item("y", d.y); + display.set_item("width", d.width); + display.set_item("height", d.height); + display.set_item("cursor_embedded", d.cursor_embedded); + displays_value.push(display); + } + displays_value + } } impl InvokeUiSession for SciterHandler { @@ -215,22 +229,18 @@ impl InvokeUiSession for SciterHandler { pi_sciter.set_item("hostname", pi.hostname.clone()); pi_sciter.set_item("platform", pi.platform.clone()); pi_sciter.set_item("sas_enabled", pi.sas_enabled); - - let mut displays = Value::array(0); - for ref d in pi.displays.iter() { - let mut display = Value::map(); - display.set_item("x", d.x); - display.set_item("y", d.y); - display.set_item("width", d.width); - display.set_item("height", d.height); - display.set_item("cursor_embedded", d.cursor_embedded); - displays.push(display); - } - pi_sciter.set_item("displays", displays); + pi_sciter.set_item("displays", Self::make_displays_array(&pi.displays)); pi_sciter.set_item("current_display", pi.current_display); self.call("updatePi", &make_args!(pi_sciter)); } + fn set_displays(&self, displays: &Vec) { + self.call( + "updateDisplays", + &make_args!(Self::make_displays_array(displays)), + ); + } + fn on_connected(&self, conn_type: ConnType) { match conn_type { ConnType::RDP => {} diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index b225151ff..5a83ee572 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -761,6 +761,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn set_display(&self, x: i32, y: i32, w: i32, h: i32, cursor_embedded: bool); fn switch_display(&self, display: &SwitchDisplay); fn set_peer_info(&self, peer_info: &PeerInfo); // flutter + fn set_displays(&self, displays: &Vec); fn on_connected(&self, conn_type: ConnType); fn update_privacy_mode(&self); fn set_permission(&self, name: &str, value: bool); From d95a03924ee2421205390b45574ab2be7e5a7f08 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 17 Feb 2023 13:47:09 +0800 Subject: [PATCH 169/199] fix build Signed-off-by: fufesou --- flutter/lib/desktop/widgets/remote_menubar.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index d7d944cb2..e82e9d26e 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -415,7 +415,7 @@ class _RemoteMenubarState extends State { if (widget.ffi.ffiModel.isPeerAndroid) { menubarItems.add(MenuButton( tooltip: translate('Mobile Actions'), - icon: SvgPicture.asset( + child: SvgPicture.asset( "assets/actions_mobile.svg", color: Colors.white, ), From 4bff430fdb196d8211d30d8ab9de7b8c923d9b43 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 17 Feb 2023 13:58:16 +0800 Subject: [PATCH 170/199] fix svg warning --- flutter/assets/linux.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/assets/linux.svg b/flutter/assets/linux.svg index 5427305ba..1738a02ee 100644 --- a/flutter/assets/linux.svg +++ b/flutter/assets/linux.svg @@ -1,2 +1,2 @@ - \ No newline at end of file + From cdf9867b5c368370c4c9a79c2b5c99bd13a912b6 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 17 Feb 2023 14:33:01 +0800 Subject: [PATCH 171/199] fix update options without auth Signed-off-by: fufesou --- src/server/connection.rs | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index 1a974c51d..2e2bce3e6 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1092,7 +1092,8 @@ impl Connection { async fn handle_login_request_without_validation(&mut self, lr: &LoginRequest) { self.lr = lr.clone(); if let Some(o) = lr.option.as_ref() { - self.update_option(o).await; + // It may not be a good practice to update all options here. + self.update_options(o).await; if let Some(q) = o.video_codec_state.clone().take() { scrap::codec::Encoder::update_video_encoder( self.inner.id(), @@ -1496,7 +1497,7 @@ impl Connection { self.chat_unanswered = true; } Some(misc::Union::Option(o)) => { - self.update_option(&o).await; + self.update_options(&o).await; } Some(misc::Union::RefreshVideo(r)) => { if r { @@ -1665,8 +1666,7 @@ impl Connection { self.send_to_cm(Data::CloseVoiceCall("".to_owned())); } - async fn update_option(&mut self, o: &OptionMessage) { - log::info!("Option update: {:?}", o); + async fn update_options_without_auth(&mut self, o: &OptionMessage) { if let Ok(q) = o.image_quality.enum_value() { let image_quality; if let ImageQuality::NotSet = q { @@ -1691,7 +1691,18 @@ impl Connection { .unwrap() .update_user_fps(o.custom_fps as _); } + if let Some(q) = o.video_codec_state.clone().take() { + scrap::codec::Encoder::update_video_encoder( + self.inner.id(), + scrap::codec::EncoderUpdate::State(q), + ); + } + } + async fn update_options_with_auth(&mut self, o: &OptionMessage) { + if !self.authorized { + return; + } if let Ok(q) = o.lock_after_session_end.enum_value() { if q != BoolOption::NotSet { self.lock_after_session_end = q == BoolOption::Yes; @@ -1818,12 +1829,12 @@ impl Connection { } } } - if let Some(q) = o.video_codec_state.clone().take() { - scrap::codec::Encoder::update_video_encoder( - self.inner.id(), - scrap::codec::EncoderUpdate::State(q), - ); - } + } + + async fn update_options(&mut self, o: &OptionMessage) { + log::info!("Option update: {:?}", o); + self.update_options_without_auth(o); + self.update_options_with_auth(o); } async fn on_close(&mut self, reason: &str, lock: bool) { From 6def4ccdbdf1ea70fae0183a61d52809d17ddb08 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 17 Feb 2023 14:47:42 +0800 Subject: [PATCH 172/199] await Signed-off-by: fufesou --- src/server/connection.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index 2e2bce3e6..9cdbf974c 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1833,8 +1833,8 @@ impl Connection { async fn update_options(&mut self, o: &OptionMessage) { log::info!("Option update: {:?}", o); - self.update_options_without_auth(o); - self.update_options_with_auth(o); + self.update_options_without_auth(o).await; + self.update_options_with_auth(o).await; } async fn on_close(&mut self, reason: &str, lock: bool) { From 591314617557b283155ddbf124999f2beab9829a Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Fri, 17 Feb 2023 10:44:43 +0100 Subject: [PATCH 173/199] Android adaptive icons and monochromatic icons --- .../android/app/src/main/AndroidManifest.xml | 2 ++ .../com/carriez/flutter_hbb/MainService.kt | 2 +- .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 ++++++ .../mipmap-anydpi-v26/ic_launcher_round.xml | 6 ++++++ .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 3114 -> 3990 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 7492 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 6161 bytes .../src/main/res/mipmap-hdpi/ic_stat_logo.png | Bin 0 -> 1028 bytes .../src/main/res/mipmap-ldpi/ic_launcher.png | Bin 0 -> 1667 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 1939 -> 2207 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 4348 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 3525 bytes .../src/main/res/mipmap-mdpi/ic_stat_logo.png | Bin 0 -> 715 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 4087 -> 4827 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 9515 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7604 bytes .../main/res/mipmap-xhdpi/ic_stat_logo.png | Bin 0 -> 1524 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 6636 -> 9171 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 33762 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 13879 bytes .../main/res/mipmap-xxhdpi/ic_stat_logo.png | Bin 0 -> 2091 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 8908 -> 9893 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 41583 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 16113 bytes .../main/res/mipmap-xxxhdpi/ic_stat_logo.png | Bin 0 -> 3162 bytes .../res/values/ic_launcher_background.xml | 4 ++++ 26 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 flutter/android/app/src/main/res/mipmap-hdpi/ic_stat_logo.png create mode 100644 flutter/android/app/src/main/res/mipmap-ldpi/ic_launcher.png create mode 100644 flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 flutter/android/app/src/main/res/mipmap-mdpi/ic_stat_logo.png create mode 100644 flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 flutter/android/app/src/main/res/mipmap-xhdpi/ic_stat_logo.png create mode 100644 flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 flutter/android/app/src/main/res/mipmap-xxhdpi/ic_stat_logo.png create mode 100644 flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_stat_logo.png create mode 100644 flutter/android/app/src/main/res/values/ic_launcher_background.xml diff --git a/flutter/android/app/src/main/AndroidManifest.xml b/flutter/android/app/src/main/AndroidManifest.xml index 04b2ccc9a..9b25f4973 100644 --- a/flutter/android/app/src/main/AndroidManifest.xml +++ b/flutter/android/app/src/main/AndroidManifest.xml @@ -16,6 +16,8 @@ + + + + + \ No newline at end of file diff --git a/flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..65291b96e --- /dev/null +++ b/flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index eac2fe7241381b7d162fb15323837ea7101e4a84..d05404d3af59e68e46ab3009c3684052323da15a 100644 GIT binary patch literal 3990 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 AwEzGB diff --git a/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..3742f241f4dd449bbf2a5a9fa9289d7e1da2ecb3 GIT binary patch literal 7492 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 O0000yxZ-s@cgAfqT86=9BZr&zDg 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 N002ovPDHLkV1k>~Gu8kA literal 0 HcmV?d00001 diff --git a/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 8c01e98de01749763dc3f6e0557ac66303ceace4..f16b3d61d9817e58e916df8748a8b833981e6b23 100644 GIT binary patch delta 2196 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 literal 0 HcmV?d00001 diff --git a/flutter/android/app/src/main/res/mipmap-mdpi/ic_stat_logo.png b/flutter/android/app/src/main/res/mipmap-mdpi/ic_stat_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c179bf053492ab2f17f09a22cbbc47088a36566f GIT binary patch literal 715 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 BRjdF2 literal 4087 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 diff --git a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..f8ced45f132b86c5080b55773415e84325b8130b GIT binary patch literal 9515 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 a0{;Sf*2F-`L=oly0000 z%X;latk8zMwmHnMv;@c~VuNJ@FnGH;*>NRA!$aV zp6NOJ?yBlKGV|>pRay1&WoC6%byauu(BFvaeEIU1FEii!2Fd_P$^kc2kbQ<+uPX9T( zmPRab!aCMb7rI!g_m(z-D5;@t*J3&9*`oG4L9-GPc3Cu*LKG*746R&`p>*1)?OHAS zXv8LL7fsir*#}q+8(|$?j5YrTT;OPAd{&iaoq<*#OL;wAO{2Tk_CAIueOKaNqy{Teoh-vMl=g`hph~0x4x^+baAvIzhA4Io*tWJ`ce8^XFMu zSm3~c11n|&HI0#x5pKQpRx+6kZ++`q4cB!I%d!wc_}eONn;EECmDf~R2+`5@{j!wO zsPs`=%3c|Go`>T&UOt~El}b4eKm0J?`qsCYn3#y`&!$k1f$Din0o;E3?Oc8J)yA$} zy9~>+EW2b}8|!Ewi%jEx8I3A0gzHUQp$PPbx#$G#V2NFX1+N&Ir-H1`1r|OE|e8F{~rz@*GX`V)JVdarUU(h#`gjysIkyyi8=(9qDpD_{A_w`|z3;eF|J z`t7!DUu_u1s1PC*ckVhm4-Ml!%X_uRw&{rk;yW z+lPmTKWG@nXwy%$j@5uL3}aw$aPWp?GC6Vn{P~H2fq}x!H{a}j^PAs<`T`}iuTEj8 zc`zzMb$8u$7n?V4ws!2;u_v8Qzr`?&k=CAN9jgV)vbJv8wCUfCj*jjYLZk)<2aR33 zc0rQ?QZ-j6F6tbp+p%K@7hQCbkw_#`LqkI^OC%C6Q;+-Zy1wls&02;KVzquoUtix1 z!^6YZxUSo4S(cSdCZRqLgmSwYnam8-9XN1+;o)IRN;xn%ICxvdP_9Dp00P@i(yV1j zDOcqOVB7ZC$jHb(A;g$%+lhPbxu;QYJTfB@8jwK6Vv%$@Z6y+kfn+jyrB1w(F8WH) zTnJLG9@6Rb<-m}XGST1PpQFjVpr$=0*L4jkrIAjjhfLEP(~qx)1nMNsg&^hXA(>2W z1O}v(3EQ@tjt4f)KqDg~2q6;5WO6_V(W@Wt7Rk{rnhRY5S(cTyZ982ml`O+Bnw~9a znt>e0!8A>PL}i{RfNqf-?V`ERB@kc;AyQIGt5hmAe{yBpup4jN_67e|{+hv{EJ%C- z)Y7t^dZH^dk5#$Q`sC;ZgeAr`){R$HSTpOCE;#1NyONobWX|!JcRY$-ZGun$mH}x~ zFk~AH*#>Fz!ji&52&1B|>*ceD_8VDP7=>6%y<3dmdNhtFIh}X;$*jY(8HZnGT%ON5 z)sx%9ZZVrIG;0F|bYM41mnt&>XYwA8PC7hvw!mXE4$mw&EV?qB zYalB-LMmmzzn&14dQUl4Ya{PTj$~bqWV3wrbdF2>i@ue>Oi}Wc7xMhyQ$-HXJDe?4(wbC;@yD~FvMe)^f3mC`P?@iS zyeoNfp~RD!690H^kxTn5-Z<97JGP}6vyE;}j%sN!!?JRBx=8|+Jjt{3E)R?s_`2#*1ePE}tA<;Omo% z+_j~T-`>#6OM0wsl1MEXY^@s6U9)!U7t;m`DQ8b^s)rbEK2_O3q?No%SEz|`ql^Z7PZk+205gv{5pMKrO}Ni9V*jRXgq}pt4uA) zO36P@En*7z`@LhB-DhAo9F11JFrB;VNurMVr%?%Hdim#5E}wiV&p(|gl65uaQ6&dj zT|iP)8>A+EL{J5VR9K9s7GM(X<%%=R5&X++fvJ+m#)R2W-*gyJ-nNe=1Bs2S>8_qA zgy?u6G4kRPzJJ!??niSxGv`wBC=YsdP$4*4l@SZrD3u+R;Z(-Z)mhcfQ6*WWeP5kP zzFG_zW8jBkNq;w`Mj(R zG!S5#K(!yg(c(IhB0-Zbej^b=r3Rn5geGYerc-%KYwz-2o2|*NkUlL#&xQ6?nEr_s z)Uuz(5j88XtDshCR4h7@Pd!=SW4|bHeo<3F)v{JEMQtGcL6jomJpY)i9ru|?r4O}N zkv0K(za6#Rz>`<^M=g`PxA%9GJKt0$qK`NgR(DCWl(J)iL|>)(<-7}j@l=sdJy9U* zRH+o{LRd9b)sK2y@tz{>;wbN|nX+m?OX>w1)0W_!+td8^hMrL0tRfH2hl^pF35~|W@wg{j6Wr0sWSu8V;pu#wj7A`^SS+hAp1F6|ZArC9gV~g9@5W04m zK5ZEs*ge1>?dT)bJ<>-nF;oDnVt2 zCDoaJ9zk`bAD1%vyk^V3FflW$-vUt#%FV)L>a?^}a_|M%pxZ$BsVUb5S~A z_tXnltW>#zC}&D{?RB%J`q`Cwk?5uB+X~l{{P?WL-QUkMvnVwU5inW^1_z^{yHMKs z^@5E;)V%e8`ioJQk?og}HU%HPq@Q>0?CU0@xgOSTAn`;-^6{S*IW`+%txCdin$R?b zUoV4W8D}h4&Py7SZM_CtdJMMp8l-JO!YDs<(Srro<9xy6Ou^&iqD#(C3kjKG!EnG( zLhz2Ay}WB@nnZV}2VEF}#4jFk`1T1G*Mr);mf~SSjgg|_I-x=J4Oxk4?ee zw%FXf$!6FV^jCff0}KQb@T8_H>TVPGmS%C^2XnynZyrfr|&ZDAhf)___^LnI zs}iW*6hyNUXnfw|V~>|8`tG_aWqe5_AvoKvNE}^-_{K}6oH7J2AG5gYk_4~YYO}r9 zV7caqAz&;a`2CA|__teVc;C;9YvhEI=YYB}3} ze@Maqo6d8~RG$B|xwq4^_I^9IvzrZz@&7uM^$lP-auti5?2R3prxy2{preoOGaYm`MAvvLaX43Sx$l{IKJ@GYi;h&D zi`uh#P@Uzk{KgkuzIrBy*FY*v3y70Iq5Vdq&`}pyHcn?H|9sRVAbA355`MiZeIRHY z)Fe;RgtzQT@ZM_`f4hsZ=t}d38ZgB z5-3v7>GezH@2E8O@_`;b<#A-P^14-Rn>brYwR_Z`e~#dWjTV1=U4o6NZkIdd#fYCe zn&opZX33P4Foi04)H2G8MY;ZPro>}&#h5E}1R(*VOCX&vk_+nv5`8RXL@yL2-+sa4 zTvk&{fsRyvQt3>TPA!)=_6q*;RY~>?8eMHL`}pqpBKIH9vFM;U2kKTT41}iI%D=Nk zmv2w!S28UAxVt}0=i1X@xOEhT|Y%u60W) z*Or0=o|tnuSMb8mrDqjXRhaVN)hfZExe_M})lWgJIHD%a)cQCHgu0EwQWEItNy#%) z$}u&TBbCaK3$K+C+_=f)j!Ud+)VCXQZ0ePzgp!mM&y%2iQ-vaFtCCq!XO%uUk}dIc zrui?6HGqcmf#Ob98;MGJU6urT=$OZ1aS6R2$WbP^UpYRO6#U^;HZM`!^==3Z)+-wV zuI{(lkTiqO;{`p+==fJqwPhX2x2LKR*mk0!e4tecBo{fl zp5xS6kpA_%Og8teG4262r%ZmcGH<3k1Z4>ASE-9|=U>PR^&uDr1~ZZ_G}yY{4yk4;IFmk!zl<6lImY^e2zDv_w3@NCYZjp*(` z;;$xs{jJ(N5M&Z+WnG+YQhw7^OzmsWmOOgAjUf#zsk0if#R_Gfy7Kv z^5QI-aTpgogh0nCZfvgw`?ju)xKBO2YAjLy{GDn{$Mr?vr2^F>JD&5(DIlH2>IV{M z7a{M+u+I0P&xM6LuYB&s0|sMVwXm}_j3o>{iZ}t&o+C&NyD}p$#k&v;it}f6~FR8v} zB+6i2sN;xWurf>mBJSXtf-FCQ-XE%XiG0k6{%a4S_#u`TT2%s7NmWmcML{Tk!(n>` z0Z&Assg$K^V6I_!lJY`I-Az|R7=b{mPosHftWIzEwoa6>92L$6>Pw!bU>jfxNp}i^kcy`k#Q7X7Ixy^0HxV6gg{D*O=UiF-Y+vlNEBip? zcN`JeLeSHme?ir>3Ts6IwQOvsZ7Lrc5D}6_71?M~N%2Z#Iiqnd-&VJERWM(aoLa1O zXFxrnNn~I8`2FNFQdycOgdlAO$GMSsT?88Tz#tqbjH(YES8uG5w@$@y5A;|lDoiU^ zFG6%(#n?PHBbi)m>e5|_iA9goi&|={o2l_%Pb!b1qjvkYd_I!Y- z^AbpHpjin7uy>QHt}2Lp+Z38m=fb~?dw6S@hf1F02j@!EJglgfN3nSV>P)4VT-D#U zPrfuQfm()|9@z}8+$6#b5pq*CngGfjcW6Q~9ihfL?UA($M;AN}&-(9TgbG6`>$ag5 zM7*khb^d8fs|-|CVXa7@8+Smjt*b2=A)d8;ErW3?3lILv3-MQ{J+gM75&<^%!|o9XyhTx&h(6mzvpBAhA_9J}?*73SPShdaMX(Q}s9^ zyfZ)-;pZ~&^!diZ3EdJ%d}7+=t4B-ZotPIRBxz*C@$$JzLvZWH#FE!-0^J=*^l{ZD zxNL(G%kU=?19`zq2ODie&^`b3Z)rFl{LQmPo}E>fBUiXW_e298neL62=`T&2Ts;`w z0T*N-(Z`MQ*Bh7-Pta|F#F7X9{qX|dop36uD=HEn5j+g+E89w6+_ojb_MX*M z->VWx^kEu;-@6`m3=)3hVuEW!DZ!`TVDSF^27@tQBI}kwqATI)8IOPeP>#PpRw^$h z@Rw-E1rI}g5KX^1W$*_tNztARj@Gd%fkYLp^8-rBnH|7e)n3qZmZvc znju+<361m!?tPWPyKXcvBL2lwHw6+)9(?Dd!`mNT`Zhjg>`KN5>;Hb0p76>hEu8=OSMx+df|V)NpR1R9lrsX(H-tVdtA z5!*;R0*NXtQ}CuMB|kU@fBRGAO%;Nz1Mt3A!+*S11k_fe%v*BdyDxY=aaNLd;Hr&+ z8@CCz_KUW8ZzWf9WJdD!;|^bXy2R0W4_VeVRMT~lR2>5W!DTv19T5DtQ7VVQD@JVI zygh+kS=Li87N@R?Y2szqm++`{p*4f8=0t~XaJksr4u41L*|x*@A>wUVA1fube12lAI&R6eXv!lJAbYJba?eG}RTw0!8XT zbz(S>2#>-%Q0`u#)v26RuCxuo+b>JE5Sz|@%ZvDB#R{!DWJanl1u@<_oC!`FM3!OZ0>{2X~Bj*NGAjdQ{n;Hl4Pz3 zlZ%qGImvWh;-N?kQI-&>jO0s{sx*;`s!|vW{k+Q74>ndL&#N|C{Flo+`UUhCE>N>OgkG+kAgD1+z>W#p zZ?yv`_Dqx=q^LP{zjC`f(+2ll)5D&D?)x56s|=(X)IfJI(&+{G&_Rzc{Y+AD3C3D< z#tF>I23~y)I(^jn^`KeGs;vX%rKy1>-N9QKP6|G=uZP!e?Rd&TOO{|;0*R~1JpQ#a z1^DnmkFWe(Qg9%k8X#!M)=Z1=ezmN`p(1HS482~K!8W3%C)9f7zJ?Nl_v}sb8{1bT zU9~08dRc+QZh#Pi`8@o|!yaGyi6mE$F$_U4rlO~dh9+@+F`$=~i|g%HnLtrh)oZB? zCnDlNGCQ{*P7P=+)JkZ~XYSP(=F?|5Ch0tGV+5o2P0F1-3nw;Jw!*_~YwR^u=hdMqKnJ z-S~QA8gk$D19741DCyMpKw?Du4byz)hwyhlr~KcZr81DJ`iWF$LZMfrmfD7{e)P%C z>Ukna6HRhkH_Tp$rvmT`2|c47w3%=_G+*eQYN?-J^)TBoTq^_Uvt30$jQ z`}$x-9Ir5^DFnNR1fRJ%#cQ`)tCb+vQXnyEB2}+nG9z)NjB5QlLp{)IEA|Y8)r*fP zZwi8pp{uY^W}x4OYW!-~i&CMXa;^P@DR|uui_gENhg)~78uLJlfy8AS!>j4WZ|R5E zUJ`bHqgQrBxXUtzz6#$~igURYNffz~#6MSP=a$QsA-MY*o6o#D#Z6nfFG$!3)^s4z zGU3nORL*QX_#Dg@)Ysp)QwRs@>IM^+I<7NV6n)hBH4G+T|2B(zUtx3ewyt5EmV`;G z4Ai!a#N}eonB>!MhOa*jpL-OZJPodzm?hLfRlFjhcw*J@R_H^#AVba6Tzbt;s?w+- z127E1-Z8=3ud(>8-4@%@tEaCb(2{4}YCcda*Xjs_5NsQS-@gHFx)>gKLh_}@1t(^u z7S~avQlJ5}fmbTjm`}iy@;yA&!_vzr^su?l;P>{L{D;ddt{fFuqDHQxkvyJu(KLuq z9*~bUA4v3JnXq>=?AG7 z)EOZCd>vV%uiixU;#C#an}Ewk1TWuW@|s-+`?ncvO_%L7{XXNQgI?E38U<>Qu&Inh zB2HKnfkYMh+h!Vqi-zH%VZj?OhZkogPo5DRJSsSJLh|CQAXoBLk>3d6a@Dr+nMj1} zqLeMTcu25+r^#>Z7F;oEu&o~klHtM9@3WIZ!u3F+hE{d?P!A0C2(H}BE{BwoTrQV)T{jbZXUo8H24F3HD{-k~D?w8%7ITi{6on8@DwT>OYhOm9<2Xnu zom?)LaU5ql_Rf}pRs)Gq+vU=9y8TXO8#Y3d&1R>8Jm6FU#7%#pOx;G+2ae_Qd4v#- z>$;gtCi8r^7}3oH=$fJFbo;9rNR(1CJ3D(4$N)~MRDvc0iE5xwh02Kmt~F1R0ssI5 zc}YY;RP5{P^E}V~p}ZVHm|ysT383 zCZewOjKpjEX%5z@yoRX+JW^z zVi>OLdh_%1GlvcxdT@Gr`Vru;X_}K06BEU+eeG*ax7RcS0ZdFxuy5Z!1_uXSAw+3z zZf>qnC^&{;By8L6w=BzEDF&*S#?VO0L^Ej>YaN+PCUff4siQ}Z9C>7BX6B%j@@c~` zP8SM=+yf6hAYXXlg{IqVDI*ceIy95XdBv$;rv10|NtBjg5_6IW#o1 zxwp4>z_#s_5W-L=U84q0-F7P)xzBE*ky3hvLLr~YWaejPX3kAbO&yz`pMR!MC>#Nv zG7RGkkU4SUgm>`Z!8rQ|>SQE}IwviOJ$v@>{`bG%=s&~0bhko@F*&a5=70q$<+Kpuln~+!aE`L)RXl$DxO3lq_qBYjmNF83JpcUj zOioUEM~)nEuD|~JX(2=gI47k%3G@Of*L73GN4(e30Z1vm%8Tj&MIbMP$Vn;ZD`S+; zX0y)R+?;pcefPy?8!CvUS(XG+@rh4-g8u$~qqn!$ux;BCLKuK0rL=r8yDoiJ2I?fY zX_`)jp}bP5RLtk|-h&T5$dgY#>Am>ki_5;2KcdZMB1nk+0V zVB0o5Jw405;dLwx$8lI#SYTja0L!w-X0uF9P4R&be4wdp!$xS@CV@JH^C&-P$>gNVRrL dzIC*K{}0+eht(&$YL@^2002ovPDHLkV1l$fv6}z@ literal 6636 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 qM%IAf*eA5}h(mYB5E4T(^hh^D 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 P6AX}(Qj)9@Hwpb8pbK#N literal 0 HcmV?d00001 diff --git a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_stat_logo.png b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_stat_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..2cbe6eaf1e94d22a655131ef5ddae7ab5917fc55 GIT binary patch literal 2091 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 SCmh2&11QO<%T`L82mcS4z>B&7 literal 8908 zcmV;-A~W5IP)Z-1uTUF=Wd+x2f zSY%m-POwl+p}2tJJc^MNgDA=6jr zak(0Qrg)v=FBB^&yovm9q5@zleT-sGg1;jm5G|O?_<|a{DZWLqJl-FRR{&OuUsK#k zA;tU01q2er7>c(jo};*r;$*Bp5RU+^gOB_};y718fHznMd^5#W6!%m7EtZpG5dix` zk5k+m%O4OBFbE|SPr+x$S$(uCnv<4mnwKp<7YN(0v9KA)m7(wCzVKm~kF zPZuVZE&&0;eu2J-2^1et%t9CugcAVs{EH#TFVJ@}g5phz z83<7)2qyqm;r*fxi4qVn5)mk3hO$0ep7oslWrl1mJ(kB_NPa z_$_>0Cu+4jg9$*qmMuJs_Z=`tMzavlpKZWSq zBaq%;b=#lKLmlv&W8J!e%*$es)Y&@Qs%G_)dkY_fbFiS!yML*h6nPb8rs~I)977WcbV|2a+ zHS}4ML?N9ZzJ~w-us6QfCM`LA6pZYv8=Y9!?83HIH#WDrP)CG6VBY?DY%av+(t%69@s1 zEMsen8|#`}M0%Il|osc!T8R z8g@n^vvz+1A_zRM#U*1!qXR3N*qHKmWD)mgkO*$5%)=M@=aQW=^SmwT0@kznLH|JH zOOl+JlmX-1CbBfo9kt{6V-B3Kt4+qe2#WZ~+~beMa9#uvXwRdn2HtCSQaEte-e$}# zvtnU&K0aHKOX3h#Nd+5tKb(=D)rt#~oS3BJV21}U65*E~v13P@`^n5b|uRj?O5JqM;Y0oYb&g{r79nnl;()aG%%eB;DThQBAHls+J&F)>cSiKPTvkc z=_Hjwv5-#7yd>G7-swU2@g}zyPap46Vq~5LcMU4Q?Nl=4n=`h)FrEqE++?RB8F-Jp z{lnY3u$Ba%pcg6Q6hYYY)IVgP9SU`Pf0r9~?{C2`j<(~WnnElbkdNHV6T%24fRV{g zg;8)*rS_$xPW)lN4XaP7HYaW}a}t4pb|~0+bq){i+Sh_dYTJ|&;jTf2D9y?kA=EGd z3`%w?jKBvcUASvw2e!3%d^@<1nPW*{upJ6Kewu2|Kdf!VZ;rL${Tw)Dz zXVU`3OaQsbPK6$5cggtOZX15J%Z@geN zEAl%%GJdqxhF|WmEB$Li+nsE(adkXjA&`^v*#ekrl!Y z`FDi*UDjy)QiBcOsSyH58FWzP_SO$N@yZcb2*UXHMIxOBsVIUTX-l3R+TGez1qwqlg!FC{a7)@qZ_c&?g1(=1~{ExT;fW>QoTvA^VXkb||X! zs265rNf5yfjm}Fi`@_)Z1n~SpCl;*giqOgvq$EfsI}}rT5+E~YJ9Jff`n?qEGXhX5 zyE`2C&d2sMMRBa81EM3V5q2o*^r&AlS_A>te_j7vOf1Ysx}i@9z)KJD>mS*Ght3cy3; zzj=iXFCGeP3NIdsa{Bugpu7&PZ7SJhI8*BR>N-8zJmaG$JT6OIS9s zJnhNPkdgxU(@rNI-KZU-ixdz+D%zn?*AXE-VMM@Yp<6bo9ODYpugyOgQceI%_BwIr z`*z<4N%No>Qb7dDcB;8Ibe)kx~NKa>|1{R@npgrkV%>l9wuW zD4z6a?rpI}QgZ%GB0odQ2%yO>>0t*_O#~@uhvH2S{WFAe{$-QPa9)Pk z9xYNr0Bp8~>)*0rUvmVKrv63#N2m-bZvP0$Bz`Oz3m|dBLS^X_IyNJ(X7ANc;Y-o1l-3BNA z+u*>Sc27cfXh@y~Yc4F$=$zk=qzd4{)eanO*W^|V(U8u6LP!yWC_!`(L0OK3n}%fL zvxBp7F%e&n*_W$`sMic&k%Ql&5QjTGc(KlorN`{p+7{|%VZe{5`%0)(dVNZ%;pA_3 zd$F?FjsrF~m3$J$7i3{ZaZb_~_DGrlUOwQ)pSEeHCk$C^$7_f569F3+a@|?kxUMD} zmse-OYBu&E4#_v;fl>KN>?e!#&rj`m=BN$(Nf_bm(8MAOmP{(aSOaYjn|kqk2V3y3 z1{-W%|AX^Av+8U!?i^Hr`vw=mnxtvllOllAHnKh|H0}Lb()HNq-;UP~@fAT4dG~Le zor7?M9AC?eMP`HbLZ$B z{CHw6N^??!_<>+`jX4!LO6(+Cw5i1n2j#d-Ak#~YFQtx<)tR&D6p0|%T&(I2^_jiZ zXvdEax8jkZ#;?+4xB&Jwd-40tngu9gYQ-?t4l&Lw7@mXgjP>iM6$pp z(z@c4(~A935VSQ)J)l0b$BwjNet9mY73CP3Rzn5AuJvmx9k40?^hiVE< zcy?AEW>#f(GhwW#rH6Yt<^1*C>#1oTMi>z=O5Cxh30tOA8ESO2F1&ooh8z0l8J1?l1Ymc| z_|_`*g;%I1yCmD8V#@6QI3pkbGdz7qKg%rKD~r*AAUiV!~_l@-e)q?|A)G!9mLTFW+z)C!F5MIXyx>FW>R) zRDXEpL?^yHpumu{8zKP4!#{3`@WLjEcId)NGv2zQKvjc9Q4Ty~1; z5;hN>tna`#2kF0hukHe16Iy+LwPsRPIh+WLwL^EDosB1FSz$`@>-fG1)=BS*j~V&B z;WMo`Mevi_HhiVJ0699B0=f&}?ZaMdKc&gJ8N!MHagjOfdHz4HIq%vUiy(iTVC0{( z?lcIH zU{fc55&WgDqi+jf+bJ*JKNf7AF8{Xic4%BVRe0y;qfmJMn6bN^*`6kxc7}MSoJFv* z*@1msE{xJ~R%%@Z@ZuhSC#WcFivf11BnNo)$~-~-IIy$bg}EOQ`5kfs-afYTWTh17 z#IHx{*GEV<0k8>epWo#P$kF2!LAdQG?GWM5vvV=DC^b8?r84U69$ZG`Kk4An)EV1( zi|ae_qhUoT&}oHA-30I+lWBWZ>ZKV>z$M z?Zv-NcH#DFeO`>|CV-_oJw11DDGB1UL>7|ILi_Tin42unCHv|1@yyhIU(j5iC8? z#{|&jlF9#Tic1DAB|)U^-gd}BCFhdMa#4^ae1AMx-|RxOe<>=4UN#fDi(nO1hU@HZ z47Bp6t1bfg&p|I*luRSY#T#sgzD(BVf-2F!CtlQcc{KMGXP%j!BJe{!kBy{!v(b)4 z1NoN%x(ML4eH!}{P!aX*gCTavLcD)yvgp~F2=e_S$$}CT`iLONbAGqU(KiI(^vHN) zzc)0Q0mJOjz2mbmtXQ=AB!cN>S<1*bWnH!qHd8kd;LLd|NdO-DyS%$ea~HtI<1$(t z>W4@7bkqFCJ%3&bOyio2(nKnJG6pIfs_}(+GA>pqaib(iydO)!S5a~ z+WdLpGgUblU^V&P1q7*UDKNkeEkA9?4~G?Ty*BOwSXbwt=r9U0Yxi@sLpO}FpwudI ze_qI;O2C~%^YH!M9U;gHL+sGUWP#e;US;Jju2|2NDQk~u{C~H_G1d-M*|7BgaYI2(9LI?4H-+CiMVC370Z>%a5%C;S3AU7 zf_Q?f0JfdfSekgqtVNt6_}p+|fAqq9sthyt?M>pMA#C~;p5Vq7#?dV&LS2~pS!X*` zT_|CCwRrKR7s|3tm|SX6)+*q!NJl&(IPUP`w9|ur*?ep+Hvu$s$~e`{O!oa} zjVu-9Hv(*hnNbB6>}d1X17NrvYH)bL3;YD&e*+w=2sqiH5hbGXYZS&5k^ox$>$|~l zJJdZDBL{F3z^P95Z{VQZf zv^DzJq4rc1KqnJG)MSn%0>o^GN^(RAU=&KSG}x&d~`=}kcaxV0@)+)9Gz?NGMC>)>WGEZsRbVz4#*?2w0F zD=5d_pt73Cn8_R<0%(uNV21<@gB1b5&#>llaRNM-wt&*!wTq*CWqFl#A|Ef7lD;y5x`9Vh1u%c z;Djr<@!6p!o3KDe;e)SL?9B5 zcByUNv630!Vuy-Taaxp${7{ND#tt286D5FA*w^L>c#i4;gX~Z#$EohQ381nd1~Thl zhmN!g0T_k7ZGkI9!caR@o|S?nz~Ew_$IB_bM1bJu?QIkSFak{uFIgbXo2Yn2fH=>i z|0;9%4SnS%fT5-8hTbC3LSu&xQwh*)m&KHbI%8XlYJU)ftOnVkfz}i(0fv+Y$)b(4 zL$Ysb_REF}0qBXX&Dw>}6BI$5c4(kA1xtXlE70>M_;UyY?a<0vF9|>ljL{QoPq`wK zRcAXiG%p1OFrZLEc^=Tn<^~G92!rg$PWE;rw0#RBy12*{BE~B;5j0J zk|5G{Xgf5sUkaB1EZx%v1F!B1M^@-=hyJnEi|kkaIL!%1K z7@W&@0%Y#lppWA zz^tL_MkKQ8Y=<8Cz>Sp)#6sDLqS@ia-}d|01q(rD1_SKS{Hi`6fT?7G3Uh$=?sh z>f*beT1LT3_xr1ByWn7b-(n#%gAR7+3LWeZgDwKNW{k!LMJodBHYX7bFXf+OWD5RO z>%u$rD*I#YwW)xw9lB~j9}<8yOTuT)0hW9cjO-EY4uag!I7Gk(>0dZl2p}HVJu)8N zWQ%;?m2C}~5qO-B9U7c#!nk66-s|Z0wrJrL62K;e$kD^uA@x{-CBe5ZFd>iQ*wIV@ z>+$*ZHC;H!hCRk=Yr4M+P9pGa-#)|&lTI&nbQ8erVG@Rw1BaSI*X*OJ_{JfE^D2Sw z%@iv}#)IGQa^mUz>T2v0?v{Qms8z^~sA;S+VTL#0;Wx$7)S zYm`82*wX011M6+U?{|{y(7ehl4B|KdCJ?%Im{~BH2>=mn4L>_HycAe=vjt--1o>md zDVrBJuI%)!{ur)=RSp|xhwdGf$N5ToLRSHd8zAB0VKP=7VxPk0=!x5*5oEVk-(gXw zXAy`6?M@k&ztxF-t?FzKk;sY&$2r-dQAK9VtK!@FEfBhQnY?=@u<9Vr`UT-kyE&vl%;~%-4 zgs>P<=IGIjU{nR!p0AjF(=rOggjTXWpZ#wqRvq`x^Qj?gv?AyRe>-$Lk$<3$m0v#$ z88vb>l>(zHWb8eKh&g(+BIutFym5;le>`ZQ3h%r(I-iC%2z0PRZ05rU#u&5g zry&Ah)44w|2e{*3>W-*dc|wa|OofEk7ML)yT#!FjY&zw^P4C!npw%1Mwj{|8eRGr* zLko;~k7L-l!CNOwcx;7?LrvW3Hs41f&W>8s4KXEBB(5+3hx3z{#emPj9;|Mj-~rG-D9J* zEe6}6MI)>v0^`PT7&>zNrU?=je=OsjeF!dsFm{Mdy0z>!3B$?+`J;u0NVsH=8$a6M zz~OeyjLxx=)gU`mnj_)9vBr3RKMWNByC+XxFJa6tX- zBV+YZ83&qVShIlX15Fs!2P)ksyS(_}dMEzA%LSJ|(~q+?#z;GKez_TUjO0J$F%pt8 zdCpI+0FE@NqsBssfIdta47|8NLXCj~TiD#9kAC36bKAXW)eh*9+_-44gfERZ-(oZBj z^rnouR`j?zEXV!!ED1lFCn=R@qp-6<#=K|UiU@*^cdh$R61bxOg}Ww>&#e%DChoZk*~;cZ7M!t#;t?bsjvv-h(OC5^g@%glk8c4VjLS z&C$8K&WjiJy74?kn^Qxoprj48of>v%K%RsL&o#EW*0EBCV6Lq27ag-}WeV1ZI;aS4o&qWx{zC5{eSAO04bvD3wNAPkAY1tfjxR zhN9i+PiOD+bnllVT?dkGhgfUp3$qK9w*O=y=>kYLwo}eOe~Aap-Q-6U4Vi!TL<7du0-oe6) zm4`Lo2}b4~+j8&y38g2}w()joCK3OhvBtOh1R`SvuX-D5#U3Sj#QmHb-U6VXP*W`}emUBc-Be_Ip7d2DjG zf6vXwu)@@sv>@XIz{o%MZy{_?l(vT7dFr{*l9`J=i?pq;2%eahi&;7kbBqNUC4hBD zDd+!(SG7IhOlDz+BAs{Vm~4Dwtp20CVnN0TVB2w({EF>S9~6ULBG5wEAzV7pf+waY zuf0DMGC}}biTsy7% zILCst6~NYd8FT-t=KKu7$l-^uL)`7q&>{)%UR{6zd8tc&hO`vG&XY3cJSqDsyx~YE z>`?USf#x1X_}z*9DCZ~Jlj`JWNJ|0S^D54~Dhb9sE-@ZC8rEQNEX(xbv zr(~==z_!AXS=b>x?9gR{&3NhZyp*l*21D8j;I*CTnWH00(g`~heR`m|N9w*i&&|RU zGps4u)EN$GCxHE@)eY@K_CMWJu|wT#(;zC+A;jMcNKdct>Su?t%rbsA(~7U3+c(!_ zPCEhgm{KMxGAF?f=`>5W(RQd`u7rPInTwg#eV0akH{~HPt{S09r;8mz0MWwyZoKJ< z`aCt!2H2t5)n+Wd)QX`+eWS7)3TY>R&z~pZ?pI=)cPQ~38;RSYSkDWaW9w&!vQ096 zc~K7T9-pQA-2Fx%?F7&-pDoQK;i>hU?1vx14q;${gcr#2%&1O{zWh9pb^>7d`BlKC zPbF-w<6u989YSsvaNndXJTN)CPc?By1111B(o6{DR0^#ACVL?krX)xwJEUVtFl~Sd zi|1J}rmSy!{Sfdl;I7eVNyf1jq^}){B|SaP3;nz0xm1$Q z%)#yFSW;r_u1;_=;!s7Q%vf%p{ zWT7z4e0~@-FaaE7y@gSg!^U#U%x=cZ{7!fwF+qWPk@`Dc8IOUxM-XO_fN6l ztUlhR9|!7~0Jc)xlk1fOW}EMY6!AIKO==l=!4h#*-fbOE*jCbcG^M-?l^Ka&YBv6YiO8!Q3I{UWHi( zVk;BC#}sS^%D&l~i4!IblaYd37lM-MH@ZB>56cP?L%J|P7VC4ZB?;Qf#*x2GeV27;a>!%Gg z;qsv-Ts^{sb1S5@=(fNa$0>IB+y7sH5I`>sFPCuFMWB%2bj$eguulxj_fytUClTLL z#10Y8E;r%g8VU1>@Rt$strkJ}2zZIY>leUl@VyXA{&c-5ifkg(+!3IV@EBzkja|$l zNhAsx>yOFUdYtXkoS>r}Dj>5uu?m<)IqVbjlP!lTAh&WicI8+p@XYB7AHa zQy`3RgvqoSe#agzZ?E>)^ z2m!XlmDkJO+(Y})E_?dr0D*#pz-bFFi@Z<;x7>9!tulhe|1;CaW{1(N3(YrxS zKpoZfv`dX>4DcNeiiIMxRGG2xEMt-RgV-22pEJD z6kiKI;#^0yn}Zg7+y#@=KS1fbL-ApQ-*I z644BXXT9cTQ@lYjDe-g(2WsS5ytjt z5zBNatgqv36dzDLK~WHM(gXzb18d)BO`SnodqN?e`GDB_ft3``g7y0n5J*1Q8(x;{ ze-!J9@lJ?!7`}SJ4HWlMOpEh(1OyTR>)7^lip21bRz4I#%3&Jd}8xfZHCY1CzRU< a!~X-%3Vit*sEg+S0000FMh{Bkhw8{rh6HH)hvo5RWrw*ji3sbSMBD-VSu2qrM7rlqFWpe|iElX4)Cj(xiO zhwI4ZZ{)daty>3E;>w~}YgAsLQbw*(;uLZ`I?=<7b87StxAH1`4+~Z;`@)++d~%^F`0O?tJsAp zYhS;zyz*3yvx- z>D1Lxwzxb)0|!&KaX%d}6gKvxEA_keWn6zGL5$2@Np{`4g{lDGP0JI1Z%$L-N669g zmm1yJPRrhH(<#NJ7$AQcp(Lm{41+;g+1ShUgWwX`n2NB(%G}Yirc)fS0=OAf5O zEM95UUgz8xUAJBPNh;HN;$anS3-S7S+g}Pc4{kRzGEc1TO3$HzNyP=>q3cUCgq2l~ zZFeJJKxWPqZ|`RZY>=6H54a5Io4TkDY`BZ-KkkNF(1Zh_#Ech{ozLYLIqEcvC*a94 zD)QK#sJ7w$l*P&IgE6n)!K2~`1FS);iJ^V=OY&bxKP(;O@ows(T*XpFJk1ErEn?SP z7*~qK(shY#n;?%GPx5>F?iGUz1CHVu!t?Dqzth0}C|Na<4SMGs*8C+1Zj5I5IahHJ zod-=`YX|j)b|*P=f-#1#JT9+weKo{%8MZ35{Cr;jWF)G>ICZ7VDSgLujNl0mbgn_L z-ZoWQAH!pv%8Jt|buty2r0AHWLsW$t=#|>lA_3%~so;l+Q=z)WdLR9Ccg}Hh7nbUa z0WJ2Ks&*-09bsuEo#i92)Ls4}PgI2pncMDe1I;~tlo6isIQN)hx{n^~;Wwy7Djl9u zP8kzW>7|!_rb3;@Kb63?b21>&^jicwB~%4BIwdl5eswFs`P*6SZ{8(3fN_%3Tae2Q?4c$OSE7%W^#s88#2bPK*hUttwqW$QTb*$TSMW%uK z;mHb^lTDpwAY(2+5*KDFgS|cZd0q_Zhcg@N-vdEt5CeYj%bHhVc$@Oj>rQ$$W!+;> zRtv8K()Q$^IBHoGdKQX=c%T9S3A!upsy+B6egTz;)R z@et&%C&^{<*@TA28xYesJJLR{Kfj<^1NqI=3Ks)ZT_3@;ZNbFzB{wTroZn{K4J&^v z6Gs~_6*~C+WXp8WYACu@bZSkdwWO7fz150ytKVn}EG=VX7Jo-cR_PN{1NNJS5!!~g z5Tlz17ik|)!{TQvYBn~efWN2IMR_0w=5AcxLy&=uX6%357eRE8;$(iZqL zbytl+#g16Z*oq>(D>6{wzM}HBGMzEx;a!N$nARzC?u>Ic1wI5 zMk#)S0%DgTe>z98xYkl}Wt;6J67avTv&K+jd>OV>KC||sVau-*fodhmV_*?1VJ-T6 z39c2tScYAuv5qnMW~_qJkwABQsfz0IS)d&=dLG6Fj859MT=u1odBG*(;HvEtOWY@? z_-d2lL~&a zNrN}(2!TY0TdaNM2r^S=tVaoJzLoBXx>9)A^I{1B$tk=sgak*de5^^lzde zB--!k_Sos);vQEo-N7BfDNo9E#9BQvK}ySafcA7)YY4*{5r3Tr4bXze+9UcZZmZdB z8`88g0)E0`Bc#}FGUtlBzdpsX&HuQVp3?lIKUpc~37$ZZ>b8MU#~PuKXdA@($Gmk1 zZ(J$u7>#C@2s#7aV>PY^L%~1;P@Ln^DRfW=g_TNTOUZI)NflW#PT*g zaqq=V;Xky;qB$d%snjq3M?KEBnku>x-!NLa4EWcWCw|gb;fwvTJV)3Zgyou%d^dzY z5ei4E6ioeaQn(~|8T7J?b^%r(ow{WNnFpr_rNQz^6-#E>r{RYaYkzy`szg@VeV|1| z{5{RYw(XtRPX$!f0IV%4AJCIzk3Z#c0YSL=jvYRuHR;}lo^pi?jPunp-kKnHz+Able+Zi_s9EA<-LVm zo>RPL;M3g*OMF=LQm^mpPG(TiXOm7bZOP_}>i*I$Q#Rz~Z>OTh%8tztVI{ZC@7lb` zR$RK&w&0fpF6`l0se2QHRD^_$G!sC0 zWnL)3o#16U^&C{7Hb*9>mVwv&$mC40lF|=G=Vkf0{&Y4+FaF?dg%Yh-uL znCuI>*TZf1H3xW_3yy$WBXs6vnZR?7Z40gCRf6Nf{?TIX!C>;)#tXwMINIZhv2KqQ z6V%$A|2rTSRH?MVeK7I{vc?ePrrg(`W(^(fbiXwa*%~q>4ST70s*Ky%mKTV) zW_L}D%7!T^Bc<=bGCZ)@%=ADBvOVahU$v0-k{l?`0~(WC{4Xta01Gh_f;WnQA5Zoy zze+Fquww+`H=^2ZGUI<2O$!ZnQ7{r#lIg;N&T$mV5D+W-xob=ikOHzn?2|V_PBEv1 zJ_|G$R&YySP;uTLz4J{<#a^_osvqJ8tEmV{1nio$mMf%#_r%ThN@yvc7|M`L+@o2S zjUSn6gWz zt%`L`cLHjGbc{aQx27zPCUX2PhtzmttcPj~OdzW^e%?iYPbqW;V#D~NC+*AAtF^+} zelIF3i2hRKGKWm#GF&x>UDQIh5xl3piz2k|2s|{PDcQ=dCUG?xjR)|A7u|O%t>S-% zc(~6ATls?S>0cR(uIf*gN$(m{1L|l;3J&f#;P2Cm=*M45)N&e#F?;^QIY_`OSbbH=8%>x0}ghdcD=d75)K!)EU#!Gz1~IE%>FR~d43s36AI1Y@EHf_u%6~D<{==73?I}Lm_8m4OF5UJx zRP3Z!;bQfy5&vw7#0V?ZxU4%b$qH!ijkoIsoF=D$A%BSh&zfotKP-xKzA7UBrNcGa zYg#}y|jGb;m~%Ma-X;jC%8}#Iam7Z|0lr3OiS&| zHlAiN%EDTojZ2z%$#r+Hydk%H#M8>{w)`imW9+)csYX$h#~%8c5uFNuAc@{n-=E;k zKTGH9a??yeIT&R!w;aZsK;|JeGB&A&!T0n|ea+@7pIGcLzhxAjCy9$R6-;AHiLd-e zRuIlJl4e*u&16`aZe(r$U+&uf4v;k+T_M`c*j%tH1MWz~$#GM<=9Y^Rjm3l!i)FNU z5BKZyeL(fN}WcBuTX^-tky`8*Z|D2Uu{a|mEdQsl!+uh$%hD{aog4Vx<*EygAs}<2HCXz+u zcVfs8oSA7XW|Cx}cWU`_z&DrzWTt|i`}L|L$1CPM+4!U>=rT^BWMWLM6Lm;SXZZER z1JK3!CbY5}rE?bvJNl10WDRb*JpYh`W*FP}SVDQwzO^HM_cTqCRUlQHX;Ax($$!C8 zC#f>RnKU41bZ#k?5_?1QvW*pbDlxYJn7cplbHHs!ZDyZQoY^*I;+=6*m@3aISf=+Y7-Ytb*L3eIEMK&tY<&o*6h0|9&D`*2 zonAVmQPjmIP^#Prlr2vt=ZIO->m|t!;kDihMd%)YchT%^&H|fI`Cn+5Q<#Mt|i;ZefOC=JU={o`)aGU zQsx5~Xn9mRs~&F27Q-X`3q%gY8B)g#{|8rA>rfeo;(?|9XD1J=&|yPcj&GMTV})1k zv;)vKRXZNE1nmzb`!_e#W*mHKd`k+4?`D}b{2AYELLh`89K@X6lRuMFkaGdb2n1k_ z|CS94@0!xUdtGhcu}F^A4>5+krzX0xexUuJJ9z|8;}Ig!!7r)OokDfQy+K_VSjiux z74(sm|0yp1f^*e{_3twA%)^UHY2F{)*7Xfsb4bm!>eM_F(U)Jo*PcjKe5xJ%;=jtD z4=4*}8R#cnOwO-jMEN5B=Q1#ne*cauwxl{60S%E|G#6y?3=F3$^o!W(OKsVt(BDC(f5% ze9eBevdR5okvJyo=gpmzp0DonT||MJQ;J^h z?tPerKfXx`r+o*v{Q>m<1m^@ylSh`SJ@He?Mv#Bl+iL~Oi?%UYHDXTMxH8Hf<4oph z28a%hlNB*Pb${D5mTavZMpz#m>i!asnR{@kU%6}}E$c>J#$;<=A9#r9P*FzfIR&6`oee{cEcRZGh4#Il; zm;OZ9A*1sr=_?&Z-jtrB-f78|$$agaE|(cGIS}s3(lmcQ^NlZy0xMNqD3eZ~zWsHP zHhc6V2cgPGz!{}bWTWhil51MylXpu($7mWU5`>;yFNOLblF>J z#HN^?_a&5FjX`Ix-*A^q2tZUn;*+G-u)~(%l1Xq~lGv)ai559ro$N+a_R~@w6U%7# zE3mPX`>J&uzekSay*i@pPzvht@S)L>C7=1S;Qb`6QB{9*ghz`npe5wdOySw=ixc$x zqW8^Z9(c-SUG&$_ToFYWpExMjz%+K0ktX|4|U_V zD{A_tP{RQQqXb*LI>M}+8c^9 zzDeT~Ae_Pv?sawkA)%d8+dJB=NT(U-V=<><`%oOZYK!Xi6z7o!jG2)e>Z)6R|9%k{ zKEGCJaC7T1y#

    +WjD(p{V6>cm1!nR~JVNHztkIF2IQ0EH`BcrWJeX@09~52eq?3 z@UcCm_}c1b>1?ZARKIq_@O0a+(=4Zzr88FcV1e;!8jR1;Bh09CCWQp`&v7RNlo;v& zR4Z1?xO#LpfM6J`j*y5?!%ZL8O1vvT%o)92qL-xqeI{0D2~V4c&R%32ykZMn%D9=zQY(Eo+q-;MKVt`gFLcB4qKItSY4Xn zdWN{pRHCHQX;%h99`Ugpn^{#C=i6?@1>+lL!n>i@NF&#$_%>Xl3^g-y(UO2fHYUEU%uI>tqu}$1 zHoGQ0Tl{{-ZV)dlX7nOiS<zQ z*m-Mj(JHouJd;X9r_i_aP)YrG)IDk0Nr>5nSt0uUM}dM84H`#Z0{p($2(;~&P2BeA z4yQ$Y5*&qVbWiJ6eBe?_x@sF!PdQuny{g46`60op%r>yNUg9KC3KzG_$No6LNQ0!9 ze&9|&eP-h6%t80w7n|SKZ&5*_i;ikQ}m+I)5e*_1Mq6JC}fz2tM zTM7u`u!Cs=`787;*6L(TqaD#C5Wd+LHEW@70_A+s0M1krmD)i2OV)Gq2W>Q+|KkJ1 zjEAU-LVr0j^Jt;03J!$}cmm|?_wHS4lH`~#(WuT@n)07~Hma>XE-Rh_WP#4A1Z7+7yKxqKiu zgGizh_@1NGgLo$z`ih=&h>BFr8#dfuan<}D)$|iv9)u$>j_2K~lseioeJA*OQJ$HY zBNzAvXfUz}*DDSw7{(;&VtZ(=SBR5v)aJ~)v22@{4HuPfc8Ns-3=sA1VFlfI!rg7F z<*sAYVBcp*p+FB6xb%hcJtOd0)=+7s{)GBMYoi)NcJD$JQ_`@E(MDq2@)$x@FL|3C ze>LM;q`ONU6kx6BW`6woY7p9P94`F9&1>Y5uUt#Pca;1Pn2i=)*Y;A4!qy* zr-+2moS_v^ny8DiC8wA5A*HLVJ4PihJHaD4Ds@0{?EW@ytR6}feB&jT9IxL^FG}GU zbGfzkvY+!U{_2Zm=I`g-zI_$*-?zcl6V_Gs!!7SwNZrChfgD#~7oF9KGBkApi{qt!9m~CKhs6V!~AGPom zTqym+ck7#`$hA2hPj0PvI+g=_6h0?`-OEK&9=?FmcI<;EI2*yq_~mDbC3%0bH0yS26{^Ek#?Jy$KURQ@cq3a8dv>E|X z)a%(tx4EYcERVV8X&qde7?BF5)VrK1O3V^w5fxotuZ%pLv-MU@$54h!=aC3;tvX$h zkJC`B?A;IFY8cV^lY*;Rm;e6lce-T363FFxH1i|$FFnidjJmFt;nK|0zG9i1!n65^ z0PUD0t{03^Qq_?U65X|$3wO5cTnBuY9V^z@i3x`Cb>%G&Yol{C8!pZHRW`9p|9Nt( zzi_)F-5eMsIkoD8^6CJCs(A749Yv``^>4OUKL9lS)#82jx8TcjEhTS^zr7MGRO_K8 z>%Qy)HyozT6V^RmyF?xsH+HUl{Q^zM!?G1hC)uJ<<9`RLn>AQEE6T(&TBE*Ci9(RS`$YXz84Iz3}px660TD5MgXVqW$=?7ecmn8$Y$ToJ&}jzwFn%@Gekc6pDBa zhC~H;?P3xSmnDKyhTgWTo{Q&eRzSbHl~iRIR48pQ<_8JL5sjiExhrW!i?)%EQ-#K8 z31IaD{#qG5ey0KjiCXVhi5cqi``hm+ygb~)+bmFfvzvBOTEvg+d`fI1@K^nSFuTG| z({m7A?@?y9{%><5|D1sR0ReZ{t-HIESB(($I$PvP`zeneTidJ{I6LX&@!L3Be zal1e@!EuvCZ775(M%M0}-LgNB$Wd^!WZ-IOeo0X=PGte~NNQ@JZ>aMtc*{sh1*=c7 zztxW*QgIf4yB}B+ljM%YHhVmKFC@B2t(^d;@fYn5tq!Mzht{5?S0|(=Mj0XZz%tF z7XX=5*rlW?PP&WOd@!5U;gsJWgV zq{34B4l%%jchuD=;@ghx9*Mdsx(kOai)7!>qP=uEw6G>%_wDsZ4pzXuf#TSXhdW+S?P1WMno zE7={5qO9fBFOJ|ILwnW7hWI}Yk~+)5HU2*`V{9RQLz%YOyZH?+=as7(fBVo&ioRD9 zPrDA|g8fa$f%>6C!8-g+ zebNikx}#VApY^~`DOH0n3J!$`;dW5%PZdHyg11F~4q)|CxeSvXyn`MQmVx?|_-8MXioCjtkAo?<^^yUhF6Z>=0|4))EbmO`ynEs2 z%_AU+%D0ge(9JaT5uXg09xD3sCgukkpmQCQyJp{1IXW)3cJE2xB_+H`;FO`1c9Ig5 zXPAU`br^ePR4>Svy0)c2T9DHtO2rO|xMGwaW`Xg(qjEGfpGv2AR$_ve8>1hUq+L=J z$}c=o9=eU70<+w-N6NvlKNwD6v#hFw*SfuX|3$YR9W&W;;DPu0DpSSekSJuLkVHFO#H73t;*AmSgbb8n9|e$=GdR~qa>z0 z|HzH_GbFeO>t}Jj^AR5~PT2h`8pOnj3eam0uqO{>#g(G7cFc{cni7lb@s zZaFQ19!?fi+M?tLg#Ne~=v9E1f@z&;Zv!Dz`$&Tfy(dekH4tr)r=LE{pQQhZa878@L|u?no<#$aIm)^H@PZ* zW3aQa?5RMPTiylM>og;2);N_Zsjyg z+4?*R@FJ4zAc#Ek*iWsPy={M&Z-gfcqmAtJeRP!(0lJ;*GmkXOcRoPhhqbA(0<2JzWyu&OMXf7k*QaDPTTYH2zkJFkmT=7-?OE1GVaGQ0&vWmXl?a zK@TO!YC>B|H+=?9xFy$|IQ^kkHJcM4)L;@o#0M@LAqa~)rPG-@Cqbj*0G=8&7sRd`ql+W|4ADPx~ zw^4LZVAoNaQZBP}7hoV&K}5#L=#K4dRlY*oI_!!Mpu6J?+vStxgZ7?W3XbE>*3KLw zSEk|UzC~|OQFd-Ve#%r{uOwvx4NmlM;i^O|AS@`8L zTVd_US?+B>O}@=$p`WKgLZ`V;25$x607)B*32`>$lR%Y*?v7 z>BUOk9EJ9>kckJZM%#DS()@actPqN<(3Lt9Q68w2IIS+`J0JfSlD%Ku>xbZ!DiSbV zPoY-@6qmCJAptc%8I>-OgSzO9wp&cw`tSF^zApJ~O?-XoE@O`6M+Zp;Ud25%BPpd| z?>Sw~SJYgG$R8gMMeHS%ygu-s(SJ7e&v&D=-$!jZwYYwXMFXmO(lK+su9bevB8H6( zU{oxl+f$GzFJE$TyT2}=ziSo^jH=p_6I*|Dk3p?%+vNlz>wU*m*^#@_$?3<eIrd<1EnpZC3Ox)=6O37vsgs)_A=iUZ;@He7$F^@N-g`U2 zvxW@HeK#TmZ${XE8FyVKztn^+RITNGftoY=ToQR|9)C5r!3#aUIJgFJt1JC3muVG{ zB^=}D}tRaoT#1EiJ@A`jQePT`Y+5w7z1A%9){{ky;f=hDn9 zOW>1$l0hem7l6&mrBZ-89$e=uUtiFtx_`S=Y7EG3XO-K$q?AAj#fv>dNOL z4*T(U47aRhhemC`$p6@2i_#oggIOjv)Qv~pASPT2&aQ1@Yp&FEH&74c6&=SX(>i^0 zNTBATrgSzCsmQ13s*^F+h*i{&y|pbS5H(26)X?TJp$)cfR@hVfJ%bqzTS@ka`Bfnm z!I3e(VZ-*1ajz?@p&n7Um`}#PmzepE`wQcC>OC??_dVag`Nn!Db0RozU-nEy{gd~9 zxU%dShe)$QhcWh{TAf+5ep?CwH@h$lx@M|Q z3{_jYczGX&@n0_DdAt7u)LHIWASbJZ?zP)t{yu)wtll@q9L7w2629C;8h2)y`xZSg zCEdAG7aH>FQMNcg5y_$7K4FLl0S%u(pL|XR?3p~B+Os)S4jp^%jIM;LqHt#_h|%tT z8>Q}j$PZO%W;Y|``}u>UB^5H1O8S`{#cI424F1s?>s?YyuUMXWTmn4>N=bU{(hbFt z%ETx2#g*M>Xw}i0L>R@b-?!v4;pRH*pfBk66+#JJJzwI6U&6%}A!Z=ES9cS)QuOy5 z*kug1)YejPB!P5U!X>@ll0;4WN)bOLslT> zv_s&>rbVvus7QuY^PMcw!hrO z{-_!*1!lAhYv9kMSciv7wr;~y!V_}!ORHED{B;+B}-X}H0->)D!=eVB1_#^vj)*pdF(j-tLoeA!yh zNa4VhjnV$&UA8PwO8?f?eNg$BvUn96eCm?%*RyPE%*6?<3qLxAXO`YkQx4K4>))t; zPjn)sUnK3N=kK2mjo}cql>s5t(5{PRfJL}YZX~VJQ#KMo(et>Y_%kyOwQpQXTU=n< zIgp2`cHJwXgX!$szaUIL2&s)4^+~Y92g9GA8$`tpt~>pSdbb8jU6i@7Q8A=*MW<#+ zH}m0s2U~*E`SHfy9gA@f9p{Iin^`Uf^ZPp1_3Gm1m%Hf3E>wvZjZol$79NebzC@OXkgjzd9z;cO^pBQt&bul1OPl~QcV zL{xL3e$|`n#n&Vs2VPxr4wklyWL4K9gbgk1;Uy*8Q$=Mj?{4ao^y5o^=yHJ)+ca=xEtHWd|q)m5*`mf zdDxTissQM8+aS=;X`K}Cz3 zy@9^f@THd|3b1theC8Q_HMZ*r-Z^{Trb3B06=JKi0vq*WIIR;O*O)?Yz?glOYIolL z#!f@mjjHnl>kR~lS6R0yzty6dLUU-`w@rzarw)$2D3+w zM~6oCK-!_e57)=_^NGMykZKoJaS;3kXV9p5!W?X8snXS$EdI?Qt*aAzQ~nnT_P>XL(!L&xAZ` zws^dKskYW+H28p*eWHJB<++ld#?Pd0@bx1BuF4UXf_Js{=T&&-%yrquCYWXX1r&Gt zv61H|*|#~SyP{oc){^_g1_CMl>!Y}T+kg= ztuAB8ksX8$=UKY5Q`Da_jq- zm>4kBU^`0#1$~z@WY0n5wyMbmD$AzO`39}?8$y*_H(*P5#*d@%_bu!Pe%HzPO;-KY zYwomR4YgU5Yj>D4I#11^>l1ZVPRPakd#sAr#k13wygBeL4)M-#IWAGPSAKf!0o_}* z+K_19g^NAEJ#GYF2rba9GOEg_9G?xJzTKuKGUX|$H8NYAkupSEBrWbTy@Tv#>E^uw zRH392$Smxa<>l0bvFT-ie6LcXl2alf-MQHyWKSl1Sn5yV7uC+XV*`PR2{OQ2DOmyi zQ2s-@x$#GB10z+PUNGWd{6~F?OUSWs+u)~?$j?a`X*WG9c1~8ORi__$@P%_- z_M;0OPs8QM?{(d)x-zTOawK$oUb5zo&wH@{-xQ-_wFW^`eY!rOP4h;R$0}pFYjyRG zq4Ate(09}-2fueE{?>{}6y+-pQ>r77+MPV4UZ?K2)@XSZ6rHs8p!ZWtuL?dEUe<=L zb=fZ{4g`c-y!O4>pvOcJWl=U4*2mkQc+J#Qa&nq0yC{cmy1ffwIfl+9uu}!%*X#3? zi`m*MA}7MTZBUqicF}mZo&1s;Z7y`#tP?E!Tz$5D3I;Q(EqhMH1vGnatEpYh-_3be zc>!hM%+*FxD8BA$plI__2tvy{x-X~#BpC^*q&3b*$#6!OoqMP;!`ZSI7Or(~ z%ge$reRv49+Si*ST&ebKCgjYa#p8FKz9}eP0(5D6rRDr=3o);3!#M&>a4k6lp2y zNOg6nzT4F7M3VJB*r<1);+~*ZC)pj{a6z|PT;N+-)%D3mGbIdaO}pzkOTYiU%8-IM z*N7nt#n-v(D1kcY_fuOx5)DOf0EDV;w)`O+Y#Bx4*NWa#P!@6@>NY1ELLLeceK)xG z2*Z8HNm~NBvpvrTOJ4HWPcyaGzSVdsoaPS1Gjxl8e7kAVav(NSazJ0a8Cy^kMK?E9 ztYJ=D3SRU1tO_+K&^$bpRA9RAeI_R!6B!Q}7F2VU{&ZqxJx3hesP&@jGEotE(f)da z@R(_dAZZf$q!=O$g%Lh6Px@7)n^*hLWtqp zR;FgZ{gC_VK?upAf$ZA06szm@9*}>3>luAn!cEkxGdjqyjMuOXUS0IJNrnmum&>$K z!1m24pT9metJnVE!Au2g0IctYGPk+g;!AIf2uD_?a(PB1MY|GyOq|cCI3SiVZa%HE zfDIj3@rNoEhvt6xtdQ~jnzaSoJJ*>`BrKsS>({@DL1Fra@6JbZ*8_juA|Bq|s#y@Z zYS`9$iM9j=mQ$&VD|zek5Ww#zwy87o#boc_|<=ks7xa<9zxM*v0X{tc0iZ`SBWvMlDZ_c+#1t&H0KukOS^E46;Uzlm-$7wh^(a~s4u8bb_KIa~D2(lG)##`2d}8Qmxo}-YKGqm5XQV%$=bWyE{LsJUvWyE8 zu4z{KL&ZKi5N*UCvfX$W5_OUP$!E3AkkC=V{FwKuPH!=80s7h_75Y;8ali(qIrhiC z>`Ml)c}jdj(k&#TLxolmty}FXPC^2%abC^|Hce{nfuD4SZALB`aHgAZ`0<3-HK?ZO zj9vfylQA!Ey|e+pY3)4+^xF_+e<}bXzpg;tahNjTvy|1krCcnM-d?eW@y{0B&&+N6 z=~?6s-mBZBt>H(idH#BkEfT&Vn784?rQaG@b!l%-HmIgZUg4}YMs_N47d)xmMz_iv zZV|faUw`yqsi3T!O}729yE!QGL}vL!MI39M_qj}_0C4oVd*7~X%FCizKY&T7o}&pE zI^SI6aO{2OAB*o-7bjT_2I|$8z2P%8j~mKXO3Pn@G_EHqiMUvz52bp`ZTuX0T+9p3 zp`~s9w|xTc{NMITfEQmA@O8l`)VrhjsRVBhR&wt|YX4d`d6%tYYepf~$t~yZ4phM?gU(gJW5Ftiv zsP2Azp6tZgWf$r#FosyN>I}XmYDU&HR5Fq1%jg+H++ow#bf-(txw#2kGuxTMh3x;g@nf6(Dd|bBgN<6XN28X!*WqVh0tBou1`pEN!0o2(VJ-LDG zp_11v1)R+$?*pf}y}*!r!0p$DwoNZ_bdN4n^`kE`qW`Aqd`R>NM%Nn)((Z3m&W0TA zyrAudWR~0Kt@VJys`o*lu{Dz%>Y?;J$4T8PRx>zSt{ydCaBr(_>5+>dg6~5TmNp@- zDmxGAy#4bPm;0Q^__sC5Rr`~lVXPa$GuNzrSsxI?JJ12;GF7w-aSz>Z%9f_!21VRN zLrWIJ*!jmzY7X69vxix`#T4cSq`yoe^-EhG=$K33Yu@;Z1FJYm`nfI8W%oVw_j1J& z1#aZHI-~MbJpBp!q54JiZU(#+cdJzb1hS>dWgUtm(HFSUQ)dG=>#;>Hq4K6k9;{$dK{HEq>Q+sgDU1D}9-%hi1!mW7fx9ni9lwS7%0}06~@$vGM zwgC3edVeAKEw8e~+DL=Te@t&y?F_L@S@nRxZOPyx!@|3tHi)?SL)BVl`7_}H*1qz} z>yo-zCZ;81mOJZbr0O|G9}@P6;-Ic)Y8CbqVpK$`cr$H-B)C*)^UBzPM&kvZexaU$ zOgrs{qrLsF+crnGH~U^MqxJf@;7UqS&ij<-E=O*5k}Ko!s%UyN^cmhM2l8;*U_tB8 z-}Z;e$uE@Ny~JIuGLz_JP60JU+j;FPmW^o?>vjGuzYaQIMPEV>)3MzMfZ2?FBF9a5 ze{n=$4{4ik1<5yOP4jT70K{M?z!*PkF31?PyI_M5I~ACS%!V; zmCF$B-eh)e@Z_}MxoozJ$_`^cDMxQYi=`QUV=e^Xp?U@FVk*`RqJlSRR}5Q@eh9>Y z?A;eZWw<|NvNDNnvMwCJDgW0zf=Psob+gTHrNa z!LmR(^fd)0751jiP)^>=Ltf&7T#G`bID7E3y6n0Zzn_23k-CNP{zF72|ply&l0=^AEJw4XQ!;i&!OKw*`1jS7Et#Qe@Cw(kl_Zxq!lOe@^ zk;h`3TdB;bar@mmCAK!mRW> z-8qG7-22C}li}rrz0e=P@%OTdz@x;WQIqz=I;N_F6Q+jizKWA6$vs83Jo%2IrT}00 zdc5&mi!Ymu!BU^#gtZaPri*r+7P%>51s8{<%FZW*5i>`-=+r&Vn|`WT&CE9TE@E?M z7?%QT)GF7uzqaR4m^^AuUNN!$wy9Zi!2naGno7)&GVp(EMKmkwaeN<&@)Y2 zP}Oqk@{=o$l7|c9FeOp4R0tyw$soL5aoaq7e+d7$nqkg|qoyY0Mgfw&P4_7>5_Ciz z8xj}asp9DI=I(ubY&xbjb-@fHxmwuVm7W3jdxk6>>d8no)uyDG*4_ zwEQiE@J{K`S$)NEEj{Th?94j}-iSEu_mxEjO0qV}Zk1iD&fySQVaX0EFeHOe+nnMc58 zb8h{(_q`nU|r*ae`PQO$##VLDq#SkYSemZ`@p-bbWxqDzu1k}+cida=*V zH!O~J5i(binz;=V0U++I8-*6I;^!1NB&T#apZIsOG-*?8dr(qk3>~SqGf*t#A))K#d(54RbWaA3SoPen?JW4{DR|MqWC-Vij2z-y4H*6raQU86ElN&I-|_y3I}DLu)dEcRt>|4S0oQ}fO|3^R>x?KHSdI<2EDyV$g^d+ou8$QZA&kQqD+YbZ^-Th(J|dQL@s&P`OA$dBUGs*uwamf|C5J zblasjryFgu{hWEr>y>c0pt3gGHV<#K#}*@Np^FpgS&)+#oFY_3zr`Oh_}{X_N#Epp z!DYpD=K_a@mY>?m{&QI)jn3|lirmmAFX|)j>nErK7uE^0l1!A9s+^sg++SUlN%={i zD?S;Q+E%X#n${DDj%)xKG)9;g1e-}Qj`sim~kE_|6lt+G_lzk_-k+w*Cc|kg=w80@%4p?c9tQ6zvN6Yh= zsUlO5m+tjE_JI0sWtn(a|m%L&o51X@^2$j@_yRb zfFm+YLmQ6y=HA%PA3d1huL!w@+)!70b#y!QpSSmhGwI#;8@`8g3#ozsE^*Wi-u=o^ zptTmpZ)rMq0J|wjiPX<$Rg@6j8RyU;$}^YSg<pwGSgB{`y7Ct%XtZ{tX)o%ji z#Zg$%3_06e8A;rG^ti#sYvcP(pRM`}?csahgJ4V_;5>s;$xt2#JS0s>(xWg=wsx0T zZDJ+00m{ayOJoUyy-ixj2L2BpZ{ZbX*tL%m3W7A!DH0+BNW+i-^R^>zqGe)_UgI_jB)k#lB`@F!x8k6NveP zrsc(=iUd%$!K04Zuk=@JAzE!sav|Ox-QLpvJNVmKR)hPaM!c-@@bxZk;^|dzcjCUH zj48j`4P?Us0jF6a3W+0c45CPpGQE<9r+wXT!+Ai}6g$}Z8HTl}=r^4cD>_=;Tqoge z0Du0cfa^4A9S2?6p<|ge8sDjMNB~oEe4V@u;yKi^ThRh%;QsXFhe_VbJNHlG^_N^P znKm5kBRbKg@Q{Jeb z4qgvi<`GcXUXj#AcZP=8sV?E1Gsb{RW*36?&)i=Ed{i|bU%%byYO zD9S1Ok+gG;?8dkCp{WoL=;k&DMyhnm1>M`y^V|XT1ehe_F9K7^Er%)D#jBOX_3oc zN2o1*y4<_mQtBMI^3oY!NxT>a)g+lZ1hSMDLuu-Lp zuK2lu*ib}5D^{SR7RG>>geZF@q$53-?hZW!0Z+|rA-sM5`i^*TqT`vk0Qc-od3I}} zUVxZ2<7>i}e;Lmgdzvt7E}NfxNZ25XgKGb^K*W_`#r)R+AuL_Ok6$xcx%e!^TgVyD zxz4DFpG39JW!-~*%8&e@fXSZfmFSBg^_rW&+pXdN&uN%K6{n^>7O3ddz7TG7Us!ft zwVL2XdDW2q>Q(Xkjz#iq%)IiXq`0&yF=*;QLG~Vykm2KD6oeTxZNMeCC?a<>Lal!W zN@ML~UFK}?T!d?U2KA4c2f_We)S0fh(2!}@iA6$#$P0@LV!q>t4Kd_QnZLoYMfu_j zpw1VIBD{`pi_4K-Wa+ir`}k*&{r)EX*+=|hA^lago^3Ky2Wj|ew^R<@`d#)cYGTiK_>WLQryhal_sMH)O>Jjtaz9d;UT7_fc`{V zguaNpuAplT-rx#6$|IcYp5aOFB){M1QgirzeVfQ*%`x_cDNhcGor5}_Z3NG0!6z@vMQ<-j zmq5d=e~&!Z2oOeom-xFrOk}XfASx7H;b!i7ixr4Ls=(N3>Q}dZXhKibNx{tNp5MwK zq(wmVi#NSD#h9h0!}Eh0E=1;~x%Az34YIPOHb|9WM95w@SImxXN+o?UU07Li*V@OS z=aareXZjvI)nmPgwDU~Td7w_=PH{mmM+}rZN(i2R0 zJO&mkA$ACVg)u)g{xQJ)=ANEq-1r%Fuz}bt;5FV26oE&C(2i294NLQuCJl5b?AX~( z^7F-65xZAlAF{CqxtL6afFr;e$DFvx5zb2bt=6u(JxzlgBbL&IMm&FK>e9NfKl_Sa z=P8Rk4hFw3>iI?~B!(|SjoVkeP4I(L+)M-&rj~iK;)aqTLU+Xy0;9HdL{IHER0cD8 zp^5+AZP!H|*y+v95hv7ip|N8%d}ZOg9y@1$T{qu06i&-_zYWSD3ph#}8ag!J9hdyK zuK_c5duafvvUbjBPrQ4l(EyJu5G|?VlxAoroFlvce8$fN#TTjds!b0ZszqhDioX>6 zu2vYJz}!g+v6UEn5!WZ<-JUZYGnyD`rJ<#?B0<`(T!GNYnN5vRCLu{D_TfFXlb8UH ztLqCczS`Q+OW>6kCuW=sVt+y4KB_geS6GK%IaBP*HYCX@8XqU$(Ecz1%2zi7K=^kj zU4^F#%(DPjQe|5e5@TmVR{eoh*zk}(t@e*gP+1ZT_U zGr*J!|IBCM9{}3(Jxc9+ln9p13V1fLqaDejO`zpEGK#FUur4HDL8%Q~ zfodbcg#jgo19n%B9Gc}H(n5@#OnxJ0m1gmfqE!U&M3oG-WFp=TJYv15Q6 zZK?8GkQ5mTy&A*gb%2G8nUeD`h8`7kc&cb7o4%lB{`nWBt*ag08l)KbiwD2lIiWph zGvOlrCBybU)ShV*h%Bm<@-vXJlxgvY=bTq##a({}eL2dxj?waOD@~$x(PX-)!&j+$ z5~h~#__ zF0PRW|9e z?C6X^*6@^&Ek!{51WM6Y7h`64k@Z=$Pw&&rf7}c_pThFVIAvuFs)>^%vqY*?oWQ$9 zF`XUOxl4YKaf+))U8l_e<*i`X{jyI5p~`PFXzdA0e~(5^GP^pcUZkE^nFP>`8R2jS zZ3|A5h;T1B*47Q3d;WC0Sk)Z&GK*OASy%>;vJ&etY2(@+&j#lbKQno36B=Hdun6^; z8n8+c84j(VHDuzc>y2d0IB))uy1Y6IO7cs~24b|d3`Zr6vvS-^S5(5mVW7vSvM)#o zM1rwwWPQjX>HmqIhvD9l{YyT(-?qBet=~kh^{X%MDKK=Fsr|8iVrZ|%1O^k7b}6VP zWQoyahZo|?j}Z8-(4*XlY-LoL5+O=3^DwwK<7!pMZ`@N3uWRU|)vR_k)8>lpJJps- zsq6w$ps()W-{S{X%wnOF zt_gj2ik`mo9X6pS1~(hD^~MZHztN0^fjzKU#s^Gz1SV7~#Fj~d3R=kFfe4v@^@ZBh zz+8}J)?SheZN`Bwla|9#lb)31pm<`)8(KyMH7ISa7j!7*A1fw55G9$8G+LLWr!BXx zy+_p@Jy`(_Ca8XDgSO?!na2U*yY4?AI_Lky^iE}Qtj1!=OXWF zx$-1>iY72zebqV$B;h^a%uD%=Ormcvt&EXOI3kzI;P(hx-)ee?RjiUXo2t_m$4^#@ zK5axXY4LAapS&yaURmxJ=DYPxr$Lpw_`gE+l#M+YRir#nxQg?|>;N~y0+FL`ZtIX+ z&;ly}owm}!d2}mIw)y{MpexGN0-b%QJeyKl#z|0qGd-66zLe%Csr^(cGg-X-U)Iz} zlmg}f|HDVV9z2PkVuec(4OddZ2Aelt8Xjxb*v`PNV5<@x^9sm4zBH#kqTJ zcS$BEYdha!jZty77Ov?h!yFt^yrJ%7226D-nG@&Pcbu5*p7bJe;4ESy^=g)A%FDl> z^S<-*^gpn75L?e`3^^dEZ&h#4n?tpR?x*3)7yUL-=(WwJZpK(s57wTFo~cKu ze_hDYJm!?o((~XvM8vYqtrS((-m}ly_YY~i{S@2}ubdquTR$}U@vz~PzdtnV@Eu-W z%M^;VIQO~ZU7kXphhyrsp_dLx4lnG(ge<)o{Mrj@fm>JTl$S{vWG}55CW13X%*sBW z>wS_cXP!U0xY6#;w#sMRiG#>a@1 z6sr{PKNH@z-dyNlDNc*@08LFkArv%?CXWA51fnm9Ys$>yNdiA>s9JTdr7ORYPMZKb z)=4!qjHo*_C64c?JcTA>)^2J?)h+w$pu=Deh1nC%(2Q80l1Mc>6{2Ig2hs|tuQX=o%U zJ3f;Ci!IZ;#WH2BI~Ap$akTD3lCr52KPxso@nbo@)sLn-=d%g@!iW+$S z8lzNmM_|;KtEu9th8)hY9O^|*2Va1Krz^Hw^0>hHdC4T(4=2tGdhLh zPN0x1wL;A8FAhjL7{2?R=uo`%HTp)w6!dh!a>QKyfL{Qxgz4I1FQM>hRP?%Fv4k+) zp#;|vKCx}5h5oJ2>QrY`X62Hkxzn93@Fd`~S}JP4?pfJwm#seYBu{GnfXZ9cxpl8? zI;QFk?+8-OO){;#g!GQT-)n&8!s|9w1AimyDe(a>%B+Jv#$1^I9Q+1Jm zGSZYiMjIZPlHWdX&-%QZ)*t7Cd=e^ipT=YBE_hh*KnvDHX#Sr6zJ^%}0syy*!Bp^T z(V^d?65OC>_d?M!*K$}PF(G<~8{->*nfF(DSFDllL8n)1rxod!X{W81Eb0b~v@{zl zGlxBDRGF;#;9Vov`i3=aWs_d4EuIY5`@OyqB6GiDJtY+zFRs5%kk+aNdi$!)SQ$m$ zTgy~1BFxhy@RKfQI$smmcP6DWcuttX5js)3JBgm8nu@u632_BY{AZ|p&@(&`V(!$q!IS7A+s(@@Z-Y7P&-M2SzbD1o~1 z4e+Kj&#@3PoelrgXCW8PXf!8do8nDyBsgtGlY3JOi#gF2Y>|gvmt=1~Dhx#o{04&8 zKVO4zXD>`&16?%*46d0yC~ zHR}D?49P|*W-U}Kci)$z>S^kdvi zKN((dm1#QSl@b(xRpa7|dePAo=WE2THhyfMcHHk#5cc=(Ds}j0=fFU<6e)MNs|ng; zWKH!>I3Iax@Hk~fB91^UTdC10lt^{amtjpSI9 zPPnU{P;tcDt!tAUS&viD2=oRP9F}$M*e>S+m6D(pJqbSQuB8=`dzB0omE4%-{ua>4 zR{aO#$;h3Qsz`m#JM<}4oU0#3#dgXk-BJrJtlzN|fiGN`ES~__+e8GZtOf-x9p4O? zc|P5wDZZ@pi&gN$yGc_%wzlJ7<5J1^b7G&Wkqjud-SCozzgQtUvUSg zZ|6SW?}}0yXjvCcED^HyP(scZNvW<2_o_pd!#XNSS|D@XTVU;7%fdV53j!X8U!^m{ z=WCNyoRld-?K3~5TVK+uc{ISS*c+0d)7nw8Xn7 zrb91Cz1miO4*!BvMT=o4{!phD4Vzrp>artk6y>-Xyw=lfLSkX^B(MqR^5T)A@D`s*NZu|CuEtiZGtZo;ExVWezv*McK%-h6zU#SJcJx_zidL+{x zvc9|8nk{n(Kbz4f>CHIz@o3QS^Q{MA%T}&J5^)BZbd+Fy3m3 zeX8nJlCFErMIx}~>NFvq(q=be>wEJhH1yIOU8i~2(zD;?Eh+ntd$bXBJp0}t!o+W$ z9k#hw5R(i+V9y*Q{LA8~)(AfNuU3xR;tST6DV)77GrnA9AGz|NyE<@i{c)U!zVPSz zFP?>N+7OpZFcD|(Y~tO&>(VDG5$hSZt*h-Q?rMb2bJWDvl9gN6t47sPg76=0*wk!2 zihvtF8P3X-m}{A+-zlS-t|l+$;TW_{R^yxMsAe>pjkH>{Z0H$3XxKigja{RO3%fte7vrr6x7-8*N+iMFM3V zc6$ZN9cC@<=F`Fr_?B15OU#dUDzyr$d@M!P!1S8)jkZhu)3ATBN)LdQ3uO1xt_7xW zS7(e6XcmL^hAAuI-i7FrJSua|aRUoVA^0JKf>|kI4t4Njsp1#ssiTCokILm5>S*@u zQxv;sRKmK}<(aFzI8LQ2O%`#;4p>QA$BdBw5WW}40*?k2Kv`KIq})SKeRN>jS6_fR z>_{_2D762Biez+2U^y`-e|vRl?bw5uQJRCozh_^V5Jr>|-WZ*tQWaP#_Rk=k;cYMd zd|}@PM8mX{9W+fbEMTBkW?yO%kPQxP*&_$G2C&=I)k@SHjJKXfS=|GdjhlwyMY;e9zm-r$E$ z_)uKxbaI--I`dcSAQ5;D0g4whG^%DPJ(%n$-`y4IoLk_o6pm$5gIzt?8Ja~y#n*ELL>J4=Y40C)_{Jidgv8b(er7mnJph=5ICi=M^Zou^T!c(cPe3lFY>A$19 z!IhgXW?9&yrM`W z7W8;EEA1Hd(T}G|GA|(ZYsz_&hHI@#2sM8v`7wqsHaE!3WcR(x>sma~7g)qwDGY71 ziyzp_vYj=7d;V<3*UtJh=7FEcoOb9S?TC9R-#lcTw@zp0BDQ!6WX1Hefe?=uX=c%7W|$GL-KQ#%gID@w6{Z|L4{GSRYZ7FQm;89tp)t*^qNc6U6VqNv zi=9WRU}0DJg2}2n165Q-5=UC%Mr8Lu*^%y`U1LU{L(-gO+l&K)YY@HClDPF!%`|$0 zBr;wc<(<`sYwzOleH%-*0{0IREL=hkg>mEW85L{#%$nseJZc_AT9t_AUohtB9@D$& zz{7(ATra|ZAi2GkEyddzXS{c$Fu;nV2G>CM+&|#>^yoU1Hr7mhjSV|I)>-~)5`p#O=p}bZC4pHSojUw zPiG|V*U>Ga5(%ZWP-GI2R?~FWRAev_e4AvRzSvyNIAwT$@k}6)H=(BGs3S_IChKJo zWmT%oX&`84FUYQZ(JT)(hVuF-&afZH$nWr_-8HOI&#=t zo-`H2RD8BlZxXgv!Jt47c+Rb5=TriO;?L7aj4NKe(Uas;UuB}{9&H_0cdS_{+&isL z18QRMzg_^-G11Pp181`TSS^Ov+MCsU!jQ19i^0z;MEqQx(MY|}xM!o01wH^Dy;94L zj0{-2Qt-FX8J-j1jK-U9TlEl3DelG7X%b7-d&v#*lL(p}9zXB@q@j~RV zdnbpDOxGau(j8#{7Y58Q$-y>O3Y789BU8+IvqYBT===e7uwJBU>alAIH%l<*pyhs8 z_h!CYYiBjrVjQIn|By(33uK$fkBjb&c=t;kC$2iLleT*%A}+D4+SQt*dARyq)OPuF zH}d*@|42yPYEn}vq~&gvS}DC)-dpAiS`~Oq*3wBm;0HckaPVc9@fm-`q;2=o%652s zRwY`h%kflTN(W;Iae{3a6(o!EI>^Q2R$D9W=zG@gYW2@to^Y+H?%1Wl*#R<7{J->_ z(L73?`C0FZV+9^K(iOAxKLqZ$gjRMuJ+EnJVw5ev6N~^yOu^M}xX!ipX-X%|RWXTh z1x|o$#IVYzf1+%f8Qq(+I(x>Jb%>On{Qp2q z4|vdmy~4k^_K_|D4%|?*cXWqB)UBcV1z;+!9@#GN)l|)9-BuCQ>?u}6?8wFgF&!Lj z4)79yi%K#5<7_fr#F}_#1{9Q3F%?5qB6R{`lTg%LyXnzRM{25@tA$Jj?!Z6?Vm}ES znTo~lM(l~*OqBpF=V&ljjtk zzjUFOL`>Uj-nY)E)dFki>qU-|3rz6`KZVNds-bKtr7KwYQqN(dS*%}Jur&4`bIk>v zl+wB0RCocW>>{em^h{4*FV+-UTTe?Hl8&E*m^7a49^Cg}3=iK^cD>8UQ|b5I4U|l0 z)f}K8&63ofEm(>LUY_uh?W~c0vmjr_q!W8kr_a~Jtgr_DvZU7z8y~Cf-0EFw53g&gj0Iw zV(DXB!2%4?xwLiByClXly|~rkYl5Ho|3ae%CdLeA@PLnlbH!H^c!E4xo^7gACK5qh zwY9@e@+aEj2F2=yVuM!Vv^KcP2U(#+^?UnOqm+M4Pv4qLMk2CvCar7xvCih?Usbp2 z4p(#X7S>q>A+Oh6^_3bZS1++*?nbm-)C~MG9ne0-#M;KaJO+mbI({w@e_OvnhVJjD z?fw%C*>fUQ@NCeha9Sdc=NuKSItr!w7``PAvx(JQvyFWlSRRVd?ddZmzj`k_JJ<6@ zTwv?zXbsI_n5lK_>wo5x9sE3Bn3fgDWd2+}8e@9*O3=iO)*w^2q7>tWj2TP0Ht>zJ zI*;hCS@gZKUcL#QyO>R>c9=HYd8xFEvN)0MwPch}UnuTfrBV{{gHVUHCOh*B%qJ!L zd0;@}Q>zWIDNHY`*3wzI`YNWFX6ovH5pQQ{Z8ROmEb^ctu3m1ceEx_-7&~$7V?rXUJU2(l^YRTW zRr08xy9NaE)IUmN-s(>E7*~kZ4=((6yYBVdYJp_?1P(W!j`#&EZ6$avrMYFF`e(aY zZ%S=tUb&PQmo!gsYFBN0#w6Dzp!P;fo%6v5+`OI>L`q6*d-V-F%1wAjRv#mn+ZXm$ zQ19%R^FrC}FtKpMo;e*)y-bwvV>_?Q$_Ne5LGi~Zx%2?RUyO_@gI%*%SdMMq(I5*} z+uUxFuD|%QBWzovFQ(_%A&m~Sm<6zb_BTo}j)px;+g56S8+tKb0K{TKe&u3nsYti*tWUc<9?)w#z z+l;BbQmOC_fimS*;n&OV4Q}=+@M-|!MU1F)lW>sHl+rz(GL0$GvNV?CK8f0Qe1M)x zSgX5ZLYmBE*4*gAX}s!|RMwV1LBubmW6C816Hx7eE-9de+xRF+I@j#J>8S@e2wAv){(HoUjzd@LC=6JFJH4?!Zum; zv@E#$vE*=ZHPODe4Oz|Suj>))MrK)2lYec!3M3==SaougPHju3zIy1wM?ySBrvyoi z>ptkCp>n=l@yu+kZbH=MBU6#1#MBMs^RM)VTRD(dW?9;2zH|}ilA0`dNQRKJS)X(d zcp+VDk*1Z*_gP|%#p)s_^fN8rXBCa%Bqy-xaASh0uK;4Rw;zbRHr$wSra#yaw|me} zylWi28kz+Cf}(M=W|2$W`s$4>a(5jQ{_~go@dOGxX0?6{erZ^|MuXPye&O;H8U|jNAYuR7X1kqFQFeugt zY*LT8WX$pPNGx@2v8Bs}b)qu~T@L0qb_al)_!}*IrKG3QoQ*R@rpDV+f^l5I?iAbs zIESsD1HDFc`DHYL2QkSV5ZTHGGbM*)DKuj)jm*~cCwLrW`B$k4@v@1fku<1|`Oy#4 zu6Wqlz?0tjHM^OY?r1S7iKL0tx(ZF!!iVTu)V!auBST$%D1sflO7R%d49xWQD9P?6 ztx)brWC-CO$WEMu{odV;KYGsEMkvQ2ogpwc;_u7F>*b*rfJ|b~#B=8Qb*A5Z6U9{Y zXsj((A@;5&4e^!qPM<}hJ|tR~bt4JO5pPVIG`_*Y*gok)tcN3qps~fMK2Mva*9nx} zpD=?#Hj+iUbyA0x{btR(OrkW}PU{TizMY)F({LWfdHk^}`+NkaH}+6V;k@+yUZXkh?mk}A$%_dNr}ENy zd{0%T!to_-KciGD11Y7q5d)B|D)JTSs-nHUw@7W}$+-lQfX@>VhJ?#? zpp{vDo*GmA`9}FT39k-U-$c_+eXe^uT!{y=U@x|<)JjstN>eC#k*?L4E$jjKYiO`% z)jj4uKQI}PdnYf%tPq1=wI`Z+;{EvBV`sF%77txHEmMhGu@=pP)V0wI-lpc4lNJ4_ zlhonrG!|t;tb3HblDAS(YJn%DFfP0eZ#i)NmV4goD+yTcqENq0Y1>C0U-c%k&^S6q zdSK-if#=?-$0g`ITz^L!oYZ&ax8DBc0xm5O$_#`~GiQx{)TFaT3|aQ&M82{gg?OUt ztIq#ywjR08IuEQ-!F^gqDN+%|>;j)3bf716*g+BI{dmmQ5t!PQZ6HQRLrgzaKMfZ)8aQgwpg)3UfQtse(ix zL)~DC)n|m|RJfTU<&NI)t?!TGk6?;#99IJ~m9(?i2{;$Yfc~j$lO8&(Hqv!3kAtxB z%lNI!il_*YkH+9^EaY6mZrs`u(~rX$n>Tr;E&tw-Pix%76wXZf=QSlR1j2)Y%H_-B zu+b3gdE9mAT(8%q|MEdTjKHeZy}V(RgYzZT!B?xd;92Y=Xy3UHAfNcs@~W3d4**rh zf)d$$(0OX9WmP+pOHK;nQ5sYhEjvk9>-e!hL!2#H((~?~UF({B{nj`uF1MQL$vzcr z+LGx@ofBKj`EQqw{%XewbB<>Pr(aMTCzp~S(AtU3^7`Io)ii9$ze&Ewi%rLib!!fr zk*cjSpsaxl48aYb$$l)#+H|X-%{OFI7dcpHP3P{X4cO){I{&AV zfN>L@m!KLBZuO>Z2}#5c{LE&mzx3 z8RTQE?{~&X=9;!a%+oW5S-}^jBFts}{l(acQ6g-nR3O6a1=fd<9kzIxr_0~Uxc*?> z^((eBbhQ#L8_V4%uhN_+WbD+is@IeLoY9U&pt83A!qqt=e7+*JiFTO9Ma&s)$~Llh0#; zH$#8KnTBb&S>2eDCcF_RzjO42U%KKFux_E`?kkkh;Gwo(!3%OQ^CiQ-le_c1eWpqp zhi`t#L|!ufIGj$tAJ>l)#@W@afN*^)a-;bv(taB4IR|VzaS!7;qF`;qrjz~hrSuYp z=N0z*f#8TA5BI_~Ph5xR5CPMqNOVh(VPsp>1g(qVw%e|sMf{x?kE!}^ImHI)BMjwV zGi7AG-ueeX^UB= z;hC~gUotjJteg`iAN_Kp=Uxg;()E|OoMF0;U%Rrmw!I0!bDmmu;A9!48oAvk&%JkC zzs39b&)m{}06W;{eLkf&Z7$HT*QTf#I}XCe1$X|{SB-bGIe)9;3mCK((?AkOk@t){R3 z+c@tXunEKywk3ek8x=HO!_H`rW01$o`rT>qp#jDt4L2qQGuwmRprKn*hxR9i%Z(fDY`86g*J{wb#Vr4v42;aKjSU=Nzmaetbp9Md7S@ z&+6LB+`jMkswe-{FRN2G0j0t!EBDM?1Ck=}$*IWc`a3 zaqEt2oL_^G;H-x*qX&s#?Wa}Yliy4fklTrZ+gE=~`DO2q2@OY^pXpqiV97pNF6wXmtL{GW$mBNCc5#MR&deEA#c-}t_s1iaYAk+y^(kEA<`Wx4vh9{eK@DcyzH^ak-ZWmJwL7*e?&bYJmj~SUHM+@Z*nD z)sB$*Ys@@p=Oa#-H$I1D6gQD6IkYzIClqn2j24b>d$R2HVN1z*Rhh1@sGNrA#yDs( z=>D~q?_-KUoOWL2tG70>9Jxy<#*1Z=aWAQV;{)2v5z~JS0-^G-AUcA}wK|&Yh! z`DM!!oz}+^54Z*Hg9q)N4DF)vEVw(PtSr8dGHjn7s(0$`JCtU&nRr<(ON=r;F861m zIC$)7N^Ul4;{tn+dj$v{#r~&YZ!Z-)m^#X8@swcByVo3@$)d%-``t}&(C+~pmMrFc zg%l`=$%sFRV_iwXa+Ld%(AY8(O4pSRbCOTU@#lO7pskk_$hsg?!2YxYrwra zv1qtEv|-BgV2X8X{wzYrBM>GY`pn*VBKY776>STf#~w$-+$AHHo$1r}^nUi#G)E$^ z&0VzzMMtEyOqQ}F5rh%ex^I!yN9E}m`(mS>bt5QjO3UWCfl|NMmo036^7b5@e}WVv zY{fOosdbfAjiEbH^u5|K7FZ@iVq?W7z8U^fy8kkWjOJ&$MlLw5K_R{I&LY97SpSOU z?z0zittLRXv$gT0w}a-{f3ds?){MpL$(n6^Z3%efLf zr26&;$KrV5$9L7*gb<}QQL6^5j^ma(xS-K;R#YLP#61_06#n!_;U52#F`c+8TKv^~ zSh6YpGk%B|+k$Q$ljY^co^^=}L%TkfDL&Fd?rvHcy$8r$QDM(~n=+bL&S!Qr;4t*L z)m9N%f{%5DQ41)B>V8{LTeBMw_R0Y(mGs55Qatj*(19+~W8`W4 zkvyv@=`yguT{^x(;q;fnZeEzHA)(Ypw8~Sg4azTI2H@vNQEezX^`0RFcIq}7NW2Wf zI{35C|L$75Pv39NMh zYJ}!oT8#p#02_Z{$rMg`>o?fzv>&{F^0D$JA_&Dv_W1InzeAJq}ttcul#%e7ctj zOY(OOg30W1(Y%K`*nxeb&j&JX|9G9S9q+w^nP}4N{N1#@6PB%lzkk5Njbr7C%wzD5 zYg|3btIfc}`fT&MBqJd*(lsKmDu3g}iWbJ?qIPy^Z;a(k@ss&6|GbZ0k>BVCZeG2c zV4c=JDfe1liI@9^Lfo;&`Ez+L_bHy}z36Mc$plo&ZBdhUWi2k~T;w>~9TVWH*LF5c zc-}*9)a1`k;fU{CJJ~3B_@@r2&PUANw)Z+C)g_}sCfN$oL>2Fe`(6fF-HFGVdpbTt zA}i4P+Bf|$%hSwS941T0MgL zJ(j-d%kNiiyRr#US~`piKil|HsP;>8ZsyIOAVpQDuKm%rCoN7tZsyHU^tsRgJBH;B7 zxDo;QTIZsyErfjwnmJWMkFBT1$vHp0F13|sbK>E_jsgw+se|=w_$=-YEYF4WBvZ*s zc6>7*7NY=mlp^#ug?twZd+FP0g3n|a0~}8g#IqIe^Oy?VnGFKWD=(gdp0XjoW@NZH zW2HMi2Jrp0l}WLx)&I7hYNbudSEO6MVF$|fRTx_EBFCUFBhll9t1rHw$+za4 zZEwDNkw17@Y!!P9h-#zP?z@McJ&k>*$%VFQ+SdNi8D$c1emv3Am*9mG-JMOuNmxL? zV@-bQ9ii4WRQ@uV-u3@jqW1&@xUW8+rhcIdX1re(9h?bpm^`E-xQ+cWq*Kjx5c zM8;1vaDDQ{$rpjH& z9H9IsOaYSPxl>|&L%45uMqYN!P*JJ~eV1b!dJi1@`bWze(qHEN`UfR#)Qk>PKlZqW z%AEFLfR5GSFxIi8vuYqK`l8oc!m%;)FQGRL)-C?TdGArv8Vzes1X7K8NOT|2*dag$ z4;WT8`mr3X8$nsYH+lMRg^F9xRp{H>8SQFemZDdZB$$U1d(YHFKyZkALYmvS)h)9u zhx6_bpZaGkCfm2Mt+cp>(Rkds*^8F{n-11<1F5h zx!Eai!&$v#!zul~lwps@GkCzztNiq5cfw3StMvhAHH}mao<y+D<=jr3Jy!^vY z3O4xDTM1!3d`d!Eb7KzbMdk|`V)4H+LT>riZwc2|Lx??H*Ypx`{?Tbbd{nZa#-GbX5XMW$$zV;@h0FX9b_}Wn-I%V@YzHNW`*c$A7V6qv^#5m|X^Tc{sTELb& z>0DEfe!n(v*^m#%cxp%fWT2^bw!rAm$5e;)Rz=;Wibugnma6gX>9$);?e@u!k8PZqgQlXT{7H3OOz(sDKtjhGRR=9+ zpDdeM`YLeVN7g(+o4}GsJEWzwBIB+^8`l)ZL`wMMJJ2hAtI=M@(n{+RB3GntLrPv zi6B;#$0~Sjo$X`(a!7Xg!fKjWlpMZ!p-aK;m@KNOVxyA$ zE4Ig$<(-p7#mK|JPcA|q`N&*CiGiJo@7{vU3R7{fm@OHd&Xef;27NyTzXT3y%zXMi zz5&}exc<%Hiyi=|F^)iIKx1~X_Z12JFy65io!093>?kqNJa*0?5xpa;^z{a>iV-^AC_yjpP z&io`R!Ux_5Y^3&I;3dj0oGQjRxJHSh1<^JpPd|g-=dY^dXO)XNr2qzU&$sZX-E$a( z0deO|Mk{I8;B|k{;HSpu)U*Kg2t(#|*syp2c)BAWm|2m2r$J{ffvVZ--+6YqDr`JS zqCVKcxjy|_X8+%| zC`8k+>2o~)Op-Ua#b$$bCzu++Uk2Ykwr*J&LW=W&?8EVgn_ zixi7LHn%sv(r8|}f;=b9ZU~_e>&r(YtHmHfdZVteXDdI)u%1V*zt+{*Il6KGMUinj zKDRBLGa`t_UbajF@TnWP)x8s|9p3I2X5M`*8t-;V`Ju2@vdJ^MZs={BD_`VwTG1cp zRo22R&LFR>=*5UnxNx(Ib8artSBQY5>`mwi5IaNQc_eMa%Z#ULj}n7Nq`RXl*wz9k zQV$h?)3++m(7tv^p6Z1s)5qf{AI0?WqY-Zil~)N2w3MdB)aU9yC$k$!RH&C^YuCUp zDv`o|2G^h9z)88q^z(6_ihPHIL+6_BS9K?^yKY-Qpj@oAz?Dy5nEL5`K8=}$60Eh= z#%L5TEli&PfjIR#GK2d|f1Ui-?%4@<4NLF%aJ--NvEclY?aheI;Rop5MQr)*%uPSS zc*B}&GPJ@+;u|)q1nE&TA&~ykTA{9Dt<*Il+Z>&m+BMXcUd&(jEpLS65iQ{O?L$wXhIHj@saPZbRWbZ>#_yfwjJi?a8Q_ZQqToimP`Y$s(}!*q z0yntW+M}myi-?wV;g#>^8+<8@E8p9DXZ(!-D6Fa`pG&=fTIW0|F_y17>=MKNco zf(TW&>%#2Xk6bg;7sb{e!h2qw!cW$!Gz#cbk6#Bn)k65YrMW(Q;i!rw`9~)Yl}rd? z&*6LEc%EDNf3^3e@ldx>|3f6Ql(H5>NSZ;)I@Xdb6K+e)AY^Bv>?yl&mt_#6WGgdC z27|F?n_Je&HZ-!YgRzydWvlOJt4AqvFE9~)xrvJ$yd?YXYU2da;)#2QTQe>shQ~728;F+RDji}Fyd2v_E{MSZz zwBIx0unzYR%Ph zDQ6(tTYta+Gt-qo28G#s;f?-3;K!Vlc|BtAg}dpu5RA-^j_3H`9&aiW(Wy3jsIkjj=daP)W0R{-Qkrc z7dUQy!oVy?*}%DG-XcQmVJ1zpfW;@m2xDm~d&THr$fT%-lM3#EKfx?6aT?v{Kqs%w8CtY-%Y(?72c`UDAOv2^IBRhEC+G4lu68(SiU(aPj4tVVi_JzKZ+;v zJkUn4rMrf`h=JYKR)&Y(orI`rJI&{yC z630c+_i*z0?Yf{Q9-XS)d>#L?gs;4?Te-KUMeiCUiuUvc=+{ZBsHASavGNHueYSI; zp~_sJx4V4HL3RC?)dGCKC+rrb{o-e$-Wa+>|5MbV8VJlxX?-&1pLtFaI#9+6M zWKE#{T}2Z7d1`oo;w@MEn5h__?h0T2Sq1Aq)r+%;Xjy&c!ogov#>I(Ml*y+YZ6E_- z3E5Mz&w*gcsN7YSv&shSqz{TWYmlyKhDedt-eU4{)!5(1Oq~>)69`D{8t#iq_@E1dtohn>` zjaS`gk(2$^ueki2x~JP!869g0midVkA-bXm@AD_1EOdO-d$kH%MVx&iG^i7Ms*!oD zKL>T1*n84~^4QoHJZ%>3<%};Q1%%e5|7h^|oLV)Ret@~79`zhAZA7xEYWua$k(W0Nqi1!^XFU)sXp*j)=s`gJ6hZ3lXt|jqLE>2(EP5f2IoeHlxob5)D2D2U@ zBu*!(u_ln&23~c0L}hL!sVj82Puy!}VGni+a#+2W4ZJz^ zGBHcSrit&P1_}KkLm6vw&+8iM%k)On2UC*Q>Xz$(qTB>W=UipDI9M;E8Qh}H^piuf z!2`3lWXTlPK47@{bvZlAB~|KkQ`5G+iOc%AtQ(d$c~s+{%ZtsRnGV_4wVt}EOpKX6 z`EvCF-m$8>@y4|t8A#4D=xNJ}^I(<&r#CeD=83YQcc!F@U!aKVO!u`R?`vRGc^9!6 zFYMuUkT%zJhR);}cSRj_)%1g~6Nc_PE&;zs!z6>MCBCmLT_C5jPe^l?h)Gun{&`}H z2K~A(rX8tko!x#vzD|Fx$RkDsMd%tiWkrA~@3RoJL@HIpxZZ?4etIoHGm3E5^vwE= zWx=<4JkTl}ZI|m;@qCSo1&p)& zc#<2zg?o2$Nx?lsY5DN62kUd5VpHgJ(OX9DkNfTk3~25|P6%b}k8Z#BW`iJd*5?1( z5hP+wuQ6C^wN@)QU;E__=r{nckUjB5-E#4PruW5X1XChFLfUN8ZlJys1iC%!!uk1+ zO~Gmdl`ifY1{C+pR?9`z{aluQpC5|T3PBo)`wYW*GJ3A0LtxMO6UJOW zMbP)jBJnEHkr6bXyucjqR%rUyHcL(sLfF=e$9qhjffQLO*?#bF(U$4ZU$WsMyZo*8 z2g8j;3hU}2nQKocg3VW-gre5e-J&<8ZU@~75p8j}G=olEVI#Z}?HEJ*wB{PeVA0!v zJm26Im<0vr%(eQ1A|bXXub4PgO0z1IJzy zyf*2(*QMTuQ9ihM2GugfE|l#e)=a5=iyZoL4oT@gI_&?^u)D&qMAtIC0)~HWxp*6-G*w>IasQ=)_neO`E z6^^wS==Q4C*b8{OIreF||Io4I<#x5*jCmu|yo65u*_PszpK~A|5aV(r7tucDG6t_h zF|xLgTL=Jhq0;w9-@_`WggcdhsHGhX5KR7UF;C`U`UOj=eoX)5oox`No}nj&APHwE zDMl)_+`*6B0w4Qt3NF>xCBGv%%hs>xQ$mF?gum`du`Q%<67ubZVT^<6-Iqu*R%b51euv_k(Z_(4{u=sNsiezTP z_%vye4)KtGw`eK3^@i8T(@y_7(a*JM$F?Hw`%Xnwg+&E%tEoWHGhe|ag%E+1_KPhE z4c)O90zB3!HpaF=Eq9AANdMZrdLs4fM^2+s9?)m->c^;t*yo#|CZ=gl^QRo05wYo_ zdfn*Rr409t4_^4Fp-XFEyrXQAYfno@9(y-1z4br7f+R|Hv&nx}la-MBH!S{a>@< zfo(=yn(-Bj5K%I5p6lMMwY+~G!+MZy-4i4c*7wbk6?GjG%sz0AuzzPz%X9MMgv#{J z(F7WQXW1!la6-r31FR}_tmp<02xgnAQ|Ns5g~fFj=!B|gMoev zla~!c$>-Y+2n|k2^&ROlh|vw9&qvWp+~kVBy&LnP>{bNoOFeb#o{xM1R~NvUrR6w3 z-YH5U%)C)m(>ub8e-~bZ(+Ps39WRCS)jXXwO50@?HxcEmc+A#;ATk zXm>^)EXQ6U5Gw#Jt1mw(#K2mJK&H3L>1)^ciwNhR)HM_#_?u1HY*QmhHlhjVlFRlA z0?z}3ZORq@X34O>TOdz2GH-f~AHkou@+{^JIQVURt!SW5pvbywb~OYs>if!`pNIMc zy5Pf=rwj9XTYkvV~q4XXf z-m&CD0w4UQAI1mH^fzQ>L)RI~n<%R-1-1gP6^=SrdW3CoG;U?g`yWiApa>Fd32#43 zw`}C2P#R3UMDFDEPOW67D@(yEB?3!_;;iKna|G2f`+?pYeM;x?jv@h8@~Ic+bjG#O zNlhY=e0Pak<%}3Uyn@SJ+hbX=h^a7GUQzXHhH5B8#$=djt1RWLTnw(N5c4QwF7y<2 z7Ao7*T^YKHk_YDg3vbjZ$P2Aac;-P~e6CNYyPgXajv$Tx|F$Enzm`341%bMt*{Y7sUzkp zb++)E2A|zjn~$e-8a!Vk-YM;V?ja> zvbA-4GlBCdK@k1FCEDC9bo%W}7q-1By&)U+N#a|=;2;oc@NEvT?XK)*MP1~t62EUD zis>jkwhoobFGbdo&**({#r^Tm$Si-H@ydkszD$^0v(t`JMBSa~$&rd>Cy_|kiX|p0 zA~zVSs-D&3$mMJ}EZ8##G?*Cw>~VbSD)5mO%q89>iI&xe!K~DybQud zv(BL?saCn5Ljo~ZA$i|tiv0d7u-qKPYTLsXi?bi9J!!Q@MtgQ4XmFNxT3=eJltnpl zXAm(NG_w^AU_mAM1r4yPe0WxzDVrFpsJyR&nK z6hLoJbj-?q(Yo0MvD5$IirP2N<@+l6*?gi(`Au}2x2+RvC|m_N@%``B2wgUHVDV8d z$aZ22juo6mE!?bOj^P8w~&4 z^;gthyC$rt)&`K}OB)s{=hX|xGLPNFOm1R8i`0ieCsvdK2Dex)vj(^wBtK!o#Q}z= z^aC|7n%M>O#ezVCgna@BW~h{(JQfGsdDpdKf%h$x?voJ2O1rR2=PQ#yeB^M)hniLd z_8;?U8smc#@=#0Uq26y+R0z~WwP^uOH3R4as*NJXYxz{cc>hFXTJBcI3HSvn_&UVF z9%KJ}=hRxLs=CsvY7Tz)zE(=|+pUTxfOYj0a7HFO4|qR)XW<4QmJdFeSz1)2*bc0T z28V?b?r=Gm7rJ>H=eSps0Y+LoZU&f^R*g?91$TyXpSbCs@<|H!odT zJDSGQN*t3~(*&Ilf#svfX^~;r=mxA6-yV>%Td0gpem5e&mbrs{gz{}NBx3>`D2q)a zO00008%5;{_WHQF+&3=38n4^E>&X7>sHWLf2586x0*~NGs|>K^(6K|J!NSg|jt}=_ z_6S!oRC}AZkJqGWtSDiObl%vmy)z)ko9^4ZriIHTDVT*o^ObA+0l)2=u|yeU7Lb~Y zQ*L-l5{_>h*?Ae18&diX(Ea?%r!VFO*=B!-JOt8N@s96t=nQ~~CW$+b0!A-d3RIUW z{hFv%k88^Kkg>b|OxPzi`LPgC;HK*M(L|TP@9ybol}mGWcy_EqPWNuLr#O=CkLyA@ z_S%oQJ%_{}maL{jWgAWNX44s#NJcNkz0$Q7dNoI{gvI7RQcEiI#cFifPfZ%Vv<0ax;(`3v9|60y!BGu4{E zJ!$*jQ@erGC+#}ggnjv)3Fq$PT(885a6pi*Ba4SZeog;s|NER`w;>J~-zFM9{M}sb#Ur|f zNRfSZZnd^j!U1l~Jola)U?)Fv(X$u?4J}p(f-SeoZ1r*9C_KXc{lMa64<#F@#rQ*O zz?AO4$5&alM^<9q?Zw;^SOGy_>G2TS84b>)k%5ONQG)RLb`Q~D6N{Niz0-i9PIG0= z=JoJKxne75%}wJkN2tH-ZLNgpj>aU)a@UcO_m?UmO;M6?rWD=pz@Fdr_-21`l6Qk^ z2&hyULptu6cQ1B7JM2G94aFQM^5ClS+8NcS3js+Yv+kQfrvOXiD0?Lg29o*tL`@h@ z**?i-TIkZ14oxDDq*5@x7o%M(wy#h)=}Li4EqkC47Vit`UlqIfE!UOr4{BM1NA*FnPBhWpZ#@}#EJCYrtii6u<|`M48u z3MJa|8Bn2$>E;G}a%OGNQf?G!zr=@O2KvnNk!N@5ju3`CV2gbJ*-(lgCSL*uLog>( z^C=0Q6fh`#3#%=P7$y8pLGCdswk{F%Jr-L>irx_K5mbSc^q_HUW?PJsDEzY^+3#Y4 z&*15t>i|v!ItQLt-B?1<_Lo8qm*!%WyaW?8rQn+nK2-pm5_J{~oPL!%vS4t*`m~QC zdN3ngiofGHgwr7r7Ts$Jg{baKU>*X9iuocqrvj|V7Q?bC!8b2e^{G?7hOSj59?01f z?%7O~4zo?zG`6Y69;9jQ_LRyXC0!TM0*f=*v@8A|0E(2+=O}F%dBh+=V{g1^R~S%2Zdh^V6WF~+u|&nP zS2pnjg`2zkQaIx-%Bn#Qq-y5dX{su;x7wPx`PHB6Sod?j!6iFX&+PU&7d&EfWIZPv zhCc-EIH%AcVD$K)Or1#@B<%Kp>~;xI4%j&E+z(K$0Cdp9m}Lu_JH^BiF!I$Ek=m^R9^k&bp{;8) z-s~Pr-J%2Uk1DlK4{HJducbN7)UuHIhdTj7uApc_;m5r2w3RuhS;3Y<@cO_VI)o*R z?5U9?K{MRh|GlnnB|(!Y8i%k04fM7}KRe2{Q@*D=?6=^yGyVk)Id_jQ^8wHc0dEtA zGE(G5e!dJ=ycYLAp0p#^;AwVtH8CevHQ9XG_r*Bb{}P}}dIRvv+!!=&ZC9uzWcB!_ zcKRhOqz9(L5A2W$4@RhmDO;*Odwmu@9Q@uqMR=?BQEuul=o!rw4N9p{?=fD9AU~Nw z@;*B%xl4+Wxog%oOd-P^$(G?>n1IJsLGRY^dz~ZP09&Tpr6iXJta$_-6A9-M%XU9U z8Xx=dH}dC_p025y48vUA{QQqr#!IDTQHu|bo*nM1JI8_~Yc2MrYyyEH8wmfJol=2S zf6?m*yZZLy1XFn4vcdN37V)SEIW9keVc`7$JE`YwFq|Gw+mHd6`A>Y3Q~Wck{r)Cz zlGd&dX2A}qFTDFA+$fJ+_WD2nHnYyd<3fBxtt61N@y zwL3EtC-6@5mO8lCqz79P-8QmGAkJNGgo@%f5Tk7f%p4-%t^HEE&@nA2E@*k7-*I(V zzi&<-491(aB31#C#AA`cMP(p-*F0iCASRmO?VFYfzs>;THOay`%FQ;93qrC>w;<)J z$^xp2>Z0^>0RSB(mH()AfdJ?#*)G@&@S4#INk?#zn0(Paz3@8Y-6F0cXOA8D0Zx-X zW|RGhQXw^wxmEWl$e5It{AP!(O%pM5x_*OT*&vDO1UHQ*DzNFS3RU09CAxs?vdF?@WrU69Dx$|}P zG2XK4_{_6kEgY_Sq{Kz9vO*{Q1?duIS_w#mwH-}^pn56qp9$VgF; zL_8MQ?eF$&@@>`?4m@QGQI~?lf1LABJJLJJPNE>KkUv^*=y2ff^g@Fd>0IWM$yihA zeBVaWx4CBA4Jr6RM(BGO-xKYaDmzTZ`UB)KN3!UfUZObn?#sIl+GR(Gw*XF=G($8S0tbF1_U#{r>+v6?-J$3)xv##|;F+`19Wllgfla z3<6Prbrea>F|l(lLaYKY{hZG?0udt~k3M|PjX41^Qo*8YBH*o}A` zYPYrUM=mk|cff7yh_B}A!+XKsJa+<4#|p7i#xOj5pAnfeS-{iXb)tQI6hqY z*J`X)#yr%IY^na)6TVsIaf6X;e;(Kd5g6p^4i{;UJ@iXug55sK7rYl@^DtQoX4p$7 zbjkD#BT>w{Ro++Tt$ncVet%7M7rU+h;*tJs`bY%Hmjsmw|1aLkZPE0%?GOChoEX_4c)AiOB!qsjw=(Y zL-XBUkTRGOHVxS@G5ed&vhwx0al>7n0BzRpObf$Eo4O1rlNUw`F1&W2`LtIA( z)xdb;b_Zio>5j&-z;p0|+%T=4oseKz1`)eQ4K3Hu)-hr9QH_PMA2NOF(|;*=QJi6e zB0wD|>pE1vIgzpbs+{*~C<2s%f6d)$7bzycWJo2c^T4!VbdyTDL%xj$vdSiA1|b*| z2d9T&!O_1SYs>9_GMMbxl&X`G*O8OdhFn2Gj*>VYyGoJZQ6V;8c|_1NVs{sew*(m5 zy)l@URT_@{?oi4p72&YK_GeplGnn$!YIhmtCkIKNV@IYOTR#Unsn~mLVcKGUO|=w_ zk~U^SNf%ww8Z3IUgmx`0Xj0@Uk~3;_5R4vUA&hr?CiyJa39nusrMLu2vFM57Dp=F)AlLzLL#0N#3hRH$>94yX5tHCd{BDgKR7u#5ua|<63KHC#D0Op)`C4yh(9wxWt8E&b{bG8@D! zOLNBT6H-UZVQ6rocA^WPLnEt&$w~J0u`L6&=5JKm^sC8Iy6shXR-JDZ*QBNI^?&Ke z$;d=Za-HZ(sTqQtSxPM_G6*vt!>X=%K=X{zmtRV3z6%Nnj0T3Q|5d;V@jPCjtZqMT z(~I2a0HyXxrz{?pQVIAL3P|JN^twcG#Bo$ZdWgOz=@vN={ds0Akj5|bz$IEmAzFb| zA`3xmR=37ai)D99%sdNx*h$Z8-v}p`hxHrv9ip6EY_OW`@9%$;mVA?yJoP83UU229 z*fLh_{hc2Uw?%|e@+&`$@zI>eN1oRzhm#1d*j~f8N@2aw75{x|`Dg9j9w`gPx%Bqh zzolX+vZUKMmhq6QJaja)XXA^_zJ5KHU>vdhZxzss3zz19XK#qu0fe-T=8Mc|#$3Po zux8GefAbj<#cqkI_R5NvG`lO5DiD@c2jw^Hfy`<<2vycpKp z36^sUBM6pha1y28pBTd~`ghKRgyns`tsp$?3g{@J&i-N5^x!q&vAhXo#TuBJqz` z+lTWoVM-xiQ`np$qQc~{jCu+*ozStKwc|1ylN2vq9I?C5_`UGL`@2qXyfinn2a~mX z%%(Owh^kFeYqkW5-2WUQZ=&>UrFFV&RZ-MkpN$a~5P!?j;UTcaG>|p0wfFWueC|;k zby=oM5dk#t-II{>AR|ix$g$#T6Lk!~rJx_K30@kWVW(F@LLz8!Vc|0u2_q5g&CN|Y zIy(A4JaHj-yffvB@z{Ap?q?4xf*OnuCE-80B6^<%eLp?*0E1P$NPWz&(rb=ps&3hYp_CLnwzEb+hFjpR*ivu`yJ!l)<-;D% zs8^IA_0(NNmGNZNwN!n!I=NZgL&Ond5(K-Pc~*3=Vg z+kW8-u%8BOo%g1!c?|}6+VD$tW)TY=K37ew=|?(L6^$`lSNJ)ED!H*+Y@RZcxJeI` za=Op05fb5=?O;(|?TF>W7y58^H+H~qY#h~ez1kkeU$lma0Y+?dT=2~P!dplWhIy#V z_Bd(UB#P~Y^-I?(Z^%?=QkA`TGOEcq3khdWA(}mT{N|i5`JsVSfqFtA{X7>p_sicM z*SKQKa1t2`Gk9nJcl*}|)33A&Y4&u5c6)x|dr_wZo%MdOXW>d7St?HaW^8(~hN60g z{0}N24HB6a&-YZKWO~|2#j*{O>ciT8etu56zwCAJuGmp&6yGvD%#kN7lQl#GpIiKZ z!~73NobU3unlNnCmmS9cVGJzN35oR@usH3cSQc6K2InrWbxhE`v@SqEyFJ6!uS$&e zPpWudFTTvPEqxG=w0qBH_*}I$knmk}4Yt&M0DJ#pfc(9-QNYbREv+}!t@U_$FWJp4 z!QO=>2VGB2rjATD62|ofjLpeVb9iO7rg-@Np&0-3v04lVH5WgGCbY$JXE^n3!?QiY^K`}L{+;m^b0Vh}lMTiw z;YRIbl7f?yQ>jY8p~l+d<0F8SM2Jfk3W)u32t|c(23EV`jOnxD4j_LR?^*+`GNr4J zLWTai4zDpz@ShMebuf0;7vhNYF%~@FQZZ`dS!gIIjNo!}b7z%fX9ReRx4Ebp+Yc^G z__pt)=}Ee|UA>Rm*9Gm;h1t(%5+V=XFAH<{U)&%|HpRk>Mb{+H+6&)u6c1e3pTzN1 z3T@ZtZ7kUM)XM*p{mOjoR@ziNm0+71{VJ(r3%j+nySvM_<~qW%8#AwHl#}e(Sv!`J z$x2mkNa5w~9@%6!@2IJDQ+X94jEpSQ$Q0eRiYuk4k}C@$98d18D_JxfPP%MT5mdI+ zXDwy@Bgb+)O(HVA=Kq{It-Gahk(xGaS~$_Dq%~VpZP4y%c33Chy&GfFgN8b1Ne%3b zqXn=QPs%^DAd;L-G&+K}lT_sACyuK=N5pR#H$Ppn^Im=^72Nuia2Ex@*lahdsC3Pi zZlDl9KQNMLwats0W0yUxgU zf6XI_+dFj``=*j0I2IVb#>e&~G(Al0#Qc{YFGBsnCqcUplAcm>1qZ@5Se==<9!L;* zEIVn4{AGCH4u}Cbn}I%n5u_st%n+s|47&Bjd-JDoWQ@{cvliHTxbFW1wH&`@Id(X^ zaj0)DOa741t)R_i9MFnd%sfg0Tr1bh|6Xrr(pdEDq1cvsm9`EJ9`kc^fidbe%yM&pKEr%uMuK5*o(XPho@eL8!^rwP7 zENrLPysef%nsdN7l}$~v&wWJv+N(V9zRwo}eM?PtH5zz3p46iZ@Q5Rigqi=U zkb3DpmYDqrEAAmY7?6u;kR9PP3fIs}VS&7m+{TfG80i~E#Nijm3BTLC^@SQa+1dG# zih+y2i0L&u;#XBwk#krEWPf{+!13zh3u%cjFAy=(XV4+pJh-i`{my{tvpLCG^W{W) zrl_jpx^BQ0S6qK}Y3VTua3_|36aBEoPGAI;@Qg@6M&af7Klaeokg%m;L(nN#FmXPY+K@ z$1JxP=D3#(@w|JS!1Hc|xy-np*@dLS6;-npY#6q#28tu(K$i6AR~4r!5&S9-fV$J%+n$x^DE^Z2ItyFr`+A3(%9#Zah0Bd~A2&^6JVv9qhxpaVtY zWe%1YH&YajV6K144O_8%6fw}ShMmUdHC}J0)A`y{iIgfq2XTp=2`kD44-!*GAEx_WBX-@g^VbHKM<8f;7KJGyzb(bHRo0Pdw1 zxOZtW=q0r|Ah0^ZU}_lSo5R_{*4BqlRJu;j-KYF-UeYwLduQqp5gk8wMKAq^&;1tH z`H-d3z~sKQQdkFW6i407{+k$m>&j26dnI2)KdPTlX}06(YZk2cDhPqT#cfWuguh4H z^sZ(L_v+^ky@`F7XyN{OL;L(1UvAG!o+h{;^v^*CMou#7;h{m#RMVMcV<#|yx!FqX zH=|J#+TLueGC(|l?vXp&7F+3C`R~l~AyQ{5r(1j~2Jr*}z-7Z-v-H@aaKmUkRxs_o zmX^~B0I0GmDi|9ywxJ}xpBltwXJ>8A%(lp}lMes<>^TjLA}3W0Fn3;RH0viD>^YQv zUa^YcnPI#?ofTQBgW7)bejC3wIq<5KH|nozmJ&yIHF-3(V}0i$78=R zc@I4cs9BFH{eoTA(V@?5*m18*DH(%pLI(>yul{qJ+=i{5udy566}9;N1NS$LzWyy7XK~)I>jVo(Aj@2S2({Mf@V;o;+1Wu5<^IGD z`|uLz<>T`}Hx_S~ZVfn?^sS*Lk?_wOD9kOvpMfh=AwKYJCu7|J;GpMTB{1J% zmY~Cy>JY>p(taCDm9_gUO{yD8C?CS-PF1+kT*M(Wtg5EwlDvH4+1%Vr94PDsuiNTM zdw6(QvFdo2n%wsbJ0w@=r_})qh|Xmn8iW6QvqLuWCC`Zp#=n6n)Fm$I483}BGjUu@ zrpt`L-NL}Y;AS|L>8gIhj=vF0oLRTgy0ws=MLN^~JVTCX$Zyg3u?T0;lq`?EG-7!A z7^VjG{?rdz=B%_6(aM)+%Ywj9I(0Gf+EU)B9{>F=*}8ki^;!`zCwH#lt6HdsbgS)| zM|vTBRm?jU-N4<^Oc-;aF4N*Y59-k!*QlkXC8jVeLp?WC<7I4NpRJKHMGC0tG~)u5 zW7@4z>u8+P&CqbO8`t+LL;FSEG%@<5he4Z?D>D=a=A)HRAu;Kk(o*gcIUxvNhzif* zlM*MDD}e0`4_sSse|*`E7ZV59{5$I!CNUX^nx{Fxs-D>>Mb9Z=|L(FcoC=%gq<25j zzr;Yf6}Ob;{q>sfyR)_38~iBm%Lz``zaXv7oSWDNu>_&Z$VslTBhPj=4w;1zF$vu6 zQxO{j6Uexb`4Y9$lj!2U_*a$DCm@#u#rqYf#i*|-CiAT!1oSY9@xq$nA31PZ-?DnD zz@>Cma*8tHz@n<8g#M{ofAJ@M^ofzD|IyDs&hT@+aZgo%>-`4a9XBzE$E^PnRQxSEgDidgIlQ*vBaw?EA?nYW&Qq@lG42v zCtE%bH8kf@V+2%>g~<=(PU^L6Vt4PYnVox6LSI&#_nXvWwDrf;La|aA@^W0z{l-h? z*VZu7M`)h|-@+A_fg#P;o6jt7gLN30_f+k$c``rw>KElX|MQ(UCu5~hee@i_Vbgsc z9>s8?#()apxD_(@oZUG&x9az}i-PE{r56roD+`X=mXB_#$1EXw{328?_I%umS-j?p zRxu3~*UYzfKebJS@(f~$4HvJ~7gu2Kk5P=sJjQhnv)vZ1uDJ*oM02}US6-Vw8rFDK zRUe%Ku%wqJj8c^z4;JDFoF6;O6@BdeNeVL|48EKK@}FO!GD7 zX&PsmpBr}-Cs67d`e9egTPx;RrhDm(b8n$k*7T9v0q-?rbF}nb9a!_w9pPn(X(nNd zAiYZ1)Dr1J^^6CGVjT(zVEtl6bc>+NiYvuE-eZ<=iL}Lnmx9m}4an$ztm;IuvJoJ8;*-iLD29^-t#G}9*7Xukqq_)u8NW{q6b()H|qyUXwc=Q8_ z0Q>2nhN4wY7V^|_M%`abhWxto=wg4HI z^oXVTak6v%;2*=Sq~XRAmwOAgR7JJ>MJ#|)ky2%R&vVSAJN9Z+$JyGfH;fL7 zf(<+QsaW}i7xN$UQQT4&BmGW0KLefJb~HW{MJQHu;f!wI1VRWRbqQHWaF3x(E_y^_ z(yr!8i6$I9&ml7&JZ!L%Rs}*eUxdWha_*0w(S`JYfv*E%SZew=o&5fO>DYvXc6_P2 zm*i47N^{LvgaKmVFdZ4`1unnJWcZ)e3^^7PfXXJ%?#Ms#gDgd4Y?O0Ovg?aq#123D z2ui7?$Xj~y@+gZdzJj^<_`skFtilz>Kvp_GZ>pkvjIGJ&;lC#~3R>?n$t%aihOcWi zTlnqY@p0ibcR^$ys8d9z(C9l&=1K^ScDvQeRSo&4x%6u7UdUl81 z2_i_!Fh5Eb78aUb27RVTEWY;_`D0pVL*B_Vr%$Y&(fgfdc@(6$fv`2{?x)0t{zoU0 zfXfH5YCLJt8nHvJdBRnB@wH91n9g4$58FISEb~mW`S=Dx+J%)l$Nn>1T+iA1QI&XF z?V3!4d{w`zxKPI9msl<{^+ThZWC=?z50l8^_cye|51{Z-#E{|GnqsSX_}Cv1mBU@{anrm4x3-tW0|axoR!0|gw$yOzxq+tby$Y1d zE9NJm;}?`g!*>kD{iA>J(nVpzdQGsBW;}!+R?_0Ynj0as)jyamqa@~Tfhs{(76KE( zj1E2c+VS(abNq2%0g8uhYqWvIl!vggBqs?5oDLTKl}X6Zn?{0!yuo}?Fchj#d~uFR zjBoiWTGaEGWOYIBq9`nYu|rgo2x`l-N>HyLCrUVeuRPo6W{y+Y^0U&ts+SS39?G0m zB9*CALKegIboV0{KV-#`H&XTkPSj8Yf@vz^@pvFO6!%qDLkGT39Od?KUpkY2u-#XM#&hWv5ykNLb2oVkBp z2_kIuZ>d#<$S^q?O6XBk_n8x`k2q&R(lCB1kLp{U@ZS$ZV6bTY0yqaU8HXbwZ*#7m zk$;Z`pA?F;$4ZDLc#Pi^2koyD5lKk618Ql;v;hT?`!Es1z?!gF@FAfV%6#GNpOVRK zFh5;O=dry4ZuEa}1ygQrID327+w)MWDxj-6#>zv`Gn0851`1x_VvRfQ%aWD;+b3BS z$$==BRi2K_p*1#el|}T41#(1huXyDB)S{$4FrigX)t(Ic&trItck6thvZK|9sPOb z)@@NXsV{?ZLV@TSsf;Uv5>H3n`-lny*dr#*DkUJyz!@lQx=CR%g9`oJe z+utuh)NV+HQeGjjg~VA;rOHK=I!c0!__cJj*Q4Ou7|h!8mlfeC9sBMAiLXKrN?+9f zgIW}b=Q&=5lcTKPCXVQ=%C6j>4!T-P#vOumGLkMAWx0sBeV=-;W#>J>lvLEz)TMD! zF2MmTH_)>oH4}`DNopshIN0L9SMP}ZG6?DVX><3-_g_LtlpSXLED2l}tU@`Q1E3?f z_4{pPigM#=KI!dCd#SP315t!xD^0Eaz-rrEHkd2LvFuD7zZvs$Z^z z_y4hI=GXco#T9rwCQ~LrByZz594{Qu`qfo*DwMc{1o8w)s*j`MuKZ$g3Fp6?O8PmO zNa`PRnqI6}wN$rQfWyiI3xzOVc=;7gA5E5tBDw4z>b_5Vz+WE0kCObdwY4=6ML)B; z>acC>35Ew@3STE`;?ZWw()a=!_4gEMq4-}bGND%(YF|O-ak9(zbnI9AtC@T+5o%Sw z-}irsaS39Hs4Xx`JwXv7ZKFrBt-xcxxKWZoS!gPP%y$sSPZbPXOfxQI^IvK($xh(lywY+;5Jaux6|$BRlYE3?G*T)5!T^Ic|BzQLq73Q zzz4`TornyNoV^~(1X%yT)cK@(OyIK4^28Hk-)Xp|n0&eW#|A)J`0k;#wYBQT&Bb6~ z4|P#j>fa3t#9E$Sbr+=B_MqOCeb&wkGMuo0_uce{&0FFKK^|KVq{*4D$H@6zaL49q zrg1Yv%zwgcHq5I6l$ZvAVe>qY8+9g7LqL(TG=(txM@L73fLu@>t1U#vDd0Ck?9@%t zBpmt!8SxdK@Ml9yMGK4rBo0g`{3(He2PtB+#cKe*(B|r;^=LAXgi5G(2`TlM&5SB( z5)e8DXkX#M_S5ml0x-OovZA7_!FvVsg@wSpMoSmOL8~gh|=T{%~ z;^N}+r;8P-0w*IkmkdlPKDy#@I8!dsFV~0~61=Us&+91fYWh*4%0};#)cp-q{xYgT z=t|puwLBbO9+dYw2LHNwL%%7Y?$Mt%e!zv%3clYh;iTVI^wAJmqzwh%TwiCPSmMGq zH8(2&oQdn}o12puNh-B%hA_TfKfMsq&k+ZJajajTthepa$irJ$Xk@P)kc0QNlHB~Y zM}04+4g0q~;#UvxcG*?y#$=ol>q_Ioj;s%Ll5As5jT3n=nI#WdNaOnuP<~gJ5o0t? zq;%2K)8ivaD?cEBkS8JC+(syl8^~fo zWDnjli+u{h?S0CPU!O1KG_+1_{3V^?(_O&GyFvW@S#>Zovl{i4XaB9CYLNYx(eZqSBGP&a7z@&AvKE;8Nb<4}f)7~Tx?WtyrRVY9L zVI?!)AvrcaUfR(2pqN~qp=)AvK;*VA0pC3cMK+L@vtgdsYX14{2N@XNJRsl&pog5` z(kYX)AJ>oY?3%u|I=gB};Y7SXBN_|!W5H`afgLRv0|eR1(DwfM244(xO=>_pT_}o> zc7_a{tYPo>cs-tto8z^bLhqV!s*PBk`%FXn>$g)JMFyH|pT7|n@HzWtUu-gvb_tYk z#fgv>ar7Q2(tn#^ReX+^HI3yC>|=V77xSxw8#sNl$L7_KcBZzvg? zfe4h*tYa@u3AW4=r?{!=yu>!|f!|d>{F^g&tv$xbKKRl1yP(Esv$u_L*7KO-8h~5- z2h*khOn2c%NjRaeSk6Si=`Sjhz*2)&ACc=e+0Lf3c0FI-EZXPT6L@N$|B{Gp4=hnJ z+};(w*4vOYybUWcqVE|M#aM+7$B}n6qbU#P3j1M@3VI$3Mi&b8j)t)4{le>zC(P($ zPAqU*tVhTyL4Y|vIz0`95TGe{^3ZAk*#gLcC-;ZoNta-ke;PO(=p~D2!yLG#(OUpB z^kir$(DhST{esnQCCIPROl2e}t!3z+j(H}sHc#380MpB_3E->&3-R{yy88~gwaI$7 zQg0EH^tHn>aMbrBZx83qOv=6-!%j7DCGXa#3xs_5)aJ0@rn=dnq7XY^d_wHNFJ;z?C}4lT2`u1;y(mAkijzs2@%dN1kML!nS|!z~$T#9wJ^y_yLCRyG-o`g3yM z=uD2WH5g9D)uLhFt@+hx;#s6*-en82`$IUE{{pnJ_)gOk2@Vo$v<6Rk2K0Vq7VPP>=CH158)QvkR0IU^49#9!_c8lya-}`7lS(mEX^(ggY-}y8pWg}j}Pphndnw-o}Bfr z{rmg;mxp)>6ztbsb#h0YH2<|3y=qMN>~uUrb-rloJ!yfCV(*Y_V$kG z>gs+ftH`oE-lk_K4p@d+A0MFI+|{EqFR)sku=NB_)7m|TExDy})$a`S1irHgaD)=S zUHc71=x3)$QiaSL1qEmek-ZUOpHLQ_!(j>;OA}Q(no0+ZgW-QnfTgICv@yv=R_sn~ z6p%weJ#zuwrN3MsIXAMszXF!qzU3Q3@mC3Q{oO40z*1oL|NSpLMbs!0?;EJQjOL*%+c-k4G~h7r?0G8PvBK z9?MvRs-4d}Yyi=g0{djjBjD+DwY}Z6ZsF&g4IULF4$t7L?D)-I4DWkqYtEl5Qum^~ z-WNG`(4qJ5x^<3Wf~~n2z0THFi;o^Xsg{atbk9eOUh7+@!${kzz~=PPg|bwKX1m%l zGBRGCo}RaaBQ()rV(=NhW*XHo@e&`NYa7{EF&xUY&R1AO{5_$|b){{b9+uKpR#yM^ z_xF`815+a(@uWD|ELfG`Ij;e`HYiJjW3_80+4wS3uHi%?8YpyPw(NQEuvF~`(nQdR z>$d#{E#phc43}CE+M0BzVtsz(A+Of^$rmk<`FOE4AOWx%e7{Eif?U-BBdJu~HX`_q zww-{@_wQER63ln8AZJPHBL5Z_Tj{XCc+Y>70g=j10bslF1O8|FwQMUs%_Bw{3Po;-{ZH`SW(a_Ota&mI~exyu5uE;DgR^29qf0-uec)^FDK`SdqGVp!1kea}TRUb@U^!}!$;{AY zimPC-b{JMjbv0|Rk$o>&g9tK0Ig~>Zt3+3!x|6N6(``?=^JY#@M=gat7)T%0U?6@~ zQb=d5QpdB9E*+EMy`zB0<&d{j<9i!Qk%bI2`(#BKa`=-FGdCykr&SKwsKcWruANP#z*q|vl?1J^?;3Sf^?X=yu4km3dq;#_B70!g2{Oa#XGdEG{CWLr&`djgOkUo{& z&)@l-)1|km9u@mYkT#5uE;KtC)wZ`kX@QnO9oQ&gu^z9B{u4aH4HN9);4Aw{eGASp zhvi>Ul2lXh>u{3pcO^>St2_1!W1hR8DH2m<5e*LzJ8y-~m)fP@C&|0%ni-=aeUA%} z)-O2FD^?4BDUlB8YM+a{YmOY!%DxH{$n@3L*8b7ZaHV7!h}Km~0H?u-gI!*_Zvc19isr;22X{?pxzhf?+7y~wgVGxDd; zb5!)j!Wg$|Y;7tEB(hy_`x@E^VlmSc`>L zS66eq4~o+-FQ8#jV&!iZ0Gm8fy$$9kjB{m0g_k~nMvwIL^j5o{sZ?2!Lz5xWq&eNU zMX`}nys3N`sjVU3_3-U=O4sl7;BBFOzrWl20tovVu+1VrY?gRHU7$(Y(8$ucd%<{N z%YZfP!NmA@i~{;-ZGdtQ5&1K1gB4 zWt(f(un+w&1%{n&nNC(3xh)(Q7Z+-mj;!CHXlzDZFo#q|)fI2n>A^tT*49?|bay7& zJvssFt*n;PyW!0Dc%TxrL|;ex`YF*651#X=oOjfxIK?jG;T`3l2<ITucer#9@adQCc|`1K^A*pj;Yzgp zU?3kA@N_E`4}eU*zUK*K%C-!CT{Hk|NJ&sM952*K02a`PTgeW_88ui`zs>!izp}cm zLS1Y2n^^whKJ=q>5SRcBg4okN2+xj#@Bs%c2gU$a*A^3`wYDwOTL~4DAS|duvo8O@ z%$iq@sGzOx<^9!aJbMvoT?aH#Ug2B+>lcsjRvhm;z*?(o0_&R)x{8hLGj#;9fE>+t zmx+josyjBLnJSo=nB3kK;$;U^`_^IiAqx|rZB{j}5s{LR_$NmOVyyGZ1`hDP7GMSp z@&#N0S^?b$o6i9)3X%Xp0+liiYsv$3O3%d9epyjjUEySHja3{i=9X&8D?)cIMnU0q zvV;$g=yif!=pSWMVDJOZrrLka6Y(_&oTQ16zj-C158KHdE?d$7{hWZRumKd#tyJXo zFQ8doYc_gmN!Nr61ma7-c9Y5=1dz(C=gHEIBhb5o-JLAim>l^CWYNFLLPrLUjihE3 z5JN^E0fGM+9E=23+9cVS6#2@89gyGJ+IsU%f%W3aUnHYg(-+#h-`9^XcpVjme7xGu z`*(WU?Mr6R*eKwML=pxd`WhP4PAwaVTPgVX0P#YOQnlo@=IycO{OKiCvN1Z+5%K&L zXd8)P1453a@bGX{u%`H9cQ?rMY)x25L?jT@jXL_wq%1Oq|2%3Xl6;^+Ta^BXS(Vl2 zxNabHVCzDj;X8~yqH}T~y>^Wu)OV=O<7kC-eg;)X;WxHrC7Y%k4URX!2HOOddot_s zf(}|v@(+_!0bq1i8dv&{6dG8ILsC-GNe=^j@fpaKG>8{_%0o*_s}e9g^}|3O$=pP~ zJ86Or9t9qDp}|VU8AvM-K)TS31+h<()e%FnmU6t-bU1-Rh1YcydkmKi&ttNRh@69i z!+$yho1YNr3lrOK%CQ4*%6IBMk7{UZm*TnJwWR!5h^tW*PLef#dwscp`rX~CC@OaR z9-RQ)(?ZTt)#E)qJPwTRd)|+dQlwWHUV>8?fN6?@H`3I3P^LbdT7~(03kT*LH7~VJ2 z2Q&B|=M`~uVRq!e79~S*2LL=B1P88))32E)6(9ej5WQnFJht5RrP4)R_qAMO4*k5m zvZCMRxGLbKrIo7G=d2vPdhdGh+ZYQ7V50A?ui<~BL`3a-(B!#{CQ-wNW za!65g^Lskw?8v*jy9CZFInWd6nGSLW=L{UsDM_k=fafi8He~lY1yMSzLHWSzB68&B zaGAD03b!|F~f2|OzQ{`5fPm)w}-qbup)u}EEv#0;_-zb3<*^s z;Ap-^Ns3=R__GHjRGLrrvA$$W=eo7catV2Ac*WYty2RDgW5pHR=aTGk+|x^Fbo!< zk>mU`LnV0);1ngYzRsf>;Rb1 z)YMeflZ6ZThj7kR++jXg-N**>elW3|ZvM{B zsv`I6Uuiu$v(wy&LR=62BNUZYSBEd3e2N;ZvC(A-2HqEGO$uqonYz?8D+5mA8em?% z0s^3ARaN)^s}SJUV2R`5hqxO6SX7K2ujl-{77ZTO)!UnoEKa(ptvW&dQi8TLavvVj z1-DKUY^bFbk;#vfFwitLrS@a)ARRt!v@~#y0!yBeZr zSk@o@>%3ypt`7Zorp>K%#o3S-X$!+uLL>&kW|4AjEVJ6RrK(U|^obfgxq8s-`p4!k zLf&Cm1E^2WiKm2?R#^p#l-dWEVJw6Bf+GPgrLQG6fCT~k3MWzAFyJ~Qsj&GuI0`u2 zvNMY$@l19pu;XCCpnZ#72*{Vd^|NzuW<(TyVTCOhPL%aP0=bNz-_sWXa^(@97$B1b z&|0h=9Kup?05OPbX>q@c@qxLIDpQx&VG1pis>kMnwU*^hv9)BSD|7_~f($J`!L%fi zn80I4#XiFYn_FfMe?_EFmLv+M4+cXbBI!eEYB1plfQkeNhGC}`%QPeND~O6Lfvy5b zG+{&(W5EIkSPbZvX|0xr6_x+k6)c7@_$U{H*Ya?q&J!cvG!QWn9myMe{PVkwb@piQ6}NSzd^ zsik#$cSr3(smGb2HvEVHpHS+8~7LSPvFbo zm4NZzrUE1H>lFYTeoq>6KX`ZeaCm8C;|gT`o9~vxr@%J?#?2k+osJ%W<8}J389$TH z`}X7jhonp4Pr?@f#NWEMqw^ks^S7aqXTocB-PjU+zvug70pWvP+SLgFIQ_0PYB%^L z@UyyXU;|x0%!a3P07FocQgQ%r{$pst3fPzHo#Es<13Y#cGOmO71ca{!$eSo4nKQC+ z{)K2h4t{9LMkJK&tZ73)_(v)oNdUn4zEsbKw}S7I%J>fCu0qFw0B}|+UEu)W{GBoO z9q_|a8Qrnm_W{5`sdR<{fb$)0UI{zRTqtxj0GyhtF(CkCvz%ivayFHrJ;^nMu?_(I zA(j5t0O0(4VBG2OKCu5@DD-z4asdaT=P&OO)Ki`n{bqWpGZ6;hCWPLn$k3d`FwbLcv48@1$<}Zv(b}}K1TwRNyfXr z2d}6TJE{x-ij3czV@)~qn5p^!{Au`18uG^ol(Ajsn04C`UJBm{z7{?K9bay$1G#%} z@7w43Kj+7{>iFN@z+sS=i;J z=y*L`C)EBUuHS%mPAmv317KeSEDvs+1yUPD&2WNk8xxleJ4f+_FWe@yU(p(4A^_Qp z;Uj3=p^4TB_gux_cf_FI2Fgq(3LiLIIvO_DVJT!2`V2SiKB*TJ0pN7)`*7YgtBD~_ z`J5j+ogD-Lz`+=5dewD9Q?j&WGurK9a*T=qa87QbC5jAP&tixrA5%|R%={QVOiKSGr&uQ$Y7~x8H5QGnwVd&*>#02C@+HS#dwH|=-O}O%kAWvDH zyfX$^gE<(&2Ri->@Z*AvUK<}=JzrMt5QK7HJZ}tA{3YbLDoj?~6lCX!m;{rMSK)&< zmjZxd7a4|}O}a$T{wPkG*p4tVPX@k-oQAuYbrRw_Xf=)XfGl$OS*~zLN?URUVApdP z8XRQ{(Vt>?L}{h(!R_V;8uI{Hn+px88c%OSh8vvq7&jR#0m2(F{xz6z9K&G6qWkU=Y%HRhzYd?KhG19Q^T>G5`!Xx%g;H-$lSSC`?vW z*4Dr(_XB`YPiKTrta5$HHDh6kydlU1Lqws~`N-J|018ux0gXy3B6v1*D*kYWxtj6- zFh%Du)Rs<1nT5K8rfgB_h+rO|EuD)`_AUd!_|JN6HF3Y;=643j>0JaMSb%0BT3fmT zpKMkJfaM<%%kS+`W+ZU%l;wZlAJCAi@rkp{)s!~`8MTb4c-sh*?HWK^jed^Ic>@~I zi-xn!744PS4)|R$KS;SM*){fTq1Hdi_`FLwVw;zizQrMOUfcs-3HrQE` z@zDUIdlvx!FT#k3os!q!lacUt9sp+Y*9mC#ktlm>fSle%0KhCpoDg8hRrq8i$e9O# zRcc!YD6_Wn&;U8TipUc4Rs}{x2QI}Y?wwvuc>uV0ig4UT$Z;6z?I@TCzlrFGMNE)6 z)x`k7$z1)K>UR`gc>&+alndRb$d9N0!SPyajxYg$CpX2@crkxW)}b$L~Rhxw;KE$I3gM_=bpo9zq={N zs0e^ZbG19-+a5vVegId+gCGEK$GoScMC3$lvm46Ijf~ZW`ehLSI^Jle3y~&db+yP_ z9We;P`On5sXMklJ@anY3qGU&a+}hZ(ElTc-%I!K8EbuskOHkZs2YLnhp_kzN4G`$g zMmMHJ(xLcZ;<-a*0Jtb~7^;&a_8bS54kjUw^Vi2_V>D4y&N%?xEx#VVn7ARlG~R=8 z*7nrdpymSZ!T~+`?t$|Ojehb4VITZZc>GNj0L%}62Yzx%zclJ;v~LhE+^q815`1g+ z>-6xd!;_(hjmyF?r2wc6W33AFT`;LY)m`rUqRcUL>WTI^!}F{=5ATSY?aXiY0r;ad zjad}{ll1O%$r~g6^$L|Wp6z5w56j`T=Q?C1`EdpG_+t>x>MS*kC_I?-u(^nHNP?d^~T*$fLvEH(+<@T!!3`O?~KaeBTdN*VEvX zhYWiDh4X%+sHIdHfD8m9rJKV~f_<}}kD|sudXU9i918N5{Kk;{&Xu%_nVYKw^pL46 zkJFR|^=0s0rJjhnz?!PpVQ1w!(pmt-*m%2?%=w0U%XZ=jv|ZX*lEa-Zdp`|r`0g=S z=CLu#c_i$1B5PZ9{@mF>k){{Pa#y8!a37dT)Hwh+UY!m(5qalIjqK31b;UJvv*&4N|GXQYDFIemBXW_Q{t!g#0gNxl$eH;Lc zY(cG}w-W%eC*Qb|I}R7plkYUsqAjZc8^yem1Ay~=+D&g7(nvY|tMb4_Uvn-3J?gk| zuA>V8{80z@*)AmKyFT$uQ=Q-H=Mj?vz|-%3C`(+NlP@H4o_`9?_n7-ulo|k+{O&tm zDCDB^L>&K?s0O75z%kM@HWvc9;oP9)MNMWqlN^v502f?_6NXIIGg%+xxo@SxwYm*W z4S?YR_lX%h8X}q9GheWxP0XvP;Yk5daaLpFi`Z!DkjN7Q_Qvs*4&iRvu;c(_AQ*c$ zBIyhHsZEXT=sv3$TwM9{wzXHbb#3effH|Y+4gP?$zb|PlC`AsoZu{4~-nyN`V2=|B zAL}O>)+qpM0O9e!=3_n6-vm#}>{JmbMlh|{G@*1}n-y(S9gV19EJgwzX*)Y|nV!@%rgpO5u*TT-oR^srf zImK(L09d1E7&kJrWp%ba#N9fN&w7Y}IK6ckvxLwDzPaf+He=>7!S2p+@#NF(QTgst zpSn|frxJkqJ!2vNe-VJ5(Om>!5rCdO(6jpg0~3*Zjyq@>(*OVf07*qoM6N<$g8lpR ATmS$7 literal 0 HcmV?d00001 diff --git a/flutter/android/app/src/main/res/values/ic_launcher_background.xml b/flutter/android/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 000000000..ab9832824 --- /dev/null +++ b/flutter/android/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #ffffff + \ No newline at end of file From 5dc0c5be5e2dfad0b99cf7c65927e8812242e403 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Mon, 13 Feb 2023 14:58:52 +0100 Subject: [PATCH 174/199] invert color of checkmark in darkmode --- flutter/lib/common.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 85aae4c80..e1dd1a1f8 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -217,6 +217,9 @@ class MyTheme { style: ButtonStyle(splashFactory: NoSplash.splashFactory), ) : null, + checkboxTheme: const CheckboxThemeData( + checkColor: MaterialStatePropertyAll(dark) + ), ).copyWith( extensions: >[ ColorThemeExtension.dark, From 7dfe20417ed75264e86c12660a85d23507cd603b Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Fri, 17 Feb 2023 22:34:46 +0100 Subject: [PATCH 175/199] Update de.rs --- src/lang/de.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 1672af2b9..38f4fddab 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Von der Gegenstelle manuell geschlossen"), ("Enable remote configuration modification", "Änderung der Konfiguration aus der Ferne zulassen"), ("Run without install", "Ohne Installation ausführen"), - ("Connect via relay", ""), + ("Connect via relay", "Verbindung über Relay-Server"), ("Always connect via relay", "Immer über Relay-Server verbinden"), ("whitelist_tip", "Nur IPs auf der Whitelist können zugreifen."), ("Login", "Anmelden"), @@ -272,21 +272,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Total", "Gesamt"), ("items", "Einträge"), ("Selected", "Ausgewählt"), - ("Screen Capture", "Bildschirmzugr."), - ("Input Control", "Eingabezugriff"), - ("Audio Capture", "Audiozugriff"), - ("File Connection", "Dateizugriff"), + ("Screen Capture", "Bildschirmaufnahme"), + ("Input Control", "Eingabesteuerung"), + ("Audio Capture", "Audioaufnahme"), + ("File Connection", "Dateiverbindung"), ("Screen Connection", "Bildschirmanschluss"), ("Do you accept?", "Verbindung zulassen?"), ("Open System Setting", "Systemeinstellung öffnen"), ("How to get Android input permission?", "Wie erhalte ich eine Android-Eingabeberechtigung?"), ("android_input_permission_tip1", "Damit ein entferntes Gerät Ihr Android-Gerät steuern kann, müssen Sie RustDesk erlauben, den Dienst \"Barrierefreiheit\" zu verwenden."), - ("android_input_permission_tip2", "Bitte gehen Sie zur nächsten Systemeinstellungsseite, suchen Sie [Installierte Dienste] und schalten Sie den Dienst [RustDesk Input] ein."), + ("android_input_permission_tip2", "Bitte gehen Sie zur nächsten Systemeinstellungsseite, suchen Sie \"Installierte Dienste\" und schalten Sie den Dienst \"RustDesk Input\" ein."), ("android_new_connection_tip", "möchte ihr Gerät steuern."), ("android_service_will_start_tip", "Durch das Aktivieren der Bildschirmfreigabe wird der Dienst automatisch gestartet, sodass andere Geräte dieses Android-Gerät steuern können."), ("android_stop_service_tip", "Durch das Deaktivieren des Dienstes werden automatisch alle hergestellten Verbindungen getrennt."), ("android_version_audio_tip", "Ihre Android-Version unterstützt keine Audioaufnahme, bitte aktualisieren Sie auf Android 10 oder höher, falls möglich."), - ("android_start_service_tip", "Tippen Sie auf [Dienst aktivieren] oder aktivieren Sie die Berechtigung [Bildschirmzugr.], um den Bildschirmfreigabedienst zu starten."), + ("android_start_service_tip", "Tippen Sie auf \"Dienst aktivieren\" oder aktivieren Sie die Berechtigung \"Bildschirmaufnahme\", um den Bildschirmfreigabedienst zu starten."), ("Account", "Konto"), ("Overwrite", "Überschreiben"), ("This file exists, skip or overwrite this file?", "Diese Datei existiert; überspringen oder überschreiben?"), @@ -386,7 +386,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland erfordert Ubuntu 21.04 oder eine höhere Version."), ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland erfordert eine höhere Version der Linux-Distribution. Bitte versuchen Sie den X11-Desktop oder ändern Sie Ihr Betriebssystem."), ("JumpLink", "View"), - ("Please Select the screen to be shared(Operate on the peer side).", "Bitte wählen Sie den freizugebenden Bildschirm aus (Bedienung auf der Peer-Seite)."), + ("Please Select the screen to be shared(Operate on the peer side).", "Bitte wählen Sie den freizugebenden Bildschirm aus (Bedienung auf der Gegenseite)."), ("Show RustDesk", "RustDesk anzeigen"), ("This PC", "Dieser PC"), ("or", "oder"), @@ -449,7 +449,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Sprachanruf"), ("Text chat", "Text-Chat"), ("Stop voice call", "Sprachanruf beenden"), - ("relay_hint_tip", ""), - ("Reconnect", ""), + ("relay_hint_tip", "Wenn eine direkte Verbindung nicht möglich ist, können Sie versuchen, eine Verbindung über einen Relay-Server herzustellen. \nWenn Sie eine Relay-Verbindung beim ersten Versuch herstellen möchten, können Sie das Suffix \"/r\" an die ID anhängen oder die Option \"Immer über Relay-Server verbinden\" auf der Gegenstelle auswählen."), + ("Reconnect", "Erneut verbinden"), ].iter().cloned().collect(); } From 116649eaf2dede3874a50ba8a27568249da94e3a Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Sat, 18 Feb 2023 09:42:40 +0800 Subject: [PATCH 176/199] Update bug_report.yaml --- .github/ISSUE_TEMPLATE/bug_report.yaml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index c2d92097c..a955c2a2e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -1,14 +1,5 @@ name: 🐞 Bug report description: Thanks for taking the time to fill out this bug report! Please fill the form in **English** -title: "[Bug] " -body: - - type: checkboxes - attributes: - label: Is there an existing issue for this? - description: Please search to see if an issue related to this already exists. - options: - - label: I have searched the existing issues - required: true - type: textarea id: desc attributes: From 38cb44a89c17f605d714c3b5f70e708f64812546 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sat, 18 Feb 2023 09:44:45 +0800 Subject: [PATCH 177/199] remove title and checkbox in issue template because title cause guy empty title and no body care about the checkbox`` --- .github/ISSUE_TEMPLATE/bug_report.yaml | 1 + .github/ISSUE_TEMPLATE/feature_request.yaml | 9 --------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index a955c2a2e..ec23aa7a9 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -1,5 +1,6 @@ name: 🐞 Bug report description: Thanks for taking the time to fill out this bug report! Please fill the form in **English** +body: - type: textarea id: desc attributes: diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 50cd6d0cf..29b0d0e0f 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -1,15 +1,6 @@ name: 🛠️ Feature request description: Suggest an idea for RustDesk -title: "[FR] " body: - - type: checkboxes - attributes: - label: Is there an existing issue for this? - description: Please search to see if an issue related to this already exists. - options: - - label: I have searched the existing issues - required: true - - type: textarea id: desc attributes: From df8c7b1c3096eff65da615cdad08d69d00df11a5 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Sun, 12 Feb 2023 12:59:51 +0100 Subject: [PATCH 178/199] remove boxed layout of nested option --- .../desktop/pages/desktop_setting_page.dart | 94 +++++++------------ 1 file changed, 36 insertions(+), 58 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 25c485a2a..34398dd0d 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -650,7 +650,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { context, onChanged != null)), ), ], - ).paddingSymmetric(horizontal: 10), + ).paddingOnly(right: 10), onTap: () => onChanged?.call(value), )) .toList(); @@ -675,6 +675,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { if (usePassword) radios[0], if (usePassword) _SubLabeledWidget( + context, 'One-time password length', Row( children: [ @@ -756,9 +757,10 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { controller.text = data['port'].toString(); return Offstage( offstage: !enabled, - child: Row(children: [ - _SubLabeledWidget( - 'Port', + child: _SubLabeledWidget( + context, + 'Port', + Row(children: [ SizedBox( width: 80, child: TextField( @@ -772,28 +774,29 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { textAlign: TextAlign.end, decoration: const InputDecoration( hintText: '21118', - border: InputBorder.none, - contentPadding: EdgeInsets.only(right: 5), + border: OutlineInputBorder(), + contentPadding: + EdgeInsets.only(bottom: 10, top: 10, right: 10), isCollapsed: true, ), - ), + ).marginOnly(right: 15), ), - enabled: enabled && !locked, - ).marginOnly(left: 5), - Obx(() => ElevatedButton( - onPressed: applyEnabled.value && enabled && !locked - ? () async { - applyEnabled.value = false; - await bind.mainSetOption( - key: 'direct-access-port', - value: controller.text); - } - : null, - child: Text( - translate('Apply'), - ), - ).marginOnly(left: 20)) - ]), + Obx(() => ElevatedButton( + onPressed: applyEnabled.value && enabled && !locked + ? () async { + applyEnabled.value = false; + await bind.mainSetOption( + key: 'direct-access-port', + value: controller.text); + } + : null, + child: Text( + translate('Apply'), + ), + )) + ]), + enabled: enabled && !locked, + ), ); }, ), @@ -1614,43 +1617,18 @@ Widget _SubButton(String label, Function() onPressed, [bool enabled = true]) { } // ignore: non_constant_identifier_names -Widget _SubLabeledWidget(String label, Widget child, {bool enabled = true}) { - RxBool hover = false.obs; +Widget _SubLabeledWidget(BuildContext context, String label, Widget child, + {bool enabled = true}) { return Row( children: [ - MouseRegion( - onEnter: (_) => hover.value = true, - onExit: (_) => hover.value = false, - child: Obx( - () { - return Container( - height: 32, - decoration: BoxDecoration( - border: Border.all( - color: hover.value && enabled - ? const Color(0xFFD7D7D7) - : const Color(0xFFCBCBCB), - width: hover.value && enabled ? 2 : 1)), - child: Row( - children: [ - Container( - height: 28, - color: (hover.value && enabled) - ? const Color(0xFFD7D7D7) - : const Color(0xFFCBCBCB), - alignment: Alignment.center, - padding: const EdgeInsets.symmetric( - horizontal: 5, vertical: 2), - child: Text( - '${translate(label)}: ', - style: const TextStyle(fontWeight: FontWeight.w300), - ), - ).paddingAll(2), - child, - ], - )); - }, - )), + Text( + '${translate(label)}: ', + style: TextStyle(color: _disabledTextColor(context, enabled)), + ), + SizedBox( + width: 10, + ), + child, ], ).marginOnly(left: _kContentHSubMargin); } From 7dc0cefeee2eb5e6ee3899b0ed63c200dc82ba85 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sat, 18 Feb 2023 23:34:28 +0800 Subject: [PATCH 179/199] fix #3257 and opt svg --- flutter/assets/GitHub.svg | 2 +- flutter/assets/Google.svg | 2 +- flutter/assets/Okta.svg | 31 +-------------------------- flutter/assets/actions.svg | 3 +-- flutter/assets/actions_mobile.svg | 3 +-- flutter/assets/android.svg | 2 +- flutter/assets/call_end.svg | 3 +-- flutter/assets/call_wait.svg | 3 +-- flutter/assets/chat.svg | 3 +-- flutter/assets/close.svg | 3 +-- flutter/assets/display.svg | 3 +-- flutter/assets/fullscreen.svg | 3 +-- flutter/assets/fullscreen_exit.svg | 3 +-- flutter/assets/insecure.svg | 2 +- flutter/assets/insecure_relay.svg | 2 +- flutter/assets/kb_layout_iso.svg | 2 +- flutter/assets/kb_layout_not_iso.svg | 2 +- flutter/assets/keyboard.svg | 3 +-- flutter/assets/linux.svg | 3 +-- flutter/assets/logo.svg | 2 +- flutter/assets/mac.svg | 2 +- flutter/assets/pinned.svg | 3 +-- flutter/assets/rec.svg | 3 +-- flutter/assets/record_screen.svg | 25 +-------------------- flutter/assets/secure.svg | 4 +--- flutter/assets/secure_relay.svg | 2 +- flutter/assets/unpinned.svg | 3 +-- flutter/assets/voice_call.svg | 2 +- flutter/assets/voice_call_waiting.svg | 2 +- flutter/assets/win.svg | 2 +- 30 files changed, 30 insertions(+), 98 deletions(-) diff --git a/flutter/assets/GitHub.svg b/flutter/assets/GitHub.svg index a5bd1de81..ef0bb12a7 100644 --- a/flutter/assets/GitHub.svg +++ b/flutter/assets/GitHub.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/Google.svg b/flutter/assets/Google.svg index b7bb2f42f..df394a84f 100644 --- a/flutter/assets/Google.svg +++ b/flutter/assets/Google.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/Okta.svg b/flutter/assets/Okta.svg index 0fa45b93d..931e72844 100644 --- a/flutter/assets/Okta.svg +++ b/flutter/assets/Okta.svg @@ -1,30 +1 @@ - - - - - - - - - - - + \ No newline at end of file diff --git a/flutter/assets/actions.svg b/flutter/assets/actions.svg index 5403853db..3049f3b89 100644 --- a/flutter/assets/actions.svg +++ b/flutter/assets/actions.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/actions_mobile.svg b/flutter/assets/actions_mobile.svg index 6aed6053e..4185945e1 100644 --- a/flutter/assets/actions_mobile.svg +++ b/flutter/assets/actions_mobile.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/android.svg b/flutter/assets/android.svg index e46dab11e..6fd89c9ab 100644 --- a/flutter/assets/android.svg +++ b/flutter/assets/android.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/flutter/assets/call_end.svg b/flutter/assets/call_end.svg index 39367c3c5..7c07ee25d 100644 --- a/flutter/assets/call_end.svg +++ b/flutter/assets/call_end.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/call_wait.svg b/flutter/assets/call_wait.svg index 42a11fe56..530f12a97 100644 --- a/flutter/assets/call_wait.svg +++ b/flutter/assets/call_wait.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/chat.svg b/flutter/assets/chat.svg index 7088107b0..c4ab3c92d 100644 --- a/flutter/assets/chat.svg +++ b/flutter/assets/chat.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/close.svg b/flutter/assets/close.svg index 7488acc9f..fb18eabd2 100644 --- a/flutter/assets/close.svg +++ b/flutter/assets/close.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/display.svg b/flutter/assets/display.svg index b5a88106e..9d107d699 100644 --- a/flutter/assets/display.svg +++ b/flutter/assets/display.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/fullscreen.svg b/flutter/assets/fullscreen.svg index cd01f93f9..93f27bf7b 100644 --- a/flutter/assets/fullscreen.svg +++ b/flutter/assets/fullscreen.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/fullscreen_exit.svg b/flutter/assets/fullscreen_exit.svg index 8d4414897..f244631fe 100644 --- a/flutter/assets/fullscreen_exit.svg +++ b/flutter/assets/fullscreen_exit.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/insecure.svg b/flutter/assets/insecure.svg index 37bb196e3..5a344dd04 100644 --- a/flutter/assets/insecure.svg +++ b/flutter/assets/insecure.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/flutter/assets/insecure_relay.svg b/flutter/assets/insecure_relay.svg index f08bee6a6..17b474e6e 100644 --- a/flutter/assets/insecure_relay.svg +++ b/flutter/assets/insecure_relay.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/flutter/assets/kb_layout_iso.svg b/flutter/assets/kb_layout_iso.svg index 69f0c96cb..163e045e1 100644 --- a/flutter/assets/kb_layout_iso.svg +++ b/flutter/assets/kb_layout_iso.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/kb_layout_not_iso.svg b/flutter/assets/kb_layout_not_iso.svg index 09a055be3..cfbb046ca 100644 --- a/flutter/assets/kb_layout_not_iso.svg +++ b/flutter/assets/kb_layout_not_iso.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/keyboard.svg b/flutter/assets/keyboard.svg index d5481d7a1..d72033f6d 100644 --- a/flutter/assets/keyboard.svg +++ b/flutter/assets/keyboard.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/linux.svg b/flutter/assets/linux.svg index 1738a02ee..2c3697be9 100644 --- a/flutter/assets/linux.svg +++ b/flutter/assets/linux.svg @@ -1,2 +1 @@ - - + \ No newline at end of file diff --git a/flutter/assets/logo.svg b/flutter/assets/logo.svg index 13eb73f22..4d43f8bcd 100644 --- a/flutter/assets/logo.svg +++ b/flutter/assets/logo.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/flutter/assets/mac.svg b/flutter/assets/mac.svg index 8092b3af3..ccf9c7aab 100644 --- a/flutter/assets/mac.svg +++ b/flutter/assets/mac.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/flutter/assets/pinned.svg b/flutter/assets/pinned.svg index dd718b96a..a8715011b 100644 --- a/flutter/assets/pinned.svg +++ b/flutter/assets/pinned.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/rec.svg b/flutter/assets/rec.svg index 33a57e9d0..09aa55e2a 100644 --- a/flutter/assets/rec.svg +++ b/flutter/assets/rec.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/record_screen.svg b/flutter/assets/record_screen.svg index e1b962124..bbd948c73 100644 --- a/flutter/assets/record_screen.svg +++ b/flutter/assets/record_screen.svg @@ -1,24 +1 @@ - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/secure.svg b/flutter/assets/secure.svg index 29e1d3c4f..fcd99f2f5 100644 --- a/flutter/assets/secure.svg +++ b/flutter/assets/secure.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/flutter/assets/secure_relay.svg b/flutter/assets/secure_relay.svg index 8ecbdb47b..af54808a8 100644 --- a/flutter/assets/secure_relay.svg +++ b/flutter/assets/secure_relay.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/flutter/assets/unpinned.svg b/flutter/assets/unpinned.svg index 9e9e3de8b..7e93a7a35 100644 --- a/flutter/assets/unpinned.svg +++ b/flutter/assets/unpinned.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/voice_call.svg b/flutter/assets/voice_call.svg index 5654befc7..bf90ec958 100644 --- a/flutter/assets/voice_call.svg +++ b/flutter/assets/voice_call.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/voice_call_waiting.svg b/flutter/assets/voice_call_waiting.svg index fd8334f92..f1771c3fd 100644 --- a/flutter/assets/voice_call_waiting.svg +++ b/flutter/assets/voice_call_waiting.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/win.svg b/flutter/assets/win.svg index 326f7829d..a0f7e3def 100644 --- a/flutter/assets/win.svg +++ b/flutter/assets/win.svg @@ -1 +1 @@ - + \ No newline at end of file From 11d5cdb4f119f0fc523cce61bf6ce67cd2013777 Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Sat, 18 Feb 2023 23:24:29 +0100 Subject: [PATCH 180/199] Update README-DE.md - Translation improved - Missing parts from the english readme added --- docs/README-DE.md | 115 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 81 insertions(+), 34 deletions(-) diff --git a/docs/README-DE.md b/docs/README-DE.md index e537d41f3..8ee4a51fa 100644 --- a/docs/README-DE.md +++ b/docs/README-DE.md @@ -1,63 +1,84 @@

    RustDesk - Your remote desktop
    -
    Server • - Kompilieren • + Server • + KompilierenDockerDateistrukturScreenshots
    - [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    - Wir brauchen deine Hilfe, um diese README Datei zu verbessern und zu aktualisieren + [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Dansk]
    + Wir brauchen deine Hilfe, um dieses README, die RustDesk-Benutzeroberfläche und die Dokumentation in deine Muttersprache zu übersetzen.

    Rede mit uns auf: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) -RustDesk ist eine in Rust geschriebene Remote-Desktop-Software, die out-of-the-box ohne besondere Konfiguration funktioniert. Du hast die volle Kontrolle über deine Daten und musst dir keine Sorgen um die Sicherheit machen. Du kannst unseren Rendezvous/Relay Server nutzen, [einen eigenen Server aufsetzen](https://rustdesk.com/server) oder [einen eigenen Server programmieren](https://github.com/rustdesk/rustdesk-server-demo). +RustDesk ist eine in Rust geschriebene Remote-Desktop-Software, die out of the box ohne besondere Konfiguration funktioniert. Du hast die volle Kontrolle über deine Daten und musst dir keine Sorgen um die Sicherheit machen. Du kannst unseren Rendezvous/Relay-Server nutzen, [einen eigenen Server aufsetzen](https://rustdesk.com/server) oder [einen eigenen Server programmieren](https://github.com/rustdesk/rustdesk-server-demo). + +![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) RustDesk heißt jegliche Mitarbeit willkommen. Schau dir [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) an, wenn du Unterstützung beim Start brauchst. -[**PROGRAMM DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) +[**Wie arbeitet RustDesk?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) -## Kostenlose öffentliche Server +[**Programm herunterladen**](https://github.com/rustdesk/rustdesk/releases) + +[**Nächtliche Erstellung**](https://github.com/rustdesk/rustdesk/releases/tag/nightly) + +[Get it on F-Droid](https://f-droid.org/en/packages/com.carriez.flutter_hbb) + +## Freie öffentliche Server Nachfolgend sind die Server gelistet, die du kostenlos nutzen kannst. Es kann sein, dass sich diese Liste immer mal wieder ändert. Falls du nicht in der Nähe einer dieser Server bist, kann es sein, dass deine Verbindung langsam sein wird. - -| Standort | Anbieter | Spezifikationen | Kommentar | -| --------- | ------------- | ------------------ | ---------- | -| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | | -| Germany | Codext | 2 vCPU / 4GB RAM | -| Germany | Hetzner | 4 vCPU / 8GB RAM | -| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | -| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | +| Standort | Anbieter | Spezifikation | +| --------- | ------------- | ------------------ | +| Südkorea (Seoul) | AWS lightsail | 1 vCPU / 0,5 GB RAM | +| Deutschland | Hetzner | 2 vCPU / 4 GB RAM | +| Deutschland | Codext | 4 vCPU / 8 GB RAM | +| Finnland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8 GB RAM | +| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8 GB RAM | +| Ukraine (Kiew) | dc.volia (2VM) | 2 vCPU / 4 GB RAM | ## Abhängigkeiten -Die Desktop-Versionen nutzen [Sciter](https://sciter.com/) oder Flutter für die GUI. Bitte lade die dynamische Sciter Bibliothek selbst herunter. +Desktop-Versionen verwenden [Sciter](https://sciter.com/) oder Flutter für die GUI, dieses Tutorial ist nur für Sciter. + +Bitte lade die dynamische Bibliothek Sciter selbst herunter. [Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | [Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | -[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) +[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) -## Die groben Schritte zum Kompilieren +## Grobe Schritte zum Kompilieren -- Bereite deine Rust Entwicklungsumgebung und C++ Build-Umgebung vor +- Bereite deine Rust-Entwicklungsumgebung und C++-Build-Umgebung vor -- Installiere [vcpkg](https://github.com/microsoft/vcpkg) und füge die `VCPKG_ROOT` Systemumgebungsvariable hinzu +- Installiere [vcpkg](https://github.com/microsoft/vcpkg) und füge die Systemumgebungsvariable `VCPKG_ROOT` hinzu - Windows: `vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static` - - Linux/MacOS: `vcpkg install libvpx libyuv opus` + - Linux/macOS: `vcpkg install libvpx libyuv opus` - Nutze `cargo run` +## [Erstellen](https://rustdesk.com/docs/de/dev/build/) + ## Kompilieren auf Linux ### Ubuntu 18 (Debian 10) ```sh -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 +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 libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev ``` +### openSUSE Tumbleweed + +```sh +sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel +``` ### Fedora 28 (CentOS 8) ```sh @@ -82,7 +103,7 @@ export VCPKG_ROOT=$HOME/vcpkg vcpkg/vcpkg install libvpx libyuv opus ``` -### libvpx reparieren (Für Fedora) +### libvpx reparieren (für Fedora) ```sh cd vcpkg/buildtrees/libvpx/src @@ -105,16 +126,40 @@ cd rustdesk mkdir -p target/debug wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so mv libsciter-gtk.so target/debug -cargo run +VCPKG_ROOT=$HOME/vcpkg cargo run ``` -### Ändere Wayland zu X11 (Xorg) +### Wayland zu X11 (Xorg) ändern -RustDesk unterstützt "Wayland" nicht. Siehe [hier](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/), um Xorg als Standard GNOME Session zu nutzen. +RustDesk unterstützt Wayland nicht. Siehe [hier](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/), um Xorg als Standard-GNOME-Sitzung zu nutzen. + +## Wayland-Unterstützung + +Wayland scheint keine API für das Senden von Tastatureingaben an andere Fenster zu bieten. Daher verwendet RustDesk eine API von einer niedrigeren Ebene, nämlich dem Gerät `/dev/uinput` (Linux-Kernelebene). + +Wenn Wayland die kontrollierte Seite ist, müssen Sie wie folgt vorgehen: +```bash +# Dienst uinput starten +$ sudo rustdesk --service +$ rustdesk +``` +**Hinweis**: Die Wayland-Bildschirmaufnahme verwendet verschiedene Schnittstellen. RustDesk unterstützt derzeit nur org.freedesktop.portal.ScreenCast. +```bash +$ dbus-send --session --print-reply \ + --dest=org.freedesktop.portal.Desktop \ + /org/freedesktop/portal/desktop \ + org.freedesktop.DBus.Properties.Get \ + string:org.freedesktop.portal.ScreenCast string:version +# Keine Unterstützung +Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast” +# Unterstützung +method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2 + variant uint32 4 +``` ## Auf Docker kompilieren -Beginne damit, das Repository zu klonen und den Docker Container zu bauen: +Beginne damit, das Repository zu klonen und den Docker-Container zu bauen: ```sh git clone https://github.com/rustdesk/rustdesk @@ -122,13 +167,13 @@ cd rustdesk docker build -t "rustdesk-builder" . ``` -Jedes Mal, wenn du das Programm kompilieren musst, nutze diesen Befehl: +Führe jedes Mal, wenn du das Programm kompilieren musst, folgenden Befehl aus: ```sh docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder ``` -Bedenke, dass das erste Mal Kompilieren länger dauern kann, da die Abhängigkeiten erst kompiliert werden müssen bevor sie zwischengespeichert werden können. Nachfolgende Kompiliervorgänge werden schneller sein. Falls du zusätzliche oder andere Argumente für den Kompilierbefehl angeben musst, kannst du diese am Ende des Befehls an der `` Position machen. Wenn du zum Beispiel eine optimierte Releaseversion kompilieren willst, kannst du das tun, indem du `--release` am Ende des Befehls anhängst. Das daraus entstehende Programm kannst du im “target” Ordner auf deinem System finden. Du kannst es mit folgenden Befehlen ausführen: +Bedenke, dass das erste Kompilieren länger dauern kann, bis die Abhängigkeiten zwischengespeichert sind. Nachfolgende Kompiliervorgänge sind schneller. Wenn du verschiedene Argumente für den Kompilierbefehl angeben musst, kannst du dies am Ende des Befehls an der Position `` tun. Wenn du zum Beispiel eine optimierte Releaseversion kompilieren willst, kannst du `--release` am Ende des Befehls anhängen. Das daraus entstehende Programm findest du im Zielordner auf deinem System. Du kannst es mit folgendem Befehl ausführen: ```sh target/debug/rustdesk @@ -140,18 +185,20 @@ Oder, wenn du eine Releaseversion benutzt: target/release/rustdesk ``` -Bitte stelle sicher, dass du diese Befehle vom Stammverzeichnis vom RustDesk Repository nutzt. Ansonsten kann es passieren, dass das Programm die Ressourcen nicht finden kann. Bitte bedenke auch, dass Unterbefehle von Cargo, wie z. B. `install` oder `run` aktuell noch nicht unterstützt werden, da sie das Programm innerhalb des Containers starten oder installieren würden, anstatt auf deinem eigentlichen System. +Bitte stelle sicher, dass du diese Befehle im Stammverzeichnis des RustDesk-Repositorys nutzt. Ansonsten kann es passieren, dass das Programm die Ressourcen nicht finden kann. Bitte bedenke auch, dass andere Cargo-Unterbefehle wie `install` oder `run` aktuell noch nicht unterstützt werden, da sie das Programm innerhalb des Containers starten oder installieren würden, anstatt auf deinem eigentlichen System. ## Dateistruktur -- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: Video Codec, Konfiguration, TCP/UDP Wrapper, Protokoll Puffer, fs Funktionen für Dateitransfer und ein paar andere nützliche Funktionen +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: Video-Codec, Konfiguration, TCP/UDP-Wrapper, Protokoll-Puffer, fs-Funktionen für Dateitransfer und ein paar andere nützliche Funktionen - **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: Bildschirmaufnahme -- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: Plattformspezifische Maus- und Tastatur-Steuerung +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: Plattformspezifische Maus- und Tastatursteuerung - **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI -- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: Audio/Zwischenablage/Eingabe/Videodienste und Netzwerk Verbindungen +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: Audio/Zwischenablage/Eingabe/Videodienste und Netzwerkverbindungen - **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: Starten einer Peer-Verbindung -- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Mit [rustdesk-server](https://github.com/rustdesk/rustdesk-server) kommunizieren, für Verbindung von außen warten, direkt (TCP hole punching) oder weitergeleitet +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Mit [rustdesk-server](https://github.com/rustdesk/rustdesk-server) kommunizieren, warten auf direkte (TCP hole punching) oder weitergeleitete Verbindung - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: Plattformspezifischer Code +- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter-Code für Handys +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript für Flutter-Webclient ## Screenshots From b733ad93796de81735a52068a78e89a2ef30c170 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 19 Feb 2023 10:19:28 +0800 Subject: [PATCH 181/199] refact register_breakdown_handler Signed-off-by: fufesou --- Cargo.lock | 6 +- Cargo.toml | 2 - libs/enigo/Cargo.toml | 3 - libs/enigo/src/linux/xdo.rs | 4 +- libs/hbb_common/Cargo.toml | 2 + libs/hbb_common/src/lib.rs | 1 + libs/hbb_common/src/platform/mod.rs | 83 ++++++++++++++++++++++++++++ libs/scrap/Cargo.toml | 1 - libs/scrap/src/lib.rs | 2 +- libs/scrap/src/quartz/capturer.rs | 2 +- libs/scrap/src/quartz/config.rs | 2 +- libs/scrap/src/quartz/ffi.rs | 2 +- libs/scrap/src/x11/capturer.rs | 2 +- libs/scrap/src/x11/ffi.rs | 2 +- libs/scrap/src/x11/iter.rs | 2 +- src/client.rs | 2 +- src/core_main.rs | 4 +- src/platform/linux.rs | 85 +---------------------------- src/server/portable_service.rs | 2 +- 19 files changed, 101 insertions(+), 108 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b308de149..eb26f2ed4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1566,7 +1566,6 @@ version = "0.0.14" dependencies = [ "core-graphics 0.22.3", "hbb_common", - "libc", "log", "objc", "pkg-config", @@ -2598,6 +2597,7 @@ name = "hbb_common" version = "0.1.0" dependencies = [ "anyhow", + "backtrace", "bytes", "chrono", "confy", @@ -2608,6 +2608,7 @@ dependencies = [ "futures", "futures-util", "lazy_static", + "libc", "log", "mac_address", "machine-uid", @@ -4813,7 +4814,6 @@ dependencies = [ "arboard", "async-process", "async-trait", - "backtrace", "base64", "bytes", "cc", @@ -4847,7 +4847,6 @@ dependencies = [ "include_dir", "jni 0.19.0", "lazy_static", - "libc", "libpulse-binding", "libpulse-simple-binding", "mac_address", @@ -5046,7 +5045,6 @@ dependencies = [ "hwcodec", "jni 0.19.0", "lazy_static", - "libc", "log", "ndk 0.7.0", "num_cpus", diff --git a/Cargo.toml b/Cargo.toml index 9588d10b6..0ebe49fdf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,6 @@ cfg-if = "1.0" lazy_static = "1.4" sha2 = "0.10" repng = "0.2" -libc = "0.2" parity-tokio-ipc = { git = "https://github.com/open-trade/parity-tokio-ipc" } flexi_logger = { version = "0.22", features = ["async", "use_chrono_for_offset"] } runas = "0.2" @@ -121,7 +120,6 @@ mouce = { git="https://github.com/fufesou/mouce.git" } evdev = { git="https://github.com/fufesou/evdev" } dbus = "0.9" dbus-crossroads = "0.5" -backtrace = "0.3" [target.'cfg(target_os = "android")'.dependencies] android_logger = "0.11" diff --git a/libs/enigo/Cargo.toml b/libs/enigo/Cargo.toml index cc4173a97..fc4db9a63 100644 --- a/libs/enigo/Cargo.toml +++ b/libs/enigo/Cargo.toml @@ -37,8 +37,5 @@ core-graphics = "0.22" objc = "0.2" unicode-segmentation = "1.6" -[target.'cfg(target_os = "linux")'.dependencies] -libc = "0.2" - [build-dependencies] pkg-config = "0.3" diff --git a/libs/enigo/src/linux/xdo.rs b/libs/enigo/src/linux/xdo.rs index 2115d7283..f0f7d49af 100644 --- a/libs/enigo/src/linux/xdo.rs +++ b/libs/enigo/src/linux/xdo.rs @@ -1,8 +1,6 @@ -use libc; - use crate::{Key, KeyboardControllable, MouseButton, MouseControllable}; -use self::libc::{c_char, c_int, c_void, useconds_t}; +use hbb_common::libc::{c_char, c_int, c_void, useconds_t}; use std::{borrow::Cow, ffi::CString, ptr}; const CURRENT_WINDOW: c_int = 0; diff --git a/libs/hbb_common/Cargo.toml b/libs/hbb_common/Cargo.toml index 59f0896cc..e7a7eacd1 100644 --- a/libs/hbb_common/Cargo.toml +++ b/libs/hbb_common/Cargo.toml @@ -31,6 +31,8 @@ sodiumoxide = "0.2" regex = "1.4" tokio-socks = { git = "https://github.com/open-trade/tokio-socks" } chrono = "0.4" +backtrace = "0.3" +libc = "0.2" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] mac_address = "1.1" diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs index 1c49adfb7..99cb6f408 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -39,6 +39,7 @@ pub use tokio_socks::IntoTargetAddr; pub use tokio_socks::TargetAddr; pub mod password_security; pub use chrono; +pub use libc; pub use directories_next; pub mod keyboard; diff --git a/libs/hbb_common/src/platform/mod.rs b/libs/hbb_common/src/platform/mod.rs index 8daba257f..05ecd292d 100644 --- a/libs/hbb_common/src/platform/mod.rs +++ b/libs/hbb_common/src/platform/mod.rs @@ -1,2 +1,85 @@ #[cfg(target_os = "linux")] pub mod linux; + +use crate::{log, config::Config, ResultType}; +use std::{collections::HashMap, process::{Command, exit}}; + +extern "C" fn breakdown_signal_handler(sig: i32) { + let mut stack = vec![]; + backtrace::trace(|frame| { + backtrace::resolve_frame(frame, |symbol| { + if let Some(name) = symbol.name() { + stack.push(name.to_string()); + } + }); + true // keep going to the next frame + }); + let mut info = String::default(); + if stack.iter().any(|s| { + s.contains(&"nouveau_pushbuf_kick") + || s.to_lowercase().contains("nvidia") + || s.contains("gdk_window_end_draw_frame") + }) { + Config::set_option("allow-always-software-render".to_string(), "Y".to_string()); + info = "Always use software rendering will be set.".to_string(); + log::info!("{}", info); + } + log::error!( + "Got signal {} and exit. stack:\n{}", + sig, + stack.join("\n").to_string() + ); + if !info.is_empty() { + system_message( + "RustDesk", + &format!("Got signal {} and exit.{}", sig, info), + true, + ) + .ok(); + } + exit(0); +} + +/// forever: may not work +pub fn system_message(title: &str, msg: &str, forever: bool) -> ResultType<()> { + let cmds: HashMap<&str, Vec<&str>> = HashMap::from([ + ("notify-send", [title, msg].to_vec()), + ( + "zenity", + [ + "--info", + "--timeout", + if forever { "0" } else { "3" }, + "--title", + title, + "--text", + msg, + ] + .to_vec(), + ), + ("kdialog", ["--title", title, "--msgbox", msg].to_vec()), + ( + "xmessage", + [ + "-center", + "-timeout", + if forever { "0" } else { "3" }, + title, + msg, + ] + .to_vec(), + ), + ]); + for (k, v) in cmds { + if Command::new(k).args(v).spawn().is_ok() { + return Ok(()); + } + } + crate::bail!("failed to post system message"); +} + +pub fn register_breakdown_handler() { + unsafe { + libc::signal(libc::SIGSEGV, breakdown_signal_handler as _); + } +} diff --git a/libs/scrap/Cargo.toml b/libs/scrap/Cargo.toml index e2eb43177..82cb88faf 100644 --- a/libs/scrap/Cargo.toml +++ b/libs/scrap/Cargo.toml @@ -16,7 +16,6 @@ mediacodec = ["ndk"] [dependencies] block = "0.1" cfg-if = "1.0" -libc = "0.2" num_cpus = "1.13" lazy_static = "1.4" hbb_common = { path = "../hbb_common" } diff --git a/libs/scrap/src/lib.rs b/libs/scrap/src/lib.rs index 504f0a4b3..77070d1a2 100644 --- a/libs/scrap/src/lib.rs +++ b/libs/scrap/src/lib.rs @@ -2,7 +2,7 @@ extern crate block; #[macro_use] extern crate cfg_if; -pub extern crate libc; +pub use hbb_common::libc; #[cfg(dxgi)] extern crate winapi; diff --git a/libs/scrap/src/quartz/capturer.rs b/libs/scrap/src/quartz/capturer.rs index 5be55ea22..cf442c2b4 100644 --- a/libs/scrap/src/quartz/capturer.rs +++ b/libs/scrap/src/quartz/capturer.rs @@ -1,7 +1,7 @@ use std::ptr; use block::{Block, ConcreteBlock}; -use libc::c_void; +use hbb_common::libc::c_void; use std::sync::{Arc, Mutex}; use super::config::Config; diff --git a/libs/scrap/src/quartz/config.rs b/libs/scrap/src/quartz/config.rs index 11a6d5fc0..d5f992f0b 100644 --- a/libs/scrap/src/quartz/config.rs +++ b/libs/scrap/src/quartz/config.rs @@ -1,6 +1,6 @@ use std::ptr; -use libc::c_void; +use hbb_common::libc::c_void; use super::ffi::*; diff --git a/libs/scrap/src/quartz/ffi.rs b/libs/scrap/src/quartz/ffi.rs index ca39c0a61..6b8c6e0e1 100644 --- a/libs/scrap/src/quartz/ffi.rs +++ b/libs/scrap/src/quartz/ffi.rs @@ -1,7 +1,7 @@ #![allow(dead_code)] use block::RcBlock; -use libc::c_void; +use hbb_common::libc::c_void; pub type CGDisplayStreamRef = *mut c_void; pub type CFDictionaryRef = *mut c_void; diff --git a/libs/scrap/src/x11/capturer.rs b/libs/scrap/src/x11/capturer.rs index 0dcfcfdab..6486af55c 100644 --- a/libs/scrap/src/x11/capturer.rs +++ b/libs/scrap/src/x11/capturer.rs @@ -1,6 +1,6 @@ use std::{io, ptr, slice}; -use libc; +use hbb_common::libc; use super::ffi::*; use super::Display; diff --git a/libs/scrap/src/x11/ffi.rs b/libs/scrap/src/x11/ffi.rs index 5df5c46a8..500f57615 100644 --- a/libs/scrap/src/x11/ffi.rs +++ b/libs/scrap/src/x11/ffi.rs @@ -1,6 +1,6 @@ #![allow(non_camel_case_types)] -use libc::c_void; +use hbb_common::libc::c_void; #[link(name = "xcb")] #[link(name = "xcb-shm")] diff --git a/libs/scrap/src/x11/iter.rs b/libs/scrap/src/x11/iter.rs index cb3310be9..406c27352 100644 --- a/libs/scrap/src/x11/iter.rs +++ b/libs/scrap/src/x11/iter.rs @@ -1,7 +1,7 @@ use std::ptr; use std::rc::Rc; -use libc; +use hbb_common::libc; use super::ffi::*; use super::{Display, Rect, Server}; diff --git a/src/client.rs b/src/client.rs index 51e7f9a29..8683dad1f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -101,7 +101,7 @@ pub fn get_key_state(key: enigo::Key) -> bool { cfg_if::cfg_if! { if #[cfg(target_os = "android")] { -use libc::{c_float, c_int, c_void}; +use hbb_common::libc::{c_float, c_int, c_void}; type Oboe = *mut c_void; extern "C" { fn create_oboe_player(channels: c_int, sample_rate: c_int) -> Oboe; diff --git a/src/core_main.rs b/src/core_main.rs index e2f3f80e0..7d722e6c5 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -1,4 +1,4 @@ -use hbb_common::log; +use hbb_common::{log, platform::register_breakdown_handler}; /// shared by flutter and sciter main function /// @@ -38,10 +38,10 @@ pub fn core_main() -> Option> { } i += 1; } + register_breakdown_handler(); #[cfg(target_os = "linux")] #[cfg(feature = "flutter")] { - crate::platform::linux::register_breakdown_handler(); let (k, v) = ("LIBGL_ALWAYS_SOFTWARE", "true"); if !hbb_common::config::Config::get_option("allow-always-software-render").is_empty() { std::env::set_var(k, v); diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 8fa95ac90..2ff2d3729 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -1,7 +1,7 @@ use super::{CursorData, ResultType}; pub use hbb_common::platform::linux::*; use hbb_common::{allow_err, bail, log}; -use libc::{c_char, c_int, c_void}; +use hbb_common::libc::{c_char, c_int, c_void}; use std::{ cell::RefCell, collections::HashMap, @@ -642,86 +642,3 @@ pub fn get_double_click_time() -> u32 { double_click_time } } - -/// forever: may not work -pub fn system_message(title: &str, msg: &str, forever: bool) -> ResultType<()> { - let cmds: HashMap<&str, Vec<&str>> = HashMap::from([ - ("notify-send", [title, msg].to_vec()), - ( - "zenity", - [ - "--info", - "--timeout", - if forever { "0" } else { "3" }, - "--title", - title, - "--text", - msg, - ] - .to_vec(), - ), - ("kdialog", ["--title", title, "--msgbox", msg].to_vec()), - ( - "xmessage", - [ - "-center", - "-timeout", - if forever { "0" } else { "3" }, - title, - msg, - ] - .to_vec(), - ), - ]); - for (k, v) in cmds { - if std::process::Command::new(k).args(v).spawn().is_ok() { - return Ok(()); - } - } - bail!("failed to post system message"); -} - -extern "C" fn breakdown_signal_handler(sig: i32) { - let mut stack = vec![]; - backtrace::trace(|frame| { - backtrace::resolve_frame(frame, |symbol| { - if let Some(name) = symbol.name() { - stack.push(name.to_string()); - } - }); - true // keep going to the next frame - }); - let mut info = String::default(); - if stack.iter().any(|s| { - s.contains(&"nouveau_pushbuf_kick") - || s.to_lowercase().contains("nvidia") - || s.contains("gdk_window_end_draw_frame") - }) { - hbb_common::config::Config::set_option( - "allow-always-software-render".to_string(), - "Y".to_string(), - ); - info = "Always use software rendering will be set.".to_string(); - log::info!("{}", info); - } - log::error!( - "Got signal {} and exit. stack:\n{}", - sig, - stack.join("\n").to_string() - ); - if !info.is_empty() { - system_message( - "RustDesk", - &format!("Got signal {} and exit.{}", sig, info), - true, - ) - .ok(); - } - std::process::exit(0); -} - -pub fn register_breakdown_handler() { - unsafe { - libc::signal(libc::SIGSEGV, breakdown_signal_handler as _); - } -} diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index c783fef52..fd17fd469 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -492,7 +492,7 @@ pub mod client { let mut option = SHMEM.lock().unwrap(); let shmem = option.as_mut().unwrap(); unsafe { - libc::memset(shmem.as_ptr() as _, 0, shmem.len() as _); + hbb_common::libc::memset(shmem.as_ptr() as _, 0, shmem.len() as _); } drop(option); match para { From a333a261fdfe636f4bd9830dc25a803f124d63b3 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 19 Feb 2023 11:40:59 +0800 Subject: [PATCH 182/199] add alert for macos Signed-off-by: fufesou --- Cargo.lock | 12 +++++ libs/hbb_common/Cargo.toml | 3 ++ libs/hbb_common/examples/system_message.rs | 15 ++++++ libs/hbb_common/src/platform/linux.rs | 40 +++++++++++++++ libs/hbb_common/src/platform/macos.rs | 55 +++++++++++++++++++++ libs/hbb_common/src/platform/mod.rs | 57 ++++++---------------- src/core_main.rs | 5 +- 7 files changed, 145 insertions(+), 42 deletions(-) create mode 100644 libs/hbb_common/examples/system_message.rs create mode 100644 libs/hbb_common/src/platform/macos.rs diff --git a/Cargo.lock b/Cargo.lock index eb26f2ed4..48981e169 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2612,6 +2612,7 @@ dependencies = [ "log", "mac_address", "machine-uid", + "osascript", "protobuf", "protobuf-codegen", "quinn", @@ -3926,6 +3927,17 @@ version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" +[[package]] +name = "osascript" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38731fa859ef679f1aec66ca9562165926b442f298467f76f5990f431efe87dc" +dependencies = [ + "serde 1.0.149", + "serde_derive", + "serde_json 1.0.89", +] + [[package]] name = "pango" version = "0.16.5" diff --git a/libs/hbb_common/Cargo.toml b/libs/hbb_common/Cargo.toml index e7a7eacd1..0457bb19a 100644 --- a/libs/hbb_common/Cargo.toml +++ b/libs/hbb_common/Cargo.toml @@ -48,6 +48,9 @@ protobuf-codegen = { version = "3.1" } [target.'cfg(target_os = "windows")'.dependencies] winapi = { version = "0.3", features = ["winuser"] } +[target.'cfg(target_os = "macos")'.dependencies] +osascript = "0.3.0" + [dev-dependencies] toml = "0.5" serde_json = "1.0" diff --git a/libs/hbb_common/examples/system_message.rs b/libs/hbb_common/examples/system_message.rs new file mode 100644 index 000000000..26320e329 --- /dev/null +++ b/libs/hbb_common/examples/system_message.rs @@ -0,0 +1,15 @@ +extern crate hbb_common; + +fn main() { + #[cfg(target_os = "linux")] + linux::system_message("test title", "test message", true).ok(); + #[cfg(target_os = "macos")] + macos::alert( + "RustDesk".to_owned(), + "critical".to_owned(), + "test title".to_owned(), + "test message".to_owned(), + ["Ok".to_owned()].to_vec(), + ) + .ok(); +} diff --git a/libs/hbb_common/src/platform/linux.rs b/libs/hbb_common/src/platform/linux.rs index 7c107d11c..191ea2e6f 100644 --- a/libs/hbb_common/src/platform/linux.rs +++ b/libs/hbb_common/src/platform/linux.rs @@ -1,4 +1,5 @@ use crate::ResultType; +use std::{collections::HashMap, process::Command}; lazy_static::lazy_static! { pub static ref DISTRO: Distro = Distro::new(); @@ -155,3 +156,42 @@ fn run_loginctl(args: Option>) -> std::io::Result ResultType<()> { + let cmds: HashMap<&str, Vec<&str>> = HashMap::from([ + ("notify-send", [title, msg].to_vec()), + ( + "zenity", + [ + "--info", + "--timeout", + if forever { "0" } else { "3" }, + "--title", + title, + "--text", + msg, + ] + .to_vec(), + ), + ("kdialog", ["--title", title, "--msgbox", msg].to_vec()), + ( + "xmessage", + [ + "-center", + "-timeout", + if forever { "0" } else { "3" }, + title, + msg, + ] + .to_vec(), + ), + ]); + for (k, v) in cmds { + if Command::new(k).args(v).spawn().is_ok() { + return Ok(()); + } + } + crate::bail!("failed to post system message"); +} diff --git a/libs/hbb_common/src/platform/macos.rs b/libs/hbb_common/src/platform/macos.rs new file mode 100644 index 000000000..299a21f93 --- /dev/null +++ b/libs/hbb_common/src/platform/macos.rs @@ -0,0 +1,55 @@ +use osascript; +use serde_derive; + +#[derive(Serialize)] +struct AlertParams { + title: String, + message: String, + alert_type: String, + buttons: Vec, +} + +#[derive(Deserialize)] +struct AlertResult { + #[serde(rename = "buttonReturned")] + button: String, +} + +/// Alert dialog, return the clicked button value. +/// +/// # Arguments +/// +/// * `app` - The app to execute the script. +/// * `alert_type` - Alert type. critical +/// * `title` - The alert title. +/// * `message` - The alert message. +/// * `buttons` - The buttons to show. +pub fn alert( + app: &str, + alert_type: &str, + title: &str, + message: String, + buttons: Vec, +) -> ResultType { + let script = osascript::JavaScript::new(format!( + " + var App = Application('{}'); + App.includeStandardAdditions = true; + return App.displayAlert($params.title, { + message: $params.message, + 'as': $params.alert_type, + buttons: $params.buttons, + }); + ", + app + )); + + script + .execute_with_params(AlertParams { + title, + message, + alert_type, + buttons, + })? + .button +} diff --git a/libs/hbb_common/src/platform/mod.rs b/libs/hbb_common/src/platform/mod.rs index 05ecd292d..89a3a1569 100644 --- a/libs/hbb_common/src/platform/mod.rs +++ b/libs/hbb_common/src/platform/mod.rs @@ -1,8 +1,11 @@ #[cfg(target_os = "linux")] pub mod linux; -use crate::{log, config::Config, ResultType}; -use std::{collections::HashMap, process::{Command, exit}}; +#[cfg(target_os = "macos")] +pub mod macos; + +use crate::{config::Config, log}; +use std::process::exit; extern "C" fn breakdown_signal_handler(sig: i32) { let mut stack = vec![]; @@ -30,54 +33,26 @@ extern "C" fn breakdown_signal_handler(sig: i32) { stack.join("\n").to_string() ); if !info.is_empty() { - system_message( + #[cfg(target_os = "linux")] + linux::system_message( "RustDesk", &format!("Got signal {} and exit.{}", sig, info), true, ) .ok(); + #[cfg(target_os = "macos")] + macos::alert( + "RustDesk".to_owned(), + "critical".to_owned(), + "Crashed".to_owned(), + format!("Got signal {} and exit.{}", sig, info), + ["Ok".to_owned()].to_vec(), + ) + .ok(); } exit(0); } -/// forever: may not work -pub fn system_message(title: &str, msg: &str, forever: bool) -> ResultType<()> { - let cmds: HashMap<&str, Vec<&str>> = HashMap::from([ - ("notify-send", [title, msg].to_vec()), - ( - "zenity", - [ - "--info", - "--timeout", - if forever { "0" } else { "3" }, - "--title", - title, - "--text", - msg, - ] - .to_vec(), - ), - ("kdialog", ["--title", title, "--msgbox", msg].to_vec()), - ( - "xmessage", - [ - "-center", - "-timeout", - if forever { "0" } else { "3" }, - title, - msg, - ] - .to_vec(), - ), - ]); - for (k, v) in cmds { - if Command::new(k).args(v).spawn().is_ok() { - return Ok(()); - } - } - crate::bail!("failed to post system message"); -} - pub fn register_breakdown_handler() { unsafe { libc::signal(libc::SIGSEGV, breakdown_signal_handler as _); diff --git a/src/core_main.rs b/src/core_main.rs index 7d722e6c5..2619a1c07 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -1,4 +1,6 @@ -use hbb_common::{log, platform::register_breakdown_handler}; +use hbb_common::log; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use hbb_common::platform::register_breakdown_handler; /// shared by flutter and sciter main function /// @@ -38,6 +40,7 @@ pub fn core_main() -> Option> { } i += 1; } + #[cfg(not(any(target_os = "android", target_os = "ios")))] register_breakdown_handler(); #[cfg(target_os = "linux")] #[cfg(feature = "flutter")] From 626fdefb18ede90d7aa65511feaae4dd5630543d Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 19 Feb 2023 12:01:46 +0800 Subject: [PATCH 183/199] debug macos and linux Signed-off-by: fufesou --- libs/hbb_common/examples/system_message.rs | 6 +++- libs/hbb_common/src/platform/macos.rs | 32 +++++++++++----------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/libs/hbb_common/examples/system_message.rs b/libs/hbb_common/examples/system_message.rs index 26320e329..347bec47f 100644 --- a/libs/hbb_common/examples/system_message.rs +++ b/libs/hbb_common/examples/system_message.rs @@ -1,4 +1,8 @@ extern crate hbb_common; +#[cfg(target_os = "linux")] +use hbb_common::platform::linux; +#[cfg(target_os = "macos")] +use hbb_common::platform::macos; fn main() { #[cfg(target_os = "linux")] @@ -6,7 +10,7 @@ fn main() { #[cfg(target_os = "macos")] macos::alert( "RustDesk".to_owned(), - "critical".to_owned(), + "warning".to_owned(), "test title".to_owned(), "test message".to_owned(), ["Ok".to_owned()].to_vec(), diff --git a/libs/hbb_common/src/platform/macos.rs b/libs/hbb_common/src/platform/macos.rs index 299a21f93..0008c6266 100644 --- a/libs/hbb_common/src/platform/macos.rs +++ b/libs/hbb_common/src/platform/macos.rs @@ -1,5 +1,6 @@ +use crate::ResultType; use osascript; -use serde_derive; +use serde_derive::{Deserialize, Serialize}; #[derive(Serialize)] struct AlertParams { @@ -20,36 +21,35 @@ struct AlertResult { /// # Arguments /// /// * `app` - The app to execute the script. -/// * `alert_type` - Alert type. critical +/// * `alert_type` - Alert type. . informational, warning, critical /// * `title` - The alert title. /// * `message` - The alert message. /// * `buttons` - The buttons to show. pub fn alert( - app: &str, - alert_type: &str, - title: &str, + app: String, + alert_type: String, + title: String, message: String, buttons: Vec, ) -> ResultType { - let script = osascript::JavaScript::new(format!( + let script = osascript::JavaScript::new(&format!( " var App = Application('{}'); App.includeStandardAdditions = true; - return App.displayAlert($params.title, { + return App.displayAlert($params.title, {{ message: $params.message, 'as': $params.alert_type, buttons: $params.buttons, - }); + }}); ", app )); - script - .execute_with_params(AlertParams { - title, - message, - alert_type, - buttons, - })? - .button + let result: AlertResult = script.execute_with_params(AlertParams { + title, + message, + alert_type, + buttons, + })?; + Ok(result.button) } From 8852d97efc3f119ee299447e4f23f40baf7ba7a7 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 19 Feb 2023 12:52:41 +0800 Subject: [PATCH 184/199] fix build linux Signed-off-by: fufesou --- src/platform/linux.rs | 9 ++++----- src/server/portable_service.rs | 4 ++-- src/tray.rs | 4 ++-- src/ui_interface.rs | 2 +- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 2ff2d3729..32c32efb9 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -1,10 +1,9 @@ use super::{CursorData, ResultType}; +use hbb_common::libc::{c_char, c_int, c_long, c_void}; pub use hbb_common::platform::linux::*; use hbb_common::{allow_err, bail, log}; -use hbb_common::libc::{c_char, c_int, c_void}; use std::{ cell::RefCell, - collections::HashMap, path::PathBuf, sync::{ atomic::{AtomicBool, Ordering}, @@ -54,8 +53,8 @@ pub struct xcb_xfixes_get_cursor_image { pub height: u16, pub xhot: u16, pub yhot: u16, - pub cursor_serial: libc::c_long, - pub pixels: *const libc::c_long, + pub cursor_serial: c_long, + pub pixels: *const c_long, } pub fn get_cursor_pos() -> Option<(i32, i32)> { @@ -637,7 +636,7 @@ pub fn get_double_click_time() -> u32 { settings, property.as_ptr(), &mut double_click_time as *mut u32, - 0 as *const libc::c_void, + 0 as *const c_void, ); double_click_time } diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index fd17fd469..7514ead38 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -2,7 +2,7 @@ use core::slice; use hbb_common::{ allow_err, anyhow::anyhow, - bail, log, + bail, libc, log, message_proto::{KeyEvent, MouseEvent}, protobuf::Message, tokio::{self, sync::mpsc}, @@ -492,7 +492,7 @@ pub mod client { let mut option = SHMEM.lock().unwrap(); let shmem = option.as_mut().unwrap(); unsafe { - hbb_common::libc::memset(shmem.as_ptr() as _, 0, shmem.len() as _); + libc::memset(shmem.as_ptr() as _, 0, shmem.len() as _); } drop(option); match para { diff --git a/src/tray.rs b/src/tray.rs index b449bbbd3..12523605d 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -1,4 +1,4 @@ -#[cfg(any(target_os = "windows"))] +#[cfg(target_os = "windows")] use super::ui_interface::get_option_opt; #[cfg(target_os = "windows")] use std::sync::{Arc, Mutex}; @@ -80,7 +80,7 @@ pub fn start_tray() { /// Check if service is stoped. /// Return [`true`] if service is stoped, [`false`] otherwise. #[inline] -#[cfg(any(target_os = "windows"))] +#[cfg(target_os = "windows")] fn is_service_stopped() -> bool { if let Some(v) = get_option_opt("stop-service") { v == "Y" diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 26038218e..f44bb4eea 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -128,7 +128,7 @@ pub fn get_license() -> String { } #[inline] -#[cfg(any(target_os = "linux", target_os = "windows"))] +#[cfg(target_os = "windows")] pub fn get_option_opt(key: &str) -> Option { OPTIONS.lock().unwrap().get(key).map(|x| x.clone()) } From e1254c0b2415baaf8ea5be7b2fd38b8c12d93f0a Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 19 Feb 2023 21:11:17 +0800 Subject: [PATCH 185/199] macos better alert Signed-off-by: fufesou --- libs/hbb_common/examples/system_message.rs | 11 +++++----- libs/hbb_common/src/platform/mod.rs | 25 +++++++++++++++------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/libs/hbb_common/examples/system_message.rs b/libs/hbb_common/examples/system_message.rs index 347bec47f..0be788428 100644 --- a/libs/hbb_common/examples/system_message.rs +++ b/libs/hbb_common/examples/system_message.rs @@ -6,14 +6,15 @@ use hbb_common::platform::macos; fn main() { #[cfg(target_os = "linux")] - linux::system_message("test title", "test message", true).ok(); + let res = linux::system_message("test title", "test message", true); #[cfg(target_os = "macos")] - macos::alert( - "RustDesk".to_owned(), + let res = macos::alert( + "System Preferences".to_owned(), "warning".to_owned(), "test title".to_owned(), "test message".to_owned(), ["Ok".to_owned()].to_vec(), - ) - .ok(); + ); + #[cfg(any(target_os = "linux", target_os = "macos"))] + println!("result {:?}", &res); } diff --git a/libs/hbb_common/src/platform/mod.rs b/libs/hbb_common/src/platform/mod.rs index 89a3a1569..0a4299ae2 100644 --- a/libs/hbb_common/src/platform/mod.rs +++ b/libs/hbb_common/src/platform/mod.rs @@ -41,14 +41,23 @@ extern "C" fn breakdown_signal_handler(sig: i32) { ) .ok(); #[cfg(target_os = "macos")] - macos::alert( - "RustDesk".to_owned(), - "critical".to_owned(), - "Crashed".to_owned(), - format!("Got signal {} and exit.{}", sig, info), - ["Ok".to_owned()].to_vec(), - ) - .ok(); + { + use std::sync::mpsc::channel; + use std::time::Duration; + let (tx, rx) = channel(); + std::thread::spawn(move || { + macos::alert( + "System Preferences".to_owned(), + "critical".to_owned(), + "RustDesk Crashed".to_owned(), + format!("Got signal {} and exit.{}", sig, info), + ["Ok".to_owned()].to_vec(), + ) + .ok(); + let _ = tx.send(()); + }); + let _ = rx.recv_timeout(Duration::from_millis(1_000)); + } } exit(0); } From b4beb78e8f6ce185807581bc5e40f6c50c4f837d Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 19 Feb 2023 21:28:48 +0800 Subject: [PATCH 186/199] macOS, ignore alert for now Signed-off-by: fufesou --- libs/hbb_common/src/platform/mod.rs | 37 +++++++++++++++-------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/libs/hbb_common/src/platform/mod.rs b/libs/hbb_common/src/platform/mod.rs index 0a4299ae2..b65980c1a 100644 --- a/libs/hbb_common/src/platform/mod.rs +++ b/libs/hbb_common/src/platform/mod.rs @@ -40,24 +40,25 @@ extern "C" fn breakdown_signal_handler(sig: i32) { true, ) .ok(); - #[cfg(target_os = "macos")] - { - use std::sync::mpsc::channel; - use std::time::Duration; - let (tx, rx) = channel(); - std::thread::spawn(move || { - macos::alert( - "System Preferences".to_owned(), - "critical".to_owned(), - "RustDesk Crashed".to_owned(), - format!("Got signal {} and exit.{}", sig, info), - ["Ok".to_owned()].to_vec(), - ) - .ok(); - let _ = tx.send(()); - }); - let _ = rx.recv_timeout(Duration::from_millis(1_000)); - } + // Ignore alert info for now. + // #[cfg(target_os = "macos")] + // { + // use std::sync::mpsc::channel; + // use std::time::Duration; + // let (tx, rx) = channel(); + // std::thread::spawn(move || { + // macos::alert( + // "System Preferences".to_owned(), + // "critical".to_owned(), + // "RustDesk Crashed".to_owned(), + // format!("Got signal {} and exit.{}", sig, info), + // ["Ok".to_owned()].to_vec(), + // ) + // .ok(); + // let _ = tx.send(()); + // }); + // let _ = rx.recv_timeout(Duration::from_millis(1_000)); + // } } exit(0); } From 0491950e012f9d3ac86601126e21ee346eb1439a Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 19 Feb 2023 22:29:10 +0800 Subject: [PATCH 187/199] macos remove unused code Signed-off-by: fufesou --- libs/hbb_common/src/platform/macos.rs | 2 +- libs/hbb_common/src/platform/mod.rs | 19 ------------------- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/libs/hbb_common/src/platform/macos.rs b/libs/hbb_common/src/platform/macos.rs index 0008c6266..dd83a8738 100644 --- a/libs/hbb_common/src/platform/macos.rs +++ b/libs/hbb_common/src/platform/macos.rs @@ -16,7 +16,7 @@ struct AlertResult { button: String, } -/// Alert dialog, return the clicked button value. +/// Firstly run the specified app, then alert a dialog. Return the clicked button value. /// /// # Arguments /// diff --git a/libs/hbb_common/src/platform/mod.rs b/libs/hbb_common/src/platform/mod.rs index b65980c1a..aa929ca99 100644 --- a/libs/hbb_common/src/platform/mod.rs +++ b/libs/hbb_common/src/platform/mod.rs @@ -40,25 +40,6 @@ extern "C" fn breakdown_signal_handler(sig: i32) { true, ) .ok(); - // Ignore alert info for now. - // #[cfg(target_os = "macos")] - // { - // use std::sync::mpsc::channel; - // use std::time::Duration; - // let (tx, rx) = channel(); - // std::thread::spawn(move || { - // macos::alert( - // "System Preferences".to_owned(), - // "critical".to_owned(), - // "RustDesk Crashed".to_owned(), - // format!("Got signal {} and exit.{}", sig, info), - // ["Ok".to_owned()].to_vec(), - // ) - // .ok(); - // let _ = tx.send(()); - // }); - // let _ = rx.recv_timeout(Duration::from_millis(1_000)); - // } } exit(0); } From c2fa74dbbc5ed3cbf0c222876d5ce91525d7f20c Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Sun, 19 Feb 2023 22:30:58 +0800 Subject: [PATCH 188/199] Update mod.rs --- libs/hbb_common/src/platform/mod.rs | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/libs/hbb_common/src/platform/mod.rs b/libs/hbb_common/src/platform/mod.rs index b65980c1a..aa929ca99 100644 --- a/libs/hbb_common/src/platform/mod.rs +++ b/libs/hbb_common/src/platform/mod.rs @@ -40,25 +40,6 @@ extern "C" fn breakdown_signal_handler(sig: i32) { true, ) .ok(); - // Ignore alert info for now. - // #[cfg(target_os = "macos")] - // { - // use std::sync::mpsc::channel; - // use std::time::Duration; - // let (tx, rx) = channel(); - // std::thread::spawn(move || { - // macos::alert( - // "System Preferences".to_owned(), - // "critical".to_owned(), - // "RustDesk Crashed".to_owned(), - // format!("Got signal {} and exit.{}", sig, info), - // ["Ok".to_owned()].to_vec(), - // ) - // .ok(); - // let _ = tx.send(()); - // }); - // let _ = rx.recv_timeout(Duration::from_millis(1_000)); - // } } exit(0); } From 0d321918d4cbe22924d2378005de1ab112ccadc3 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Sun, 19 Feb 2023 15:47:52 +0100 Subject: [PATCH 189/199] improve input of change ID --- flutter/lib/common/widgets/dialog.dart | 93 ++++++++++++++++++++++++-- src/lang/ca.rs | 6 +- src/lang/cn.rs | 6 +- src/lang/cs.rs | 6 +- src/lang/da.rs | 6 +- src/lang/de.rs | 6 +- src/lang/eo.rs | 6 +- src/lang/es.rs | 6 +- src/lang/fa.rs | 6 +- src/lang/fr.rs | 6 +- src/lang/gr.rs | 6 +- src/lang/hu.rs | 6 +- src/lang/id.rs | 6 +- src/lang/it.rs | 6 +- src/lang/ja.rs | 6 +- src/lang/ko.rs | 6 +- src/lang/kz.rs | 6 +- src/lang/nl.rs | 6 +- src/lang/pl.rs | 6 +- src/lang/pt_PT.rs | 6 +- src/lang/ptbr.rs | 6 +- src/lang/ro.rs | 6 +- src/lang/ru.rs | 6 +- src/lang/sk.rs | 6 +- src/lang/sl.rs | 6 +- src/lang/sq.rs | 6 +- src/lang/sr.rs | 6 +- src/lang/sv.rs | 6 +- src/lang/template.rs | 6 +- src/lang/th.rs | 6 +- src/lang/tr.rs | 6 +- src/lang/tw.rs | 6 +- src/lang/ua.rs | 6 +- src/lang/vn.rs | 6 +- 34 files changed, 254 insertions(+), 37 deletions(-) diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart index e96a2b406..cdce6f12a 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -1,18 +1,74 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:get/get.dart'; import '../../common.dart'; import '../../models/platform_model.dart'; +abstract class ValidationRule { + String get name; + bool validate(String value); +} + +class LengthRangeValidationRule extends ValidationRule { + final int _min; + final int _max; + + LengthRangeValidationRule(this._min, this._max); + + @override + String get name => translate('length %min% to %max%') + .replaceAll('%min%', _min.toString()) + .replaceAll('%max%', _max.toString()); + + @override + bool validate(String value) { + return value.length >= _min && value.length <= _max; + } +} + +class RegexValidationRule extends ValidationRule { + final String _name; + final RegExp _regex; + + RegexValidationRule(this._name, this._regex); + + @override + String get name => translate(_name); + + @override + bool validate(String value) { + return value.isNotEmpty ? value.contains(_regex) : false; + } +} + void changeIdDialog() { var newId = ""; var msg = ""; var isInProgress = false; TextEditingController controller = TextEditingController(); + final RxString rxId = controller.text.trim().obs; + + final rules = [ + RegexValidationRule('starts with a letter', RegExp(r'^[a-zA-Z]')), + LengthRangeValidationRule(6, 16), + RegexValidationRule('allowed characters', RegExp(r'^\w*$')) + ]; + gFFI.dialogManager.show((setState, close) { submit() async { debugPrint("onSubmit"); newId = controller.text.trim(); + + final Iterable violations = rules.where((r) => !r.validate(newId)); + if (violations.isNotEmpty) { + setState(() { + msg = + '${translate('Prompt')}: ${violations.map((r) => r.name).join(', ')}'; + }); + return; + } + setState(() { msg = ""; isInProgress = true; @@ -31,7 +87,7 @@ void changeIdDialog() { } setState(() { isInProgress = false; - msg = translate(status); + msg = '${translate('Prompt')}: ${translate(status)}'; }); } @@ -46,18 +102,47 @@ void changeIdDialog() { ), TextField( decoration: InputDecoration( + labelText: translate('Your new ID'), border: const OutlineInputBorder(), - errorText: msg.isEmpty ? null : translate(msg)), + errorText: msg.isEmpty ? null : translate(msg), + suffixText: '${rxId.value.length}/16', + suffixStyle: const TextStyle(fontSize: 12, color: Colors.grey)), inputFormatters: [ LengthLimitingTextInputFormatter(16), // FilteringTextInputFormatter(RegExp(r"[a-zA-z][a-zA-z0-9\_]*"), allow: true) ], - maxLength: 16, controller: controller, autofocus: true, + onChanged: (value) { + setState(() { + rxId.value = value.trim(); + msg = ''; + }); + }, ), const SizedBox( - height: 4.0, + height: 8.0, + ), + Obx(() => Wrap( + runSpacing: 8, + spacing: 4, + children: rules.map((e) { + var checked = e.validate(rxId.value); + return Chip( + label: Text( + e.name, + style: TextStyle( + color: checked + ? const Color(0xFF0A9471) + : Color.fromARGB(255, 198, 86, 157)), + ), + backgroundColor: checked + ? const Color(0xFFD0F7ED) + : Color.fromARGB(255, 247, 205, 232)); + }).toList(), + )), + const SizedBox( + height: 8.0, ), Offstage( offstage: !isInProgress, child: const LinearProgressIndicator()) diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 3220c824a..0d1eeff13 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "El portapapers està buit"), ("Stop service", "Aturar servei"), ("Change ID", "Canviar ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Només pots utilitzar caràcters a-z, A-Z, 0-9 e _ (guionet baix). El primer caràcter ha de ser a-z o A-Z. La longitut ha d'estar entre 6 i 16 caràcters."), ("Website", "Lloc web"), ("About", "Sobre"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Servidor API"), ("invalid_http", "ha de començar amb http:// o https://"), ("Invalid IP", "IP incorrecta"), - ("id_change_tip", "Només pots utilitzar caràcters a-z, A-Z, 0-9 e _ (guionet baix). El primer caràcter ha de ser a-z o A-Z. La longitut ha d'estar entre 6 i 16 caràcters."), ("Invalid format", "Format incorrecte"), ("server_not_support", "Encara no és compatible amb el servidor"), ("Not available", "No disponible"), diff --git a/src/lang/cn.rs b/src/lang/cn.rs index d0fdcb3fd..63b59e8f1 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "拷贝配置信息到剪贴板后点击此按钮,可以自动导入配置"), ("Stop service", "停止服务"), ("Change ID", "改变ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "只可以使用字母a-z, A-Z, 0-9, _ (下划线)。首字母必须是a-z, A-Z。长度在6与16之间。"), ("Website", "网站"), ("About", "关于"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API服务器"), ("invalid_http", "必须以http://或者https://开头"), ("Invalid IP", "无效IP"), - ("id_change_tip", "只可以使用字母a-z, A-Z, 0-9, _ (下划线)。首字母必须是a-z, A-Z。长度在6与16之间。"), ("Invalid format", "无效格式"), ("server_not_support", "服务器暂不支持"), ("Not available", "已被占用"), diff --git a/src/lang/cs.rs b/src/lang/cs.rs index aca4778e6..f4d63cba9 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Schránka je prázdná"), ("Stop service", "Zastavit službu"), ("Change ID", "Změnit identifikátor"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Použít je mozné pouze znaky a-z, A-Z, 0-9 a _ (podtržítko). Dále je třeba aby začínalo na písmeno a-z, A-Z. Délka mezi 6 a 16 znaky."), ("Website", "Webové stránky"), ("About", "O aplikaci"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Server s API rozhraním"), ("invalid_http", "Je třeba, aby začínalo na http:// nebo https://"), ("Invalid IP", "Neplatná IP adresa"), - ("id_change_tip", "Použít je mozné pouze znaky a-z, A-Z, 0-9 a _ (podtržítko). Dále je třeba aby začínalo na písmeno a-z, A-Z. Délka mezi 6 a 16 znaky."), ("Invalid format", "Neplatný formát"), ("server_not_support", "Server zatím nepodporuje"), ("Not available", "Není k dispozici"), diff --git a/src/lang/da.rs b/src/lang/da.rs index 7b959a778..b3bf02dd2 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Udklipsholderen er tom"), ("Stop service", "Sluk for forbindelsesserveren"), ("Change ID", "Ændre ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Kun tegnene a-z, A-Z, 0-9 og _ (understregning) er tilladt. Det første bogstav skal være a-z, A-Z. Længde mellem 6 og 16."), ("Website", "Hjemmeside"), ("About", "Omkring"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API Server"), ("invalid_http", "Skal begynde med http:// eller https://"), ("Invalid IP", "Ugyldig IP-adresse"), - ("id_change_tip", "Kun tegnene a-z, A-Z, 0-9 og _ (understregning) er tilladt. Det første bogstav skal være a-z, A-Z. Længde mellem 6 og 16."), ("Invalid format", "Ugyldigt format"), ("server_not_support", "Endnu ikke understøttet af serveren"), ("Not available", "ikke Tilgængelig"), diff --git a/src/lang/de.rs b/src/lang/de.rs index 38f4fddab..ddc347605 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Zwischenablage ist leer"), ("Stop service", "Vermittlungsdienst stoppen"), ("Change ID", "ID ändern"), + ("Your new ID", "Ihre neue ID"), + ("length %min% to %max%", "Länge %min% bis %max%"), + ("starts with a letter", "Beginnt mit Buchstabe"), + ("allowed characters", "Erlaubte Zeichen"), + ("id_change_tip", "Nur die Zeichen a-z, A-Z, 0-9 und _ (Unterstrich) sind erlaubt. Der erste Buchstabe muss a-z, A-Z sein und die Länge zwischen 6 und 16 Zeichen betragen."), ("Website", "Webseite"), ("About", "Über"), ("Slogan_tip", "Mit Herzblut programmiert - in einer Welt, die im Chaos versinkt!"), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API-Server"), ("invalid_http", "Muss mit http:// oder https:// beginnen"), ("Invalid IP", "Ungültige IP-Adresse"), - ("id_change_tip", "Nur die Zeichen a-z, A-Z, 0-9 und _ (Unterstrich) sind erlaubt. Der erste Buchstabe muss a-z, A-Z sein und die Länge zwischen 6 und 16 Zeichen betragen."), ("Invalid format", "Ungültiges Format"), ("server_not_support", "Diese Funktion wird noch nicht vom Server unterstützt."), ("Not available", "Nicht verfügbar"), diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 9c9097f6e..99752b3b6 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "La poŝo estas malplena"), ("Stop service", "Haltu servon"), ("Change ID", "Ŝanĝi identigilon"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Nur la signoj a-z, A-Z, 0-9, _ (substreko) povas esti uzataj. La unua litero povas esti inter a-z, A-Z. La longeco devas esti inter 6 kaj 16."), ("Website", "Retejo"), ("About", "Pri"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Servilo de API"), ("invalid_http", "Devas komenci kun http:// aŭ https://"), ("Invalid IP", "IP nevalida"), - ("id_change_tip", "Nur la signoj a-z, A-Z, 0-9, _ (substreko) povas esti uzataj. La unua litero povas esti inter a-z, A-Z. La longeco devas esti inter 6 kaj 16."), ("Invalid format", "Formato nevalida"), ("server_not_support", "Ankoraŭ ne subtenata de la servilo"), ("Not available", "Nedisponebla"), diff --git a/src/lang/es.rs b/src/lang/es.rs index 63c1d26fc..ac367898f 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "El portapapeles está vacío"), ("Stop service", "Detener servicio"), ("Change ID", "Cambiar ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Solo puedes usar caracteres a-z, A-Z, 0-9 e _ (guion bajo). El primer carácter debe ser a-z o A-Z. La longitud debe estar entre 6 y 16 caracteres."), ("Website", "Sitio web"), ("About", "Acerca de"), ("Slogan_tip", "Hecho con corazón en este mundo caótico!"), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Servidor API"), ("invalid_http", "debe comenzar con http:// o https://"), ("Invalid IP", "IP incorrecta"), - ("id_change_tip", "Solo puedes usar caracteres a-z, A-Z, 0-9 e _ (guion bajo). El primer carácter debe ser a-z o A-Z. La longitud debe estar entre 6 y 16 caracteres."), ("Invalid format", "Formato incorrecto"), ("server_not_support", "Aún no es compatible con el servidor"), ("Not available", "No disponible"), diff --git a/src/lang/fa.rs b/src/lang/fa.rs index db565fe28..1d2fbe529 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "کلیپبورد خالی است"), ("Stop service", "توقف سرویس"), ("Change ID", "تعویض شناسه"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "شناسه باید طبق این شرایط باشد : حروف کوچک و بزرگ انگلیسی و اعداد از 0 تا 9، _ و همچنین حرف اول آن فقط حروف بزرگ یا کوچک انگلیسی و طول آن بین 6 الی 16 کاراکتر باشد"), ("Website", "وب سایت"), ("About", "درباره"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API سرور"), ("invalid_http", "شروع شود http:// یا https:// باید با"), ("Invalid IP", "نامعتبر است IP آدرس"), - ("id_change_tip", "شناسه باید طبق این شرایط باشد : حروف کوچک و بزرگ انگلیسی و اعداد از 0 تا 9، _ و همچنین حرف اول آن فقط حروف بزرگ یا کوچک انگلیسی و طول آن بین 6 الی 16 کاراکتر باشد"), ("Invalid format", "فرمت نادرست است"), ("server_not_support", "هنوز توسط سرور مورد نظر پشتیبانی نمی شود"), ("Not available", "در دسترسی نیست"), diff --git a/src/lang/fr.rs b/src/lang/fr.rs index fd46b4cf2..ef76a8fc1 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Presse-papier vide"), ("Stop service", "Arrêter le service"), ("Change ID", "Changer d'ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Seules les lettres a-z, A-Z, 0-9, _ (trait de soulignement) peuvent être utilisées. La première lettre doit être a-z, A-Z. La longueur doit être comprise entre 6 et 16."), ("Website", "Site Web"), ("About", "À propos de"), ("Slogan_tip", "Fait avec cœur dans ce monde chaotique!"), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Serveur API"), ("invalid_http", "Doit commencer par http:// ou https://"), ("Invalid IP", "IP invalide"), - ("id_change_tip", "Seules les lettres a-z, A-Z, 0-9, _ (trait de soulignement) peuvent être utilisées. La première lettre doit être a-z, A-Z. La longueur doit être comprise entre 6 et 16."), ("Invalid format", "Format invalide"), ("server_not_support", "Pas encore supporté par le serveur"), ("Not available", "Indisponible"), diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 90c8e105a..9a813cd0a 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Το πρόχειρο είναι κενό"), ("Stop service", "Διακοπή υπηρεσίας"), ("Change ID", "Αλλαγή αναγνωριστικού ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Επιτρέπονται μόνο οι χαρακτήρες a-z, A-Z, 0-9 και _ (υπογράμμιση). Το πρώτο γράμμα πρέπει να είναι a-z, A-Z και το μήκος πρέπει να είναι μεταξύ 6 και 16 χαρακτήρων."), ("Website", "Ιστότοπος"), ("About", "Πληροφορίες"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Διακομιστής API"), ("invalid_http", "Πρέπει να ξεκινά με http:// ή https://"), ("Invalid IP", "Μη έγκυρη διεύθυνση IP"), - ("id_change_tip", "Επιτρέπονται μόνο οι χαρακτήρες a-z, A-Z, 0-9 και _ (υπογράμμιση). Το πρώτο γράμμα πρέπει να είναι a-z, A-Z και το μήκος πρέπει να είναι μεταξύ 6 και 16 χαρακτήρων."), ("Invalid format", "Μη έγκυρη μορφή"), ("server_not_support", "Αυτή η δυνατότητα δεν υποστηρίζεται ακόμη από τον διακομιστή"), ("Not available", "Μη διαθέσιμο"), diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 78648a034..31a6d8d19 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "A vágólap üres"), ("Stop service", "Szolgáltatás leállítása"), ("Change ID", "Azonosító megváltoztatása"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Csak a-z, A-Z, 0-9 csoportokba tartozó karakterek, illetve a _ karakter van engedélyezve. Az első karakternek mindenképpen a-z, A-Z csoportokba kell esnie. Az azonosító hosszúsága 6-tól, 16 karakter."), ("Website", "Weboldal"), ("About", "Rólunk"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API szerver"), ("invalid_http", "A címnek mindenképpen http(s)://-el kell kezdődnie."), ("Invalid IP", "A megadott IP cím helytelen."), - ("id_change_tip", "Csak a-z, A-Z, 0-9 csoportokba tartozó karakterek, illetve a _ karakter van engedélyezve. Az első karakternek mindenképpen a-z, A-Z csoportokba kell esnie. Az azonosító hosszúsága 6-tól, 16 karakter."), ("Invalid format", "Érvénytelen formátum"), ("server_not_support", "Nem támogatott a szerver által"), ("Not available", "Nem elérhető"), diff --git a/src/lang/id.rs b/src/lang/id.rs index d06cc649a..8176c9bc5 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Papan klip kosong"), ("Stop service", "Hentikan Layanan"), ("Change ID", "Ubah ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Hanya karakter a-z, A-Z, 0-9 dan _ (underscore) yang diperbolehkan. Huruf pertama harus a-z, A-Z. Panjang antara 6 dan 16."), ("Website", "Website"), ("About", "Tentang"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API Server"), ("invalid_http", "harus dimulai dengan http:// atau https://"), ("Invalid IP", "IP tidak valid"), - ("id_change_tip", "Hanya karakter a-z, A-Z, 0-9 dan _ (underscore) yang diperbolehkan. Huruf pertama harus a-z, A-Z. Panjang antara 6 dan 16."), ("Invalid format", "Format tidak valid"), ("server_not_support", "Belum didukung oleh server"), ("Not available", "Tidak tersedia"), diff --git a/src/lang/it.rs b/src/lang/it.rs index ab0c8064c..2431da441 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Gli appunti sono vuoti"), ("Stop service", "Arresta servizio"), ("Change ID", "Cambia ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Puoi usare solo i caratteri a-z, A-Z, 0-9 e _ (underscore). Il primo carattere deve essere a-z o A-Z. La lunghezza deve essere fra 6 e 16 caratteri."), ("Website", "Sito web"), ("About", "Informazioni"), ("Slogan_tip", "Fatta con il cuore in questo mondo caotico!"), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Server API"), ("invalid_http", "deve iniziare con http:// o https://"), ("Invalid IP", "Indirizzo IP non valido"), - ("id_change_tip", "Puoi usare solo i caratteri a-z, A-Z, 0-9 e _ (underscore). Il primo carattere deve essere a-z o A-Z. La lunghezza deve essere fra 6 e 16 caratteri."), ("Invalid format", "Formato non valido"), ("server_not_support", "Non ancora supportato dal server"), ("Not available", "Non disponibile"), diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 6e72d4b04..a51795236 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "クリップボードは空です"), ("Stop service", "サービスを停止"), ("Change ID", "IDを変更"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "使用できるのは大文字・小文字のアルファベット、数字、アンダースコア(_)のみです。初めの文字はアルファベットにする必要があります。6文字から16文字までです。"), ("Website", "公式サイト"), ("About", "情報"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "APIサーバー"), ("invalid_http", "http:// もしくは https:// から入力してください"), ("Invalid IP", "無効なIP"), - ("id_change_tip", "使用できるのは大文字・小文字のアルファベット、数字、アンダースコア(_)のみです。初めの文字はアルファベットにする必要があります。6文字から16文字までです。"), ("Invalid format", "無効な形式"), ("server_not_support", "サーバー側でまだサポートされていません"), ("Not available", "利用不可"), diff --git a/src/lang/ko.rs b/src/lang/ko.rs index b7b59ed9c..b6e992fad 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "클립보드가 비어있습니다"), ("Stop service", "서비스 중단"), ("Change ID", "ID 변경"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "a-z, A-Z, 0-9, _(밑줄 문자)만 입력 가능합니다. 첫 문자는 a-z 혹은 A-Z로 시작해야 합니다. 길이는 6 ~ 16글자가 요구됩니다."), ("Website", "웹사이트"), ("About", "정보"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API 서버"), ("invalid_http", "다음과 같이 시작해야 합니다. http:// 또는 https://"), ("Invalid IP", "유효하지 않은 IP"), - ("id_change_tip", "a-z, A-Z, 0-9, _(밑줄 문자)만 입력 가능합니다. 첫 문자는 a-z 혹은 A-Z로 시작해야 합니다. 길이는 6 ~ 16글자가 요구됩니다."), ("Invalid format", "유효하지 않은 형식"), ("server_not_support", "해당 서버가 아직 지원하지 않습니다"), ("Not available", "불가능"), diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 9fdc29260..aafec8b01 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Көшіру-тақта бос"), ("Stop service", "Сербесті тоқтату"), ("Change ID", "ID ауыстыру"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Тек a-z, A-Z, 0-9 және _ (астынғы-сызық) таңбалары рұқсат етілген. Бірінші таңба a-z, A-Z болуы қажет. Ұзындығы 6 мен 16 арасы."), ("Website", "Web-сайт"), ("About", "Туралы"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API Сербері"), ("invalid_http", "http:// немесе https://'пен басталуы қажет"), ("Invalid IP", "Бұрыс IP-Мекенжай"), - ("id_change_tip", "Тек a-z, A-Z, 0-9 және _ (астынғы-сызық) таңбалары рұқсат етілген. Бірінші таңба a-z, A-Z болуы қажет. Ұзындығы 6 мен 16 арасы."), ("Invalid format", "Бұрыс формат"), ("server_not_support", "Сербер әзірше қолдамайды"), ("Not available", "Қолжетімсіз"), diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 2502cb34c..9a239238d 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Klembord is leeg"), ("Stop service", "Stop service"), ("Change ID", "Wijzig ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Alleen de letters a-z, A-Z, 0-9, _ (underscore) kunnen worden gebruikt. De eerste letter moet a-z, A-Z zijn. De lengte moet tussen 6 en 16 liggen."), ("Website", "Website"), ("About", "Over"), ("Slogan_tip", "Gedaan met het hart in deze chaotische wereld!"), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API Server"), ("invalid_http", "Moet beginnen met http:// of https://"), ("Invalid IP", "Ongeldig IP"), - ("id_change_tip", "Alleen de letters a-z, A-Z, 0-9, _ (underscore) kunnen worden gebruikt. De eerste letter moet a-z, A-Z zijn. De lengte moet tussen 6 en 16 liggen."), ("Invalid format", "Ongeldig formaat"), ("server_not_support", "Nog niet ondersteund door de server"), ("Not available", "Niet beschikbaar"), diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 24563d21f..be61e94ec 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Schowek jest pusty"), ("Stop service", "Zatrzymaj usługę"), ("Change ID", "Zmień ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Nowy ID może być złożony z małych i dużych liter a-zA-z, cyfry 0-9 oraz _ (podkreślenie). Pierwszym znakiem powinna być litera a-zA-Z, a całe ID powinno składać się z 6 do 16 znaków."), ("Website", "Strona internetowa"), ("About", "O aplikacji"), ("Slogan_tip", "Tworzone z miłością w tym pełnym chaosu świecie!"), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Serwer API"), ("invalid_http", "Nieprawidłowe żądanie http"), ("Invalid IP", "Nieprawidłowe IP"), - ("id_change_tip", "Nowy ID może być złożony z małych i dużych liter a-zA-z, cyfry 0-9 oraz _ (podkreślenie). Pierwszym znakiem powinna być litera a-zA-Z, a całe ID powinno składać się z 6 do 16 znaków."), ("Invalid format", "Nieprawidłowy format"), ("server_not_support", "Serwer nie obsługuje tej funkcji"), ("Not available", "Niedostępne"), diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 078bf3761..b4befcdcb 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "A área de transferência está vazia"), ("Stop service", "Parar serviço"), ("Change ID", "Alterar ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Somente os caracteres a-z, A-Z, 0-9 e _ (sublinhado) são permitidos. A primeira letra deve ser a-z, A-Z. Comprimento entre 6 e 16."), ("Website", "Website"), ("About", "Sobre"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Servidor da API"), ("invalid_http", "deve iniciar com http:// ou https://"), ("Invalid IP", "IP inválido"), - ("id_change_tip", "Somente os caracteres a-z, A-Z, 0-9 e _ (sublinhado) são permitidos. A primeira letra deve ser a-z, A-Z. Comprimento entre 6 e 16."), ("Invalid format", "Formato inválido"), ("server_not_support", "Ainda não suportado pelo servidor"), ("Not available", "Indisponível"), diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index e08700d44..3fe0ca868 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "A área de transferência está vazia"), ("Stop service", "Parar serviço"), ("Change ID", "Alterar ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Somente os caracteres a-z, A-Z, 0-9 e _ (sublinhado) são permitidos. A primeira letra deve ser a-z, A-Z. Comprimento entre 6 e 16."), ("Website", "Website"), ("About", "Sobre"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Servidor da API"), ("invalid_http", "deve iniciar com http:// ou https://"), ("Invalid IP", "IP inválido"), - ("id_change_tip", "Somente os caracteres a-z, A-Z, 0-9 e _ (sublinhado) são permitidos. A primeira letra deve ser a-z, A-Z. Comprimento entre 6 e 16."), ("Invalid format", "Formato inválido"), ("server_not_support", "Ainda não suportado pelo servidor"), ("Not available", "Indisponível"), diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 5be2a914a..b06d1fa0c 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Clipboard gol"), ("Stop service", "Oprește serviciu"), ("Change ID", "Schimbă ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Pot fi utilizate doar caractere a-z, A-Z, 0-9, _ (bară jos). Primul caracter trebuie să fie a-z, A-Z. Lungimea trebuie să fie între 6 și 16 caractere."), ("Website", "Site web"), ("About", "Despre"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Server API"), ("invalid_http", "Trebuie să înceapă cu http:// sau https://"), ("Invalid IP", "IP nevalid"), - ("id_change_tip", "Pot fi utilizate doar caractere a-z, A-Z, 0-9, _ (bară jos). Primul caracter trebuie să fie a-z, A-Z. Lungimea trebuie să fie între 6 și 16 caractere."), ("Invalid format", "Format nevalid"), ("server_not_support", "Încă nu este compatibil cu serverul"), ("Not available", "Indisponibil"), diff --git a/src/lang/ru.rs b/src/lang/ru.rs index c389d6821..9746e8a41 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Буфер обмена пуст"), ("Stop service", "Остановить службу"), ("Change ID", "Изменить ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Допускаются только символы a-z, A-Z, 0-9 и _ (подчёркивание). Первой должна быть буква a-z, A-Z. Длина от 6 до 16."), ("Website", "Сайт"), ("About", "О программе"), ("Slogan_tip", "Сделано с душой в этом безумном мире!"), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API-сервер"), ("invalid_http", "Должен начинаться с http:// или https://"), ("Invalid IP", "Неправильный IP-адрес"), - ("id_change_tip", "Допускаются только символы a-z, A-Z, 0-9 и _ (подчёркивание). Первой должна быть буква a-z, A-Z. Длина от 6 до 16."), ("Invalid format", "Неправильный формат"), ("server_not_support", "Пока не поддерживается сервером"), ("Not available", "Недоступно"), diff --git a/src/lang/sk.rs b/src/lang/sk.rs index bf4b85b1b..27bf78dd7 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Schránka je prázdna"), ("Stop service", "Zastaviť službu"), ("Change ID", "Zmeniť ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Povolené sú len znaky a-z, A-Z, 0-9 a _ (podčiarkovník). Prvý znak musí byť a-z, A-Z. Dĺžka musí byť medzi 6 a 16 znakmi."), ("Website", "Webová stránka"), ("About", "O RustDesk"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API server"), ("invalid_http", "Musí začínať http:// alebo https://"), ("Invalid IP", "Neplatná IP adresa"), - ("id_change_tip", "Povolené sú len znaky a-z, A-Z, 0-9 a _ (podčiarkovník). Prvý znak musí byť a-z, A-Z. Dĺžka musí byť medzi 6 a 16 znakmi."), ("Invalid format", "Neplatný formát"), ("server_not_support", "Zatiaľ serverom nepodporované"), ("Not available", "Nie je k dispozícii"), diff --git a/src/lang/sl.rs b/src/lang/sl.rs index f464cb8fc..4ccc9e35f 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Odložišče je prazno"), ("Stop service", "Ustavi storitev"), ("Change ID", "Spremeni ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Dovoljeni znaki so a-z, A-Z (brez šumnikov), 0-9 in _. Prvi znak mora biti črka, dolžina od 6 do 16 znakov."), ("Website", "Spletna stran"), ("About", "O programu"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API strežnik"), ("invalid_http", "mora se začeti s http:// ali https://"), ("Invalid IP", "Neveljaven IP"), - ("id_change_tip", "Dovoljeni znaki so a-z, A-Z (brez šumnikov), 0-9 in _. Prvi znak mora biti črka, dolžina od 6 do 16 znakov."), ("Invalid format", "Neveljavna oblika"), ("server_not_support", "Strežnik še ne podpira"), ("Not available", "Ni na voljo"), diff --git a/src/lang/sq.rs b/src/lang/sq.rs index a6b83d9f3..347d12794 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Clipboard është bosh"), ("Stop service", "Ndaloni shërbimin"), ("Change ID", "Ndryshoni ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Lejohen Vetëm karkteret a-z,A-Z,0-9 dhe _(nënvizimet).Shkronja e parë duhet të jetë a-z, A-Z. Gjatesia midis 6 dhe 16."), ("Website", "Faqe ëebi"), ("About", "Rreth"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Serveri API"), ("invalid_http", "Duhet të fillojë me http:// ose https://"), ("Invalid IP", "IP e pavlefshme"), - ("id_change_tip", "Lejohen Vetëm karkteret a-z,A-Z,0-9 dhe _(nënvizimet).Shkronja e parë duhet të jetë a-z, A-Z. Gjatesia midis 6 dhe 16."), ("Invalid format", "Format i pavlefshëm"), ("server_not_support", "Nuk suportohet akoma nga severi"), ("Not available", "I padisponueshëm"), diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 09c34b4fc..19232b1e9 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Clipboard je prazan"), ("Stop service", "Stopiraj servis"), ("Change ID", "Promeni ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Dozvoljeni su samo a-z, A-Z, 0-9 i _ (donja crta) znakovi. Prvi znak mora biti slovo a-z, A-Z. Dužina je od 6 do 16."), ("Website", "Web sajt"), ("About", "O programu"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API server"), ("invalid_http", "mora početi sa http:// ili https://"), ("Invalid IP", "Nevažeća IP"), - ("id_change_tip", "Dozvoljeni su samo a-z, A-Z, 0-9 i _ (donja crta) znakovi. Prvi znak mora biti slovo a-z, A-Z. Dužina je od 6 do 16."), ("Invalid format", "Pogrešan format"), ("server_not_support", "Server još uvek ne podržava"), ("Not available", "Nije dostupno"), diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 2154b2729..da7f4df43 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Urklippet är tomt"), ("Stop service", "Avsluta tjänsten"), ("Change ID", "Byt ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Bara a-z, A-Z, 0-9 och _ (understräck) tecken är tillåtna. Den första bokstaven måste vara a-z, A-Z. Längd mellan 6 och 16."), ("Website", "Hemsida"), ("About", "Om"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API Server"), ("invalid_http", "måste börja med http:// eller https://"), ("Invalid IP", "Ogiltig IP"), - ("id_change_tip", "Bara a-z, A-Z, 0-9 och _ (understräck) tecken är tillåtna. Den första bokstaven måste vara a-z, A-Z. Längd mellan 6 och 16."), ("Invalid format", "Ogiltigt format"), ("server_not_support", "Stöds ännu inte av servern"), ("Not available", "Ej tillgänglig"), diff --git a/src/lang/template.rs b/src/lang/template.rs index f46a301f6..e988b648c 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", ""), ("Stop service", ""), ("Change ID", ""), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", ""), ("Website", ""), ("About", ""), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", ""), ("invalid_http", ""), ("Invalid IP", ""), - ("id_change_tip", ""), ("Invalid format", ""), ("server_not_support", ""), ("Not available", ""), diff --git a/src/lang/th.rs b/src/lang/th.rs index 93e984be3..570806412 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "คลิปบอร์ดว่างเปล่า"), ("Stop service", "หยุดการใช้งานเซอร์วิส"), ("Change ID", "เปลี่ยน ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "อนุญาตเฉพาะตัวอักษร a-z A-Z 0-9 และ _ (ขีดล่าง) เท่านั้น โดยตัวอักษรขึ้นต้นจะต้องเป็น a-z หรือไม่ก็ A-Z และมีความยาวระหว่าง 6 ถึง 16 ตัวอักษร"), ("Website", "เว็บไซต์"), ("About", "เกี่ยวกับ"), ("Slogan_tip", "ทำด้วยใจ ในโลกใบนี้ที่ยุ่งเหยิง!"), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "เซิร์ฟเวอร์ API"), ("invalid_http", "ต้องขึ้นต้นด้วย http:// หรือ https:// เท่านั้น"), ("Invalid IP", "IP ไม่ถูกต้อง"), - ("id_change_tip", "อนุญาตเฉพาะตัวอักษร a-z A-Z 0-9 และ _ (ขีดล่าง) เท่านั้น โดยตัวอักษรขึ้นต้นจะต้องเป็น a-z หรือไม่ก็ A-Z และมีความยาวระหว่าง 6 ถึง 16 ตัวอักษร"), ("Invalid format", "รูปแบบไม่ถูกต้อง"), ("server_not_support", "ยังไม่รองรับโดยเซิร์ฟเวอร์"), ("Not available", "ไม่พร้อมใช้งาน"), diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 214ee83df..393357ece 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Kopyalanan geçici veri boş"), ("Stop service", "Servisi Durdur"), ("Change ID", "ID Değiştir"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Yalnızca a-z, A-Z, 0-9 ve _ (alt çizgi) karakterlerini kullanabilirsiniz. İlk karakter a-z veya A-Z olmalıdır. Uzunluk 6 ile 16 karakter arasında olmalıdır."), ("Website", "Website"), ("About", "Hakkında"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API Sunucu"), ("invalid_http", "http:// veya https:// ile başlamalıdır"), ("Invalid IP", "Geçersiz IP adresi"), - ("id_change_tip", "Yalnızca a-z, A-Z, 0-9 ve _ (alt çizgi) karakterlerini kullanabilirsiniz. İlk karakter a-z veya A-Z olmalıdır. Uzunluk 6 ile 16 karakter arasında olmalıdır."), ("Invalid format", "Hatalı Format"), ("server_not_support", "Henüz sunucu tarafından desteklenmiyor"), ("Not available", "Erişilebilir değil"), diff --git a/src/lang/tw.rs b/src/lang/tw.rs index db26e5387..17cafb8f0 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "剪貼簿是空的"), ("Stop service", "停止服務"), ("Change ID", "更改 ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "僅能使用以下字元:a-z、A-Z、0-9、_ (底線)。首字元必須為 a-z 或 A-Z。長度介於 6 到 16 之間。"), ("Website", "網站"), ("About", "關於"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API 伺服器"), ("invalid_http", "開頭必須為 http:// 或 https://"), ("Invalid IP", "IP 無效"), - ("id_change_tip", "僅能使用以下字元:a-z、A-Z、0-9、_ (底線)。首字元必須為 a-z 或 A-Z。長度介於 6 到 16 之間。"), ("Invalid format", "格式無效"), ("server_not_support", "服務器暫不支持"), ("Not available", "無法使用"), diff --git a/src/lang/ua.rs b/src/lang/ua.rs index c3894726a..7eeca7deb 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Буфер обміну порожній"), ("Stop service", "Зупинити службу"), ("Change ID", "Змінити ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Допускаються тільки символи a-z, A-Z, 0-9 і _ (підкреслення). Перша буква повинна бути a-z, A-Z. Довжина від 6 до 16"), ("Website", "Веб-сайт"), ("About", "Про RustDesk"), ("Slogan_tip", "Створено з душею в цьому хаотичному світі!"), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API-сервер"), ("invalid_http", "Повинен починатися з http:// або https://"), ("Invalid IP", "Невірна IP-адреса"), - ("id_change_tip", "Допускаються тільки символи a-z, A-Z, 0-9 і _ (підкреслення). Перша буква повинна бути a-z, A-Z. Довжина від 6 до 16"), ("Invalid format", "Невірний формат"), ("server_not_support", "Поки не підтримується сервером"), ("Not available", "Недоступно"), diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 45c2cc519..3affb52d2 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Khay nhớ tạm trống"), ("Stop service", "Dừng dịch vụ"), ("Change ID", "Thay đổi ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Các kí tự đuợc phép là: từ a-z, A-Z, 0-9 và _ (dấu gạch dưới). Kí tự đầu tiên phải bắt đầu từ a-z, A-Z. Độ dài kí tự từ 6 đến 16"), ("Website", "Trang web"), ("About", "About"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Máy chủ API"), ("invalid_http", "phải bắt đầu bằng http:// hoặc https://"), ("Invalid IP", "IP không hợp lệ"), - ("id_change_tip", "Các kí tự đuợc phép là: từ a-z, A-Z, 0-9 và _ (dấu gạch dưới). Kí tự đầu tiên phải bắt đầu từ a-z, A-Z. Độ dài kí tự từ 6 đến 16"), ("Invalid format", "Định dạng không hợp lệnh"), ("server_not_support", "Chưa đuợc hỗ trợ bới server"), ("Not available", "Chưa có mặt"), From b4d4b4249e2c43db6abe7865a02b1f1545f50c5a Mon Sep 17 00:00:00 2001 From: grummbeer Date: Wed, 15 Feb 2023 13:43:38 +0100 Subject: [PATCH 190/199] unifiy left labeled text input --- flutter/lib/common/widgets/peer_card.dart | 41 ++++++---------- .../desktop/pages/desktop_setting_page.dart | 48 +++++++------------ 2 files changed, 32 insertions(+), 57 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 3c9a438a0..f1b94ecdf 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -996,14 +996,11 @@ void _rdpDialog(String id) async { Row( children: [ ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), + constraints: const BoxConstraints(minWidth: 140), child: Text( "${translate('Port')}:", - textAlign: TextAlign.start, - ).marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), + textAlign: TextAlign.right, + ).marginOnly(right: 10)), Expanded( child: TextField( inputFormatters: [ @@ -1017,21 +1014,15 @@ void _rdpDialog(String id) async { ), ), ], - ), - const SizedBox( - height: 8.0, - ), + ).marginOnly(bottom: 8), Row( children: [ ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), + constraints: const BoxConstraints(minWidth: 140), child: Text( "${translate('Username')}:", - textAlign: TextAlign.start, - ).marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), + textAlign: TextAlign.right, + ).marginOnly(right: 10)), Expanded( child: TextField( decoration: @@ -1040,19 +1031,15 @@ void _rdpDialog(String id) async { ), ), ], - ), - const SizedBox( - height: 8.0, - ), + ).marginOnly(bottom: 8), Row( children: [ ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text("${translate('Password')}:") - .marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), + constraints: const BoxConstraints(minWidth: 140), + child: Text( + "${translate('Password')}:", + textAlign: TextAlign.right, + ).marginOnly(right: 10)), Expanded( child: Obx(() => TextField( obscureText: secure.value, @@ -1067,7 +1054,7 @@ void _rdpDialog(String id) async { )), ), ], - ), + ).marginOnly(bottom: 8), ], ), ), diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 34398dd0d..187ffc9fc 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1856,12 +1856,11 @@ void changeSocks5Proxy() async { Row( children: [ ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text('${translate("Hostname")}:') - .marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), + constraints: const BoxConstraints(minWidth: 140), + child: Text( + '${translate("Hostname")}:', + textAlign: TextAlign.right, + ).marginOnly(right: 10)), Expanded( child: TextField( decoration: InputDecoration( @@ -1872,19 +1871,15 @@ void changeSocks5Proxy() async { ), ), ], - ), - const SizedBox( - height: 8.0, - ), + ).marginOnly(bottom: 8), Row( children: [ ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text('${translate("Username")}:') - .marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), + constraints: const BoxConstraints(minWidth: 140), + child: Text( + '${translate("Username")}:', + textAlign: TextAlign.right, + ).marginOnly(right: 10)), Expanded( child: TextField( decoration: const InputDecoration( @@ -1894,19 +1889,15 @@ void changeSocks5Proxy() async { ), ), ], - ), - const SizedBox( - height: 8.0, - ), + ).marginOnly(bottom: 8), Row( children: [ ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text('${translate("Password")}:') - .marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), + constraints: const BoxConstraints(minWidth: 140), + child: Text( + '${translate("Password")}:', + textAlign: TextAlign.right, + ).marginOnly(right: 10)), Expanded( child: Obx(() => TextField( obscureText: obscure.value, @@ -1921,10 +1912,7 @@ void changeSocks5Proxy() async { )), ), ], - ), - const SizedBox( - height: 8.0, - ), + ).marginOnly(bottom: 8), Offstage( offstage: !isInProgress, child: const LinearProgressIndicator()) ], From 95ff8e4bbd3fc015a7f5b90dfb824c49e5cce040 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Sun, 19 Feb 2023 18:00:58 +0100 Subject: [PATCH 191/199] unifiy left labeled text input server --- .../desktop/pages/desktop_setting_page.dart | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 187ffc9fc..971c713ce 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1074,7 +1074,7 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { Row( mainAxisAlignment: MainAxisAlignment.end, children: [_Button('Apply', submit, enabled: enabled)], - ).marginOnly(top: 15), + ).marginOnly(top: 10), ], ) ]); @@ -1697,33 +1697,30 @@ _LabeledTextField( bool secure) { return Row( children: [ - Spacer(flex: 1), + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 140), + child: Text( + '${translate(label)}:', + textAlign: TextAlign.right, + style: TextStyle( + fontSize: 16, color: _disabledTextColor(context, enabled)), + ).marginOnly(right: 10)), Expanded( - flex: 4, - child: Text( - '${translate(label)}:', - textAlign: TextAlign.right, - style: TextStyle(color: _disabledTextColor(context, enabled)), - ), - ), - Spacer(flex: 1), - Expanded( - flex: 10, child: TextField( controller: controller, enabled: enabled, obscureText: secure, decoration: InputDecoration( isDense: true, - contentPadding: EdgeInsets.symmetric(vertical: 15), + border: OutlineInputBorder(), + contentPadding: EdgeInsets.fromLTRB(14, 15, 14, 15), errorText: errorText.isNotEmpty ? errorText : null), style: TextStyle( color: _disabledTextColor(context, enabled), )), ), - Spacer(flex: 1), ], - ); + ).marginOnly(bottom: 8); } // ignore: must_be_immutable From 9cdc66dcdf2e330f6ee6abe9b614f64152f4873e Mon Sep 17 00:00:00 2001 From: "Miguel F. G" <116861809+flusheDData@users.noreply.github.com> Date: Mon, 20 Feb 2023 02:17:14 +0100 Subject: [PATCH 192/199] Update es.rs --- 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 63c1d26fc..3a467cb16 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -415,7 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Si tienes una gráfica Nvidia y la ventana remota se cierra inmediatamente, instalar el driver nouveau y elegir renderizado por software podría ayudar. Se requiere reiniciar la aplicación."), ("Always use software rendering", "Usar siempre renderizado por software"), ("config_input", "Para controlar el escritorio remoto con el teclado necesitas dar a RustDesk permisos de \"Monitorización de entrada\"."), - ("config_microphone", ""), + ("config_microphone", "Para poder hablar de forma remota necesitas darle a RustDesk permisos de \"Grabar Audio\"."), ("request_elevation_tip", "También puedes solicitar elevación si hay alguien en el lado remoto."), ("Wait", "Esperar"), ("Elevation Error", "Error de elevación"), @@ -436,7 +436,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", "Fuerte"), ("Switch Sides", "Intercambiar lados"), ("Please confirm if you want to share your desktop?", "Por favor, confirma si quieres compartir tu escritorio"), - ("Closed as expected", ""), + ("Closed as expected", "Cerrado como se esperaba"), ("Display", "Pantalla"), ("Default View Style", "Estilo de vista predeterminado"), ("Default Scroll Style", "Estilo de desplazamiento predeterminado"), From d18fc32f63401dcf57acaa508592b3fd0aad2575 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 20 Feb 2023 10:45:34 +0800 Subject: [PATCH 193/199] fix #3263 --- src/client.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/client.rs b/src/client.rs index 8683dad1f..6e4033d74 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2213,8 +2213,7 @@ pub fn check_if_retry(msgtype: &str, title: &str, text: &str, retry_for_relay: b && !text.to_lowercase().contains("mismatch") && !text.to_lowercase().contains("manually") && !text.to_lowercase().contains("not allowed") - && !text.to_lowercase().contains("as expected") - && !text.to_lowercase().contains("reset by the peer"))) + && !text.to_lowercase().contains("as expected"))) } #[inline] From 4cef2c2d0cd6d89846feb07e022d74e25761604f Mon Sep 17 00:00:00 2001 From: NicKoehler <53040044+NicKoehler@users.noreply.github.com> Date: Mon, 20 Feb 2023 08:48:39 +0100 Subject: [PATCH 194/199] Update it.rs --- src/lang/it.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 2431da441..2d66706d2 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -37,10 +37,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Gli appunti sono vuoti"), ("Stop service", "Arresta servizio"), ("Change ID", "Cambia ID"), - ("Your new ID", ""), - ("length %min% to %max%", ""), - ("starts with a letter", ""), - ("allowed characters", ""), + ("Your new ID", "Il tuo nuovo ID"), + ("length %min% to %max%", "da lunghezza %min% a %max%"), + ("starts with a letter", "inizia con una lettera"), + ("allowed characters", "caratteri consentiti"), ("id_change_tip", "Puoi usare solo i caratteri a-z, A-Z, 0-9 e _ (underscore). Il primo carattere deve essere a-z o A-Z. La lunghezza deve essere fra 6 e 16 caratteri."), ("Website", "Sito web"), ("About", "Informazioni"), @@ -213,7 +213,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Chiuso manualmente dal peer"), ("Enable remote configuration modification", "Abilita la modifica remota della configurazione"), ("Run without install", "Esegui senza installare"), - ("Connect via relay", ""), + ("Connect via relay", "Collegati tramite relay"), ("Always connect via relay", "Collegati sempre tramite relay"), ("whitelist_tip", "Solo gli indirizzi IP autorizzati possono connettersi a questo desktop"), ("Login", "Accedi"), @@ -419,7 +419,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Se si dispone di una scheda grafica Nvidia e la finestra remota si chiude immediatamente dopo la connessione, l'installazione del driver nouveau e la scelta di utilizzare il rendering software possono aiutare. È necessario un riavvio del software."), ("Always use software rendering", "Usa sempre il render Software"), ("config_input", "Per controllare il desktop remoto con la tastiera, è necessario concedere le autorizzazioni a RustDesk \"Monitoraggio dell'input\"."), - ("config_microphone", ""), + ("config_microphone", "Per poter chiamare, è necessario concedere l'autorizzazione a RustDesk \"Registra audio\"."), ("request_elevation_tip", "È possibile richiedere l'elevazione se c'è qualcuno sul lato remoto."), ("Wait", "Attendi"), ("Elevation Error", "Errore durante l'elevazione dei diritti"), @@ -448,12 +448,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Default Codec", "Codec Predefinito"), ("Bitrate", "Bitrate"), ("FPS", "FPS"), - ("Auto", "Auto"), ("Other Default Options", "Altre Opzioni Predefinite"), ("Voice call", "Chiamata vocale"), ("Text chat", "Chat testuale"), ("Stop voice call", "Interrompi la chiamata vocale"), - ("relay_hint_tip", ""), + ("relay_hint_tip", "Se non è possibile connettersi direttamente, si può provare a farlo tramite relay.\nInoltre, se si desidera utilizzare il relay al primo tentativo, è possibile aggiungere il suffisso \"/r\" all'ID o selezionare l'opzione \"Collegati sempre tramite relay\" nella scheda peer."), ("Reconnect", "Riconnetti"), ].iter().cloned().collect(); } From 13b1b78f72c49d4af93d8e1bf370d011c047a6c3 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 20 Feb 2023 15:54:53 +0800 Subject: [PATCH 195/199] remove closed as expected on switchsides, which makes second prompt Signed-off-by: 21pages --- src/server/connection.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index 9cdbf974c..d2eb21ee5 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1593,7 +1593,6 @@ impl Connection { uuid.to_string().as_ref(), ]) .ok(); - self.send_close_reason_no_retry("Closed as expected").await; self.on_close("switch sides", false).await; return false; } From 172b1d5e2ddc1bb8b4f632827ec1b733144e735e Mon Sep 17 00:00:00 2001 From: NicKoehler <53040044+NicKoehler@users.noreply.github.com> Date: Mon, 20 Feb 2023 09:11:38 +0100 Subject: [PATCH 196/199] Removed by mistake --- src/lang/it.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lang/it.rs b/src/lang/it.rs index 2d66706d2..68ec10807 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -448,6 +448,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Default Codec", "Codec Predefinito"), ("Bitrate", "Bitrate"), ("FPS", "FPS"), + ("Auto", "Auto"), ("Other Default Options", "Altre Opzioni Predefinite"), ("Voice call", "Chiamata vocale"), ("Text chat", "Chat testuale"), From 1af71cc5f36c93ca91c6906ae6b0b0cd6427865d Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 20 Feb 2023 16:12:11 +0800 Subject: [PATCH 197/199] remove all other "as expected" Signed-off-by: 21pages --- src/client.rs | 3 +-- src/lang/ca.rs | 1 - src/lang/cn.rs | 1 - src/lang/cs.rs | 1 - src/lang/da.rs | 1 - src/lang/de.rs | 1 - src/lang/eo.rs | 1 - src/lang/es.rs | 1 - src/lang/fa.rs | 1 - src/lang/fr.rs | 1 - src/lang/gr.rs | 1 - src/lang/hu.rs | 1 - src/lang/id.rs | 1 - src/lang/it.rs | 1 - src/lang/ja.rs | 1 - src/lang/ko.rs | 1 - src/lang/kz.rs | 1 - src/lang/nl.rs | 1 - src/lang/pl.rs | 1 - src/lang/pt_PT.rs | 1 - src/lang/ptbr.rs | 1 - src/lang/ro.rs | 1 - src/lang/ru.rs | 1 - src/lang/sk.rs | 1 - src/lang/sl.rs | 1 - src/lang/sq.rs | 1 - src/lang/sr.rs | 1 - src/lang/sv.rs | 1 - src/lang/template.rs | 1 - src/lang/th.rs | 1 - src/lang/tr.rs | 1 - src/lang/tw.rs | 1 - src/lang/ua.rs | 1 - src/lang/vn.rs | 1 - 34 files changed, 1 insertion(+), 35 deletions(-) diff --git a/src/client.rs b/src/client.rs index 6e4033d74..f36bdae78 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2212,8 +2212,7 @@ pub fn check_if_retry(msgtype: &str, title: &str, text: &str, retry_for_relay: b && !text.to_lowercase().contains("resolve") && !text.to_lowercase().contains("mismatch") && !text.to_lowercase().contains("manually") - && !text.to_lowercase().contains("not allowed") - && !text.to_lowercase().contains("as expected"))) + && !text.to_lowercase().contains("not allowed"))) } #[inline] diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 0d1eeff13..45c552848 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 63b59e8f1..9d0d176da 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", "强"), ("Switch Sides", "反转访问方向"), ("Please confirm if you want to share your desktop?", "请确认要让对方访问你的桌面?"), - ("Closed as expected", "正常关闭"), ("Display", "显示"), ("Default View Style", "默认显示方式"), ("Default Scroll Style", "默认滚动方式"), diff --git a/src/lang/cs.rs b/src/lang/cs.rs index f4d63cba9..e2761e45e 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/da.rs b/src/lang/da.rs index b3bf02dd2..2020a2b6f 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/de.rs b/src/lang/de.rs index ddc347605..7cf563fc3 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", "Stark"), ("Switch Sides", "Seiten wechseln"), ("Please confirm if you want to share your desktop?", "Bitte bestätigen Sie, ob Sie Ihren Desktop freigeben möchten."), - ("Closed as expected", "Wie erwartet geschlossen"), ("Display", "Anzeige"), ("Default View Style", "Standard-Ansichtsstil"), ("Default Scroll Style", "Standard-Scroll-Stil"), diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 99752b3b6..c22532440 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/es.rs b/src/lang/es.rs index 599da6fbf..3ce2860f0 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", "Fuerte"), ("Switch Sides", "Intercambiar lados"), ("Please confirm if you want to share your desktop?", "Por favor, confirma si quieres compartir tu escritorio"), - ("Closed as expected", "Cerrado como se esperaba"), ("Display", "Pantalla"), ("Default View Style", "Estilo de vista predeterminado"), ("Default Scroll Style", "Estilo de desplazamiento predeterminado"), diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 1d2fbe529..00f6b70ac 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", "قوی"), ("Switch Sides", "طرفین را عوض کنید"), ("Please confirm if you want to share your desktop?", "لطفاً تأیید کنید که آیا می خواهید دسکتاپ خود را به اشتراک بگذارید؟"), - ("Closed as expected", "طبق انتظار بسته شد"), ("Display", "نمایش دادن"), ("Default View Style", "سبک نمایش پیش فرض"), ("Default Scroll Style", "سبک پیش‌فرض اسکرول"), diff --git a/src/lang/fr.rs b/src/lang/fr.rs index ef76a8fc1..1f6e9f55b 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", "Fort"), ("Switch Sides", "Inverser la prise de contrôle"), ("Please confirm if you want to share your desktop?", "Veuillez confirmer le partager de votre bureau ?"), - ("Closed as expected", "Fermé normalement"), ("Display", "Affichage"), ("Default View Style", "Style de vue par défaut"), ("Default Scroll Style", "Style de défilement par défaut"), diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 9a813cd0a..b7ebf4577 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", "Δυνατό"), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 31a6d8d19..21ab28214 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/id.rs b/src/lang/id.rs index 8176c9bc5..f48de17f6 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/it.rs b/src/lang/it.rs index 68ec10807..4c63106da 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", "Forte"), ("Switch Sides", "Cambia lato"), ("Please confirm if you want to share your desktop?", "Vuoi condividere il tuo desktop?"), - ("Closed as expected", "Chiuso come previsto"), ("Display", "Visualizzazione"), ("Default View Style", "Stile Visualizzazione Predefinito"), ("Default Scroll Style", "Stile Scorrimento Predefinito"), diff --git a/src/lang/ja.rs b/src/lang/ja.rs index a51795236..b291a6e7a 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/ko.rs b/src/lang/ko.rs index b6e992fad..d63e83187 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/kz.rs b/src/lang/kz.rs index aafec8b01..b8b9eb1df 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 9a239238d..1a806c803 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", "Sterk"), ("Switch Sides", "Wissel van kant"), ("Please confirm if you want to share your desktop?", "bevestig als je je bureaublad wilt delen?"), - ("Closed as expected", "Gesloten zoals verwacht"), ("Display", "Weergave"), ("Default View Style", "Standaard Weergave Stijl"), ("Default Scroll Style", "Standaard Scroll Stijl"), diff --git a/src/lang/pl.rs b/src/lang/pl.rs index be61e94ec..2b29c7cb2 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", "Mocne"), ("Switch Sides", "Zamień Strony"), ("Please confirm if you want to share your desktop?", "Czy na pewno chcesz udostępnić swój ekran?"), - ("Closed as expected", "Zamknięto pomyślnie"), ("Display", "Wyświetlanie"), ("Default View Style", "Domyślny styl wyświetlania"), ("Default Scroll Style", "Domyślny styl przewijania"), diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index b4befcdcb..e91cd3909 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 3fe0ca868..b0fe9175d 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/ro.rs b/src/lang/ro.rs index b06d1fa0c..d0232ba37 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 9746e8a41..6df73f1eb 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", "Стойкий"), ("Switch Sides", "Переключить стороны"), ("Please confirm if you want to share your desktop?", "Подтверждаете, что хотите поделиться своим рабочим столом?"), - ("Closed as expected", "Закрыто по ожиданию"), ("Display", "Отображение"), ("Default View Style", "Стиль отображения по умолчанию"), ("Default Scroll Style", "Стиль прокрутки по умолчанию"), diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 27bf78dd7..458002f4c 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 4ccc9e35f..2abd1870f 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 347d12794..6b739e8ab 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 19232b1e9..90a435fd7 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/sv.rs b/src/lang/sv.rs index da7f4df43..a98ea6346 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/template.rs b/src/lang/template.rs index e988b648c..61c2b5d28 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/th.rs b/src/lang/th.rs index 570806412..236ee5e8d 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 393357ece..f2a34e212 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 17cafb8f0..84e74716f 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", "強"), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", "正常關閉"), ("Display", "顯示"), ("Default View Style", "默認顯示方式"), ("Default Scroll Style", "默認滾動方式"), diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 7eeca7deb..0c4caf4db 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 3affb52d2..19e1184d9 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), From c76b971addb02f60e33032ce05d4635994c1de2e Mon Sep 17 00:00:00 2001 From: solokot Date: Mon, 20 Feb 2023 13:42:23 +0300 Subject: [PATCH 198/199] Update ru.rs --- src/lang/ru.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 9746e8a41..34a433461 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -37,10 +37,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Буфер обмена пуст"), ("Stop service", "Остановить службу"), ("Change ID", "Изменить ID"), - ("Your new ID", ""), - ("length %min% to %max%", ""), - ("starts with a letter", ""), - ("allowed characters", ""), + ("Your new ID", "Новый ID"), + ("length %min% to %max%", "длина %min%...%max%"), + ("starts with a letter", "начинается с буквы"), + ("allowed characters", "допустимые символы"), ("id_change_tip", "Допускаются только символы a-z, A-Z, 0-9 и _ (подчёркивание). Первой должна быть буква a-z, A-Z. Длина от 6 до 16."), ("Website", "Сайт"), ("About", "О программе"), From 355601396b03f781784d6ce64a5f900057bd4b90 Mon Sep 17 00:00:00 2001 From: NicKoehler <53040044+NicKoehler@users.noreply.github.com> Date: Mon, 20 Feb 2023 13:54:13 +0100 Subject: [PATCH 199/199] Fix wrong language alt --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 866063726..df0ca8328 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

    - RustDesk - Dit fjernskrivebord
    + RustDesk - Your remote desktop
    ServersBuildDocker