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 d1d96199..62f81b79 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 18a0be27..bab88a9d 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 592a28fc..2c037c7b 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 4b4172ff..d84789d9 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 bab88a9d..dd35bd22 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 d84789d9..3e248700 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 2e466815..4bda68c2 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 64d289fc..6ad03046 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 dd35bd22..b61ce79a 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 0178fe9e..bcbea994 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 109fc1e9..bef49f13 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 e4b667d5..d340021a 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 e0ac68c5..08a8de74 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 bcbea994..857f9489 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 ca9314c4..4b671ff1 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 d340021a..34adeb59 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 4b671ff1..d9f67e56 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 857f9489..bac1e5d2 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 34adeb59..2ee3bc8e 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 08a8de74..b2cd0f2f 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 e4736c0e..005b2df7 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 2ee3bc8e..1924cfca 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 bac1e5d2..f16c9af7 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 f16c9af7..d568feb4 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 628bf502..34a67395 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 8763194f..7973a0ba 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 005b2df7..248cf592 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 1924cfca..d5c2103b 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 248cf592..e3acf3a4 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 c95c62fc..99130f89 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 36b9504c..1e5723b6 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 b7965f23..da486506 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 71dd9a5c..6032ae9c 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 b2cd0f2f..54796a93 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 d9f67e56..10fd67fd 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 f2210f97..19743515 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 00d62946..c74f352c 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 453ecefb..d956ddf5 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 dcaeb3ea..9e771567 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 2d6d3d06..a112385a 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 0c7f13d7..342eac51 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 5fdb7ee2..74acd8c6 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 dd1c75ba..50e88322 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 3b7f23ab..9bfdb6b1 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 bc25ab6c..a569b750 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 49ce8f14..e28294de 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 0fa6e029..ece6c923 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 d84b56a8..e252219c 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 35e20d7f..036bc8ec 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 d03b0799..6da98384 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 2006c67d..459139f5 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 b7ccbdbb..483879d4 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 64e5e931..cff00333 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 0f64ae67..9fe5eab8 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 7e209dff..36e2a99d 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 54b064c1..31f24a5e 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 a703c079..8cf858df 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 16c948ce..0e2208c3 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 285a5173..44159fb4 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 dd943e0e..892b3664 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 3050ff63..619a6850 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 7572da9d..f0458b11 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 535e4e77..f61ba325 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 80b384c6..cade148a 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 f5d9539d..46cc90c1 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 37a7d6bc..7c355edd 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 d78f5aa7..f7640ae5 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 4fc5db74..234c9a4d 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 1e5723b6..bb207993 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 54796a93..d76f930c 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 d568feb4..af8c1048 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 234c9a4d..73414e40 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 99130f89..26e25a20 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 bb207993..9864947c 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 da486506..48b99943 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 d76f930c..649b180b 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 af8c1048..a284fdad 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 19743515..e45dc5fb 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 c74f352c..84bfcb38 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 d956ddf5..ef9cd7bf 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 9e771567..32aa1f0a 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 a112385a..f8fac073 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 342eac51..4aa2be8d 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 74acd8c6..932936da 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 50e88322..b8c45fbe 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 9bfdb6b1..64a8b4e4 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 a569b750..3918db55 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 e28294de..edad7ecd 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 ece6c923..1b2dc4ad 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 e252219c..27432303 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 036bc8ec..ae375b8e 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 6da98384..417f88fe 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 459139f5..e852278d 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 483879d4..4cce52e0 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 cff00333..29252926 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 9fe5eab8..8ec40cf1 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 36e2a99d..c4f798ab 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 31f24a5e..949eba64 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 8cf858df..7de4d10c 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 0e2208c3..bf30f96d 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 44159fb4..db560166 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 892b3664..599cd651 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 619a6850..c0616300 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 f0458b11..282b564d 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 f61ba325..b2bee959 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 cade148a..b6efeaf0 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 46cc90c1..eea71e6b 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 7c355edd..f0d85a55 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 f7640ae5..5e400957 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 73414e40..1e784850 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 dd0b3554..e3f0c70a 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 21504d20..541d3a14 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 a284fdad..9117c8c5 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 c2d5a81f..9cbc9b15 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 10fd67fd..31cb07c0 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 ac3b32a4..8fa95ac9 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 d5c2103b..20cbe0f8 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 9cbc9b15..70d50619 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 70d50619..3e6409c5 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 31cb07c0..0fe6818d 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 30d38b8d..df2a75f5 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 0501c298..71dd2c96 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 c926019a..96616e8c 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 97b46bb8..21e87032 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 e45dc5fb..e6592787 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 84bfcb38..bcb2c3da 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 ef9cd7bf..d16e3abe 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 32aa1f0a..23884b99 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 f8fac073..1839edb8 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 6eed43a7..37c08a97 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 4aa2be8d..aa882987 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 932936da..da13843f 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 b8c45fbe..7664af99 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 64a8b4e4..db49b5a7 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 3918db55..5312e638 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 edad7ecd..2f6c490a 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 1b2dc4ad..7b932507 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 27432303..31864b22 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 ae375b8e..5f2b68c4 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 417f88fe..59cc9fdf 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 e852278d..8a939764 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 4cce52e0..788aa8b6 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 29252926..c6899ee5 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 8ec40cf1..cdac5f68 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 c4f798ab..5865d020 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 949eba64..fe1de2e9 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 7de4d10c..88f09313 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 bf30f96d..f78a6e9e 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 db560166..63e834c2 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 599cd651..33355fd3 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 c0616300..8af2ccb8 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 282b564d..1abc20b3 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 b2bee959..17314382 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 b6efeaf0..07227533 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 eea71e6b..8c096890 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 f0d85a55..1934a8eb 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 5e400957..24c0d900 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 9864947c..0df962cb 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 e6592787..4404e178 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 bcb2c3da..08f6824c 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 d16e3abe..a2a19a37 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 23884b99..905f4814 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 1839edb8..4028e333 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 aa882987..fe3830b9 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 da13843f..b9b31f10 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 7664af99..0b92c665 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 db49b5a7..4965f6da 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 5312e638..e40151cc 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 2f6c490a..0e1887e4 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 7b932507..689ae98c 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 31864b22..65f91ece 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 5f2b68c4..33fb2da0 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 59cc9fdf..c874dd69 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 8a939764..01014bab 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 788aa8b6..9dd005bd 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 c6899ee5..716d3df8 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 cdac5f68..c7d0cd6e 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 5865d020..2d48b91b 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 fe1de2e9..8224cd5e 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 88f09313..5e033095 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 f78a6e9e..a75da46b 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 63e834c2..d3964a2e 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 33355fd3..78059645 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 8af2ccb8..ca225775 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 1abc20b3..4355d643 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 17314382..57dfe6e4 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 07227533..49a42af4 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 8c096890..50e68425 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 1934a8eb..f37ed341 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 24c0d900..5788a7f3 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 00000000..03491be6 --- /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 0df962cb..0004c65f 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 08f6824c..65039f0f 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 9117c8c5..dcfa7b74 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 0fe6818d..1ecbb064 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 bef49f13..616d9237 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 48b99943..323b464f 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 649b180b..5911c40e 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 dcfa7b74..67946f54 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 1ecbb064..15bfe90d 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 20cbe0f8..c3acae9c 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 541d3a14..1b0d172b 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 1e784850..147cd914 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 323b464f..ed270638 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 5911c40e..2ea33b65 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 67946f54..d0e72a7e 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 15bfe90d..e2833294 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 e3f0c70a..009995f4 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 1b0d172b..5d6692c3 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 147cd914..2f682752 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 e3acf3a4..20acd811 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 d0e72a7e..8f2b4532 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 b4f1f6bc..7062d85d 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 d610fb84..18f61884 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 65039f0f..5a9abba9 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 c3acae9c..1007c71c 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 5d6692c3..eb83890d 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 2f682752..a740b373 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 1007c71c..87b3f74e 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 e2833294..588733c3 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 87b3f74e..c4c9ec16 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 8f2b4532..e34df30b 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 7062d85d..f8d8569b 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 eb83890d..9888e583 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 a740b373..4b47608f 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 f8d8569b..e83beb03 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 18f61884..0ede560f 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 c4c9ec16..da012621 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 2bd8824d..dc941c3d 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 5d451e4d..1120a173 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 e34df30b..d4922786 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 e83beb03..4249e4d9 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 9888e583..999b409e 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 4b47608f..f63bbd08 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 e1564136..52fcc76c 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 936b9e34..b315024e 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 00000000..0637b58d --- /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 00000000..fd8334f9 --- /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 0004c65f..d06be52f 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 18a0be27..61602c5b 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 daf7bfe3..2a4c6883 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 52141364..b2f70cdd 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 d06be52f..653ff37b 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 61602c5b..14af9657 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 2a4c6883..a2fe205a 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 56dca4cd..6cd905c3 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 d4922786..aa51df37 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 4249e4d9..a27a9d4e 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 588733c3..cfca0e08 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 da012621..1e88b9b0 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 dc941c3d..cce55315 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 1120a173..ccddab0e 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 f63bbd08..cd0bdcde 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 6cd905c3..eec424bf 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 aa51df37..234f4f84 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 1e88b9b0..7a16df81 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 b2f70cdd..a253b9aa 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 eec424bf..aab12ab5 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 234f4f84..05eab692 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 7a16df81..86d83761 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 a253b9aa..66a043fe 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 4404e178..e98c6636 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 5a9abba9..64c37709 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 a2a19a37..70a3eb6c 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 905f4814..ae943e1e 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 4028e333..44bbafda 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 fe3830b9..f457833f 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 b9b31f10..22044745 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 0b92c665..c206f91f 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 4965f6da..39ee3bc7 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 e40151cc..7cb678ec 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 0e1887e4..25562f55 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 689ae98c..68a80e54 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 65f91ece..9730bbc2 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 33fb2da0..7069c0da 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 c874dd69..43eb552d 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 01014bab..49c7b991 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 9dd005bd..41239961 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 716d3df8..e69a140c 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 c7d0cd6e..0887a591 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 2d48b91b..304353d4 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 8224cd5e..1e6c6962 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 5e033095..6f6f7a18 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 a75da46b..2fb74fa5 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 d3964a2e..5d4a6e1a 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 78059645..31a3ade8 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 ca225775..e30c09e4 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 4355d643..b8861807 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 57dfe6e4..1c75aaae 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 49a42af4..a9e2c171 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 50e68425..7c49a29a 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 f37ed341..92c99d90 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 5788a7f3..8bb1d45e 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 c19adf75..b923a31e 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 2ea33b65..020bea1f 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 05eab692..c8a0f2ca 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 99d0e888..03d057ef 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 cfca0e08..84407cd9 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 86d83761..1bacad12 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 c8a0f2ca..96ddd51f 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 1bacad12..17417cf6 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 b923a31e..c61287d4 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 a27a9d4e..2d7d3fb8 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 17417cf6..a8849b4e 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 96ddd51f..f5792bce 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 a8849b4e..02888d1e 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 02888d1e..9ce53c96 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 00000000..e1b96212 --- /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 0637b58d..5654befc 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 71dd2c96..2986adc7 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 653ff37b..dcc53140 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 14af9657..bf7f8773 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 84407cd9..2e6c450c 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 6032ae9c..71dd9a5c 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 e1564136..4ac2720b 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 936b9e34..5d75b7a2 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 054a3958..bcb0650a 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 2715a264..072ef53f 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 4fc5db74..95b8cdbd 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 36b9504c..4fd702ad 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 c2d5a81f..8f8ce8de 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 bcb0650a..7d5f36af 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 95b8cdbd..3801eda6 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 7d5f36af..08ab23b1 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 4ac2720b..98836301 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 5d75b7a2..936b9e34 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 4fd702ad..c3c8ce3f 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 08ab23b1..fd951442 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 072ef53f..1d7d4773 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 3801eda6..12412d7c 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 98836301..f5ffa7f9 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 936b9e34..5d75b7a2 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 fd951442..492314ab 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 1d7d4773..133b9a83 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 492314ab..bdf1c5c1 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 f5ffa7f9..98836301 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 5d75b7a2..936b9e34 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 bdf1c5c1..91480ba3 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 7b6130ad..edf0ef49 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 98836301..93b40ca3 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 9730bbc2..a4ea5830 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 16509a3b..c2d92097 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 fe0d3107..5ebc3a28 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 699aa9bf..bfc79871 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 71dd9a5c..1e4d80c9 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 c9f9e90d..1c49adfb 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 0b66107f..ddfe28ba 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 716025dc..7c107d11 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 c001c58f..57d9b68f 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 a034b4e1..2d9b5a98 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 a7ac4eb3..f574e830 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 f5792bce..5186aff4 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 2142d973..79a4664d 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 03d057ef..0af7026e 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 0ede560f..699b0bcd 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 91480ba3..17c52abf 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 616d9237..7807c4fa 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 98e355dc..f34b7c2c 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 ccddab0e..de33b016 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 c53c573f..83f623ca 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 17c52abf..5b992071 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 dc0e365a..87ea8e9e 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 2e6c450c..bb1b8b8b 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 bb1b8b8b..ec4a9097 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 18166c8f..06656020 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 5ca284ce..f03cd0be 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 6b107ff4..dce43472 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 5b992071..28e15158 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 c1965921..35d5a61e 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 52141364..b4d7f4fa 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 a2623ff1..04e29eaa 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 3e248700..32dced02 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 b6a9e5fe..9955c276 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 4bda68c2..c444d1f5 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 b61ce79a..8320d08d 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 598b2cc4..ddc51edd 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 1eac1be3..5e4693cc 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 32dced02..ba7b8a05 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 28e15158..105b8440 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 ec4a9097..ad0d119d 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 44bbafda..1743505c 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 37cc77c8..bc31ae2c 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 2d7d3fb8..bf5746c1 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 ad0d119d..a7e32d0b 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 bf5746c1..f60d9b30 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 c7dbd9b7..b61f5173 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 f34b7c2c..c6600608 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 c206f91f..8413673a 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 f513a1c6..2385813e 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 83f623ca..f0f66e28 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 b315024e..9588d10b 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 06656020..0019335e 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 0af7026e..e2f3f80e 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 b61f5173..0c8c5145 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 e41a616d..b449bbbd 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 c6600608..8a1fc990 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 c4b07b37..853f3168 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 a7289335..211d36c3 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 853f3168..956b985a 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 1f0d5b0c..7a6bcbc1 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 2fb40997..5833e760 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 956b985a..9ae85625 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 8c37f50b..c37d0186 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 9ae85625..54b6f1d4 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 22044745..939a4831 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 1e6c6962..1792eccc 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 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 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 0001d076..965218c9 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 5c37900f..9ba7a631 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 475b4cb8..38e4d917 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 0001d076..965218c9 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 4b6cf2a6..366fb2ed 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 117486ee..40ab2118 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 2ecfca83..49e3f235 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 79a4664d..b66261eb 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 a7e32d0b..a79ef2de 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 7b94c8a2..748d375b 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 6500a8e4..8bc37584 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 7973a0ba..aede5fe7 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 8a1fc990..cd0e5871 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 d357c9ce..6576c340 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 6576c340..26038218 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 748d375b..5dcd6389 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 8bc37584..16951542 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 17f275c2..bd6a1fc4 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 9ce53c96..53ccd700 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 57fdf2c2..bc9c5ff6 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 de33b016..f5c575d4 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 a731f0b0..4ad4a992 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 ca99a5bd..feab5bdc 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 feab5bdc..add1289e 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 4b6cf2a6..5d524523 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 a7e32d0b..3611b5db 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 5c1e1218..5cd2af2b 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 4ad4a992..d86960a0 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 b66a08e7..252e1cd1 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 add1289e..eb837ba7 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 aab12ab5..b2043f3c 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 c37d0186..b1491d52 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 d86960a0..a295ad4f 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 1e4d80c9..3bfc885c 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 b66261eb..ee44cf4f 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 ce97745f..1b6838e4 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 cce55315..a574b5e8 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 999b409e..fdb6b2df 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 0b51d8fd..e537d41f 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 eb837ba7..f30209a6 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 7533244e..8ef45139 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 a12d5aca..3a0fcc5f 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 fdb6b2df..06af70ea 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 87ea8e9e..2944a76d 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 8ef45139..a2dcbdbc 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 020bea1f..ecfc5974 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 a2dcbdbc..bee4dd7a 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 06af70ea..b6663ad7 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 2944a76d..cbf6d017 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 a295ad4f..6c1245a7 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 f30209a6..e09a9987 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 34a67395..588c3646 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 ecfc5974..c6e0a759 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 bee4dd7a..bb6f85bb 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 3a0fcc5f..b4e79b36 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 b6663ad7..ecf96ab3 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 cbf6d017..85deb68c 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 7b43e397..2da6bbaf 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 965218c9..d3a3f7b3 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 d3a3f7b3..13eb73f2 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 5cd2af2b..bd2a0129 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 837a197d..e96a2b40 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 c9af6328..3c9a438a 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 2986adc7..cde1e6d7 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 9955c276..27bb0377 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 e09a9987..8cf90eba 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 588c3646..ba62b775 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 c6e0a759..a2159257 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 bb6f85bb..a60e379f 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 ecf96ab3..e44e3140 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 85deb68c..25c15f52 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 54b6f1d4..d1faa549 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 cd618dfc..91a061fb 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 8701d9f5..df29252c 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 d1faa549..1ec57b46 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 1ec57b46..63a289c9 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 63a289c9..951d63fa 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 f03cd0be..1ab21dbf 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 05fc1fc5..14a2c38b 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 6c1245a7..ba7e3d76 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 eee4c6a2..f352c313 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 cde1e6d7..af7f1481 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 80dcd80b..378ddbd1 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 6c1245a7..ca34eace 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 eee4c6a2..71660cfa 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 cde1e6d7..ced8e33e 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 27bb0377..988baca5 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 b2566e26..7540f766 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 2385813e..2ac6bf23 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 ca354f29..ee5dd9b5 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 211d36c3..f9db985d 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 9b00b481..c251aadc 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 8cf90eba..d0a2ea60 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 3af189ef..864659a6 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 a2159257..05b34d78 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 a60e379f..0161e644 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 b4e79b36..3025d722 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 e44e3140..447c2e31 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 6d3b6d38..36c74dfe 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 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

    ^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 zcmZ{IX*d*K`1YCA7=y87t29WKlo44X4WT4LqvDrLXiB6hM2HzwBn&Nx7*Vnl%5J2x z6h}Fp7|9ib3-}}pX&V4`6{aoj{&WCfI2y3fTf@m2u06@_Ew23VMkev_$ zP_UiFJ@*n30O+jsS)%FARzgAo3WY-c7hEnE27@7yNE8aCtE;;c@cl1@goJh&G#b65 z?qDY$4u@}VZ|?*<;BYwqm)zXk+%a5VUvF-1c64;yaqI2v-PqXJNvNr*+2NO$muF{Z zxBefrw6w;@$5T>Lrl+R|2M5_~_T1du!otE%&Q9{g#6(|TADvFm&d%QP@%Q)7&(F`u z$QT$H*m3LX>hkgNX=rFLGc)V#?A(!dT+g08yVI_}zn{nBf#41Bcoopr!NWE1aRemK zf#Ok+KMtO9z>7uTzXhU~LE=2{<$}OXkTU_^PJx1P@MHxPje){3P%;XAunGLQAbtUa zu7lUJAaxdGPJ!4ZP&N!AR=}GXkUIg=WBOW@@K$eIM7MnLu? zcs~g$hky?cR1JZkO~9A|AI3l=2UHJ&iXo7<({~!sm%*bokT4Itc)*(nY6c+{!*grA ze+yg?vccwTv*&oVGwbYG9<_?swzvtxS3nF$q;|CSFK=#x3zFv9t6R7i>!ju-P&x*} z)}gUWwS!zj?K~uVYHpK@i`pW6+@P?Qz^esNG0d9e7Ibp~b(7k(PRL%P_AWu+PZAP% z|CYG)hHZM=CbeyqRJ+0$<=}GXao@&p;au=wt9D|uaBK}kt>98QgqBJC*GWn-kCe${ z&2G^<)))h;AaWI-w?zH702p&3#gow72}s!}_45{`VvF!*6VF(q_j5#Eapc}EfrLeD z#yqKOmQX*7ub5?g=i#4klRj*v_N@`h7e#Vra5WQxG6;hPlZI;Cp?S}{z@ z;)+nWArCh&LVh|s7xc8Zt<`yeG>|?usKh4%mlBpliwnvs9+F=?W;%YvC`7ySjXkr{ zxiQ{LE;r36&h{rwq%=;mxw<4J!@MY)qxFi3!JHHT#0$(#jEH_c z(@FLoXKkb!^kdDXd%qi49*8}oB5bS{K@aB}I6sBrzh!+kQuj{r!cg#0T+SzTFOvdUw>LrJ{kQ$K(KOJaSYCP#j z){6b#ZlCw(tQbET*!C6LaLr)d-WQ`kb@SeSwo&XOk5Z3Pj%dbet5=ty3Y$WEkE(hhHpU- zNA&%)7%;TNFRDrYAtkBM*F8Vw*>8o#NEn@&p`MIa-RIbrX!yYY%9X!83E(Ti#-_cm z+)Gm3(<5KRO>V+|TvU?U`1Zp-F{RP7KbmYXr_s$kGC-V0O5EnaVj_PCDanv9?ZR92*>G(39 zR8KQR*U9Emne$g7{{>{TG!hT%Wicl^OOFQN+yeJHXn@$5whQ@cYH9}IXon-#XRk2~ zio_0cqol*7FV)uQxgoA_QJ6==C_BR>&Fj$~)L4i!!=N3Q5i|;G3zZ8g|Mcv*X6sH7H&hd%6)CcIbGmAOrF zKWV#78Lus%HvYLDDRJwm4#SmXr16W1sX#}6!{Fj*d41K?TIA(wT6~3<#Y;7t-IpRn z==yROk@OtqcOdD6Sfj<9KR4Evs?KjMPB$$4T!!|Hwn#=y{fE(7Oi8SAEQezdhKCI= zm*L4I{&+~(4`?av9Mk+nip)5(6FNWUR7_5LM>GD)gubAadhcQtgMq!_i3UgMvoN6) zgXH4@a=las!W|6_!VG_;r|JXJZVkE#-2dHD2^B(0Y^<~|7BNGP;<`~s^#>dH?j3_878eK`;HaRm%n#^KzVT-^FbeY^a}ukoFB>b! zpt{RQ>qf{GmMQofZ4d-0N7VW^uu?)V{dBHvt@VEaj)>f6A^{h3|M0uh;*DPC3Db;7 z4<{AQvj9JhMhoTZ{1=Xm!c^gqpSK!lG{-!Fae>i4SB@k8;Mt(|^ zU&ZMh6_DjJ0-tsJmh&gEjRp5Vw;>~-+F?Hn)qrB40cfDpI8W4B`krIsPy z3@My++11(0^p1_R%4?9lJxP+*uTu-8y7O+rP%j`g&qT&HBB(1}2yeb;#E}L@4#D?S z$&r<`=gkD!O$)cUDL!Ax_*>BIm+DR_sXW|En!=(Q4w>(^8z;@sALt3gPzq9=s5z15 zkOn2oC(;BdJBnq1ObT%Y_NbUz9aJesvL(#_{bT~0#ITT*^$(D3x6|3UbxvsIGY)*M z4&FPZBo{~HI|PkpAti363P$Ot(K(@?cEqwec&CMu+!>^7V=)`04?8^=J-$B+Y6)vw zr2-F83VFg-3%84Ai#UJw0)!i|QvT)*ChNUlpyMwpgA=OMf0I8!1bg`-Mv#u7Zvzb~ z3n_ci(CrCfPwcyj<|k7aSV)&}R{%jt3_2Mv0(+-FP~N;GLOOqU)h2{CCWj4e5Q5lk z3UnYBRbb(ho6P!RONPbx&vaebiRItH<2O&~@ApXJ;`&4(B|9Y_Z+AuCPe6BdQW zUkcaU2j`xi@lb}OL*J{vu9`S$RLA#@rcg8kG$tGGbFY% zWq!aEYf1ERCllcYifj!Of%L(HItg12VmGT`Ew#93IsqCW6FxJkf(wt8%QfYZ%f2w#@BBBeivusDtTv7E#x%Eb zh|GGF$v=rkkj$NR4aIxfNt_`dizvZ<2DYZlpUySISs3D zWDbeiL+f@PWj?AfL?d4_b31;ga@>@M<5ss4Zl*6MkUhMckNpNn^GY^vUtq#0J3BSwrZrCW|02d+u@dVF7>}ZfBxJ> zj@uJ3-FVQP@yWG9J*%XL(Jk7YWQk!=C17tS?X3}m?PYooj4(GksjPaI#^oAk_J;~| zIL#k>E+XQ$_p3^y7>nm17YygQ;^N!+R=!sb;=(h(e9@PWlEEHB&bfY{mbWALe7;#o zdp#LX9LcTKXnzlh-DeWwqI3PADML>ngv9r|X-4l#vTDJN4rx*wB(`DTqDwn-v=)6> z4CX393_TRs5om6T=>P&dbys3cVcDt-L3o{n{O>4U4MV!yFU*owtxRa9I^%R6Wut{c zEs3MJ4dhMQLjt6MvTi1=3$$&HXfza}wO$eQZCvI%e@MWRcELKda`}4wp8s+t<-LQD z#jxEl&z8Q-R;I!KjMif)J1^8iR6}_G10K;~l94KRCaxHqu6#u>!LrDZKc0`^+kcDwCyBO{sUdi7#ZR`rJ zZ~~m>Te9}R{?X19APo+QVsPQ#8!3CEhG82JH&@m3x$!ra#dYcI1e!4&WK5_)g zcE)+~>y$DtL=CuC`WQk359WIBF#~$`;N!8RDbr%e`{n3*zL()}Jmw}T+w0)cm48lr zs?|knzZ9cEDq=6bGi`WgJt$R7KnL5tiC?@IVSWyJNYv(KMxK=W7>5me{K%V(pAgrj&SY-M+65jk* z4ZjzCr7r$AKP^MS;dQ@2n6Ls`g{3A3DHlqHpFFO_4Rh5QmNBXpr#ii@k}C9gq$H*; zGkEf@%V{9Bb9C0Bk#7V*el^nPcX@9(`R()pMm&^K^jw}oKFwCmiKcFT+sLRVG3Rx>%Ui?mpIDp}(IZu-$ZJ^Yl9_>}k zJF88UE?lSX^zp${CdcHt`x%$u1J~O{l$Y%1>y+W-e0Q+_6(E9I@lU6^>d ze}r(8%wYj%5XjG}I=oMOiNE`Ko&DsulT!qkZq}9fIZ>#q8BwQ+mhwBTU9Z#-c_G*i zHVP{!OxOn#V-!f&y}liMM496a$v%8UU-dZw!<>1HgAM($q-SOxtkYO91OENc#doO*mdM5o_ubrTW7>DULm zgaar;eQ1WLLvHXly<&5Xd1p}r+h~CL)|esl5C3WhYmS4kM(C zyqO_&c0#r}d=0bw(Jf(wbK18wi#lhBIp%vC9fK(u{oMNT_+$qanl#uO zVK_@QgxI2VyYjaBEU?kxd-l_Rxij}gJuV@|x%%ccs3u7aqKZCMlcEr673CDT*-ae$ zT;$t%(e0JkreoFA%p05ZRyV1@S;O=tXsga`BC!THF&bPJX`1X z*iw&)_?Lw+QjnIpSQ7I}MpF;|>SQb`>?m`_c-`-D*(xx`5DCqo-5Bz8)^(m#u~-exkj}1&HLi$BR$|6A4|W z^8VdBv`?7r`Y}C=ugZV{Cm^K$#X#}q{}6Jw3=Rn=@_!0lWO}v0oc6E_Q_dD4-iOlv z6?eT?XbJ_YW{59d-={+2GnXX7 zDf77rPoeiM{WkWN4X;D@J5QfMAqa@Z=;p4c_FBs|Y#-sWzQ*ecm)|5Fr7z@_pY*5Z zKbeSV`A_&0Qm8O%&@-V(`VyaXfTbx()_S?=m33P6wR8gtfOZ|RsR2!v^o5eFgH&Du zD{&rrLRgEN_vl8j#8EqBVbd-(y2dLsVI}TQ@g(GFh|3{itsp&InpDFPYB47K)5+Tg z!pg($H0`2wS4mf*KshSaBo}GhdLV^AP*W}?j$=b(M6_h`;YMT+LvsZe$RZgf)kyS* z90~W&7Z}j-AVZ%YG>q-Ulp;M;)PUjt{iG^>oR*A!2gsuIuiy;hK2=vPt(6ZM$BsV* zzA>L5bnA;m3(bp?pWlw_ zhQ0N_PR{OdJI#uv&>O$~uvO_5VBZLu)_a?n)+2IV+@9d5(Ew)zx6%c}7NieYOFq#I z$z^JuWvCIG@x~Hj-K`V93h#&urv>Nm6U!4L0>@CYSXU7m+*SM8cXr@R`0U1~SIorB zNIkWXz$EjwYf^vAhKPFhnhU6xBRw|eCj1}Tgv+R=)>e8U6{;C*T8tUZ?K#?>cu_10 zv<4lp8$7Zv>Z`EhIlZsW|0Om3(VF^fld{`7#LNUXt77jub?bPVPocr>F8f}?n6&LX zS7pkwSXQxzvUf&PU%U6vMIWys>TKz0_Oy$Zp>b$j;^1qKxzHumNu^3M|C8s#96@IQhJQEx8NF3cL7y7%13Y%{DeO%Imb!<8RVf>;p z!_Y{{1TPrccf9Z}9^8b+C?3WZg^+!?M&&V0S!yh>vF zs<+hYi;RQ5cC3Amvo7`o2#s*-eoCCv8)Mzy6UH(y+ZRrH`XJS|`H~+t%t-i~+;pYL z^agr7`s9NI=>r=!PH;JroV(4W%URSbR0xVWGox78Fs0N)Pok0u~?BI*m!^zvlxaVvSt{}IehtxWQbNg@9S3{iA- 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 dce43472..9e490166 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 96616e8c..0438f9d8 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 f24d015e..3dc81c8a 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 0438f9d8..96616e8c 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 f24d015e..a50d2b5b 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 00000000..3b01492d --- /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 5410e0ff..635851d0 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 d0a2ea60..0bd6934a 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 05b34d78..77221bdb 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 5186aff4..de91b091 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 3025d722..f8ee512d 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 e98c6636..d483a185 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 64c37709..7dea516b 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 70a3eb6c..97a3ebc4 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 ae943e1e..bab81914 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 1743505c..05d02dd5 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 37c08a97..4bfa8634 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 f457833f..47eeb336 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 939a4831..4634cea8 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 8413673a..2d0f29a5 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 39ee3bc7..4e0e79aa 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 7cb678ec..09284738 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 25562f55..16c99d20 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 68a80e54..f4be0396 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 a4ea5830..15f7b977 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 7069c0da..acf1c9b9 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 43eb552d..e1bc4318 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 49c7b991..48829053 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 41239961..e6ba5b17 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 e69a140c..a1ad932b 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 0887a591..5ece4600 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 304353d4..e9b83e29 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 1792eccc..a8ef18d8 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 6f6f7a18..47a79534 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 2fb74fa5..1eb33b97 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 5d4a6e1a..1ade9757 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 31a3ade8..e5704093 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 e30c09e4..06389207 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 b8861807..4190ba39 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 1c75aaae..629c5ac7 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 a9e2c171..b683fb78 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 7c49a29a..e4957e3d 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 92c99d90..3c1d7776 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 8bb1d45e..76f61142 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 447c2e31..1725a8f4 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 25c15f52..97db904d 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 00000000..feaf416c --- /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 03491be6..830ef0d3 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 00000000..1e9a3071 --- /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 00000000..8a87116f --- /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 00000000..73d79cf0 --- /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 00000000..f2b3ae27 --- /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 00000000..569c6872 --- /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 00000000..2563015f --- /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 00000000..14546b97 --- /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 00000000..ba4ab532 --- /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 6bb49000..77d687d9 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 105b8440..9ca5a16f 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 edf0ef49..2b19bbaf 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 ed270638..7e3d0b0a 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 9ca5a16f..02f34132 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 02f34132..8aa5f72d 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 0f40cb7d..18ff433a 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 f0f66e28..2fcdef29 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 6bb49000..9f8265fe 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 b4e79b36..0e307abe 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 8aa5f72d..7e4ba2b3 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 2e1108b9..115cb978 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 7e4ba2b3..4dcbe5c9 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 59f503a1..67267bd9 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 2fcdef29..b308de14 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 9f8265fe..1a1a558f 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 1a1a558f..66a13f60 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 66a13f60..c68b394e 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 26e25a20..2b4bc7f3 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 27bb0377..fef0dd3d 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 18d42d14..5817e54f 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 67267bd9..917a815b 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 8cf90eba..a1d9ff0d 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 7a6bcbc1..a153dbc6 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 feaf416c..5403853d 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 830ef0d3..7088107b 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 1e9a3071..7488acc9 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 8a87116f..b5a88106 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 73d79cf0..cd01f93f 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 f2b3ae27..8d441489 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 569c6872..d5481d7a 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 2563015f..dd718b96 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 14546b97..33a57e9d 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 ba4ab532..9e9e3de8 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 211d36c3..dac62032 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 9b00b481..610a7d1a 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 666c9a6e..05c3059d 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 00000000..ce63dcab --- /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 77d687d9..ff586a1f 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 00000000..39367c3c --- /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 00000000..42a11fe5 --- /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 dac62032..211d36c3 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 610a7d1a..7bd2a412 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 05c3059d..47de1be2 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 ce63dcab..b2871e0c 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 ff586a1f..5029560b 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 b2871e0c..904195f7 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 5029560b..afc5b2d9 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 a1d9ff0d..865a8bea 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 0e307abe..3f994085 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 904195f7..7c9fe67e 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 189f58f4..933850c9 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 865a8bea..39b1cdd0 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 a60e379f..d0f397d3 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 3f994085..53ddb724 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 ba7e3d76..bdef5f63 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 74248b5f..5427305b 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 ba7e3d76..8a33f214 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 933850c9..0fa12cd6 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 009995f4..1fb69439 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 9f375860..c01fe891 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 28d3ae62..458ca29f 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 d483a185..3220c824 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 7dea516b..d0fdcb3f 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 97a3ebc4..aca4778e 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 bab81914..7b959a77 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 05d02dd5..1672af2b 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 47eeb336..9c9097f6 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 4634cea8..dd132287 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 2d0f29a5..db565fe2 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 4e0e79aa..fd46b4cf 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 09284738..90c8e105 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 16c99d20..78648a03 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 f4be0396..d06cc649 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 15f7b977..57215e2e 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 acf1c9b9..6e72d4b0 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 e1bc4318..b7b59ed9 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 48829053..9fdc2926 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 3b01492d..2502cb34 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 e6ba5b17..24563d21 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 a1ad932b..078bf376 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 5ece4600..e08700d4 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 e9b83e29..5be2a914 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 a8ef18d8..4af36295 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 47a79534..bf4b85b1 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 1eb33b97..f464cb8f 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 1ade9757..a6b83d9f 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 e5704093..09c34b4f 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 06389207..2154b272 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 4190ba39..f46a301f 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 629c5ac7..93e984be 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 b683fb78..214ee83d 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 e4957e3d..db26e538 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 3c1d7776..c3894726 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 76f61142..45c2cc51 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 0fa12cd6..2b7f8c00 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 c01fe891..0880fdb9 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 0880fdb9..c2f8f9a3 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 c2f8f9a3..85aae4c8 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 14a2c38b..43dc3a65 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 77221bdb..97012e51 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 de91b091..427d0a72 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 bd1f4f1a..c8f875da 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 1725a8f4..a86f07d0 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 97db904d..947f8fb6 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 947f8fb6..2344f84a 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 97012e51..51e7f9a2 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 427d0a72..c673531e 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 0aa7de07..f3bc4585 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 4dcbe5c9..3f7ed677 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 2344f84a..b225151f 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 378ddbd1..25c485a2 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 dd132287..63c1d26f 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 57215e2e..ab0c8064 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 00000000..6aed6053 --- /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 2b7f8c00..3bec6862 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 d9afbea5..b5cadbcd 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 4af36295..c389d682 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 5d4cf39c..78c60df3 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 1ab21dbf..ffcadd18 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 47de1be2..3e85cb29 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 7c9fe67e..96cc9fa9 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 2b7f8c00..c97ef9d3 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 458ca29f..1afb5b14 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 e4c9fa03..761c95de 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 7e3d0b0a..2a3fd05b 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 c673531e..b51c481a 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 ee44cf4f..02d367b5 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 53ccd700..1a974c51 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 bc9c5ff6..52b1717c 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 1fb69439..e25c0d54 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 a86f07d0..4794efb6 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 b225151f..5a83ee57 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 d7d944cb..e82e9d26 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 5427305b..1738a02e 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 1a974c51..2e2bce3e 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 2e2bce3e..9cdbf974 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 04b2ccc9..9b25f497 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 00000000..65291b96 --- /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 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 literal 6636 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 qM%IAf*eA5}h(mYB5E4T(^hh^D zGjzkp|Khti>s{-;IcuG@&))ml=Xt_3)fLG}7)b8jyGO3{K|%Z8z55FPTSSC+f6O)& z(RUwW*AIr!d-o_h{8V8o zn?=9Ef9`ePzt9PP6Yakz^1>wat%4R)k9GXt?{WgJ*@0xg4}N`%Vvi90Ve%~GEz=_= zcMB^jIE)G|6f#3)$(WX7mzJ}Up6-vmUY7A`sdmsWX7$-SJ=%D644k#yt9J0yFE(-* zFDR&cM}DgcZ461G`+Rk=DF+?QMLWedEJ!xL&Y#* zd;*W1s74t2v5LMQ@THfw{pDEhE%FB+!XQD4fjXm$(x7^wfA0@)XDb-eYDC5YO7&~G zOB1+PS0_FqQQ6e07*YQH4i#bj;G}|=TS#$9hwS{$N>{-6$)vp~)}I2y5UOjgK0B}b zG3N~PgCew{Nc%et=BKxwUonB2q-rrrJ5c{+rU2u&e0a{$y_|!1#?&@2Bmpa=Kc){dB`Q& z%b;c&P>SoCKHv2^V>5iaoz0eXClV=(^^JD5c(wvLc{>XYR>t3B$??obE*?j? z_P+5E`D5a^Wf&!gtJsp=cw4vuD&bej;jAj_j%BNb;<_2S#tae#9#Ddo4G(key4w2p z;eMw_Mk1_0V0ftl+N8ciX`qp5V9digat)CImLhwk3UKzY|82NiEY8FLjZg)MJ%SVn zUcd_w$k8lVVg$DfiU8C8dOQp8Sovj8?C1QyamgGpN(IFXsQ3syMoawesQDv^W(0R( zy3waX&Dm^BnI2uDnZ<0}lr_oipvg1rW8=pUAs%++T zk7NbIqsOvNhnuLgl!2(!2JrCM8Pcz9K_!?AlJ}GJVaTSeE+)2=z}B8#1#Z9L*dOLY zok40TLTu;qPP;c2%gGB4$qKUfj34{WDvvxcl0NC@K6SNC!8Oc`7LLzP36K8qI%l$p zBx8{4iev8!Oy(IF9nvAKuaV;b7Pgwa3-Cfx94nq44a%(sO%W^8pXn~CmQP0x!+WQh zM2J;T*mBqfoE)O$1703Twn+!9=~VrwZuoJtYtQV>(nAAppE50^4S2n$1LTNr+XtfA z9y-$Fsa-~6+m~zI29WX6RiiM_ai+2<+21k9XC*mz(`fCD1U9RT#}TGpVDAZB5Jl5F z1B(?SD$1Gs?5EZBR-f!{2IDYsyulK|_chdB_xZ$$9sidr)D@CvY4vjz04*VCaR+^< z)=I0%J4WZOAF6|k`*HCU(8da`PraWE(CVQ}k|0NVu!gM6J`^qf&ZwE1-o3Fx)pF}* z!+XMJkzQ}MB}Hq^={@Ab_J%>982B{bn}hN9%nLV&haD@wZ?s(XefV)T?q#XUTJajz zp(Cyj9uZd8$blE) zd*A5HgQG{C9b6=a?jSR+b2v3n@T{OISGPJwbOd>v`_r7KpWBWL4VO6f;#xP^K6H+Q zE3V`#$!hG_6;sbUuAh8c{NOZ7MI^No3&V1WNaiGk3O0_OwA?P zfnq`y)YJ+;IYiv#q)ObKokzFhDtV|jkC*N@msNqijrg>mk}|3?hENgJCpUWvbLBy& zjtqZh1 zBZ4JEv>5|_*k1tux83V>I3?-dDhb=*9wjz1xb{`!xTR|FE%Mk(%7O@i4B+h?p2;1d zB}Z^%hZ?eHCxs{>Z9~@RWF$jYB*6y>W7r*B`cG;>vw5tPIJfcuV_oQ`&?6~@a&Ahb zXABv4W~}`*{zB%JhNY7>^6BtlE6TJ6{cyZu^n6+PO(~ zjg&J}$0mK;WU6Z}8Cdjw16oOAfNtKDJUA@c7`hynp&UxSM7G7UDDP7-#Q${#+b zVhA^^$cjpE*;MoHXIHtCL7a&pIm^q|EqN*etw9}6`GU}o2-moW?cPx`IY>GCY5Q?4mIaV$%I5YK-KXpz<$^%5%|a-u$XfB{{p$KzQkh`EGpu*lheMX!aB3rEDND3cc=<>)x{I zne*>n&IgV7r9DC;57s*tMHUv%?&hdUY|2#j%2+0o8iE&FX~Y0?@nW2gtW%}4f1+cl zS_s-35Z_ifThd-XSD(<{NM)99)C(OURZ(N-Rj5F$N*T0mnd4v%pCoqBdb^5A4v>5~ zmm)Dlr)aL6BtLM#WzhgdYY6jxSTvxByfA5`Pg+V?Yj8W`@Bkmna9I>lIBc#74bn4< z|A>2F^wImT*r~TLoXsmAC!vBj){$N>{G(gFx@Z)~#7M;7Ku~V2~=OjwcmWa4+{jG0N1CI@M5DA9BU!?|5s%&KHg`g>KD z!YV6=sDCEL96-PmRW%y$W&&10mV3^G6}UH(J#wSG=giYjeZ!137qB{s7@~P!o~S&# zkV>reO@bhoFrb^*zMv???YIjekXXew&697dwi(YhIPf@9t8v38pTf^Ti{ZpbhbK+8 zfGB;r>2RNDin1|k=o)EdE|#^58vQ}Esb zvOY%`#&T2eNtMn(8`~(bnEB)Gs%&Z$eBe|<##JPGrm-<9gEd}9D&Ds#Fb&hPB-3`X zLs9qVqc0ul4lBdF1cvUMAGeU8@9+b)*_*Mf9d%8$l8RS+1Hm+UO*00)TT^4-#uJ~f z<+$3@g2?ae^vF}dPDH0lB$kP>k`Uu9>&cS1Fq`e3r;(!4)DjsEGrTId!=yV44D8f~CRK|)sOz-D)Y~_)g=XAf5hwlmjCbbW(BCs)S zE+6&|>K~cf|EJBUG0-p+-s~ROJrK%-m@q3b#e9dqFGT-0`_lc>wWc44BhH@76wfVR znxVWN{WrsCaCXimvUpmlI_+SkTB0hgtx2~iEuJFxmu}Q8)m6g!khJxjli2FVq7hA4 zxFjHqe}pCD+3r}-QGRf&2wnY$znhm-z)0>sH$tu2ReA-D93}`@cC6LtAf9f21m)-~ z8^2zrgNOw{?@C+#zbm>X)woZ?V+P3Gx<3q$wrcF<4+&LdYh6{!l+h(NS$_~phR_f( z4zmH9)oKo5w6nj|D@VNj#5?=Lf&|{$`d(WAAr~NE7a&yy=^r#aW7ul8m++C$+CgU> z8!Ah_lnSzMs`p_>pw;1j^S5*tX3~VcbZUfK@_(O01ssXt@3B0B$2Lt;)dHT%vgb2A zr>@Gq%J`xSz1dXLYJ$@D?==wVzJRoX5;=7EPp4?&Q!#|DSGP(3I5YL6VY1~#TCDBs zHD!<2?MyrNxEXA`gRBHh`87v0x~i;LilwyEjQx&>fT{)|gxrW!5oUiF3UO0>K<&Z{ zKp_2Utg3cZA`7_`H%t;6_~=ziO`?@ZG%#snM#H|`hJ5`rk^5!*={`Xe#o#q+OsBp3 zIz@%DQLstj=2kNEY+Da})Rs}h%m>H;i!=cVhg8XRY)wTv5aiH@vUzxAP}rj7?lc&@ zBgS3C=5$v~+jJ?LZ_aift`!}mOXS@mr}`ERlWAR?zB?_hdioYN-MQ=@q;=nhjJ{1| zIo3wmtHGJrreLN$>6GKZ;1uDgN29}jC|L+PPveQrpZFQikU5J|DT(#;Y4K|GJstL| zdilr8VuvHq$c#5yxMR9%s)0*0Uq7WlzhNt3v;UwQh+ybX^#nU5c*i3lwo&s3_Fy1~ zPC<0mm2{Y4TfpGfkaWio25LP~$sI8{$-%%el~ zsnPHq7);b-XVEargg8fW?(G6iGQ{kNGsYgfjOp=I-z;0J62@Kcyh;c;0n?#l-wv8b zP5yN^TV1IF9?~RoI#~3N&$DrPO{!Nu&d8xVG_7oA z2mG{&1`}7@vCK1>?y#voK9MXbHU1`3RB9|ZpV_N7*kuPIsbTMU$$|KhGecproaK#( z&tbSWz8JNZysK=%uv!kwZKQM6^L02gw`J+RrZx9D!<`$=mq5y$`}T7tiM|Gizz;UI#@1qt zxAhvU!{?^T0Ek-ninvx-l2JgaobE(C<#5*80KG9hL)5Kdt^~WNewEWpmvn7{6y9y| zPaN?fhXiW#$h5HPAl%o&~M6c?Y^O8lFr#y&o6kBEG}aEr&^6(CgSNSfJ4ysg+T#4Q_rNTJpI!}M84Du*%#&JSSLW7oR^ZeDL_rK{3->-@wR{Z z(6GrwJ`NbSKWV9rD%7terc_)^c@gfNDvhN@_b=40@C;`j)!DO)yYTxYHwd^-D_1)l zpR#1NkDam^vBgw>*d>>2%lI{A+CW=No7?d;O}?4%;Q`CXNJnl}Q_P5#^TWz1jSX)y z1efT)t&#S_yCPN0h6I;Bo^@<-GSt&#zx;;dhR3QHK2D`j@P#wdzUqny+35+k%rw9$?GBx{P8AR7kZ2w z4xb|KI4ThcI6M8ooa1j*5EK%kUiildv*&0Y#pcpAV~BEq5Ms`202A2LiqDJw4=#y6 z2U4BL1ONNT>Uyv3;R}eu!aY}4Em}bfG#k<7_o63D6+5lzt=tP@uCcA3zk5-i(cvXp zwHDMLaD92&FFDAEusQ6e)$mtPy4}t8ZqQ#ZJJLV3m;XcUrMJB1=2-nun6m?6zmS{$ zU*SKKkiOIKJu}>I!8r`#P=LLokZ7N+6+~=8w*Bx#bopM{{i0FW@K<_aXH3JIOIhhd zKKR2RBDQ<`Vx%B=a;w(&+J^zeGbBtQ=TBsB>z92u*Oi)ajN-axxg*O3_@m=HtA*_< z8Ob_M;~^hkDYfrAuGCfgMs1DtR+L^mj~edUq#Y}rZy3C|Snaoo4#kfX2s|!%)$yvfYM1#KtQVzrW7?sKsr2%?_Wk=K!*k;>PFox^KI16nz6QKKK6 zAr?T^0)0#hU8OK(N+9_3e`y}(g%R--WcAy7&r9c|-?MbyrBXVOA$?gJboLDg@G8}V z0@9pGPs!QAMctPD^)U!VXE1;AuK`W+sV~drC{sr_H*4VhzY{p7AFoO&zp_y?x4x2poFY%W0(q?#m&8` zM-Lm{vWV}^eW_OYSBZN<&`b~8u6Z)!DpPsV%+sNZ9c~rfw2@K0KuxKbE;!xydcb=B z=H60dKW3&a^LkMUa^p9ncTO|2&ke{)f-2=j`P>{itv47I4cA?@iC3!#W&L$5*hdDu(5 zbnD~uOX;Z3*Od~ISGVU=jnTX1R(|!M&G>rp+&U7TeUV%#YguDLxCdh&_J*`>H?piA zPQ0}h&4nKzU?F9Ca3at?6ogyzwytp2#MYZE*RYX~G~|PA!mP(#96yiXdEn2f(g>pd zLlK|3w6g9aYFSsKq}Q&JGM(*Jn>Lb`wh6f`pKyyg5nskz|cB;P7Exb-T-)Tu?!yI%kg>j-QC~Lc_@zMV4 zuhAeFe~cVk(6aBSwa~1^9C3EpG+8owvRbN&Z0V=vc6s9f4KEDP(VyFJL z->e9Ft0HJ4-bf20{Mz+)=3cyQsAU8LVu4Pg@1%~_PdHL_N+!&C34|7 zY=C-OM}`*q!AHGM$1eUlU zt>W5^iSP^CzF;%*>#@f+z=ae%T&<)Qtjf;> zLHC0B2ijDk7 zX{W+?#wOWb{#aRW*Tp3Ej9xIXOU@-Ql9csWs{f_j%*0NPMJwsdd7~ z1o|^Ax;$EqzK)8b#hqXbdGx{Bs~noV4aMB>wpQibI!i0*&n*I(PvAUkh&A4pHA33b z?G^O4BdZXCJhcHModXgKePi`iJwfyMEccaQX_+ynWfN1&$wpvn9NP`!f)>HV*OJHM z((XS4z~%*m^B#*MSL(3By(U+^(Br088a%#w22E~u&mw)TxiGK4fPjQ1m}S9$?8KaJ zo_!`N!|K6jjgO*brUPue8(BtHTF?*%Om-?u1IM-w+YE zr)5I?OtTRFmeZM$cbWb>binO28z2r~_MS-MKiHEXUgDiP=eX~RKJ}t-=CMQdM{lRE zE?#=k2IuUQ?aU4z3s^su=~C^p`c7-zQsj!Nr$v;WSQ%&YK6-NN9`#J%-iaG_w&kzc zHT+O>P|@Ye9$iQ4vWp*E>Z#Y@K%lGeIopRTQe^)S3pDjr=}D5S`Sic}JFYe1+UFzb zKu|u*o_@3w)zZr^Ne?s(#w6s2CkzN0LIQW^9pRe55dGk#4JVh#Yt7^}@#K9f-=DuwN~9%=XKRMQHVHH;n=BaF!T5bRYAQ|4VZX zOrQS1-nVc-C*YNa@}LoFd{mHm^2LF>1?*XjLl|GPgV0@q!xUOPFxvBsCG7wm_mqx5 zO3axBir=U(k+D9%FHhhkdVLbEV!gi>M#w_gO&*v8*~p%O%obf%v7Y_mkJs= ztDs|L4U##e&54gM@!upcXPp|%L6gzcaF&j6DbiXsMv1vJvP`5RR}+&K1s_P8URDAq zh0>+Xwj=F{{ySQ=9JIoa8!%!KigA+s(bY&R^$Dnx-xvkyIF{e#XTOVJFau4Y$t0x5 zCe<*odeB-7U7~(s8cs3=h)OSy-x_bg3`cF2O&%2eoFoZ$Krv&ot9spiAk3uFSkHmV zSR7I;!}hGNYvN1j+d7t|QK`BKwl7y`hF<{<4420bjt&BvlIlFt!VV@qzf!0Kt|d5y zomj()w}^e5dd-g=Xd4e!D85%k^leFgJ-vtBA4r--nTmU+7we4bh#mV12nX3d)iBeK zp-7PlmmvUp{p){UIQ{><09xPDNRTR1gFEs7^NJP0d{LV(jb|~6)aP34^CO)JhzR=S zVe+p{K`D8=!I+G#Jb_3mncoVrp$v%cJiZh%gu~c>JGI#BiJVY>32zbOi3a^e&xKl| znXj26F;?&RE@Yq4BZXA*tXL8il-UH`H-MOzMeGM58 zDb{_B6ufn`ut{#{sHQDu6N5d9-h;&XrD5gK1tZF@H-DqYS%)!1K`;`SOy#8XWF1}% zelU^Pl_u3c`Ub2Z1qjgv^E^p zJzpwhr9e^Fp~<#TRCU}T&S&W)DFa)1C7@apf5^YKBtip5w;oU}%Q}ERMO0}$gof=vFcY;;^p=?eRK)?Mpxutc z_s-#5&V4W1Z3_qTHcwy-lm=-(7oXPAql4H^4o%LhsLA$e9lu5Yq>*%7S{)Fpu|CpFJyi8+xMhu zIoeQ5c*B*x7XvPk!V8--$DnqG`O7z^;CM&lQ#P-RWtSpzgOl81Y{+ToS8O@_6jOP7 z0QOxb%kfb$PuR#7KAc}N?NmCx^BRXr2_xV&wqB;*t#J(V;7Xp&$6T?9N5{*Z9j?q@ zbL9<;z2!$>VvtyTL&V|tlRed$DqvRMOY(PpmCOu%-Q^t5WE~*z?;{@xX`;vdvMF~e zUtG}d$kwiAP1jIQj#-BfPc%J*G561PfJ@#a{4^R>5B*jRg>Eyx1btvJ*j4bUh80sF zpA_{l8lel%jeQ!ns=@Z>q1wBEz$x2hlS?d%sVz$;1u%4N8TOy;quND;H8LJq-e%jw z`0FyZ=B57Sw_2^aI2HVc;1@up>hSCBpsRveE7LA1MfE4`f6_QMZN`d-@JdGMqH zmD2#KtB$UTlwz6tZO2psHYEp+!U7I`AcC!@ue}E_X}GqZXdM zI8_;ebz(vD#3-FA39F2iwrQpl$!t(Q(65!fxRm3&V_AHaPo^k}+fhan4F97Sko(Wi z@toyyhW=6z{szy!K>aaAFpkiC|D&Pccc1IFYTS+oL`jRVR3CAVj5gq9rI50XNBf6O z{KNr`w0Q}MhuZM+NMpj6rd=iYlDUPi%pEAhE*CR6bKB$$A&h8>zbf=i<@v}F?|eXV z-|3zdbB_Gl2$gU(BmA3PcAWN9L7r!u>az*=4aVD26*g259SbR66Q?MgtBH4Az{crQ z{M!+Tg`i2KDWr@X?C{6h@TI;c%P+R@c<5JAt`Po@q-`cE_zS08^+Y745O<_)~2I#Fjx$V_V7+|5ib@b!qF~ho4NE4GO z4QZqBkFUFm=N4Z34)&2CXcqaW0ynTm-@g&=1@-TWVT%+Co)0o#1f_+|^h~UA*Ig3e zGQ56hu+A=(EJN9$gWJK4a+Hg&OwK*1$HW{QRJGLyRmA0P3!X|3pDIW*~ zx^l#*5bfWKsXhtKm-+^ntHHnWIJH1ApFR418=ULc`7=0vxpel8*A!jO6cf+#U0i!@ zXtN`{aWE5#~v zRMtd^MGRQBTaE#XwVWbKMfi%lrs#GcbyAO=IS-}@ZX{ybJ77UV(wz~bzDF80uyug3 zZ`X6KvC*ve`f067h?}K$;Bu$LQ9P?|62J2xZ7c4*44MGR$p|kv#F(%=a({DR)6?<)3SQnP9tR8sfmAqrepd`?ZXzzeH(Qq$sU*avd+vay^2eEp3yBZ zgh0c0Fv9P7N#*C3>;9!|A|P>FH5KIpAL0kO(^$#yOe#O}%qC#YLe|!37V{|m=jQGLc%2|vt zY*&A)6wk9StK8MpcNrSc$hs6w4HL){Q^EvBcM)F^LLvRt83UXgW?}t^-Q&vGi*s3x z;&v2J62SzKCXf^?9}6rXmWEnOMyvN3D1(Dq+dZ@O=jx*m1yP7rN|1Y-Wa4=PC({JK z2N_}so8{cM2mf`1(KEWa*P*sx^PrRs&mtReMRa@8r`#+Rz~f@^MrM0!A$LxExkSj4 zcF{fkup)Qh{48_4N_RQzDATX&SoCK6kn;1*ML6+(Fjb=2#PH-$$NVFw`+|!da?LR= z*9BuPwGsO@%61<}csfM^a|Ux!p~)g4P!E|2qA0e$|BM#PxJq9+Jm=ovEkFd@o}Sy@ ze11=cm4}hDL7W&l7ZVK8eHYrn5K~e`{3#j7KZI3KXRUk-=@~#4de&~@-NT7#2x!Rk z(2?>|ZkJz=w&sR}IeHvkp1Oomm43T(4lWM`L}Z{COD=z+rSLClXE~1(x<4fNuBwN1 zHi}9ytwoS04og`lp5{5Id`W@s?E5hH!<`32dn^++JddH6#v-kvT&n&NhxzE(cfUZ@_mNkoKWpgTn_hA=X*BO?_h>%4 zc1mS`Wra6J4uClz-RZ$RT0(ZoMQ$o*!YJC_^;zTiQ3n#s zW2vfX!;Lpv^VWlE-hu*x!rB-0;&q582dFPJHNlozJ-^22T+$Hk?F_;|in_#3mCmY4 zGse8ANE%)R|8$L8!g60lEq^utzLv-*NUqn6&uApyFJ$y%uBauN4zE!?GJeeVH2O~@ zKYr}>vq93WnN|>!&BN~3c$P`aSr17P!22Yr3k_k_Z zsn2oPp3<-+%C_$`&ye~`v9^@$Tl=uog2f*(ikn4e-q_f)Q_Rk)Y_5q5bL;nnq^33M zF_UT>OpC~_!twKR^-0yoTi=1qZOyAWwtN4aKxi5LkQnprBeYYbAxql01d2M2+zlvIb^3&Y2!HfUlu#T zmuKds$K&Qx(k2We{1z}dyvWUB94K;ek|@8}GdD4tS`e5_1(7Gycs!BFbtAubA!IaTl!<8B@(sqNvv-4g%zS^KA3llQVvs zh=~B2HCmv3Y!)aMnAF#6Cpb--N#&Co#^3+`xm7Jl5&**leb)W=tsFCRCnkCDUOMQp zuRor(DT87TkL)QdQNZ>gs!0*w`WY3T(4~Fj{Em9uVg{AdQ7}&71bXpQB>C zE5o);H(|!Z(DJW6wyLUI|F_oy|D~ z&d9G=GhMDkq83O)=-O=tRq3hKzS^$l<;GTS5#J684xAm7HZ#DuJ93}4@%g=;P}0q7 zS;`cP@Kd}MS@Y$^+{PTw#XVAE{=EsjHAsD+I8`tL%Cv}musJH-6PG;}W>qkl%w#Um zf#D~_T=GjYSC%y#PYBjUbT5C2d{-{?gN+WY2_tX98wU3@&8wjefaQB(eCz5KF!Ic+ z?#uJ?Ql6f6c$)f#ovpJ80K#uWBKK_d6R=OnII7KJ0C-}?@^OE9Qo}TzGaKlpTX=ky z>ccGSVrE?W;r{fYtzbvj=bI@GWfnE=cFW@MbPE0m8k-afKMlcP+n(Q8M+%L@{-akkj zp!9$JkjgsejHxZFKG$<@-dE5v8>EN?w1Q)rh~_u zHea{MuecFWK3qpZ*BGQ55Ejvjp%_U0AaBikcl8L?S3y`P&Wv@f`u2Ao?`e!u^t~JM zm_Qq91JcmjwM$rj=RZqO&LFoO}F z24V2)ltLWR{5`$tK^Zx1OM=_R2;)D*$^`B^?&xsqLVrh!CSfNI-tlYO((zTx!!`%l z;eg!Jdj=3Er^K^s&hp8|l0bZ&a^DNAD4H23IY8O`q_ zEA-P3nK!US*yhL1m-SR&H|2BXCVhUdp23=h>yV^@IRIpq+k6p9_n(KOOW ziGsMnbvnJnOsUVDjv|z$tLrvPL2V+fc`Jv@(g*f3A&+B-JmzfF9|0HGpv24tGWj=_a zYS$Y{bLq8y!{0tXQK|pF8szp+H$+^#qBHbRF1mYOul6FAy$`BP-%EW}MbVgy6tO>7 zc2p#p7sWUItQSKI!eu3Hkef)8-Pj>wE5y-4=`+^Nuc)J?B+;P z_~x0y@l@>03=?TSG(rinHrHSN9k=KvuEDC>>nJz6DtT>CW6qZ&pt4AwkW2WqMn=JK zc*>Apqu`ZQA~~+eyQi%T1yn%jWLFx5ojQPi&%Jx!LVO+lz-uj)0x&-As*a zU|X5))9E(qK8j47g6zG0iStF3A~UDL6d>&?@I;$W1p`2|YoZu1RvYvIOM$HzKB=cyOF^B8<+e_aPwq2(?5_6ulI{G(6{HmnrjD!Av8!{l+ zH@HDmCY%gykn{TP(!JyLO@y|SWu88PlcjN;44YAcfO)aU32l`yd#p=X1g(EmEs+L8 zlTLU1A(66-DDR(>DfQr~{6~6?22h<431jL17AkhxNP54AwLcp@vu?w^^4Of~z3TTl zt2FDoCi3C+TWbz-i)o|cA@6j+qs`|ol5!1vou(G8v&3xLE=4Iv*PTDjae<{a5_Y~# z#5rb1(7STnB`Z{!o|zO$Z@^22|HgF~L~v~o4CA5a;|RB6y22>W`s(xw=VECD2_@e( z--fPNJK`9mAYew5cRFN5%~mM}D=w#5Je(oQERO3{zdpG!O_w}>;=i}*kLl79(9SE~ z+K*RY;z+Z}*N>eKGP|C|PsXuY4YD4qhvSCi{AyBT-t(V?r&UOv)Q#HVNRpXX1~SW6 z`SW{j6Yax z<4C_oabZa_e6mW_iXYmiD}9{V+KEirC#wdroA*ALe$+~B$0nJ7uN11gl^?3SCCt$yg~oF3URN-{d<_yB0M0_&d|%ZC~fRA(`jw zUceTeGrm#2C0CpMdB_4)k_j96QgYSs*jD#)rne~K55)d0+5QRYZc-parN&26F}fv4 z*{jJdwTs;1%5Ck4Qb&p_&|;)7Zgr3pi}rXL3ouXds~`)1C zzQB!F?t;ID;2;bvyg|&OlTEWzBt9zpA>Lw;!56knQ1iAWvza-h`M}LPfbhoj3XGF) zSBom(8?$aFjNt5b%V8kLMe+~yV4Uy{Lgvcyw(QZB;6ld%(t*o&SB-e%T>;+k5Vm;e z7HG=l#tq%~1WA%B8Atw=Gp6H4rFg~lrr237!5yTthT?G>`!GqOp2DV)^xDRbi0zjZ z*OPC~DHz-}65pN8NUG6heeU`t1?<>mH}=l<62Vc(;%)dH>{wj5+ZEkWBwe4VE3q4Woz1jz}RFfo`((ebmImDk2mjw;HdDV@fyt=)PNxRJUnHAIb#aE$+NAGdNX7N2vF z#WDw>n*TN;Kv@6Us++H*DX#m3X7U$>h6-2Me(Arbs4Mg0a)({;vX02G{ecd-jlivm zeY}eJj*h}dr>i~y^kU3zfheeWw-Fin7uvQi-kFuz*BwCP$VtN7O?>FaoFz_9agyX>8JH?5de}eOso~d-Z z1PxMN&``B{Rd~0s9vwrj*TM+xuI0X^4Jae2GPhmUaL#JyYgi&CcDSR8LPD8ugKBS( zL{6r3%)Y&iQ8bSO7R&|(-YcqS9gbUsc7=Z##2LNhYkq4;*Pj?LL7q7+K2>4a0pr@c z>6yxWmQckQw+XJ27$eFhKg`{mA-Dg3=Fqcllw=7w>7--0Mq z^oUNZ>x3jaM9(f+8G)4Fi~Zxs^X_Lm*rWaWtKn@m@o_0}wlti5-M=rAC?$(kRwa8f zT~CmO+13-XaTyBWj*2~CgIUHM1Dvqt1?0R#6 zgF=xwLxvf_3{28y%Stbk(h(rmhe|3r#cRJWiV^hq&msee#Pfkl`%s|7lA;)p!i0AZ zeiiD$-}GpQvpd}aT-1hpy(zvE1dA=ktopm=*(@RDhq z3D-5Qvf@Qmwg07}vh;p;rp2`{mmlPDqplSu63s=fGvuD=g+D{Fp6G8wE%r{%z+z^PCZy@?&~#EQ*lj-?k|MKF?s55m zWiaNRiF2BSwm?b0!sl%U>(}&j-dNV_jI#)zJ9)+S zl-bi^%B!~4LC#a0PKU$rB#rHJ`%L~oO^HWT5+Z3YOaD#P^SsntJu-5Y>`odlM@!7e zzW&ZH4o5;%kHqfZ$rb5CnQa`&+iNF4jB^MlIQI8aE`2TUy>O?*Gb`0w37$80r8|4B zZmAr#<>Fo#JMAS}>0#@)xElt^Z}G@z19c7hb*ma3=E`fFqoJ5Jc%>eGDPCLdd0+az zv8W}MSYLa_(DTl$y4JI?;jx7|ly`6UG#UDzDu6DJ^coajC-qzN?7&g-yCx$#dqW+~nhJlAMh_d`2i@sC-6THact*zkeBlC)#c5?5amaMzduv5}_9hPIIuss^&A?7NUf|e#pOR-Xu zw)WW=S3HduZt$n_Xfvap$3G*6(=B&HIyBawJVE~zhEjhoqBM-nk56?! z*St8`S-v$O`!JNed67z;l;SjXbGc>7nvEI*Ils79HOPylC+hl!_=6J*P$<`ckjuS>QAcAbF0FXA&y69!yAC%eJibZq`lI) zzFr6<@;>sDCQaSuBB;gEDQ~XZ%Qa$r0B1JWRE05PnyNd+#^^npY95w^%q4u@rn{iX zI)n}LaHM^0#tEN0`9iTd)_nGPacVs$c0%Tw@hRa;IX5-!r5utP8e+4G=ApNvmV~=Q zjhl3pR}nU?B1}wE2P{H3o})>LsqU(URwrc4Al(CkX%wK*Jy=EwMN~`F@6F#-SgTy3&DEFOoz-m9@@SR zdchdwA?|~@^}8A+i$NQS8h-D2Ex$K;tuZQWq!DWC@Yz3oZbD`&ci8m9%GKu9qaFi^?}fc}I_zWVwrb{?EK7yu?#7u5C8kW$ zmtxfLq?DCQ7+X}M{3TutcM!!__q*Yap4iO-(5K9XQPo~W|7T1O5ccfnnwLrWq@1Z_ zwY4$k_;Wd#G5o8L(TYxLWYqW&!Uk|9aoa;oQbG7^9mH7W?eiwR66X+3V;i5^^=vs0 z*4+25Yn_&%@_EKThw0UuONOSPbjO&2EU!dTt??QYYBhYr3}y%?fD^sMx|K@Op|JIh zH)c4cy#}N)1i}Fb7xBa&&2+&fYd2m+RN!GY`p-^U`eU8K&Uq->+tk-OWwKn5XTo0K z%b_j=7AfMdE@pTW1~|WXD)mWVNT=}`C1*r)(c*9&=J70EX?Qh{H;+?& zZt-%;o04)N@X+uz_q+x-WmHqU-zngvc^Cra`J?V3$+A*gN4pB05c?wFa{lpfO&&2v zW9Ni5uS$0H98EQEtZ}u%H(ht}cNX^&xnyzD4E9^|^7Sa=9aK4ldd~DVSQVBp=hY^Q zIasO-tIstwoeur>Q{KEMi-qm2T@z1xh|OQiW2?#69Nisy~xPy<~O`dO4t z$;{(LL=L*4Zs)oeCeZODb-nG!JsdlHK4H9=y$T&p5y@GYR(Y;&X`QP6Ht((JN}ijW z6Q+bGSEirqd`4}KLX+MxQv79MbPT55DPsp2}Rv0$pmv5U9!d1Yf z?FJ=>t|GU8T_sS{>d;j;qh|3KBn#CS2?Nt2R& zDCNea6eAbzjG>_h!NQSvi~IT38t@q4qFx9a+W%z1lE*wzR#iIu4R*l%=5096GGv~r z-%N;S>u~qs^^9R@tUiRsE<`J7Yk7M!hbJYE4#PdK!p4hn6F5fyJL&pfot*Wq=ZEkt z46$KcfUur2gFQ(ow&(;Q9MjKzSPUNv|{?PnX68+SMyhN99S(S?>J$HCEGb*wU-cLW;Y#3YKpC{C{x;X@6)Szfmip?jPBak(PK3xw2!BbK zoiO@rIR}=0WE7Z+o-Dj!I_y#@o?gqIj20dV-uAgP7_JczV}KZ*LI$E!&q!1XgurWk=02{FMKv% zyE#teF!4;2uCX>k-H(#BnAqx9Jd!jg9VH z@J^W}R#f*3x*a&ew_cKqqGXvj=PgGhHzxHuTt3q{IXo?+@2u=HN{gDF~w%^W;_t z&37RV(OpB9$8PC0hoPx7nkXS_5yGR1Jd(&-@1(z>o~I}e)|zr7{FOg!-%o`I zKAL)`i2OP8-U)MQ2&}z9%dmL5Esfs12(#2SXc_gj#!tzZjXHDgr1si8C3)Um%8f21 z>A{wU1SuJ%93EbYOx?P_`=mL94*+*|y*7CoEa8n1*gG_V?L{@~F0iI<-@{lV@+cyY z<%=I#)2|$r;!Nd{%64mwVoKIpPin?>zdGZ78wVi#D`L2``&fm=ozEaUh^8T(b+OC? zG2CrO)c^NId8W~6TxyBfjbiBkntXWh!7uWOXthyz# zrOHzt^x{5-TY+2pVR7tE{@&@cB=LHNVqG%MhMps!=nkt9PWhA;&7xL7Uc~#@1W{uDE@YA)$>IYcW~nF!;b7F zhM>k+SX9EFN|lv{*gHm6?nVkyVjy%VTH0Te+Ot(70wt%DK~8o>Zuxt5bt#-LDDW z?)6X4Ao5-ouR<7}olKM)o}dn&8|rwIP3Rryyu95Q+iv*0Net4cLtbyX@47Bdd1~x( z{3Vzck8aDuDUr+X%d0=O55l)YxCU5`#k{%JZv_}{5G?b%ePh`PpM_VbH_z^zI6~z2 zd3n%%C|M&p9p=`JhnJcUHF)~j)OZ-zGw?&^+-^UX#)dmJ*i++KyUg-B#*Y*EaOY!r z@^_h=)ug=$D}&57%lZ=VoiHoNOhs z*WfJ@-Gu+yeuH+$c&YH&e20*N#U$H9ERrb8?||^9{O-5w&sR^qVN6M@-iYhzS3bR; zz;^&w0#5E%d--t+1{r}Ac#xS$Sp zjwi=wE$WiCa=0lxFaO!S{>ZP+IkR<`$gN$YB^E{}e5Py5K;;5i@>pSZFO&)ZA0f_v zolo9)41YYe{Hgv~{3)u90Gct^lQ)zv^l;Q7QLer8nW2*tJh z!7uN?q$PFw9Q8b~!t2gBe~1}3^M^l|a~g7aGhNo)YaM!+)^%>)c=^iIwZ*S<WP92JyXw?XLq1($llTXCu+xd$&!H=ILS_ zCB|2C_yZs0&EHa+&ak}dW#7`*9$twj=he4JFVK%Bg zhn)?~cy-Fd>rq>G3&Okk?5B_LC%vN!xiGFoo0ET586 z?tHPp23|u$yon*@`FcnG`V?W$yNKeQL^tWKa!%GKQ{-Z@LhOatIR|bghL>{qL+{{M zzM=o!jKir`w_e>6ywu!FErjN|)m91Kg6BD}hc_HM1mTs$a4pTZ%FDxz&X~$^irsON z*K$+r&50&@(1$4vrYM73fE)R)uRP5yw>16J{mrf48XeY*iMRQAUg(g`awuM^4>Nrs z|K@8uROZM7<$Gd^}LqGj%zNAuffap7q0lH@U`E12;y}_t|c(@J+_AQ2Bw(X;)waq zsNUFm6`N=xvd9hdN1WNb-4Vu0gG{Z%De=5i%@E8<@JezD(2dti8!ZVvb-p5%Y_;U^ zCb@j(CQuF8Q#lSj8PWzLuu0_iXb)HPtQYyjwmj1FAfmBidP#bY11|&KLTEqepO>+W zQsYpIQ-e43S?YP(%lV=A9$?S;&w=nfV7-&u^+I2Hn4%9t^hY>*hbKaU=C^ZFb%}=| zyp8i_`Uj<&KGclQP#vcDQyc2kcta?MH@A`+GM6;fZeUR^{IPhwT!wbrbGWtq!{`Nf z9$@dGR}kX&!1hQ^o|lOj(~x%|@Z zr>ma`FMP`ZX1o}90c`*9yd0nAzB4ZBS?4%BAAGc= zTlY~trNUUvS2N*Dta%Nd<;i-BTRyjT%~TFEx6773U+{eJH78*HzgYYTkePG0SA>^u zl6j^8a4UqLW%F~tYsgZYBh68&-YRm=HB^%#>CyxLDSYp*9|Ha@k?$dLkr^UOPf>b> zh;sYJz-f$O=GCk2XPA#b_&v_w-@JP`ywAgV&*m7Hwm+)RDK9DKVWh}fAXB-}sSsOV zdxqQ#lT-bjQ@$}Qcs}^*IWgWu3?BwWl3c8SnEI<)rY`gIVl0}ggLqHy6n8)tqDQ-;r>JC+S*`*X9# zyQU3J5Z8ncv3dF?e&QLaC`nUIj(xZe%MDdYn4aQWd7tGsp;(F@6Ea)5xS=E$Tq?(@ zuPvULx|Z_kx}}(!e@XxNw}-jk<*x@G2W<3S!I2?w^E=HlcZIwU&GoIs@G6LJ;y>O` z9-vs8g(qL`c?q6{nL8}=#>(61nli5)9Vu9wSeNe?kI$I10r7I+2H;-+8~PlBAPk{A z??RM8e|$7GhE4KF53wWo4m*AE5*^|1&DzLSo8y3dy0 zQe$rEJ5^to&DC}DOv|BVkQ37yr>4A_m!5o*N6N9lf9+@X5yEpJ`~ZPRLs&#fK@6K5 zlOR;n8!Myr&b7u2iYJP4g}=PKoxtn(l^^^F?|PlpXYJ{v)^AhZ@TPRA@w^RB@_Hl< zxivTBUyYaQbAuODnT3bob9HDu zH8rgvY+uK?1$Z^H@SpjqwUTeJ(cbVBIxEE+GTyaOge99#uRF0pjE?}mi@>)L;^Mcf zd&aD>#m-58%VU){*M_Ut+C%5`+1!@nDI#wmgje#MgGaGb9yyv$hj~0b?j@L0N>X}q;eI082I^5AK`+Fev7c@k-)>2 zYf3GU-dYMkycKvAn9{F(FHBy%ISkk3A>e8fSd!)QmG{4l$2U zWAUvu4+GYh`Pxq0yZozt|K_l(v0RPO)BXo6bHNf%$1fe=TEE(pEj5;T+(dRdqo&Q2YyaB?x>xBT~*>Q+3;$%20P?Wdpj@&4o8GfsSVojvDo z5aR~3^;4WWeu}+cw6*=kq+{Bwp;8^w^8HeTyH4&S#;*as7vi@NH~{$GoaHT=?-9Jm?ySw8o7Z{+otNzc7TclDF*hyGzN z7aX{fz_k#+61aF<*5eRwCeHsGTcb-d)d!gE zc@(qoOyC*gC=J(P*%~OB zV>vuao)Q^Xh8IKy9d=6cTzDlR85jSPO9@;<;4u&%M+grFW~S%u9SgOeAc+YcwqK)B#!`pb zlD{2e@p?_e-If)W`#FC*WV@B1I{z^i9muzw$6&qE0DNx)+X;p&AE59@6WX}sF9?Z2&O7?$N> z`$3OK)D4{*(T-@N$1mPB?a0-62k{idcS8Jt>UYh0E1BaZcqMkB1g|Mu6&_5~=aP_F z(imeR+tBTd#no+0g|&M6(rpui5RWqprwN?m^Pk$})<4_g%mrKg!4JZ*TgqhnKfZg$ z=)vnm_7UR&A_thoO9@;Fd=tdSL0DUQY9dEx$}0alC&IGNozrLQnQEi&@hcqJ zwho`?Zzb^K%+B1xb1v@7wsYh~i@ObTQql%7wQE~6mFHDTwQX4ZV`M%X#&-2>{>bJb zX7M@#&ts7x8hunXSF-Id2~!9s({U@K1otAs!@fF^ex@NW8+7IWUps$-0?WWvuYI zp=RXT(Abix)$N_Y^O?>6kmnxSWZ1Y*?aZvoIhv9tlhF&AOF?Tx-m#Q3WF zY~QbKoPcT)6ct`x&uh~!8mrac;#fI0r>^uIHKyqq%4;u2 zXJB;7Mc+DMw+L~q{*>~#@^UBe{eY@yg=8?ukhsKT}^^FVm39k`)mxRVW9;0s0czg1h;q#ru_!`b^{XF0IfaW)r zQ|qU8{D*AfHEXz}?ieSl1W%t0jMCvu{GE{pl8xt*t6u$@wsmJwyt!np73%8pl!4ZA zl!VR{Jg*6jr3@uxV?W^+XB>LSmCVk&9(XFTIZ$gt$!$In33fwZVKKdLitb#?aM!{? zJ(r=edhX(>YgBprPlw*JUK94lZasZ15%`U5W0)h)<4KjJ zXw3Ze_){`YjW+~0tHU*;(5h^eTgI*Qz7eQ2PiXPx>ZbTT*0B!dP+WThtrrt zcKH4#a(#q&<`Up~h?cAm_5c7N07*naR8L!+*fyu8V#NwaYT>X2aW0|4=f<#zh_k(r zA-2SB>x)n_Du(ux9HIL>DB&beNB*#Eww0q5Pzthr+jzRxbE}@D(C2A&D#~S_7lwBZ z;Vr=HITP*_fTiPXg^YUKYv!O;J{C{oSa>CJOof-iZ*?eNyRpE==FK$L7#wdWNEtfM zkfw5vIm4Oa$%Wn?<&6DsyeIwI8f;xn2-g$ivw-e8Zr5!h^WX9|-eg=ldmY0RmPSc> zg&kwoG^4{<8MDXE+8^3U#FCvYqzw&x~)_yBTZL5>nb8I1)jXLVlqsYA+QQ*JK>`!53)>zq85+D z?S!C47~8aomPd#5DunOUHOLC(+jeBgtghwHz4k~L$R<42jdf&>0H+t9{f@69tXaRD zXBi-G-gwT-lz1ii`qFt_!k5Ead7pDHW_xa_MLEw(Q|)e(N~Y%A`BYmLW~waX+iBc-yj&0QUs)fHiqoC6wYXnHV)x;z@etW9rH> zNb9$*ogdzsafYjj+(6*T?E=-IvHabS&S#b-&-SHk#d~!|*0s`Y?cWAFD*j+#8<^VQ zxI7qgM0MTNQ+@8q9s6aSd)4R8SBJ3$@g}x5zl-Z0a4Yo&r74%>MLH=w&6`{MnlER3 za!yGKZ&iJ+!pr3htPXg3WsN-E)R!fH74t^&GX@&Vl_}mH84LRD=Wo-;~_?*L( zvUM0w5&0t`uTaKtCjJdck$48qiqukTM)PW(+~%LE-;fo_<-+ll0biL8*XUbSOYqWg zwAQt?>S#erjX{ZwHHM{xS5K-GhIidi=}zn6FDZs65aM%)VLw27fY6*Qcf6{m4$Jc9 zq*RBb1JdTqVWK8r>g2%Q6=&S8%99A*2mJp_-G zsWr4)gzygFwGci`SsFxAZey&)(->3ZX_^9Wtc+E7d&G4!;>{wr_C#r@bk-(y4bBoV zc=Y{T{B7ir_mW4;X5nbc)lJDfCRDdDn}2iCGTZdPZ-DRn#C~FUGa)>vi!K&^kYzR3 zO4`4rdtKhUwn)hh>OP1vr%q3Y8Z#Uxgtsu?`uBY1w!h)u zKj;k0HKgZnC(*b~*4XPZi6_?v^w(c09^05h@HAO?sXkkj>hu5K-t~oAdR_J3KDiF# zI8Me8MN5b!2GI&J2<9PzRP@1rD1tsHK307(303o8t(YoB1gj6C)(1h6f<@354WSgQ z7;7o9EgG<Sm9n;EYLT?x%hsZ2lioRDx49~+GIcDD9J!Tr* zq%#FiS=JayPlT*??W>26RQO;-ib-Qo=+X;Yo;QVXd&S?VHKi;Aph|zw2XjnEY zN9k;ko}jkSQlt>5^-F?$6zGdrHqNr7f9gPVky#xq5d3DQh#&6erB zUsTH4`G2mg?3xU5C2DGNYuPg?@u%q)fKLGUW0b|m@Z`1n(+~VyUc0x}LjmnGrU~nVWQRO!%Zne_Xyuv!rLe>?U(SV&D2&1Hgde^m_0m>u{9rBB{QeY`GrQKOMob~l*K)A8b-9#j{16? z?-L&YUi+t)arVRZcc^AFgl|({H-g4O>hud+5?y%2US3!|m3n1QL|z{ZEkEgqhot{I?Z$^tvh zc4_u{_Ob-X`QRu8jkAE;I)id!i%oYS$$X^sz?tK zBGEMxq$ou&rGr|oL&M>1{5_Y;?r{8&*0V?BwRCI!nqM~gC%gjSgP3pw1wRSklKkL= zyZ`+Q8_C+%0?eIkYn5o#>#|1&I1yN3UL16ZLY|Wl?KUAs#fn= zkgod9v)xkw&jR>UO!y@5l{fL!{nCA*i!pj1shpN%NOQwVN-R8H6-uPxRhbcE9*x)K z!vc9x&zUBBj>*vIv|aPTDBoB&H>xMD^NAZ*aB}560Dc+;?*edjqx1zfldskH`9Ut9 zSJwuQTAEIqvg&UmBD&MwVRs;uPC!QM)_gz zNO&H<)!)KD$C!8G!$KH)7GP^&$_4;Sr?pc?vscR7i|65U9WCSqSl~UcUc%Qt@F;+v zK$(6FC=a3F(&qUdy;jcLXg}=}gB4^R^_)-8y}@Ih1!_Utu*t>5DS$6xfoCz{udrNv z8c$!Tfu1-%C0(++7o;&?s5 zuqh*_dWaq%E^l;n1Jb>s6X(o zB1qYmk&(F%N!aTo$gX9tQZ`o~dQ{!4?q6k-OqODD9%}1)K)eOuDt8{H-oUJG0TZqO(?h`Y6o4PV*^kqoJ&jhgH5R-85>O^= z1|TC_8}ij*3cr~7Njxk#p6gkn;0C7Yb13)#O8FdK_{dxM%U?J}ZoXJqOi!t3@o8-c z+Q{2`@ilED7_dLQZ1k-KS#0t}N@L9~_Xq9Cm{z&Pv_vUypiIvL(?8(t z+t1;DUwIk7``y6i#tw`*<`|7vuSZ}~>=Aee<3l=?GzC42JY+FuEH{ zvKh+m$US&xn2-M40*}4_Dy}~82nv1#!1vG z7QmEJcY}x9%I)8fT>^Lw1)l-%cUa&#yuEw_&wuzf{`Tq3F&yc_6W^n~a**x2HtmBa2e6r?0aOIaSUK28abk{7&tN)<2b)a=btzMOyW=>qrQI#3<} zrpJNuJpdj@nI1Tkofd1~q+Ln-0eG!XD}}{inUIs)Pf>6az{|k&0t)^S6JA6quj0kO zehVM@+4}Ex(mL=;Gilyr=34}h%R*@L&i5=lYs?;=#DB0pv`S{ln2|K%Y*5QWt#&9p z8g2wLVp~*?CY#w|BV*O^zV2b*TYh|jcmK`_P_Cok0Zh|l0KOjuj{tZO1()=v&k!;` z+Q+U-HHYxi@|nv}o{clbRr!1iWqKXJ=K=gPP(B4ruK~-~apQk(;6FaT#3Mhy#4{`9 z*5>B5)plu4WMnlwPFl6(wQ&s1Mk}C5ZP_vJtq%)h_M)%1$gM@44S>|xto#Vd2DDvZ zj=+lrt}?WPoJ%TTbm1(0B!=)n*hED zOfR97*8uz{3OpOlgLIC$#lGpO~SMSK_pmY@iaOZA8IAD z6(mPzNfAB0G$-CD?8wC0CFc3Pe&5?CI9*=F68B?)Zv)D=qTrhWyyNW55!V6S2jD8o zbQLI<0Gyy;VScFF`^v2{odUQ8;4J`epiEx^@H$HQ0;cID6uf+v1uot~`Knv^_0=zA zwA>>r!^-%TW-eCnisDi7Y}|J|-loykd>DPo+@E2@jq?I)OI8L!z87WgFo z8QR2q*KYtn`u-D?atQ^OaP}RHmoZIOQKsv_bPd2aqLgn3a6f=+DAP3nR{>l`nJ%N? zGA3L?DJMb}c!~wy#Dq6c@CM5CDyH%U0RMFs45u%n;4Lf`H?dsa#)R8==BxQ4q!gXU zbi5Xg#n@?rq~md7>262gK&Y zXuK$8=IpMHR}i%ELi%t){_6WB>kEUi_PHpsu&sXjNAZ@7FN4|BFHvOI%uFVacQoGU zjY8U_B_8X&M45XN8FRJ_^l0=t%Hn0lJQ{B&K3uSCO<0@YQbhK!B9psfN#_O(e z(#f)izaM$ixXnF_LBi{$ZQ3aLx`oHn-$RzXHE&z+GCXtP%|UzTL#<@ifaWLSvyQCX+d|JK`N{37(kD7q`QP@cF5CM z>5Qj0Ff7<>7B_pYAf0z*$Do~)X6%TNhm}1u+csV=f0g0>&76lyW3uqPd+D`}#*;8D zS&5E5?C>GY))tVnH~th>Q?QNTpaoP7!wP1tz9Ii&$zf;2FKczr9j;>W(lvpKl(6)Q*NCUr5>;GHr?;C)INDAUAuWK?YiYM>!LN8)UkhF@dBJ`g`ZS{Rg15KO9C*@v zYu!Lb<5k(C50iMKep97|qL{s9TaWH-k#;81I(BKJkT#H38~Mi8i$~wLU|IORcq4qb z4ZM0i0+V8oz&jWpTHlzhMcZAH6v(vfPLuQawczC0A<1Saa(2^8JsaFM>^sttc1qHs zm(CV?Quj(cxMs^+USfl^Np^A`N>bq0cALm~#0ati?8x%970ViP%Q1^bqtCEM+n1%$ zHtsv#95nlwGhhETnzEUNR$@gno*tf2_)+;0{_oB7`FKX-@wUi0vczjuHj#5_N!F^A zv!=xXHWfKQRU|H)R*Ol;v^KoUUbBnhlcEwE$6ZqGffIr#CQ1 zCbfknZ8X|-1k($A5>7ND*A}<7kfe<#TM1Z;*uz`0K5din@TB(^o<&;l(l+V~=|f&_ zE`>HpQA_HY4YV4+)XoBA$iW za^I~$nZct0+rr~*5$egNJUj{W=))YI`;-~59YJLz=V|>}gL@XBl`To*ZNc-hrwF#U z@nrF`ycw)H$J{cpC0Xq{VoQDR(P%sib2OfW--~zfKGY^7XV%JJ9_+OCo<+&ABX)6H zq&8|}X6#xSs9;u)AaXCqsgqJ0*qX%!4O&(^ijnVlN z$r`f(xTO@c^tbXH3f>%8dpBn5cbRFMlr4H0SvHVaxLP@6>5wwp$h9?wET-lojsqeG zp`G?yIy@PvPqJ?$o;FrVCW6WDm6f?<@Bke@gGXh0@F-bhjs$_D4}(6`O6IvJ6C{sx zBuYJVM+^A-j5mUKu-f-A@7#ylk4!-Uk7R}(jlhWFxAgb&AL-(emErCa=04Lw_|Qsy zB}EJ2I%|-G5uxbWRV!u$Zx<7Iw9b9Ti=HWN8*h&LzGSbXbnYwOLHJPn`Y#GLGMkHF z@bidPoDni3Fr#|C3_W^Yr%HgY@(-@>zav~#Wzc=i2= zF&}-n-X~!$fU&=+auG^PYYd~tw{JLe;f^>`Y2nk3WJcMO!O_mi&}d^6ufA_RKC(8; z3+=<`j_tKm>%^R6YD!&Ym7c-Ne&eG*UWm?@q%-@L_R*)oD6 z6;7tTWz5^etC%BcXFjz4msyLXt$~l0a9Vnt!ETX>V2;c#4vj{Sb99zCBID88GxOcs z+_Y?JG@eCAAI9+3Xd%3MySmmF$!Mfkew`s{@DW=ouOy?DAX&2H=E_1dq-Drff0lah zE4QHs&l_(RPr|J2yTapZN?HrOJ{*a+cRrMYlC=wDlu7}VUu&6S?bm`Zmz8X>WHsBR zaZtQWc^9Q9P7B`%8?F10#mV6HY$#g4)oyJD*>QV#E#%=v69Ee&(=YKKybrB!%+>%Wfw5qo-`Q?qv&Ea$2^&Ey^5`?21O)xbJv#(ClO8|I5r7`9Wk$Ru_FA#nbHJ z*HG|1`3SycV??hN(B@B6mg{N3v+VYcDLh%;#xagQ zWU>q4k1Ev^S>TcPT!)7jDgSDzYAt-}ddb>pn-nx?yJRcxkF;|g63;^5k^Hm}__f8* zif7pI?qnmBU<_P9!JPUI)o`m0vckn*s(UeBorE)DP@XFHYP4Th>Z2@UE zgSFQ`4MUUVws1U+m)h>poP1t}AH^GOqou>c&@v%2U?Rb9>9RJB(U{Ub zvvf)0wCu8|WtE*O3fmqNQGZ?*CJ=53BqJjurFTGDLH$XYgL^k?ucln+P# zrpjoFl)j_O<3lR9M;|xLg_o5<(2mHlP6jWkCxavL^7FIIIan00zCZdfiKms9Em=<_ za%Y?w;f>gz9gDWJ^k#%wmXo!+(&)2S@9fySmhe0tO@?1f`n(50{QbyG)}j_0t#&$Z z_7SgQj-;LWkd~Cur#~7+WLg)01TWJE=uhUP1r*tRvH@Je>pgm4RAj<86?m@vJdOc#($}86R@I3+qGg>%XK7tR)4Py$^;+6BczdL@qdSnd3n9?ok{xHxT(a01%vOqKYn)%AR(qlgMVh}E z^6*-4J-ikhd-^5*gZE((7&T)?XGj^$C~t&sv|b*Bk-(=IRF0MbJ$O}?3^9kBc({d$Oe`SUqnYVYti_mtV+7}?6dSnFeDpCoh#9e&hyO( z`&w+YaFD=?E}jTxgfF`>WbBJz&VlFg?Gmr@4;{0V5n2Ei;6k}pl?VAWer|8;nUiF9 z)wK|KB+Q7@DPH6Zh~EnhQzI(RuPqy+_C)NXnBJH*Ea@Iga(s*$EwPCy$x7qX#vpB` zk}VlLY9HUQGkDxa3C|j{hgUH#gb(=#v#snYTkb|PHceLJmvr}JA=zvxk{xB*rLji+ z9c|wR9t7D;JD+d#S_*{TyybkB?>XTTn5W2L(R&%*545VwWL+p?IW@g&TB$K!2T%;7$? zN|q&QtJhk(8X?Wl1-s_&xefe1w@0##r_Pi-S&2pm!-`q|q;a+2)z`W3Mi3vfL{gu| zADN>W`zc;_+}d2EYb+yc;c5L@yo%X^r_sUqkWTXYstHiANJi+D;!&cuSpn0-_8h1M ze9x(o@w5hzNEs55=dUeTo;@v?+I-=9B)t~T$ZCM%Wp#KwBk`oQ(Tg{d4#J1ji9EPO z9NaPqTQUqQNViM`irI4Gy#S-}XYnQKvAacI3;!HP6yBV%%ClC?UE)>Dk<^-uSa=u8 zhu;4(OKtVqD;pz=qVh=R0?`uP3m#rJn>`Qg!IZAGu}kFL2Tw-o<930xjlh$}smVkz ztt!RB@$dj0KZ8eQdhjTDm6=CJ9|nDRLYS89)l$SWGD#+rzK+U1HH>I!F9ii_Tu~fO z6xZ{0v@bFVGI%XEWOaBOVFX8e?`?FMd&k#@#@B+U*_XkL%-a^cxyHP=K9oL(=o#8W zqf4nnwc$v4>bY-P2E5OBbI`tzdFMW)(UqsD(b{`-&9CuMn|f?Jv|zaFbEIwJX;`h3 zUEAP$7f&=pkC1ib)QXqU6UDnLe8|f-ejeZj#_U-}l%$&wDM`8Q^_Blg{876sJk6FU zwnUM!W~lXkZNolo{_%E=vOL~g@Y=F-D{7y#@n`(0t>IQ5T6h=Ahgv;P17uH~&na_5 zq`bMbv;Y7DWJyFpRAA#RX6amj{&Aj`b1;q-S*+Pj#+nGyq>1oHb!Pds{w%+?7_4d` zVuR#2DewX@KT0HJPc3-VKCepRGLk+EFXKb${irdI#_RGS|6OM9fKqGEP;|GC&MfZLx3|S6ONQc)%EF|Q zek9%q%#m2q{TXq;dXGISWwKMPPs`5c!m}`2*Xz-TtdYO+2+IwNWFb7dOS=~Bl8um8!g$oeXUTY#fwntms1=Xz_#7{j{Y3G!YD2@*=8LpZd-i&G z)K(3zWy}`cl|J--Q$@-gCDO9Jmdww>9>E*gb+UMw9Wsj3<3sJfMepa__Ek9uI3wna zgx`Y`&Cqs%XXiQ4>owBFWoQdS@21wooIsj|zx$!^NA*T9 z=fLy$E}RdewRIHG;+5XF;4N62bgseM=zHGI7xogeqpdTMC1g7|S?4zrzUQCJB&64x zT;>dj<7;@)1)%wkPiP+4rE5&}Fa6D?GHQ%Cm{C-8}j6@!uHD(X5VqORz zN>7<7X&@u0CEJvOp_YY80ay!$oL}OzCR;Y!ie#RA9GM-~8*2nl%1|wzX?i3Zy^L4# zb?>;nOd(sncx%PNi_EVGp7h?rv#8aF(J@;(sr*iT82yncsb3;4pOM`#j_J?HkD%Vg zy$#F}c-lQ{^|v;MB7snQPuo4b%)HHRGM=2bfwbb)J_}F6Jo+%{L+N*!t;wnNwPZ;a z@95w+C*COR$i&(urgyJLV)g`M2(m4BR)&@FE6q}RvMGr?8}}WLw`sIBA4Z=tZ!5!y z1lE?U41#bFcbDSs?poa4y|}x(TX8J~iWH}~Lvblq+`YIveEWX;?Ckv7+?!02 zdvcPAQBjgXMj$`{003D|R#FXoPyX+Mg8}~@O7FCSZ}85tx^4h~GWg#GkEbHXd0l%93CDXxRN}MT)WEc zoO>NI%O8T3N?3$~E5V{-l&O+eQWBoJlRRi^ZoiW?=t(4#!8D%1q?=lr-qf)&EI8E` zbUJmOy}Z2e#mjQM+h3$`J)7?)*Uf6`_240AlTQM`0}6C~zb$pQMQX2}H<)prTefk! zsajW@O}Qr=ewAi&4aU_;30MeSt3SWFYR?XS;A?wGm*-fdy?0(&&GJu7}WB64@z8y4Y1SY3IjmGZh(L9W9+6jUY#HpvN+#;;2`<|J3gI9V@p^kMoA4 zu`Afkl=7A22Jhbl6)sFp&G@_KE|Ov+n?q9u4$DC1Z&X z0ty(aI&dxoy>t`=4U7!-x>^czO(P>CvL#(zLQ{BELjzi4kbz3J%sQ0MDKaE|`Cc3N zvz!hon-|6m;W|Tj`UocYvq*CgMb)1t6F_({&IrA1g zT5RLmE)I#2Ei)V+ma0OtRjMClq|n66#iOysqjO`9K9}H^A={4b+}Jd(Yys{-nk>g( zHh=}n;^?E>7VwK$kxg? z$L$?)fHko|(8tw%p;B|yEx;_@q`R^XKYO@=krdql${8}a3c?_^3E54U3~37+=m1_{ z_V)Hd4i2ocQw}10=_7`$iW<>P??bgaq^2bnJv22JydWOYf*AmJ^6DHxF-bIWi_HB$ z8m^~EoczHMfFB^}yEl?ztyOCjVC2(&qYB#hD6-9okIq1w`2`3gZ4pfL+|*zZ^(q0I zxZ_MG-3SN;7UL)2qkKxjSz2ZyGJql1rRb1urzQm&$ zf<}R4l8I(!r>s2PxW2xAqxNeu0$D@VFvAiWaL1gXfp~>SuNUWzTy4%`(6_8PDcsiM z4JIXwtk+z;3-j>sxEZrc+ZAE&G-Aw%ZOA%oEPP|6NRj0T_KQA>0^YHJMuC-+vepOe zc9-4_a-=%qd;adfep&4*Du ze`9NhzXpCJ#Mf5?dJN?)U;ea-u3@n9nnd5GN{Cu1N(U#w2}@sckd>p&BRB@8vkmcm zSkR~Y`c4-JUA6SQokbi~lE~&E!b9zKTq6O>rD_S4mG5-u0WiJA?CUGBHjEJQ$3$W9 zh;J^45^{-sB@r`N!<@L$`a~}By%V%%5D*liYtbhv}Cq{BUCnx6xVog!fTJt&!#GAjjBw)Iw zO-a{@&gCL~n8nIVQ6`#-D0hMwTyl1*21wQ^@==f#p?%R550kdJXZwj%;cYDse3bS@ z)dYh+bgih#2l0ZPl9G~~9PR7`7(#|W+vc-U?~sj*YX+HaJ+MaS~7GeX7# z9VO^JE>~cY94nfPr$80(X7`V_LQ4qGb>^Py;jA2<)4`gnW(;BU;^*&*P^&%u)&a3d zxyR!ZK}7ud$xiDwAtBk682u&YXyI(NMHv+ZWy(lewxWs$eHw=de-ixQIV zJhMJSZjtX<5!EuO-hmp!^>{Lad$S2lMX776(6%MMr*F+)DaL9M`#-e>CyO5;(6-i$ zCiQfdqg081ky3VqkA^4j7Gh<+%r2GBY;Pp9oRE_?Yuf^rnjKc1=M;Gs>~cI=&;x2> z?*x*Yg1by1H&5iZV?waZGW;nGk+yZIV@fOMihuzK2xxC*c{DGUn%_Yc58tg&q*Bp# z9w@kP^w8n&gCy z^I$tK(>27pY}%vhTm9us4LSQQx@?|YN)Ez##`Z543fEr|U(%@(%O!lViFbSdt;2ZV zH8eDYSNTdW(HufAO%lty8~4Ev4EmD|xms^$i^$IaA(ts~x)9lOr~lONm)p z@>t@TQ`Jw5v2Q=6-#?NoW|7_1Od1qC_x!X<>qfrZrK)b?q%k~IftAABlNVF<|q8vhwAGy zWw|Wc43!$>IAVD%zy$mJ{QPHqz3a2)LM_@%ZdLYgYv(PRVI=?e1*kwFi%>+_FSgAX zzjt}DcxxC5v=v4(L{SJ{TGvq2OOiUeKf$GIY9m+%l2holoBfUzqERj&TI~tysd9_` zQXI3ZPz9d5 z6%IJGtT&6zH93JFml``%u=X(1TBAUSHH@N;YbbzdoNrbc#k{}_AmVvS*(VTUKz;@`iHmhtO) zGuakoKqu#y?ONl{TRfCj_hO!v(4%wL2!NHQ6dk=4cb{}^!#^lO+(*1XHy=VRsOc$x z+`OA%v-+h%k(&t)^Og-?1{G7XnYa+E+iq%VYVlvceidqA?Q@ii6I241je(GbK6EkW z=;xgk_si{_xYN12mQi0N)%RL;XTF}pvx7D?sZaiy8A1QD$lOx9ZOO*;NJ!`tGJ;2qy=AzX?b{63Z+;fEEqzXV7W=@M#Jk zJ1G{!xMPQmWvh(&h{*6ep;M=-rsiFmWYd_3S;4p&Cs1cS-`~~Ya8Qpt8Lx*%g+J}g z+<3CL5~-nFMrPNFaCoIljn;P8$-QQ2g!FGg>%Q`4#X$`|>8I`B9aEr9x=yiWXtY*E zOZQTNy$AK)jCaB|{ZI@N;CJ;Kcj4}2(YTg&40atmgMEIe%ju##qQ{#pW}Q+i z5}3-R0^l-M5058{q7Nw-(`Q!LcSlndT33>%VQaY8SNjc7+iYe%FyQRO2}le#s#e>vZm}jmrCB9JUkaOOc5QxsFtl&$PSRboE$r>IA-wPE?v4NbEu8Ku*#E{N#i@#8 zZw%OzL-ku1qa;pSjKoF8F1s@O*@^fybIO;1t6zI@es>-5^^oyx0hywm9uLW=$8sDe zo3wKxv(rHUg~}_8(Fd*usvgyU3v&D|@Z%8Qyo$j`F{J^j`>j0$HprUHl!k0okzcS> za8s`sz0IK(i2v@8P2FO_n4i&3JL2q;5Ni1gf!S8e#WpRnh0|7NvE0m~E4BtrBDWBH z_M@rcK>=Z_F%Rc%$(k0)A{>6nbw*;O-$d zJcao%j{f!gZB%C2*6{ti!~&-Sruiv*5$8)8+T@06;>S%+OvP8(O`DK4fo#5AxpAgp8uGPm-5)WgSp) zTqomzv?xnEwwI=~O(WtQx5Hj*pO5lloUdz85=%=$Q=?N3w6yz5C6(~GTov0r0?S~% znA{e^G;G&qV$f*-hJ;XIXMt`2-xaM_iab)L{lvu~j#QZ5Ac*dSMF#nG?%1{yXO0}O z|2cPAFZF9l(s{6hTHUmvHbueJth9`4H<9ws!)N9r%Rgg*URad zj?eOda#G19OPJiPQZ8IYxLr=bqZ2Nyi)vki^lnD4x;4h*2%+hq;dj}8oRNjyZO}pU zSU{C17Xmq2(y++oP2tC@wSvIx{S^To58ERowu#d9O0>%j6p@XE1DW}~ zOAc|_Z?S1dm0@29*H!A%F7-g7Yf#O1Io{@Zhn@6= z4Tql`VDYw_ONIR}atQq0f1w(3xs405zarV(mBNy1QLaB3OJh8~&cMN7sO3#y8~bv7 z*&5}5KUy1SVkC~QAOCNW=3hQjZYeY=2vTIkL${I0gkQiR*b!5XdC48`fW=Y#kk@?q zT>>Yy%s+g5BgKKxOW}7w^c&R}jgqJ~(*R^9^yT{}f>$@8NGlZd7N`O|xvlR})F};u z_7bCF(Yl~b!r1Kfa{VP@a2iMm$CqZG;ZCYsJ@TS@{-eXmAe`08X z@W8urI0QYdnVDyl;aB+RflRxAoNt!LIiAvg~ z!gaCzQ!pv&Fk7)c^a&GmY_a=`e>azYi(`76(E6kJF#!?KY`+}JP}`|?_vFumu4-P9 z9qyR(6qG7#RgyAVZuOVQvu|7sHORWU6pjGRHEEaiolw=*PP10ZuneMxDI3+Cg{P z_qM+LdIc;B4)cqP-#a?|s*7n2_dOH)LJnbhVJP~4qt&2{VC^5y!r>zCk0oyyJpd4C2I}FrBCi>!2o;=TOZICJT*yvqmj3iQ zKnitP5T{vwv-jJA4-N2CK6+dVq03Xc1_e;$SqiF8(bZm*fo(}DZ$hP+%``tdd%Xcp zKD}Y6!nD@5Sm~m=R{u^18ena z105{FA3fQb;3LXSFr@Q^=@6H1KKrdsg?cSS(Bs~9uvfH!9cGsEY<6mDYGrClsaL>{ zOBrO@CO^m7)Y9U`Hq`L13Tt;+vBNx=G7lh{p!CzM8+WZ*w#=|SZ7_hXaniiJaztKv zs%mTC&EHuPu`J;sQlMmkI20gi)##o}6wGh9+_9)EzA=mU_yrbdo0Gvb_FXJQv=~hs zjIwm6@x~*m_}AzaFbnY)k=&l`B7629zO22i?Nvo(JJD>XaHGdA zY#CIdk_EyZ%?P^et(vv-Os`mDw`LzQE`tgSa105=fe^Ij%P}b7apBK79I==(Z_-AwZGd_u|9{S zg2^RcdK_vBfe*_h*06~GMF$qtm0!r0Ty9Gcj#eFC-*EWIM=Y`O;u^PYz{2Z5k}Hi> zgf1$de3@8-IN+RN3VT&!d-Y8$T{G>}v~3|PlizJQOOp4y8`6)Ty092$9z!}+XvJK+ zG3hw68k%5!g&#|*f~7?rrVOeq74&4bD4}_UVFMVx@uOyP>u zTbKD1GWc@dk))fkKgU@;V5nLVBAEvHrqeqlB8~<>k~DE)x&T+(!R{ zr{E%SZ}t3Gg5TzQKYyNEqub|kL&K=!Fdk>;GjS@O)G^9;U39-#p-?@)1p+GAQlsWY zYLGGT5+;HLZ68MTNX~4SZ8jyQ6WMQyfsy+O;fN}i7JanULphvYg{|>>;{PV<^8?`` zfT>wObj&h{92k6m>fG)3}{e!;MC(35g3+mkuL}(^mX|7<_Ym^&HhitAaw8S2vL|~*@D<_EVuwE7`K$l2*~`Od7LIfdEbAND!@@}> z=JQy87?MRLH5kG#@gpH7uw>^whye|XF+xF|N~l>TvJAz|x*%pQXl>m-6Z7Xq0;>6E zgciS=u#ohcRhxM@h*g-e@%eC>p!SlqC@&lizFP!)vLPL~;K~!jRjcTHF!hOdeZ1U06+8bNo8BSj%E_l4`}*Uz zrV&|RcYg}~AvO^{Uc;cvZMvV3#OZc^iokTIf_K2#Fmb_9rDhdbIB@7c^yo!?U>dGa zqjbRL8fVXeD z`%pUB_4})3+o3+fq`{)ouEJ*%y~Hhn=TWP#6Tfh;?a$0Pknk*i7Rw28boP3kt?W<5 zO=XHEBqWIDgzW5dm$QcyQTrh{`)W4O-kaei{rueu5l`dlU=V%L513GmvN_OfFQ*dR ztI49K8!}dK$S5!!=10W(7k7SbLBu!O0uu{4x}Z`uIyiELInG8Tjidcid)e_zy00!B zhC7SfWwZJr{xoHQbTRjalW4HL0+KW8*4tg)){jcGOM+Q)KKq`iVon2;7@MOUYKC zZbaHkL>#&NDoX!n*<{!;7B93-58=<=zr+#jj zM6?mi(Sm$v04fbKYB;&zs~b!nQ&PnWp1kX=B;#<{E|Lq`u+IJT_V11XvT**0I@%(Q zy+Im`7`CalY#-bMQ4&a@kJlRmI8%a1mle)Qi+c@0DwX=dno`B^GGO0&0b+{g$B`e= z@o>l{itDYSiS+-)FNv6L|0y|kc}xB5mbv<7hw9-u!)c2&F1{aig*+e&mZb-08hDet zIk9>{w}dpnsyK&47@3m)?MrjW;BVyKP=em4=+-WVy$bs2U^8ii1O>4!8oJ?#R$}ooJf=z+tAakCI#roK%YtSNxN{_iEoL{{%=Wt zJLEdwkW($IHQ%CW{+lDJ3ixqQ;4}YpHy-1iL8TbS;$0FoU!c#}NalpklWL90Hd5aZ zHW$Lmnj7LYsJ%wU-H;R!Caj>b)w`Sb?D%-QUdLWz@ow8J9T)S8p3+_qCDUu`HyJZ3 z=<*cK@6AgBG<|=#YINvEHnY|H5l9&^ij|26y-~TDcp*yfcALhr^mq^b9&5!s;}O7% z6PIv*d+V*#zfBjiva-S|BqRiO5E;Hh%aizV0eIn6OiYl{f<~~*XE^TnN3O)L9;V)A zATSq-shmH5gD+|Ek=IYmE9=xdkJRY!hP~P1@-8@$vEKL~C94x;MZJ z_7Zo)q`}=Qi1CKoWf%+xcPy!!(zkcihA7%ANOtq%$~(cjz=kJjNO z61LB=vS<#GBn-rEHKz>O@}<#YjjL%g`95^?bMi@W@iA$Rx_dKMd@Ni(8BRM$mLwxg zdLtu{%)f{u*aVtmfglncK58H;0?d-jkq>>J&Ae~h9G zqIlbH^8g>>-3mRTZEdL6O zr!(0?347u*@c$ZrLX+j>yHt+yI2{WtovH3#6Um0T3Jzt;}$Uj1h#ZY-Ei2fn(W(9Z12=pF~;IoiL_x^Gb;tN5pb z7Vb`qVuHMxBI-*EA8ED8 zCEdnZ0ZYiB_7J207cUGag;pzbXey7hZU(x&wvXmF{ zL8V75TF~g_6EE!G7P~RJc-{(F{>PDw$Ukbj^PyGC{QUg2hM%&W1OZ$)aS;q`^qJg{ zEkDQO9i}~l*L)iIzyDl$e(_?r*UQ+%KUDu5)faL>GJEwTL`Rg=I-g8gDHz5NNkxq@ z!{;Oq~d?1b*EDzjPugb7g^z`&>9}XQI?+5$&%kyFn3dLt>ij**Q zb#=(|qFaR!NuKj|@uDh-wjYS1HRkpQa$ZX2XpjFAkS>h!(*zGG%iKelVBPHl`C=&3 z@T-={BW@srZN1Amp89Ssr*NR*y=vjWb#kc%VY@BwVOCBKL9`?VYz*zwU2PEYWdS9B z$Bw0`8}RygwTp+3zg?kG>1APIp-i9dx+gcSm2TjPzOB3gdel6Yb1P>Ch16B1UmjH> z@w8_h`j1J*=k{_yQoo4h!Uh_x`u``=_5i3E<=#R&rx^< zBz$xJHi1x$lphDM(<_)D$Q*$HOi$61C~OhJx(f!Z^{2=smb^X5iDlclw;e8f zN+r73Hx82!D5;q?lgPfz#&u{v*MF;EKTeBzBT^_;i$WTpwbf9PV>;jL+3bA19#6V^ zcyKaqcR&6$IXQXfD5pVBNr%)Lh-|uO3@7+3u$A7&*=W#+^tdH0F0(%2fhD1U`|J>pLR`2zJWyF*!Jwl6TSuzmyt1kCDK5$Tj6_CM{R-_H%hOXzxO-*$fGon(F* zX=1+Ig#@9#!pijLcjq48V5Wiejo7cc-ola3!nv5jHJQS(6~lkug&zQa@W`MRZXdae z-Gx186*$JT!+R@eWiG-+m9ut#u_YGB6})$6zQVg^CMBowJ2UN9;h>DB-u18T?tB_; zDl6EYvkSxKNV3%RcXqCea_gREjb8$9Ri0nz{iw#gMl$;-lF-pa94-4^= zJUBS$#GkN2jDR3_IB0qPO;%M{NP_jagOWJsNl_#5de|HRl%@6t6>NFC0C44^5ypBN|}ro|FW{d!^ek@PfWZnB`Fhu_KeK47MyA1J4&5qUEwwI zuXCA?-TCXf*Jc0^>=g8}Y6|p_5D|6$tf@KwQmZRi8KQQI@na7n@HwaVigVwbT$C6=jXD9NFt2{*8Y8m5wdg?4o_1Rb@u-uY*!ndr!m?rwZZB0FFx=DYrOz9(mLa_aF zI1V1%77Gz$A>c{dV=H99f$IOngurc#A(|%Qwc2?uR?(&p9s-D>NEwL(hJ;_YQTzCY$nX*=A&}qa^SbPe_Vx9#9(LNlST-lGX8(omerf2X2|~$BB%|@T4t0XEw)&2X zoNR?dO-wn5mWQ-2MVZ;wGP>*;LRNXhTVWUzQma?AL#Q3)tQ!F#(IEm03p+rgl;^Dm zSI2~;Z6J=5&{f{wF8vM`{Qm_R@NT~B0^HqkGrj{ExK|3j=88e zcah78!{8L!Efr)(+?77wR5k`ozY311-;7oGau|G$MY+;767)BfGQuL;!qhDr=w{wq z*JL%0+T#dbt)@BD;B26rkd*X6&^t3aD)$ycQ;_8$gfIrzBM9!>E;ZSv)!42`auPV$ z+t}!+YHQ;{T{C@{wYOohh()6HCJJwbCAmtbU6VX0PBY&8yIZ=qU)7YZAqJz?C@pP^|CnE9DPngi3PNU>N7mXdO(~ zMRdK4j>?Ss)D4z)1lay}|A2RAmv@=_F)8}=9WZ3C1>@>7KuHK}M9a;N;wRCXLP+D6 zmBq&z!;T%m2%nJf#nRY#^U|QIVTRV(9Z5-G=o?CjM-+jZkHC3nZYSaC^;84T{CWm# z*ulc?#$oByny3EruJ0nV+SQ~Tr1xsE{{bD(B6#%e_U;ZjDIwtv)XR`z`P=Js3BsFj zn)`aqi%{w&B=!^*xCgh{T__0l^FH*!E2=|6LYgm_6c-!iuQH@&CI&cv4ilV~sbrDY z?-fWBc~3pm^pPzHy~8RdAFkbyd90H6U%Ia$t6?DX8JoPr$HT)rIXUr*b$fS`W^+@Sikjl8+(;E;=zODJl7byTH#?x%>V3BucGfOxW8P zj-1qa`q#(=!0-lm`)ST9x@B|uoH%w2sU{gx%f)A}&}-()S$`Ci(!WnriwM_GMJ3;Q zHm#9;GsyT{dii`(OPZ6F<&Dw1k&=S`HYIQY6$=Ib39CdkS^bi|RbvYAkA-cE)27me z-{wmyAZ=E!^Lg=(OHok~Ij2mOtBVybWJ+(m9E9%NAH=k|8%en(R8bsU)oLBAQ zWgCOP_q5utfANqdpOl@PF1C7md#9PkpXm<$FXN0~i@CVDtYWp5&t0np2{MJHfa^KZDuuU$UfY;^(%C zdPkx{fW$|2>*>2_5bsSFJCI1lGG;Ha0dq|yuW6Ha>mh6+N4f2Q%jUm>fnAlelsCm{ zh6j^KDnhahe#DsK(HCx059=<&#MlD7jvDXMQ;_vKj7G-BK`L-p(_LL%;69`sbN8c} zvWFI(J&eIDl%PB1A2%59Z>URwc#rF3&Q|<~RF=qwt{Q-Kzjmak6q(|Z@?tWi#Lw5hjTL}b2vef{m6gD4q0GAYU$DK(`PYjZ7uj?B!NQs z<11sItL^j43nf^|j-9JV)EFC<;1P~#LG^M(#EoG!n*+&+<)sEoL>wl)dD`$+gx;vX z)%p(w;8C}~tHvKpCH+>pDZH3Sh`e0To$hK|*_5IeI?q+JnDAtkaSGQ$2<7xi?0_Ul z47nT;m=x;|B`|SdFrCAQl*h&3Kbd+D9?_tWcz=H%0f)oA6F~B**ic#I&3zA6m8T~)eGx0(R3hV$sL;nqdJ{X zR&FL%BIv}3kUm;gEIAGh4dqoOHp5;lkOA6kV=dexmUy*s(GcwX6Fj2bTv`Q74Gp6m z6=Nl24k)tFu|BLXq9opL^QX;$ z1_(k_ULLXgW&NT-gB}~?oL^?!!er2TNleJ&Xb+~3G|h`Xn_S+uoO{F-<$~HjO|Rw) z^F*Ngf6htqj}@&>Iv6}-h8?xQ028l-LIKaWjghAIkeB@_lrlT0BCo7TlCe|s25j~? z_dC2D{?v3D8lYuw@ZgXMo-{ZLGNLy&S5|!Oyu7@u<>lilD}P&K{8F713&sWU9^w%a za-fn3dVjIEKV>5y>~Wq#y}Q33y6E``EUL2-OKH%Z(O^pE#Uu%ObukEfyOXBHLU5Z> zrcRR+W@DR?l}tcJMy4Ns32u^+=&>$fWE>X{3JSXZ1J26SG&F^1;;)jbUk7@gCY{v4 z8?Afe)WV`-j41mOGN@K(x5$haY09kEWYd3mgM8dhlAV=h!>Z^eCLs}~<-pUkGW6&# z#-2U6JrK1klSt&lN0Rgr?)Hv#_&TDlEL7^W*=;EKeWuZq+079WyC3Xpy7Kb!l)(vr z@ci1E|JlEVDgj((gQMIy@0kc-j??A&*7ogkd*Da^_GJ*r8>?5Cd~olqWiwDon8H=- zQ*t~wb4f57wsS7b&vPPdX|QL<7xKALQ-#~0K!I|m4;kdxHU1q>3=#5cqluH@#BDFt zn_n#&2L%{2;`ph-UEu|5r;S?NjvK$Ti?|(3@bU4yICQ=^bogz|i#wgl=}jS|rKN4< z#d)0kJ4cSj8Nya4A*y&TR%r_tV(Y8UO_pJ*Yv-0 zY#=NwtOIG_kp}osl9W-a(0Cn;!Q8nTCS{pdpnE8%$Z;#{w{Z9H*xvBFumlf$LXZy< z0E_4||5?C$`FekxFgG_ho7lgJ&A4R1fIR&(IT6ZDcX#*b+z;goZ1GouUPSTeg8KUU zJ+K{pzysxN!jo;NR8uhe&Guq4GEu0a48eE61~C#$?Z2ZVoxckUnwzt;{Rqo43tIXS zp*{;Wn~LT+)I-C=PrbdO7T(^EIut37Ou@z=UN{r?!>N2QvHl|#6ueNJjf){G4Tf^v z*XgyabDWUz^`8J{%HkIAw4c!&nB=Ut7)@2Nvpa#N2?$5ybvr;(%Hu^RB_t$Fl>RON zaSGIBx&<1je%Y;0_%C`d?yN`=A!8gN$#!MG7z7vhxiG7=_#f9mxqgs>E!6h#u3~?1|Nd`4m6Y6 zC2DjEN=jz_{syEZBq+)X3Pc==Hvsw>;4BO0LQjw|)P0kGahYix_+(J%s5+qy<9f6* znagATI~My7X97IAIKH$Vm#`OeWY5o50aQJ3lki_r=c6S|O^=r8(YZ6&o!4 z09;L7ff?L-NeY*VhM$|DN8(^Gbf=?;C5-GN_1=Qe;Ba%UaW-O2_S-y|%w0%l&}OHo z)YSaNu-jG*=}d>tR8duBXVmHK65!_6SX)*W2@Qg<$$=SK%HxPb17%qDu_FDajk5BM zGw)CEK(uu6j1Bk|5~5xPz=*P#WR4Ry9Ic*GCh_tGoGS95AQ}4nEy)O>@2R&)z#IOeI{rjqJWW)9Q)^V zC2)oVe`6#;^*ZsxyDo0NzWjV#TwK|*R6`1LMB*!P{GhEYv>jgb2pMVVR4}|b01r-I z-p(m&|5ZPPbG^`DqO2F6_(!+a?!i!1T^%uf7*=!AElP%lCO!{&1pI+Ffe%iPuUC>% z<>yqiTi~qL(p(6hKD+_z&V`3WfrrFiI1Q_*`OTI9>(fJptbS!y8TCA=bg9%)i`m1QSIYSkkWt-zGNW$}dh8C=Pq>$ge#y7tM)B{bj zdf8QuYTUsLCjF&QXvE~~jI8XkWApe503?Y8BKrUV{@JC1^qG`}Av^cVaOkkfdwIE) zmH))y9CdNP`3mX0?XbXX(QE3z7;W34`lG;k;WO=r7Wdm3ylYc%1fQnLGmFt;nSE1< zK5j?JRZ_*OQ%J=)%#%-(b}pQb@)n%hz2XhJThMmd8H6wRTbpVAAcWM5lJt8JBGk{TPIl>|hT0i1G-2 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 00000000..ab983282 --- /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 85aae4c8..e1dd1a1f 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 1672af2b..38f4fdda 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 c2d92097..a955c2a2 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 a955c2a2..ec23aa7a 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 50cd6d0c..29b0d0e0 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 25c485a2..34398dd0 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 a5bd1de8..ef0bb12a 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 b7bb2f42..df394a84 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 0fa45b93..931e7284 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 5403853d..3049f3b8 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 6aed6053..4185945e 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 e46dab11..6fd89c9a 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 39367c3c..7c07ee25 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 42a11fe5..530f12a9 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 7088107b..c4ab3c92 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 7488acc9..fb18eabd 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 b5a88106..9d107d69 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 cd01f93f..93f27bf7 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 8d441489..f244631f 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 37bb196e..5a344dd0 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 f08bee6a..17b474e6 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 69f0c96c..163e045e 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 09a055be..cfbb046c 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 d5481d7a..d72033f6 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 1738a02e..2c3697be 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 13eb73f2..4d43f8bc 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 8092b3af..ccf9c7aa 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 dd718b96..a8715011 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 33a57e9d..09aa55e2 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 e1b96212..bbd948c7 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 29e1d3c4..fcd99f2f 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 8ecbdb47..af54808a 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 9e9e3de8..7e93a7a3 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 5654befc..bf90ec95 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 fd8334f9..f1771c3f 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 326f7829..a0f7e3de 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 e537d41f..8ee4a51f 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 b308de14..eb26f2ed 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 9588d10b..0ebe49fd 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 cc4173a9..fc4db9a6 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 2115d728..f0f7d49a 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 59f0896c..e7a7eacd 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 1c49adfb..99cb6f40 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 8daba257..05ecd292 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 e2eb4317..82cb88fa 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 504f0a4b..77070d1a 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 5be55ea2..cf442c2b 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 11a6d5fc..d5f992f0 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 ca39c0a6..6b8c6e0e 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 0dcfcfda..6486af55 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 5df5c46a..500f5761 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 cb3310be..406c2735 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 51e7f9a2..8683dad1 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 e2f3f80e..7d722e6c 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 8fa95ac9..2ff2d372 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 c783fef5..fd17fd46 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 eb26f2ed..48981e16 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 e7a7eacd..0457bb19 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 00000000..26320e32 --- /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 7c107d11..191ea2e6 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 00000000..299a21f9 --- /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 05ecd292..89a3a156 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 7d722e6c..2619a1c0 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 26320e32..347bec47 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 299a21f9..0008c626 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 2ff2d372..32c32efb 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 fd17fd46..7514ead3 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 b449bbbd..12523605 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 26038218..f44bb4ee 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 347bec47..0be78842 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 89a3a156..0a4299ae 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 0a4299ae..b65980c1 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 0008c626..dd83a873 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 b65980c1..aa929ca9 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 b65980c1..aa929ca9 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 e96a2b40..cdce6f12 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 3220c824..0d1eeff1 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 d0fdcb3f..63b59e8f 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 aca4778e..f4d63cba 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 7b959a77..b3bf02dd 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 38f4fdda..ddc34760 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 9c9097f6..99752b3b 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 63c1d26f..ac367898 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 db565fe2..1d2fbe52 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 fd46b4cf..ef76a8fc 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 90c8e105..9a813cd0 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 78648a03..31a6d8d1 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 d06cc649..8176c9bc 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 ab0c8064..2431da44 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 6e72d4b0..a5179523 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 b7b59ed9..b6e992fa 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 9fdc2926..aafec8b0 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 2502cb34..9a239238 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 24563d21..be61e94e 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 078bf376..b4befcdc 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 e08700d4..3fe0ca86 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 5be2a914..b06d1fa0 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 c389d682..9746e8a4 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 bf4b85b1..27bf78dd 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 f464cb8f..4ccc9e35 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 a6b83d9f..347d1279 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 09c34b4f..19232b1e 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 2154b272..da7f4df4 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 f46a301f..e988b648 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 93e984be..57080641 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 214ee83d..393357ec 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 db26e538..17cafb8f 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 c3894726..7eeca7de 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 45c2cc51..3affb52d 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 3c9a438a..f1b94ecd 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 34398dd0..187ffc9f 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 187ffc9f..971c713c 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 63c1d26f..3a467cb1 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 8683dad1..6e4033d7 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 2431da44..2d66706d 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 9cdbf974..d2eb21ee 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 2d66706d..68ec1080 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 6e4033d7..f36bdae7 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 0d1eeff1..45c55284 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 63b59e8f..9d0d176d 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 f4d63cba..e2761e45 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 b3bf02dd..2020a2b6 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 ddc34760..7cf563fc 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 99752b3b..c2253244 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 599da6fb..3ce2860f 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 1d2fbe52..00f6b70a 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 ef76a8fc..1f6e9f55 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 9a813cd0..b7ebf457 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 31a6d8d1..21ab2821 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 8176c9bc..f48de17f 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 68ec1080..4c63106d 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 a5179523..b291a6e7 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 b6e992fa..d63e8318 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 aafec8b0..b8b9eb1d 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 9a239238..1a806c80 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 be61e94e..2b29c7cb 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 b4befcdc..e91cd390 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 3fe0ca86..b0fe9175 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 b06d1fa0..d0232ba3 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 9746e8a4..6df73f1e 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 27bf78dd..458002f4 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 4ccc9e35..2abd1870 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 347d1279..6b739e8a 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 19232b1e..90a435fd 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 da7f4df4..a98ea634 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 e988b648..61c2b5d2 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 57080641..236ee5e8 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 393357ec..f2a34e21 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 17cafb8f..84e74716 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 7eeca7de..0c4caf4d 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 3affb52d..19e1184d 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 9746e8a4..34a43346 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 86606372..df0ca832 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

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